Compare commits

..

93 Commits
1.10.0 ... main

Author SHA1 Message Date
NightKev
536b018dae
Update main to 1.10.7 2025-09-07 18:20:26 -07:00
Sirz Benjie
43f8b78c35
[Bug] Fix sessions clearing the wrong slot on save (#6509)
* Store session id in session data to prevent somehow deleting wrong slot

* Only log session / system if beta or local; fix promise

* Fix serialization/deserialization when logging session/system data

* Force loadSaveSlot to set the logged in user's last session

* No longer add slotId to session data
2025-09-07 18:17:12 -07:00
Sirz Benjie
8fdd5043c3
[Bug] [Ability] Prevent message flyout spam for sturdy, damp, and a few other abilities (#6507)
* fix: prevent message spam for many abilities

* Update src/data/abilities/ability.ts
---------

Co-authored-by: NightKev <34855794+DayKev@users.noreply.github.com>
2025-09-07 18:39:52 -05:00
Wlowscha
e175bbfb28
[UI/UX] Use pointer events instead of touch events in TouchControl (#6506)
* Use pointer events instead of touch events in `TouchControl`

* Marked some touch-related tests as todo

* Changing test mocker to send pointer events

* Also updated the ui to change the position of touch controls

Co-authored-by: NightKev <34855794+DayKev@users.noreply.github.com>
2025-09-07 17:35:49 -05:00
Sirz Benjie
344e9463cc
[Bug] Fix memory leak in egg hatch (#6511)
Co-authored-by: MokaStitcher <54149968+MokaStitcher@users.noreply.github.com>
2025-09-07 17:05:25 -05:00
NightKev
7001f78beb [i18n] Update locales submodule 2025-09-06 02:45:28 -07:00
Bertie690
309e31e196
[Bug] Future Sight no longer crashes after catching the user (#6479) 2025-09-05 14:38:01 -04:00
Fabi
ddde977a0a
[UI/UX] Auto focus first input field (#6413) 2025-09-04 02:31:52 -07:00
Sirz Benjie
4a28773929
[Bug] [Move] Fix poltergeist crash when no remaining enemies (#6473)
* fix: poltergeist crash with no target

* fix: adjust move phase history
2025-09-03 02:44:43 +00:00
Fabi
9fc31350f8
[Bug] Fix monotype selector image (#6471) 2025-09-03 02:07:40 +00:00
Sirz Benjie
848c1f01e0
chore: bump version 2025-09-02 11:14:55 -05:00
damocleas
2636f59c1e
1.10.6 to Main
Hotfix 1.10.6 to main
2025-09-01 20:05:41 -04:00
Sirz Benjie
9f19c6bea2
Merge pull request #6467 from Xavion3/sound-error-improvement
Sound error handling improvement
2025-09-01 17:24:33 -05:00
Xavion3
bc0e2662fa Clean up bangs 2025-09-02 06:53:57 +10:00
Xavion3
eb8c0c0243 Add comment explaining bang 2025-09-02 06:53:55 +10:00
Xavion3
bd7de61a56 Improve error handling when playing unloaded sounds 2025-09-02 06:53:55 +10:00
Bertie690
17c28c4024
[Bug] Fix monotype challenge using unlocalized type names (#6438) 2025-09-01 05:57:46 -07:00
AJ Fontaine
231cfd040c
[Balance] TM tier changes 1.10.6 (#6461)
* TM tier changes 1.10.6
2025-08-31 13:47:53 -04:00
Sirz Benjie
8a69c628d1
Merge pull request #6459 from fabske0/fix-enemy-level-position
[Bug] [UI] Fix enemy level position
2025-08-31 10:28:25 -05:00
damocleas
9a00bc2f10
Merge branch 'hotfix-1.10.6' into fix-enemy-level-position 2025-08-31 11:23:38 -04:00
fabske0
2a5e66d85f update pos 2025-08-31 17:17:25 +02:00
Lugiad
e0a752aa70
[UI/UX] [Localization] Korean and Chinese Visual Fixes (#6452) 2025-08-31 13:55:50 +00:00
fabske0
320641eaa2 fix position 2025-08-31 15:25:27 +02:00
Lugiad
2c89295e3d
[Sprite] Round of optimized PNGs (#6458)
Round of optimized PNGs
2025-08-31 08:23:55 -04:00
NightKev
a9f6801ecb [i18n] Update locales submodule 2025-08-31 02:53:35 -07:00
Fabi
929f721ee0
[Bug][UI] Fix ribbon and status effect overlap (#6453) 2025-08-31 01:07:50 +00:00
Wlowscha
5c22d9ccac
[Bug] Pokemon not on the field can't be caught (#6454) 2025-08-31 00:54:35 +00:00
Sirz Benjie
264dd6b2d0
[Bug][Sprite] Ensure evo silhouette disappears in evo phase (#6450)
Ensure evo silhouette disappears in evo phase
2025-08-30 20:41:58 -04:00
Fabi
4dc067daa2
[Bug] [UI/UX] Fix options during item transfer (#6425) 2025-08-29 23:52:39 +00:00
NightKev
dadc7b9598
[Bug] Breaking a boss bar will no longer crash at max stat stages (#6440)
Co-authored-by: Wlowscha <54003515+Wlowscha@users.noreply.github.com>
2025-08-29 07:36:43 +02:00
NightKev
98e65b9b8b [i18n] Update locales submodule 2025-08-27 02:10:48 -07:00
Fabi
58ba29a1be
[Bug] Fix soak message i18n key (#6443) 2025-08-27 01:23:37 -07:00
Bertie690
1f2788a438
[Bug] Fix Soak message key not being in camel (#6439) 2025-08-26 20:47:12 -07:00
NightKev
7447602146 Update version to 1.10.6 2025-08-26 15:42:18 -07:00
Sirz Benjie
e06980519d
[Misc] Bump version to 1.10.5 on main (#6436) 2025-08-26 14:19:15 -07:00
Sirz Benjie
95ddcae543
Merge pull request #6431 from pagefaultgames/fix-starterprefs
Fix starterpref reset (#6430) to main
2025-08-26 12:15:36 -05:00
Sirz Benjie
6745ce7839
Fix starterpref reset (#6430)
* Prevent an empty starterpreferences object from being saved

* Fix ssui nullish coalescing

* Update src/utils/data.ts

Co-authored-by: Dean <69436131+emdeann@users.noreply.github.com>

* Fix starter preferences clearing, add tests

---------

Co-authored-by: Wlowscha <54003515+Wlowscha@users.noreply.github.com>
Co-authored-by: Dean <69436131+emdeann@users.noreply.github.com>
2025-08-26 12:11:16 -05:00
NightKev
701eecf947
Revert #6410 starter preferences hotfix (#6422)
Revert "[Bug] Prevent an empty starterpreferences object from being saved (#6410)"

This reverts commit c8a66b2e59.
2025-08-26 03:15:00 -05:00
NightKev
cc7391448a
Merge pull request #6420 from pagefaultgames/hotfix-1.10.4
Hotfix 1.10.4 to main
2025-08-25 23:58:57 -07:00
Wlowscha
63c1c34746
[Hotfix] Fix tyrogue evo (#6414)
* Fixed tyrogue evo condition

* Added test for tyrogue evolution

* Update src/data/balance/pokemon-evolutions.ts

Co-authored-by: Bertie690 <136088738+Bertie690@users.noreply.github.com>

* Update src/data/balance/pokemon-evolutions.ts

Co-authored-by: Bertie690 <136088738+Bertie690@users.noreply.github.com>

* Update src/data/balance/pokemon-evolutions.ts

Co-authored-by: Bertie690 <136088738+Bertie690@users.noreply.github.com>

* Added missing typeof in suggestion

---------

Co-authored-by: Bertie690 <136088738+Bertie690@users.noreply.github.com>
2025-08-26 06:57:11 +00:00
Sirz Benjie
88e42ba4c4
[Balance][ME]Tweak weird dream do a party heal in option 1, random tera, and no longer revive pokemon in hardcore (#6409)
* Tweak weird dream option 1

Transfer HP ratio and status onto transformed pokemon

* Make weird dream randomize tera type
2025-08-26 06:54:26 +00:00
AJ Fontaine
4aac5472a9
[Bug] Fix oversight where mons aren't guaranteed damaging moves if none are STAB (#6406) 2025-08-26 06:51:35 +00:00
Sirz Benjie
c8a66b2e59
[Bug] Prevent an empty starterpreferences object from being saved (#6410)
* Prevent an empty starterpreferences object from being saved

* Fix ssui nullish coalescing

* Update src/utils/data.ts

Co-authored-by: Dean <69436131+emdeann@users.noreply.github.com>

---------

Co-authored-by: Dean <69436131+emdeann@users.noreply.github.com>
2025-08-25 23:48:55 -07:00
Sirz Benjie
f0c24cd16e
Remove unnecessary tsdoc comments from service-worker.js (#6417)
Remove typescript comments form `service-worker.js`
2025-08-25 16:45:41 -07:00
Sirz Benjie
622ee5ce80
[Bug] Fix typecheck bug (#6415)
Add `public/service-worker.js` to `ts`'s exclude
2025-08-25 16:25:53 -07:00
Bertie690
1b6a52e520
[Balance] Moved Future Sight after Weather, before berries (#6412) 2025-08-25 14:55:31 -07:00
Sirz Benjie
56752d6f4a
Ensure users that install pokerogue as a PWA do not remain on the old version 2025-08-25 14:08:05 -05:00
NightKev
1f4efa0e27 Update version to 1.10.4 2025-08-24 19:45:00 -07:00
Sirz Benjie
6ef57e52e7
Merge pull request #6401 from pagefaultgames/hotfix-1.10.3
Hotfix 1.10.3 to main
2025-08-24 19:58:37 -05:00
Sirz Benjie
d8fe5ad753
Bump to version 1.10.3 2025-08-24 19:52:55 -05:00
Sirz Benjie
e139f714de
Bump locales 2025-08-24 19:52:55 -05:00
Sirz Benjie
4b8c064335
[Bug] [Ability] Fix berserk multi proc (#6402)
* Fix berserk activating multiple times with multi strike

* Apply kev's suggestions from code review

Co-authored-by: NightKev <34855794+DayKev@users.noreply.github.com>

* Update doc comments

---------

Co-authored-by: NightKev <34855794+DayKev@users.noreply.github.com>
2025-08-24 19:47:52 -05:00
Sirz Benjie
6fac1a5052
[Bug] [Ability] Fix cute charm (#6399)
Fix cute charm not working
2025-08-24 17:49:28 -05:00
Sirz Benjie
a85d8cd5de
[Bug] [Sprite] Fix fusion shiny party icon (#6397)
* Fix shiny icon with fusion shiny in party

* Fix hover tooltip not working when first splicing a mon

* Fix fusion shiny icon on summary screen
2025-08-24 16:32:12 -05:00
Sirz Benjie
cd610ff2c5
[Bug] [Ability] Fix trace's RNG (#6398)
* Fix rng in trace

* Fix undefined var
2025-08-24 16:24:12 -05:00
Sirz Benjie
30b7a62988
[Bug] [Sprite] Fix third type icon missing texture (#6395)
Fix third type icon
2025-08-24 15:03:28 -05:00
Sirz Benjie
efaf7760e4
[Bug] Fix speedup/slowdown buttons not working properly (#6393)
Fix speedup button resetting to minimum speed
2025-08-24 14:34:21 -05:00
Bertie690
0f8b1f63b5
[Bug/Ability] Fixed bugs with Intimidate triggers after reload/initial switch (#6212)
* Added TODO test case + documentation for failing intim test

* Fixed comment

* Fixed intimidate bugs fr fr

* Update src/data/phase-priority-queue.ts

Co-authored-by: NightKev <34855794+DayKev@users.noreply.github.com>

* Update src/data/phase-priority-queue.ts

Co-authored-by: NightKev <34855794+DayKev@users.noreply.github.com>

* Cleanup remove phase logic

* Do not add unnecessary activateAbilityPhases for pokemon that do not have their passive enabled

* Remove leftover log messages

* Add TODO comment

Co-authored-by: Bertie690 <136088738+Bertie690@users.noreply.github.com>

---------

Co-authored-by: NightKev <34855794+DayKev@users.noreply.github.com>
Co-authored-by: Sirz Benjie <142067137+SirzBenjie@users.noreply.github.com>
2025-08-24 14:09:16 -05:00
Sirz Benjie
049932c001
[Bug] Fix status effects overwriting each other (#6392)
* Ensure status effects from same source interaction cannot override each other

* Update test/status-effects/general-status-effect.test.ts

Co-authored-by: Bertie690 <136088738+Bertie690@users.noreply.github.com>
2025-08-24 12:51:16 -05:00
Wlowscha
ae58f9de4f
[Hotfix] Leech Seed does not get cleared when user faints (#6388)
Lapsing leech seed tag even when source has left field
2025-08-24 08:58:27 -05:00
NightKev
8dea0ce840
[GitHub] Update pages workflow to only deploy on main and beta (#6386) 2025-08-23 20:28:42 -07:00
Madmadness65
58d1209d92
Hotfix 1.10.2
Hotfix 1.10.2 to main
2025-08-23 19:09:53 -05:00
Sirz Benjie
c446eaf970
[Sprite] Revert ribbon icons (#6382) 2025-08-23 18:59:47 -05:00
Sirz Benjie
746ea69475
[Bug] Fix missing fusion icon (#6381)
Fix nullish coalescing in `pokemon#isFusion`
2025-08-23 18:58:29 -05:00
Wlowscha
c44e589303
[UI/UX][Bug] Saving preferences when leaving ssui (#6376)
Save starter preferences when leaving ssui
2025-08-23 18:50:44 -05:00
Sirz Benjie
70b47945e8
[Bug] Fix nicknames not showing up in battle (#6378) 2025-08-23 23:47:39 +00:00
Sirz Benjie
842f0e88b0
[Bug] Fix saveslot deletion (#6380)
Fix saveslot deletion
2025-08-23 18:45:15 -05:00
Fabi
1192825d51
[Bug/i18n] Fix unpause evolution option in party ui (#6379) 2025-08-23 15:59:01 -07:00
Fabi
1b2c694084
[bug/i18n] Fix stealth rock message (#6377)
replace pokemonName with pokemonNameWithAffix
2025-08-23 17:26:12 -05:00
Sirz Benjie
a8b54eba6d
[Test] Mark failing test/ui/manage-item-button.test.ts as TODO (#6375)
* Ensure hotfix runs tests

* Unnest promise

* ensure workflows run when pushed to hotfix

* Mark test todo
2025-08-23 16:56:58 -05:00
Dean
d2eb3dba53
[Test] Fix leftovers test not running heal phase (#6370)
Fix leftovers test
2025-08-23 16:02:36 -05:00
Bertie690
af963c7902
[Bug] Fixed delayed attacks pushing to move history twice on charging turn (#6371) 2025-08-23 16:00:00 -05:00
damocleas
cd137062ea
Fix Option 1 Leftovers text in Trash to Treasure encounter 2025-08-23 13:28:09 -04:00
Sirz Benjie
99bf639ea7
[Bug] [Move] Fix pollen puff (#6363)
* Ensure pollen puff does not heal enemy after damage

* Add test to ensure pollen puff does not heal enemy
2025-08-23 11:42:10 -05:00
Blitzy
873e12f9ad
[Balance] Clamperl Egg Moves / Passive Tweaks (#6365)
Clamperl changes
2025-08-23 11:42:02 -05:00
Sirz Benjie
053976daec
[Bug] Fix ssui passives (#6362)
Fix passives not being persisted in starter select
2025-08-23 11:41:30 -05:00
Sirz Benjie
908886d78b
Fix unpauseEvolutions key in party select 2025-08-23 11:23:15 -05:00
Fabi
3b290ee9a2
[UI/UX][Bug] Prevent switch to discard mode while transferring item (#6358) 2025-08-23 10:05:48 -05:00
Wlowscha
16adb02ad2
[Item][Bug] No more free sacred ash (#6356) 2025-08-23 10:00:27 -05:00
Sirz Benjie
3dd1948634
[Misc] Fix circular dep (#6361) 2025-08-23 10:00:00 -05:00
Wlowscha
e63effa311
[Challenge][Bug] Fix fresh starters for real (#6355)
* Including side evolutions of starters
2025-08-23 09:45:11 -05:00
NightKev
8b371395bc
[Bug] Fix typo in i18n key for second Berries Abound ME option (#6354) 2025-08-23 09:43:56 -05:00
Wlowscha
e752e7a93d
[Bug][Item] Items can be transfered from 6th item slot again. (#6357)
* Added check for transferMode
2025-08-23 09:43:33 -05:00
NightKev
961d30f75e
[Bug] Wild Pokemon can now generate as female again (#6352) 2025-08-23 09:42:41 -05:00
NightKev
c027b5562b [i18n] Update locales submodule 2025-08-23 00:37:50 -07:00
NightKev
3eee9d9b4e Update version to 1.10.2 2025-08-22 21:43:17 -07:00
damocleas
2869e85f78
Hotfix 1.10.1 to main 2025-08-22 22:13:44 -04:00
Wlowscha
7050e15267
[Challenge][Bug] Partial Fix Available starters in fresh start monotype (#6344)
Full fresh start filter also accepts evolutions

Co-authored-by: Sirz Benjie <142067137+SirzBenjie@users.noreply.github.com>
Co-authored-by: NightKev <34855794+DayKev@users.noreply.github.com>
2025-08-22 22:09:30 -04:00
Madmadness65
be6606a11b Bump version to 1.10.1 2025-08-22 21:05:03 -05:00
Madmadness65
cdf470c06c [Item] Minor item weight change for Deep Sea items
Halved the weight of the items in the Great tier.
2025-08-22 20:59:01 -05:00
Sirz Benjie
abd9ebbdef
[Bug] Fix egg gacha 10x pull option working with just one voucher (#6349)
Fix 10x egg gacha being usable with only 1 voucher
2025-08-22 20:52:50 -05:00
Wlowscha
7af128f68a
[UI/UX][Bug] Setting shiny preferences before saving (#6348)
Setting shiny preferences before saving
2025-08-22 20:50:38 -05:00
Wlowscha
342eb33138
[UI/UX][Bug] Fix typos in trainer class keys (#6347)
* Fixing "fenale" typo

* Changing "key" to key
2025-08-22 18:12:38 -07:00
112 changed files with 1509 additions and 667 deletions

View File

@ -6,11 +6,13 @@ on:
- main - main
- beta - beta
- release - release
- 'hotfix*'
pull_request: pull_request:
branches: branches:
- main - main
- beta - beta
- release - release
- 'hotfix*'
merge_group: merge_group:
types: [checks_requested] types: [checks_requested]
@ -67,7 +69,7 @@ jobs:
pnpm exec typedoc --out /tmp/docs --githubPages false --entryPoints ./src/ pnpm exec typedoc --out /tmp/docs --githubPages false --entryPoints ./src/
- name: Commit & Push docs - name: Commit & Push docs
if: github.event_name == 'push' if: github.event_name == 'push' && (github.ref_name == 'beta' || github.ref_name == 'main')
run: | run: |
cd pokerogue_gh cd pokerogue_gh
git config user.email "github-actions[bot]@users.noreply.github.com" git config user.email "github-actions[bot]@users.noreply.github.com"

View File

@ -6,11 +6,13 @@ on:
- main - main
- beta - beta
- release - release
- 'hotfix*'
pull_request: pull_request:
branches: branches:
- main - main
- beta - beta
- release - release
- 'hotfix*'
merge_group: merge_group:
types: [checks_requested] types: [checks_requested]

View File

@ -6,11 +6,13 @@ on:
- main - main
- beta - beta
- release - release
- 'hotfix*'
pull_request: pull_request:
branches: branches:
- main - main
- beta - beta
- release - release
- 'hotfix*'
merge_group: merge_group:
types: [checks_requested] types: [checks_requested]
workflow_dispatch: workflow_dispatch:

View File

@ -1,7 +1,7 @@
{ {
"name": "pokemon-rogue-battle", "name": "pokemon-rogue-battle",
"private": true, "private": true,
"version": "1.10.0", "version": "1.10.7",
"type": "module", "type": "module",
"scripts": { "scripts": {
"start": "vite", "start": "vite",

Binary file not shown.

Before

Width:  |  Height:  |  Size: 197 B

After

Width:  |  Height:  |  Size: 195 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 279 B

After

Width:  |  Height:  |  Size: 276 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 290 B

After

Width:  |  Height:  |  Size: 289 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.1 KiB

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 218 B

After

Width:  |  Height:  |  Size: 215 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 341 B

After

Width:  |  Height:  |  Size: 283 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 349 B

After

Width:  |  Height:  |  Size: 261 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 336 B

After

Width:  |  Height:  |  Size: 331 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 364 B

After

Width:  |  Height:  |  Size: 289 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 280 B

After

Width:  |  Height:  |  Size: 278 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 206 B

After

Width:  |  Height:  |  Size: 205 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 347 B

After

Width:  |  Height:  |  Size: 283 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 235 B

After

Width:  |  Height:  |  Size: 230 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 645 B

After

Width:  |  Height:  |  Size: 610 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 419 B

After

Width:  |  Height:  |  Size: 417 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 363 B

After

Width:  |  Height:  |  Size: 296 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 208 B

After

Width:  |  Height:  |  Size: 206 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 338 B

After

Width:  |  Height:  |  Size: 335 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.7 KiB

After

Width:  |  Height:  |  Size: 3.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 14 KiB

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.9 KiB

After

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 290 B

After

Width:  |  Height:  |  Size: 276 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 58 KiB

After

Width:  |  Height:  |  Size: 53 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 471 B

After

Width:  |  Height:  |  Size: 247 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 296 B

After

Width:  |  Height:  |  Size: 295 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 691 B

After

Width:  |  Height:  |  Size: 689 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.2 KiB

After

Width:  |  Height:  |  Size: 2.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 207 B

After

Width:  |  Height:  |  Size: 151 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 183 B

After

Width:  |  Height:  |  Size: 152 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 201 B

After

Width:  |  Height:  |  Size: 154 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 207 B

After

Width:  |  Height:  |  Size: 151 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 201 B

After

Width:  |  Height:  |  Size: 162 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 207 B

After

Width:  |  Height:  |  Size: 151 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 183 B

After

Width:  |  Height:  |  Size: 152 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 201 B

After

Width:  |  Height:  |  Size: 154 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 207 B

After

Width:  |  Height:  |  Size: 151 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 201 B

After

Width:  |  Height:  |  Size: 162 B

@ -1 +1 @@
Subproject commit 58fa5f9b6e94469017bfbe69bef992ed48ef5343 Subproject commit 090bfefaf7e9d4efcbca61fa78a9cdf5d701830b

View File

@ -1,3 +1,7 @@
self.addEventListener('install', function () { self.addEventListener('install', function () {
console.log('Service worker installing...'); console.log('Service worker installing...');
}); });
self.addEventListener('activate', (event) => {
event.waitUntil(self.clients.claim());
})

View File

@ -863,6 +863,8 @@ export class BattleScene extends SceneBase {
* @param pokemonId - The ID whose Pokemon will be retrieved. * @param pokemonId - The ID whose Pokemon will be retrieved.
* @returns The {@linkcode Pokemon} associated with the given id. * @returns The {@linkcode Pokemon} associated with the given id.
* Returns `null` if the ID is `undefined` or not present in either party. * Returns `null` if the ID is `undefined` or not present in either party.
* @todo Change the `null` to `undefined` and update callers' signatures -
* this is weird and causes a lot of random jank
*/ */
getPokemonById(pokemonId: number | undefined): Pokemon | null { getPokemonById(pokemonId: number | undefined): Pokemon | null {
if (isNullOrUndefined(pokemonId)) { if (isNullOrUndefined(pokemonId)) {
@ -2318,7 +2320,7 @@ export class BattleScene extends SceneBase {
}); });
} }
playSound(sound: string | AnySound, config?: object): AnySound { playSound(sound: string | AnySound, config?: object): AnySound | null {
const key = typeof sound === "string" ? sound : sound.key; const key = typeof sound === "string" ? sound : sound.key;
config = config ?? {}; config = config ?? {};
try { try {
@ -2354,16 +2356,19 @@ export class BattleScene extends SceneBase {
this.sound.play(key, config); this.sound.play(key, config);
return this.sound.get(key) as AnySound; return this.sound.get(key) as AnySound;
} catch { } catch {
console.log(`${key} not found`); console.warn(`${key} not found`);
return sound as AnySound; return null;
} }
} }
playSoundWithoutBgm(soundName: string, pauseDuration?: number): AnySound { playSoundWithoutBgm(soundName: string, pauseDuration?: number): AnySound | null {
this.bgmCache.add(soundName); this.bgmCache.add(soundName);
const resumeBgm = this.pauseBgm(); const resumeBgm = this.pauseBgm();
this.playSound(soundName); this.playSound(soundName);
const sound = this.sound.get(soundName) as AnySound; const sound = this.sound.get(soundName);
if (!sound) {
return sound;
}
if (this.bgmResumeTimer) { if (this.bgmResumeTimer) {
this.bgmResumeTimer.destroy(); this.bgmResumeTimer.destroy();
} }
@ -2373,7 +2378,7 @@ export class BattleScene extends SceneBase {
this.bgmResumeTimer = null; this.bgmResumeTimer = null;
}); });
} }
return sound; return sound as AnySound;
} }
/** The loop point of any given battle, mystery encounter, or title track, read as seconds and milliseconds. */ /** The loop point of any given battle, mystery encounter, or title track, read as seconds and milliseconds. */

View File

@ -396,7 +396,23 @@ export abstract class AbAttr {
} }
} }
export class BlockRecoilDamageAttr extends AbAttr { /**
* Abstract class for ability attributes that simply cancel an interaction
*
* @remarks
* Abilities that have simple cancel interactions (e.g. {@linkcode BlockRecoilDamageAttr}) can extend this class to reuse the `canApply` and `apply` logic
*/
abstract class CancelInteractionAbAttr extends AbAttr {
override canApply({ cancelled }: AbAttrParamsWithCancel): boolean {
return !cancelled.value;
}
override apply({ cancelled }: AbAttrParamsWithCancel): void {
cancelled.value = true;
}
}
export class BlockRecoilDamageAttr extends CancelInteractionAbAttr {
private declare readonly _: never; private declare readonly _: never;
constructor() { constructor() {
super(false); super(false);
@ -592,11 +608,7 @@ export class PreDefendFullHpEndureAbAttr extends PreDefendAbAttr {
} }
} }
export class BlockItemTheftAbAttr extends AbAttr { export class BlockItemTheftAbAttr extends CancelInteractionAbAttr {
override apply({ cancelled }: AbAttrParamsWithCancel): void {
cancelled.value = true;
}
getTriggerMessage({ pokemon }: AbAttrBaseParams, abilityName: string) { getTriggerMessage({ pokemon }: AbAttrBaseParams, abilityName: string) {
return i18next.t("abilityTriggers:blockItemTheft", { return i18next.t("abilityTriggers:blockItemTheft", {
pokemonNameWithAffix: getPokemonNameWithAffix(pokemon), pokemonNameWithAffix: getPokemonNameWithAffix(pokemon),
@ -869,8 +881,9 @@ export interface FieldPriorityMoveImmunityAbAttrParams extends AugmentMoveIntera
} }
export class FieldPriorityMoveImmunityAbAttr extends PreDefendAbAttr { export class FieldPriorityMoveImmunityAbAttr extends PreDefendAbAttr {
override canApply({ move, opponent: attacker }: FieldPriorityMoveImmunityAbAttrParams): boolean { override canApply({ move, opponent: attacker, cancelled }: FieldPriorityMoveImmunityAbAttrParams): boolean {
return ( return (
!cancelled.value &&
!(move.moveTarget === MoveTarget.USER || move.moveTarget === MoveTarget.NEAR_ALLY) && !(move.moveTarget === MoveTarget.USER || move.moveTarget === MoveTarget.NEAR_ALLY) &&
move.getPriority(attacker) > 0 && move.getPriority(attacker) > 0 &&
!move.isMultiTarget() !move.isMultiTarget()
@ -897,10 +910,8 @@ export class MoveImmunityAbAttr extends PreDefendAbAttr {
this.immuneCondition = immuneCondition; this.immuneCondition = immuneCondition;
} }
override canApply({ pokemon, opponent: attacker, move }: MoveImmunityAbAttrParams): boolean { override canApply({ pokemon, opponent: attacker, move, cancelled }: MoveImmunityAbAttrParams): boolean {
// TODO: Investigate whether this method should be checking against `cancelled`, specifically return !cancelled.value && this.immuneCondition(pokemon, attacker, move);
// if not checking this results in multiple flyouts showing when multiple abilities block the move.
return this.immuneCondition(pokemon, attacker, move);
} }
override apply({ cancelled }: MoveImmunityAbAttrParams): void { override apply({ cancelled }: MoveImmunityAbAttrParams): void {
@ -970,6 +981,8 @@ export class MoveImmunityStatStageChangeAbAttr extends MoveImmunityAbAttr {
export interface PostMoveInteractionAbAttrParams extends AugmentMoveInteractionAbAttrParams { export interface PostMoveInteractionAbAttrParams extends AugmentMoveInteractionAbAttrParams {
/** Stores the hit result of the move used in the interaction */ /** Stores the hit result of the move used in the interaction */
readonly hitResult: HitResult; readonly hitResult: HitResult;
/** The amount of damage dealt in the interaction */
readonly damage: number;
} }
export class PostDefendAbAttr extends AbAttr { export class PostDefendAbAttr extends AbAttr {
@ -1079,20 +1092,16 @@ export class PostDefendHpGatedStatStageChangeAbAttr extends PostDefendAbAttr {
this.selfTarget = selfTarget; this.selfTarget = selfTarget;
} }
override canApply({ pokemon, opponent: attacker, move }: PostMoveInteractionAbAttrParams): boolean { override canApply({ pokemon, opponent: attacker, move, damage }: PostMoveInteractionAbAttrParams): boolean {
const hpGateFlat: number = Math.ceil(pokemon.getMaxHp() * this.hpGate); const hpGateFlat: number = Math.ceil(pokemon.getMaxHp() * this.hpGate);
const lastAttackReceived = pokemon.turnData.attacksReceived[pokemon.turnData.attacksReceived.length - 1]; return this.condition(pokemon, attacker, move) && pokemon.hp <= hpGateFlat && pokemon.hp + damage > hpGateFlat;
const damageReceived = lastAttackReceived?.damage || 0;
return (
this.condition(pokemon, attacker, move) && pokemon.hp <= hpGateFlat && pokemon.hp + damageReceived > hpGateFlat
);
} }
override apply({ simulated, pokemon, opponent: attacker }: PostMoveInteractionAbAttrParams): void { override apply({ simulated, pokemon, opponent }: PostMoveInteractionAbAttrParams): void {
if (!simulated) { if (!simulated) {
globalScene.phaseManager.unshiftNew( globalScene.phaseManager.unshiftNew(
"StatStageChangePhase", "StatStageChangePhase",
(this.selfTarget ? pokemon : attacker).getBattlerIndex(), (this.selfTarget ? pokemon : opponent).getBattlerIndex(),
true, true,
this.stats, this.stats,
this.stages, this.stages,
@ -1263,17 +1272,17 @@ export class PostDefendContactApplyTagChanceAbAttr extends PostDefendAbAttr {
this.turnCount = turnCount; this.turnCount = turnCount;
} }
override canApply({ move, pokemon, opponent: attacker }: PostMoveInteractionAbAttrParams): boolean { override canApply({ move, pokemon, opponent }: PostMoveInteractionAbAttrParams): boolean {
return ( return (
move.doesFlagEffectApply({ flag: MoveFlags.MAKES_CONTACT, user: attacker, target: pokemon }) && move.doesFlagEffectApply({ flag: MoveFlags.MAKES_CONTACT, user: opponent, target: pokemon }) &&
pokemon.randBattleSeedInt(100) < this.chance && pokemon.randBattleSeedInt(100) < this.chance &&
attacker.canAddTag(this.tagType) opponent.canAddTag(this.tagType)
); );
} }
override apply({ simulated, opponent: attacker, move }: PostMoveInteractionAbAttrParams): void { override apply({ pokemon, simulated, opponent, move }: PostMoveInteractionAbAttrParams): void {
if (!simulated) { if (!simulated) {
attacker.addTag(this.tagType, this.turnCount, move.id, attacker.id); opponent.addTag(this.tagType, this.turnCount, move.id, pokemon.id);
} }
} }
} }
@ -1593,12 +1602,7 @@ export interface FieldPreventExplosiveMovesAbAttrParams extends AbAttrBaseParams
cancelled: BooleanHolder; cancelled: BooleanHolder;
} }
export class FieldPreventExplosiveMovesAbAttr extends AbAttr { export class FieldPreventExplosiveMovesAbAttr extends CancelInteractionAbAttr {}
// TODO: investigate whether we need to check against `cancelled` in a `canApply` method
override apply({ cancelled }: FieldPreventExplosiveMovesAbAttrParams): void {
cancelled.value = true;
}
}
export interface FieldMultiplyStatAbAttrParams extends AbAttrBaseParams { export interface FieldMultiplyStatAbAttrParams extends AbAttrBaseParams {
/** The kind of stat that is being checked for modification */ /** The kind of stat that is being checked for modification */
@ -2537,15 +2541,11 @@ export class IgnoreOpponentStatStagesAbAttr extends AbAttr {
* Abilities with this attribute prevent the user from being affected by Intimidate. * Abilities with this attribute prevent the user from being affected by Intimidate.
* @sealed * @sealed
*/ */
export class IntimidateImmunityAbAttr extends AbAttr { export class IntimidateImmunityAbAttr extends CancelInteractionAbAttr {
constructor() { constructor() {
super(false); super(false);
} }
override apply({ cancelled }: AbAttrParamsWithCancel): void {
cancelled.value = true;
}
getTriggerMessage({ pokemon }: AbAttrParamsWithCancel, abilityName: string, ..._args: any[]): string { getTriggerMessage({ pokemon }: AbAttrParamsWithCancel, abilityName: string, ..._args: any[]): string {
return i18next.t("abilityTriggers:intimidateImmunity", { return i18next.t("abilityTriggers:intimidateImmunity", {
pokemonNameWithAffix: getPokemonNameWithAffix(pokemon), pokemonNameWithAffix: getPokemonNameWithAffix(pokemon),
@ -3014,41 +3014,44 @@ export class PostSummonFormChangeAbAttr extends PostSummonAbAttr {
} }
} }
/** Attempts to copy a pokemon's ability */ /**
* Attempts to copy a pokemon's ability
*
* @remarks
* Hardcodes idiosyncrasies specific to trace, so should not be used for other abilities
* that might copy abilities in the future
* @sealed
*/
export class PostSummonCopyAbilityAbAttr extends PostSummonAbAttr { export class PostSummonCopyAbilityAbAttr extends PostSummonAbAttr {
private target: Pokemon; private target: Pokemon;
private targetAbilityName: string; private targetAbilityName: string;
override canApply({ pokemon }: AbAttrBaseParams): boolean { override canApply({ pokemon, simulated }: AbAttrBaseParams): boolean {
const targets = pokemon.getOpponents(); const targets = pokemon
.getOpponents()
.filter(t => t.getAbility().isCopiable || t.getAbility().id === AbilityId.WONDER_GUARD);
if (!targets.length) { if (!targets.length) {
return false; return false;
} }
let target: Pokemon; let target: Pokemon;
if (targets.length > 1) { // simulated call always chooses first target so as to not advance RNG
globalScene.executeWithSeedOffset(() => (target = randSeedItem(targets)), globalScene.currentBattle.waveIndex); if (targets.length > 1 && !simulated) {
target = targets[randSeedInt(targets.length)];
} else { } else {
target = targets[0]; target = targets[0];
} }
if ( this.target = target;
!target!.getAbility().isCopiable && this.targetAbilityName = allAbilities[target.getAbility().id].name;
// Wonder Guard is normally uncopiable so has the attribute, but Trace specifically can copy it
!(pokemon.hasAbility(AbilityId.TRACE) && target!.getAbility().id === AbilityId.WONDER_GUARD)
) {
return false;
}
this.target = target!;
this.targetAbilityName = allAbilities[target!.getAbility().id].name;
return true; return true;
} }
override apply({ pokemon, simulated }: AbAttrBaseParams): void { override apply({ pokemon, simulated }: AbAttrBaseParams): void {
if (!simulated) { // Protect against this somehow being called before canApply by ensuring target is defined
pokemon.setTempAbility(this.target!.getAbility()); if (!simulated && this.target) {
setAbilityRevealed(this.target!); pokemon.setTempAbility(this.target.getAbility());
setAbilityRevealed(this.target);
pokemon.updateInfo(); pokemon.updateInfo();
} }
} }
@ -3576,8 +3579,8 @@ export class ProtectStatAbAttr extends PreStatStageChangeAbAttr {
this.protectedStat = protectedStat; this.protectedStat = protectedStat;
} }
override canApply({ stat }: PreStatStageChangeAbAttrParams): boolean { override canApply({ stat, cancelled }: PreStatStageChangeAbAttrParams): boolean {
return isNullOrUndefined(this.protectedStat) || stat === this.protectedStat; return !cancelled.value && (isNullOrUndefined(this.protectedStat) || stat === this.protectedStat);
} }
/** /**
@ -3668,8 +3671,11 @@ export class PreSetStatusEffectImmunityAbAttr extends PreSetStatusAbAttr {
this.immuneEffects = immuneEffects; this.immuneEffects = immuneEffects;
} }
override canApply({ effect }: PreSetStatusAbAttrParams): boolean { override canApply({ effect, cancelled }: PreSetStatusAbAttrParams): boolean {
return (this.immuneEffects.length === 0 && effect !== StatusEffect.FAINT) || this.immuneEffects.includes(effect); return (
!cancelled.value &&
((this.immuneEffects.length === 0 && effect !== StatusEffect.FAINT) || this.immuneEffects.includes(effect))
);
} }
/** /**
@ -3719,7 +3725,8 @@ export interface UserFieldStatusEffectImmunityAbAttrParams extends AbAttrBasePar
/** /**
* Provides immunity to status effects to the user's field. * Provides immunity to status effects to the user's field.
*/ */
export class UserFieldStatusEffectImmunityAbAttr extends AbAttr { export class UserFieldStatusEffectImmunityAbAttr extends CancelInteractionAbAttr {
private declare readonly _: never;
protected immuneEffects: StatusEffect[]; protected immuneEffects: StatusEffect[];
/** /**
@ -3739,12 +3746,8 @@ export class UserFieldStatusEffectImmunityAbAttr extends AbAttr {
); );
} }
/** // declare here to allow typescript to allow us to override `canApply` method without adjusting params
* Set the `cancelled` value to true, indicating that the status effect is prevented. declare apply: (params: UserFieldStatusEffectImmunityAbAttrParams) => void;
*/
override apply({ cancelled }: UserFieldStatusEffectImmunityAbAttrParams): void {
cancelled.value = true;
}
} }
/** /**
@ -3775,14 +3778,7 @@ export class ConditionalUserFieldStatusEffectImmunityAbAttr extends UserFieldSta
* @returns Whether the ability can be applied to cancel the status effect. * @returns Whether the ability can be applied to cancel the status effect.
*/ */
override canApply(params: UserFieldStatusEffectImmunityAbAttrParams): boolean { override canApply(params: UserFieldStatusEffectImmunityAbAttrParams): boolean {
return this.condition(params.target, params.source) && super.canApply(params); return !params.cancelled.value && this.condition(params.target, params.source) && super.canApply(params);
}
/**
* Set the `cancelled` value to true, indicating that the status effect is prevented.
*/
override apply({ cancelled }: UserFieldStatusEffectImmunityAbAttrParams): void {
cancelled.value = true;
} }
} }
@ -4018,20 +4014,16 @@ export class ConditionalCritAbAttr extends AbAttr {
} }
} }
export class BlockNonDirectDamageAbAttr extends AbAttr { export class BlockNonDirectDamageAbAttr extends CancelInteractionAbAttr {
constructor() { constructor() {
super(false); super(false);
} }
override apply({ cancelled }: AbAttrParamsWithCancel): void {
cancelled.value = true;
}
} }
/** /**
* This attribute will block any status damage that you put in the parameter. * This attribute will block any status damage that you put in the parameter.
*/ */
export class BlockStatusDamageAbAttr extends AbAttr { export class BlockStatusDamageAbAttr extends CancelInteractionAbAttr {
private effects: StatusEffect[]; private effects: StatusEffect[];
/** /**
@ -4043,20 +4035,12 @@ export class BlockStatusDamageAbAttr extends AbAttr {
this.effects = effects; this.effects = effects;
} }
override canApply({ pokemon }: AbAttrParamsWithCancel): boolean { override canApply({ pokemon, cancelled }: AbAttrParamsWithCancel): boolean {
return !!pokemon.status?.effect && this.effects.includes(pokemon.status.effect); return !cancelled.value && !!pokemon.status?.effect && this.effects.includes(pokemon.status.effect);
}
override apply({ cancelled }: AbAttrParamsWithCancel): void {
cancelled.value = true;
} }
} }
export class BlockOneHitKOAbAttr extends AbAttr { export class BlockOneHitKOAbAttr extends CancelInteractionAbAttr {}
override apply({ cancelled }: AbAttrParamsWithCancel): void {
cancelled.value = true;
}
}
export interface ChangeMovePriorityAbAttrParams extends AbAttrBaseParams { export interface ChangeMovePriorityAbAttrParams extends AbAttrBaseParams {
/** The move being used */ /** The move being used */
@ -4130,8 +4114,8 @@ export class BlockWeatherDamageAttr extends PreWeatherDamageAbAttr {
this.weatherTypes = weatherTypes; this.weatherTypes = weatherTypes;
} }
override canApply({ weather }: PreWeatherEffectAbAttrParams): boolean { override canApply({ weather, cancelled }: PreWeatherEffectAbAttrParams): boolean {
if (!weather) { if (!weather || cancelled.value) {
return false; return false;
} }
const weatherType = weather.weatherType; const weatherType = weather.weatherType;
@ -4152,8 +4136,8 @@ export class SuppressWeatherEffectAbAttr extends PreWeatherEffectAbAttr {
this.affectsImmutable = affectsImmutable; this.affectsImmutable = affectsImmutable;
} }
override canApply({ weather }: PreWeatherEffectAbAttrParams): boolean { override canApply({ weather, cancelled }: PreWeatherEffectAbAttrParams): boolean {
if (!weather) { if (!weather || cancelled.value) {
return false; return false;
} }
return this.affectsImmutable || weather.isImmutable(); return this.affectsImmutable || weather.isImmutable();
@ -5150,15 +5134,11 @@ export class StatStageChangeCopyAbAttr extends AbAttr {
} }
} }
export class BypassBurnDamageReductionAbAttr extends AbAttr { export class BypassBurnDamageReductionAbAttr extends CancelInteractionAbAttr {
private declare readonly _: never; private declare readonly _: never;
constructor() { constructor() {
super(false); super(false);
} }
override apply({ cancelled }: AbAttrParamsWithCancel): void {
cancelled.value = true;
}
} }
export interface ReduceBurnDamageAbAttrParams extends AbAttrBaseParams { export interface ReduceBurnDamageAbAttrParams extends AbAttrBaseParams {
@ -5198,14 +5178,7 @@ export class DoubleBerryEffectAbAttr extends AbAttr {
* Attribute to prevent opposing berry use while on the field. * Attribute to prevent opposing berry use while on the field.
* Used by {@linkcode AbilityId.UNNERVE}, {@linkcode AbilityId.AS_ONE_GLASTRIER} and {@linkcode AbilityId.AS_ONE_SPECTRIER} * Used by {@linkcode AbilityId.UNNERVE}, {@linkcode AbilityId.AS_ONE_GLASTRIER} and {@linkcode AbilityId.AS_ONE_SPECTRIER}
*/ */
export class PreventBerryUseAbAttr extends AbAttr { export class PreventBerryUseAbAttr extends CancelInteractionAbAttr {}
/**
* Prevent use of opposing berries.
*/
override apply({ cancelled }: AbAttrParamsWithCancel): void {
cancelled.value = true;
}
}
/** /**
* A Pokemon with this ability heals by a percentage of their maximum hp after eating a berry * A Pokemon with this ability heals by a percentage of their maximum hp after eating a berry
@ -5663,11 +5636,7 @@ export class IncreasePpAbAttr extends AbAttr {
} }
/** @sealed */ /** @sealed */
export class ForceSwitchOutImmunityAbAttr extends AbAttr { export class ForceSwitchOutImmunityAbAttr extends CancelInteractionAbAttr {}
override apply({ cancelled }: AbAttrParamsWithCancel): void {
cancelled.value = true;
}
}
export interface ReduceBerryUseThresholdAbAttrParams extends AbAttrBaseParams { export interface ReduceBerryUseThresholdAbAttrParams extends AbAttrBaseParams {
/** Holds the hp ratio for the berry to proc, which may be modified by ability application */ /** Holds the hp ratio for the berry to proc, which may be modified by ability application */
@ -5746,8 +5715,8 @@ export class MoveAbilityBypassAbAttr extends AbAttr {
this.moveIgnoreFunc = moveIgnoreFunc || ((_pokemon, _move) => true); this.moveIgnoreFunc = moveIgnoreFunc || ((_pokemon, _move) => true);
} }
override canApply({ pokemon, move }: MoveAbilityBypassAbAttrParams): boolean { override canApply({ pokemon, move, cancelled }: MoveAbilityBypassAbAttrParams): boolean {
return this.moveIgnoreFunc(pokemon, move); return !cancelled.value && this.moveIgnoreFunc(pokemon, move);
} }
override apply({ cancelled }: MoveAbilityBypassAbAttrParams): void { override apply({ cancelled }: MoveAbilityBypassAbAttrParams): void {
@ -5841,8 +5810,8 @@ export class IgnoreTypeImmunityAbAttr extends AbAttr {
this.allowedMoveTypes = allowedMoveTypes; this.allowedMoveTypes = allowedMoveTypes;
} }
override canApply({ moveType, defenderType }: IgnoreTypeImmunityAbAttrParams): boolean { override canApply({ moveType, defenderType, cancelled }: IgnoreTypeImmunityAbAttrParams): boolean {
return this.defenderType === defenderType && this.allowedMoveTypes.includes(moveType); return !cancelled.value && this.defenderType === defenderType && this.allowedMoveTypes.includes(moveType);
} }
override apply({ cancelled }: IgnoreTypeImmunityAbAttrParams): void { override apply({ cancelled }: IgnoreTypeImmunityAbAttrParams): void {

View File

@ -937,7 +937,7 @@ class StealthRockTag extends DamagingTrapTag {
protected override getTriggerMessage(pokemon: Pokemon): string { protected override getTriggerMessage(pokemon: Pokemon): string {
return i18next.t("arenaTag:stealthRockActivateTrap", { return i18next.t("arenaTag:stealthRockActivateTrap", {
pokemonName: getPokemonNameWithAffix(pokemon), pokemonNameWithAffix: getPokemonNameWithAffix(pokemon),
}); });
} }

View File

@ -187,7 +187,7 @@ export const speciesEggMoves = {
[SpeciesId.WYNAUT]: [ MoveId.RECOVER, MoveId.SHED_TAIL, MoveId.TAUNT, MoveId.COMEUPPANCE ], [SpeciesId.WYNAUT]: [ MoveId.RECOVER, MoveId.SHED_TAIL, MoveId.TAUNT, MoveId.COMEUPPANCE ],
[SpeciesId.SNORUNT]: [ MoveId.SPARKLY_SWIRL, MoveId.NASTY_PLOT, MoveId.EARTH_POWER, MoveId.BLOOD_MOON ], [SpeciesId.SNORUNT]: [ MoveId.SPARKLY_SWIRL, MoveId.NASTY_PLOT, MoveId.EARTH_POWER, MoveId.BLOOD_MOON ],
[SpeciesId.SPHEAL]: [ MoveId.FLIP_TURN, MoveId.FREEZE_DRY, MoveId.SLACK_OFF, MoveId.STEAM_ERUPTION ], [SpeciesId.SPHEAL]: [ MoveId.FLIP_TURN, MoveId.FREEZE_DRY, MoveId.SLACK_OFF, MoveId.STEAM_ERUPTION ],
[SpeciesId.CLAMPERL]: [ MoveId.SHELL_SIDE_ARM, MoveId.BOUNCY_BUBBLE, MoveId.FREEZE_DRY, MoveId.STEAM_ERUPTION ], [SpeciesId.CLAMPERL]: [ MoveId.SHELL_SIDE_ARM, MoveId.SNIPE_SHOT, MoveId.GIGA_DRAIN, MoveId.BOUNCY_BUBBLE ],
[SpeciesId.RELICANTH]: [ MoveId.DRAGON_DANCE, MoveId.SHORE_UP, MoveId.WAVE_CRASH, MoveId.DIAMOND_STORM ], [SpeciesId.RELICANTH]: [ MoveId.DRAGON_DANCE, MoveId.SHORE_UP, MoveId.WAVE_CRASH, MoveId.DIAMOND_STORM ],
[SpeciesId.LUVDISC]: [ MoveId.BATON_PASS, MoveId.HEART_SWAP, MoveId.GLITZY_GLOW, MoveId.REVIVAL_BLESSING ], [SpeciesId.LUVDISC]: [ MoveId.BATON_PASS, MoveId.HEART_SWAP, MoveId.GLITZY_GLOW, MoveId.REVIVAL_BLESSING ],
[SpeciesId.BAGON]: [ MoveId.HEADLONG_RUSH, MoveId.FIRE_LASH, MoveId.DRAGON_DANCE, MoveId.DRAGON_DARTS ], [SpeciesId.BAGON]: [ MoveId.HEADLONG_RUSH, MoveId.FIRE_LASH, MoveId.DRAGON_DANCE, MoveId.DRAGON_DARTS ],

View File

@ -402,7 +402,7 @@ export const starterPassiveAbilities: StarterPassiveAbilities = {
[SpeciesId.SPHEAL]: { 0: AbilityId.UNAWARE }, [SpeciesId.SPHEAL]: { 0: AbilityId.UNAWARE },
[SpeciesId.SEALEO]: { 0: AbilityId.UNAWARE }, [SpeciesId.SEALEO]: { 0: AbilityId.UNAWARE },
[SpeciesId.WALREIN]: { 0: AbilityId.UNAWARE }, [SpeciesId.WALREIN]: { 0: AbilityId.UNAWARE },
[SpeciesId.CLAMPERL]: { 0: AbilityId.DAUNTLESS_SHIELD }, [SpeciesId.CLAMPERL]: { 0: AbilityId.OVERCOAT },
[SpeciesId.GOREBYSS]: { 0: AbilityId.ARENA_TRAP }, [SpeciesId.GOREBYSS]: { 0: AbilityId.ARENA_TRAP },
[SpeciesId.HUNTAIL]: { 0: AbilityId.ARENA_TRAP }, [SpeciesId.HUNTAIL]: { 0: AbilityId.ARENA_TRAP },
[SpeciesId.RELICANTH]: { 0: AbilityId.PRIMORDIAL_SEA }, [SpeciesId.RELICANTH]: { 0: AbilityId.PRIMORDIAL_SEA },

View File

@ -1,3 +1,4 @@
import { defaultStarterSpecies } from "#app/constants";
import { globalScene } from "#app/global-scene"; import { globalScene } from "#app/global-scene";
import { speciesStarterCosts } from "#balance/starters"; import { speciesStarterCosts } from "#balance/starters";
import { allMoves } from "#data/data-lists"; import { allMoves } from "#data/data-lists";
@ -76,7 +77,8 @@ export enum EvolutionItem {
LEADERS_CREST LEADERS_CREST
} }
type TyrogueMove = MoveId.LOW_SWEEP | MoveId.MACH_PUNCH | MoveId.RAPID_SPIN; const tyrogueMoves = [MoveId.LOW_SWEEP, MoveId.MACH_PUNCH, MoveId.RAPID_SPIN] as const;
type TyrogueMove = typeof tyrogueMoves[number];
/** /**
* Pokemon Evolution tuple type consisting of: * Pokemon Evolution tuple type consisting of:
@ -191,7 +193,7 @@ export class SpeciesEvolutionCondition {
case EvoCondKey.WEATHER: case EvoCondKey.WEATHER:
return cond.weather.includes(globalScene.arena.getWeatherType()); return cond.weather.includes(globalScene.arena.getWeatherType());
case EvoCondKey.TYROGUE: case EvoCondKey.TYROGUE:
return pokemon.getMoveset(true).find(m => m.moveId as TyrogueMove)?.moveId === cond.move; return pokemon.getMoveset(true).find(m => (tyrogueMoves as readonly MoveId[]) .includes(m.moveId))?.moveId === cond.move;
case EvoCondKey.NATURE: case EvoCondKey.NATURE:
return cond.nature.includes(pokemon.getNature()); return cond.nature.includes(pokemon.getNature());
case EvoCondKey.RANDOM_FORM: { case EvoCondKey.RANDOM_FORM: {
@ -1883,6 +1885,15 @@ export function initPokemonPrevolutions(): void {
// TODO: This may cause funny business for double starters such as Pichu/Pikachu // TODO: This may cause funny business for double starters such as Pichu/Pikachu
export const pokemonStarters: PokemonPrevolutions = {}; export const pokemonStarters: PokemonPrevolutions = {};
/**
* The default species and all their evolutions
*/
export const defaultStarterSpeciesAndEvolutions: SpeciesId[] = defaultStarterSpecies.flatMap(id => {
const stage2ids = pokemonEvolutions[id]?.map(e => e.speciesId) ?? [];
const stage3ids = stage2ids.flatMap(s2id => pokemonEvolutions[s2id]?.map(e => e.speciesId) ?? []);
return [id, ...stage2ids, ...stage3ids];
});
export function initPokemonStarters(): void { export function initPokemonStarters(): void {
const starterKeys = Object.keys(pokemonPrevolutions); const starterKeys = Object.keys(pokemonPrevolutions);
starterKeys.forEach(pk => { starterKeys.forEach(pk => {

View File

@ -68875,27 +68875,27 @@ interface TmPoolTiers {
export const tmPoolTiers: TmPoolTiers = { export const tmPoolTiers: TmPoolTiers = {
[MoveId.MEGA_PUNCH]: ModifierTier.GREAT, [MoveId.MEGA_PUNCH]: ModifierTier.GREAT,
[MoveId.PAY_DAY]: ModifierTier.ULTRA, [MoveId.PAY_DAY]: ModifierTier.COMMON,
[MoveId.FIRE_PUNCH]: ModifierTier.GREAT, [MoveId.FIRE_PUNCH]: ModifierTier.GREAT,
[MoveId.ICE_PUNCH]: ModifierTier.GREAT, [MoveId.ICE_PUNCH]: ModifierTier.GREAT,
[MoveId.THUNDER_PUNCH]: ModifierTier.GREAT, [MoveId.THUNDER_PUNCH]: ModifierTier.GREAT,
[MoveId.SWORDS_DANCE]: ModifierTier.COMMON, [MoveId.SWORDS_DANCE]: ModifierTier.GREAT,
[MoveId.CUT]: ModifierTier.COMMON, [MoveId.CUT]: ModifierTier.COMMON,
[MoveId.FLY]: ModifierTier.COMMON, [MoveId.FLY]: ModifierTier.GREAT,
[MoveId.MEGA_KICK]: ModifierTier.GREAT, [MoveId.MEGA_KICK]: ModifierTier.GREAT,
[MoveId.BODY_SLAM]: ModifierTier.GREAT, [MoveId.BODY_SLAM]: ModifierTier.GREAT,
[MoveId.TAKE_DOWN]: ModifierTier.GREAT, [MoveId.TAKE_DOWN]: ModifierTier.GREAT,
[MoveId.DOUBLE_EDGE]: ModifierTier.ULTRA, [MoveId.DOUBLE_EDGE]: ModifierTier.ULTRA,
[MoveId.PIN_MISSILE]: ModifierTier.COMMON, [MoveId.PIN_MISSILE]: ModifierTier.GREAT,
[MoveId.ROAR]: ModifierTier.COMMON, [MoveId.ROAR]: ModifierTier.COMMON,
[MoveId.FLAMETHROWER]: ModifierTier.ULTRA, [MoveId.FLAMETHROWER]: ModifierTier.ULTRA,
[MoveId.HYDRO_PUMP]: ModifierTier.ULTRA, [MoveId.HYDRO_PUMP]: ModifierTier.ULTRA,
[MoveId.SURF]: ModifierTier.ULTRA, [MoveId.SURF]: ModifierTier.ULTRA,
[MoveId.ICE_BEAM]: ModifierTier.ULTRA, [MoveId.ICE_BEAM]: ModifierTier.ULTRA,
[MoveId.BLIZZARD]: ModifierTier.ULTRA, [MoveId.BLIZZARD]: ModifierTier.ULTRA,
[MoveId.PSYBEAM]: ModifierTier.GREAT, [MoveId.PSYBEAM]: ModifierTier.COMMON,
[MoveId.HYPER_BEAM]: ModifierTier.ULTRA, [MoveId.HYPER_BEAM]: ModifierTier.ULTRA,
[MoveId.LOW_KICK]: ModifierTier.COMMON, [MoveId.LOW_KICK]: ModifierTier.GREAT,
[MoveId.COUNTER]: ModifierTier.COMMON, [MoveId.COUNTER]: ModifierTier.COMMON,
[MoveId.STRENGTH]: ModifierTier.GREAT, [MoveId.STRENGTH]: ModifierTier.GREAT,
[MoveId.SOLAR_BEAM]: ModifierTier.ULTRA, [MoveId.SOLAR_BEAM]: ModifierTier.ULTRA,
@ -68907,9 +68907,9 @@ export const tmPoolTiers: TmPoolTiers = {
[MoveId.DIG]: ModifierTier.GREAT, [MoveId.DIG]: ModifierTier.GREAT,
[MoveId.TOXIC]: ModifierTier.GREAT, [MoveId.TOXIC]: ModifierTier.GREAT,
[MoveId.PSYCHIC]: ModifierTier.ULTRA, [MoveId.PSYCHIC]: ModifierTier.ULTRA,
[MoveId.AGILITY]: ModifierTier.COMMON, [MoveId.AGILITY]: ModifierTier.GREAT,
[MoveId.NIGHT_SHADE]: ModifierTier.COMMON, [MoveId.NIGHT_SHADE]: ModifierTier.COMMON,
[MoveId.SCREECH]: ModifierTier.COMMON, [MoveId.SCREECH]: ModifierTier.GREAT,
[MoveId.DOUBLE_TEAM]: ModifierTier.COMMON, [MoveId.DOUBLE_TEAM]: ModifierTier.COMMON,
[MoveId.CONFUSE_RAY]: ModifierTier.COMMON, [MoveId.CONFUSE_RAY]: ModifierTier.COMMON,
[MoveId.LIGHT_SCREEN]: ModifierTier.COMMON, [MoveId.LIGHT_SCREEN]: ModifierTier.COMMON,
@ -68921,7 +68921,7 @@ export const tmPoolTiers: TmPoolTiers = {
[MoveId.FIRE_BLAST]: ModifierTier.ULTRA, [MoveId.FIRE_BLAST]: ModifierTier.ULTRA,
[MoveId.WATERFALL]: ModifierTier.GREAT, [MoveId.WATERFALL]: ModifierTier.GREAT,
[MoveId.SWIFT]: ModifierTier.COMMON, [MoveId.SWIFT]: ModifierTier.COMMON,
[MoveId.AMNESIA]: ModifierTier.COMMON, [MoveId.AMNESIA]: ModifierTier.GREAT,
[MoveId.DREAM_EATER]: ModifierTier.GREAT, [MoveId.DREAM_EATER]: ModifierTier.GREAT,
[MoveId.LEECH_LIFE]: ModifierTier.ULTRA, [MoveId.LEECH_LIFE]: ModifierTier.ULTRA,
[MoveId.FLASH]: ModifierTier.COMMON, [MoveId.FLASH]: ModifierTier.COMMON,
@ -68933,11 +68933,11 @@ export const tmPoolTiers: TmPoolTiers = {
[MoveId.SUBSTITUTE]: ModifierTier.COMMON, [MoveId.SUBSTITUTE]: ModifierTier.COMMON,
[MoveId.THIEF]: ModifierTier.GREAT, [MoveId.THIEF]: ModifierTier.GREAT,
[MoveId.SNORE]: ModifierTier.COMMON, [MoveId.SNORE]: ModifierTier.COMMON,
[MoveId.CURSE]: ModifierTier.COMMON, [MoveId.CURSE]: ModifierTier.GREAT,
[MoveId.REVERSAL]: ModifierTier.COMMON, [MoveId.REVERSAL]: ModifierTier.COMMON,
[MoveId.SPITE]: ModifierTier.COMMON, [MoveId.SPITE]: ModifierTier.COMMON,
[MoveId.PROTECT]: ModifierTier.COMMON, [MoveId.PROTECT]: ModifierTier.COMMON,
[MoveId.SCARY_FACE]: ModifierTier.COMMON, [MoveId.SCARY_FACE]: ModifierTier.GREAT,
[MoveId.SLUDGE_BOMB]: ModifierTier.GREAT, [MoveId.SLUDGE_BOMB]: ModifierTier.GREAT,
[MoveId.MUD_SLAP]: ModifierTier.COMMON, [MoveId.MUD_SLAP]: ModifierTier.COMMON,
[MoveId.SPIKES]: ModifierTier.COMMON, [MoveId.SPIKES]: ModifierTier.COMMON,
@ -68979,8 +68979,8 @@ export const tmPoolTiers: TmPoolTiers = {
[MoveId.TORMENT]: ModifierTier.COMMON, [MoveId.TORMENT]: ModifierTier.COMMON,
[MoveId.WILL_O_WISP]: ModifierTier.COMMON, [MoveId.WILL_O_WISP]: ModifierTier.COMMON,
[MoveId.FACADE]: ModifierTier.GREAT, [MoveId.FACADE]: ModifierTier.GREAT,
[MoveId.FOCUS_PUNCH]: ModifierTier.COMMON, [MoveId.FOCUS_PUNCH]: ModifierTier.GREAT,
[MoveId.NATURE_POWER]: ModifierTier.COMMON, [MoveId.NATURE_POWER]: ModifierTier.GREAT,
[MoveId.CHARGE]: ModifierTier.COMMON, [MoveId.CHARGE]: ModifierTier.COMMON,
[MoveId.TAUNT]: ModifierTier.COMMON, [MoveId.TAUNT]: ModifierTier.COMMON,
[MoveId.HELPING_HAND]: ModifierTier.COMMON, [MoveId.HELPING_HAND]: ModifierTier.COMMON,
@ -68993,7 +68993,7 @@ export const tmPoolTiers: TmPoolTiers = {
[MoveId.ENDEAVOR]: ModifierTier.COMMON, [MoveId.ENDEAVOR]: ModifierTier.COMMON,
[MoveId.SKILL_SWAP]: ModifierTier.COMMON, [MoveId.SKILL_SWAP]: ModifierTier.COMMON,
[MoveId.IMPRISON]: ModifierTier.COMMON, [MoveId.IMPRISON]: ModifierTier.COMMON,
[MoveId.SECRET_POWER]: ModifierTier.COMMON, [MoveId.SECRET_POWER]: ModifierTier.GREAT,
[MoveId.DIVE]: ModifierTier.GREAT, [MoveId.DIVE]: ModifierTier.GREAT,
[MoveId.FEATHER_DANCE]: ModifierTier.COMMON, [MoveId.FEATHER_DANCE]: ModifierTier.COMMON,
[MoveId.BLAZE_KICK]: ModifierTier.GREAT, [MoveId.BLAZE_KICK]: ModifierTier.GREAT,
@ -69001,12 +69001,12 @@ export const tmPoolTiers: TmPoolTiers = {
[MoveId.BLAST_BURN]: ModifierTier.ULTRA, [MoveId.BLAST_BURN]: ModifierTier.ULTRA,
[MoveId.HYDRO_CANNON]: ModifierTier.ULTRA, [MoveId.HYDRO_CANNON]: ModifierTier.ULTRA,
[MoveId.WEATHER_BALL]: ModifierTier.COMMON, [MoveId.WEATHER_BALL]: ModifierTier.COMMON,
[MoveId.FAKE_TEARS]: ModifierTier.COMMON, [MoveId.FAKE_TEARS]: ModifierTier.GREAT,
[MoveId.AIR_CUTTER]: ModifierTier.GREAT, [MoveId.AIR_CUTTER]: ModifierTier.GREAT,
[MoveId.OVERHEAT]: ModifierTier.ULTRA, [MoveId.OVERHEAT]: ModifierTier.ULTRA,
[MoveId.ROCK_TOMB]: ModifierTier.GREAT, [MoveId.ROCK_TOMB]: ModifierTier.GREAT,
[MoveId.METAL_SOUND]: ModifierTier.COMMON, [MoveId.METAL_SOUND]: ModifierTier.GREAT,
[MoveId.COSMIC_POWER]: ModifierTier.COMMON, [MoveId.COSMIC_POWER]: ModifierTier.GREAT,
[MoveId.SIGNAL_BEAM]: ModifierTier.GREAT, [MoveId.SIGNAL_BEAM]: ModifierTier.GREAT,
[MoveId.SAND_TOMB]: ModifierTier.COMMON, [MoveId.SAND_TOMB]: ModifierTier.COMMON,
[MoveId.MUDDY_WATER]: ModifierTier.GREAT, [MoveId.MUDDY_WATER]: ModifierTier.GREAT,
@ -69016,10 +69016,10 @@ export const tmPoolTiers: TmPoolTiers = {
[MoveId.IRON_DEFENSE]: ModifierTier.GREAT, [MoveId.IRON_DEFENSE]: ModifierTier.GREAT,
[MoveId.DRAGON_CLAW]: ModifierTier.ULTRA, [MoveId.DRAGON_CLAW]: ModifierTier.ULTRA,
[MoveId.FRENZY_PLANT]: ModifierTier.ULTRA, [MoveId.FRENZY_PLANT]: ModifierTier.ULTRA,
[MoveId.BULK_UP]: ModifierTier.COMMON, [MoveId.BULK_UP]: ModifierTier.GREAT,
[MoveId.BOUNCE]: ModifierTier.GREAT, [MoveId.BOUNCE]: ModifierTier.GREAT,
[MoveId.MUD_SHOT]: ModifierTier.GREAT, [MoveId.MUD_SHOT]: ModifierTier.GREAT,
[MoveId.POISON_TAIL]: ModifierTier.GREAT, [MoveId.POISON_TAIL]: ModifierTier.COMMON,
[MoveId.COVET]: ModifierTier.GREAT, [MoveId.COVET]: ModifierTier.GREAT,
[MoveId.MAGICAL_LEAF]: ModifierTier.GREAT, [MoveId.MAGICAL_LEAF]: ModifierTier.GREAT,
[MoveId.CALM_MIND]: ModifierTier.GREAT, [MoveId.CALM_MIND]: ModifierTier.GREAT,
@ -69047,7 +69047,7 @@ export const tmPoolTiers: TmPoolTiers = {
[MoveId.TOXIC_SPIKES]: ModifierTier.GREAT, [MoveId.TOXIC_SPIKES]: ModifierTier.GREAT,
[MoveId.FLARE_BLITZ]: ModifierTier.ULTRA, [MoveId.FLARE_BLITZ]: ModifierTier.ULTRA,
[MoveId.AURA_SPHERE]: ModifierTier.GREAT, [MoveId.AURA_SPHERE]: ModifierTier.GREAT,
[MoveId.ROCK_POLISH]: ModifierTier.COMMON, [MoveId.ROCK_POLISH]: ModifierTier.GREAT,
[MoveId.POISON_JAB]: ModifierTier.GREAT, [MoveId.POISON_JAB]: ModifierTier.GREAT,
[MoveId.DARK_PULSE]: ModifierTier.GREAT, [MoveId.DARK_PULSE]: ModifierTier.GREAT,
[MoveId.AQUA_TAIL]: ModifierTier.GREAT, [MoveId.AQUA_TAIL]: ModifierTier.GREAT,
@ -69063,8 +69063,8 @@ export const tmPoolTiers: TmPoolTiers = {
[MoveId.ENERGY_BALL]: ModifierTier.GREAT, [MoveId.ENERGY_BALL]: ModifierTier.GREAT,
[MoveId.BRAVE_BIRD]: ModifierTier.ULTRA, [MoveId.BRAVE_BIRD]: ModifierTier.ULTRA,
[MoveId.EARTH_POWER]: ModifierTier.ULTRA, [MoveId.EARTH_POWER]: ModifierTier.ULTRA,
[MoveId.GIGA_IMPACT]: ModifierTier.GREAT, [MoveId.GIGA_IMPACT]: ModifierTier.ULTRA,
[MoveId.NASTY_PLOT]: ModifierTier.COMMON, [MoveId.NASTY_PLOT]: ModifierTier.GREAT,
[MoveId.AVALANCHE]: ModifierTier.GREAT, [MoveId.AVALANCHE]: ModifierTier.GREAT,
[MoveId.SHADOW_CLAW]: ModifierTier.GREAT, [MoveId.SHADOW_CLAW]: ModifierTier.GREAT,
[MoveId.THUNDER_FANG]: ModifierTier.GREAT, [MoveId.THUNDER_FANG]: ModifierTier.GREAT,
@ -69084,7 +69084,7 @@ export const tmPoolTiers: TmPoolTiers = {
[MoveId.IRON_HEAD]: ModifierTier.GREAT, [MoveId.IRON_HEAD]: ModifierTier.GREAT,
[MoveId.STONE_EDGE]: ModifierTier.ULTRA, [MoveId.STONE_EDGE]: ModifierTier.ULTRA,
[MoveId.STEALTH_ROCK]: ModifierTier.COMMON, [MoveId.STEALTH_ROCK]: ModifierTier.COMMON,
[MoveId.GRASS_KNOT]: ModifierTier.ULTRA, [MoveId.GRASS_KNOT]: ModifierTier.GREAT,
[MoveId.BUG_BITE]: ModifierTier.GREAT, [MoveId.BUG_BITE]: ModifierTier.GREAT,
[MoveId.CHARGE_BEAM]: ModifierTier.GREAT, [MoveId.CHARGE_BEAM]: ModifierTier.GREAT,
[MoveId.HONE_CLAWS]: ModifierTier.COMMON, [MoveId.HONE_CLAWS]: ModifierTier.COMMON,
@ -69102,7 +69102,7 @@ export const tmPoolTiers: TmPoolTiers = {
[MoveId.FOUL_PLAY]: ModifierTier.ULTRA, [MoveId.FOUL_PLAY]: ModifierTier.ULTRA,
[MoveId.ROUND]: ModifierTier.COMMON, [MoveId.ROUND]: ModifierTier.COMMON,
[MoveId.ECHOED_VOICE]: ModifierTier.COMMON, [MoveId.ECHOED_VOICE]: ModifierTier.COMMON,
[MoveId.STORED_POWER]: ModifierTier.COMMON, [MoveId.STORED_POWER]: ModifierTier.GREAT,
[MoveId.ALLY_SWITCH]: ModifierTier.COMMON, [MoveId.ALLY_SWITCH]: ModifierTier.COMMON,
[MoveId.SCALD]: ModifierTier.GREAT, [MoveId.SCALD]: ModifierTier.GREAT,
[MoveId.HEX]: ModifierTier.GREAT, [MoveId.HEX]: ModifierTier.GREAT,
@ -69130,7 +69130,7 @@ export const tmPoolTiers: TmPoolTiers = {
[MoveId.SNARL]: ModifierTier.COMMON, [MoveId.SNARL]: ModifierTier.COMMON,
[MoveId.PHANTOM_FORCE]: ModifierTier.ULTRA, [MoveId.PHANTOM_FORCE]: ModifierTier.ULTRA,
[MoveId.PETAL_BLIZZARD]: ModifierTier.GREAT, [MoveId.PETAL_BLIZZARD]: ModifierTier.GREAT,
[MoveId.DISARMING_VOICE]: ModifierTier.GREAT, [MoveId.DISARMING_VOICE]: ModifierTier.COMMON,
[MoveId.DRAINING_KISS]: ModifierTier.GREAT, [MoveId.DRAINING_KISS]: ModifierTier.GREAT,
[MoveId.GRASSY_TERRAIN]: ModifierTier.COMMON, [MoveId.GRASSY_TERRAIN]: ModifierTier.COMMON,
[MoveId.MISTY_TERRAIN]: ModifierTier.COMMON, [MoveId.MISTY_TERRAIN]: ModifierTier.COMMON,
@ -69161,12 +69161,12 @@ export const tmPoolTiers: TmPoolTiers = {
[MoveId.BREAKING_SWIPE]: ModifierTier.GREAT, [MoveId.BREAKING_SWIPE]: ModifierTier.GREAT,
[MoveId.STEEL_BEAM]: ModifierTier.ULTRA, [MoveId.STEEL_BEAM]: ModifierTier.ULTRA,
[MoveId.EXPANDING_FORCE]: ModifierTier.GREAT, [MoveId.EXPANDING_FORCE]: ModifierTier.GREAT,
[MoveId.STEEL_ROLLER]: ModifierTier.COMMON, [MoveId.STEEL_ROLLER]: ModifierTier.GREAT,
[MoveId.SCALE_SHOT]: ModifierTier.ULTRA, [MoveId.SCALE_SHOT]: ModifierTier.ULTRA,
[MoveId.METEOR_BEAM]: ModifierTier.GREAT, [MoveId.METEOR_BEAM]: ModifierTier.GREAT,
[MoveId.MISTY_EXPLOSION]: ModifierTier.COMMON, [MoveId.MISTY_EXPLOSION]: ModifierTier.GREAT,
[MoveId.GRASSY_GLIDE]: ModifierTier.COMMON, [MoveId.GRASSY_GLIDE]: ModifierTier.COMMON,
[MoveId.RISING_VOLTAGE]: ModifierTier.COMMON, [MoveId.RISING_VOLTAGE]: ModifierTier.GREAT,
[MoveId.TERRAIN_PULSE]: ModifierTier.COMMON, [MoveId.TERRAIN_PULSE]: ModifierTier.COMMON,
[MoveId.SKITTER_SMACK]: ModifierTier.GREAT, [MoveId.SKITTER_SMACK]: ModifierTier.GREAT,
[MoveId.BURNING_JEALOUSY]: ModifierTier.GREAT, [MoveId.BURNING_JEALOUSY]: ModifierTier.GREAT,
@ -69175,20 +69175,20 @@ export const tmPoolTiers: TmPoolTiers = {
[MoveId.CORROSIVE_GAS]: ModifierTier.COMMON, [MoveId.CORROSIVE_GAS]: ModifierTier.COMMON,
[MoveId.COACHING]: ModifierTier.COMMON, [MoveId.COACHING]: ModifierTier.COMMON,
[MoveId.FLIP_TURN]: ModifierTier.COMMON, [MoveId.FLIP_TURN]: ModifierTier.COMMON,
[MoveId.TRIPLE_AXEL]: ModifierTier.COMMON, [MoveId.TRIPLE_AXEL]: ModifierTier.ULTRA,
[MoveId.DUAL_WINGBEAT]: ModifierTier.COMMON, [MoveId.DUAL_WINGBEAT]: ModifierTier.GREAT,
[MoveId.SCORCHING_SANDS]: ModifierTier.GREAT, [MoveId.SCORCHING_SANDS]: ModifierTier.GREAT,
[MoveId.TERA_BLAST]: ModifierTier.GREAT, [MoveId.TERA_BLAST]: ModifierTier.GREAT,
[MoveId.ICE_SPINNER]: ModifierTier.GREAT, [MoveId.ICE_SPINNER]: ModifierTier.GREAT,
[MoveId.SNOWSCAPE]: ModifierTier.COMMON, [MoveId.SNOWSCAPE]: ModifierTier.COMMON,
[MoveId.POUNCE]: ModifierTier.COMMON, [MoveId.POUNCE]: ModifierTier.COMMON,
[MoveId.TRAILBLAZE]: ModifierTier.COMMON, [MoveId.TRAILBLAZE]: ModifierTier.GREAT,
[MoveId.CHILLING_WATER]: ModifierTier.COMMON, [MoveId.CHILLING_WATER]: ModifierTier.COMMON,
[MoveId.HARD_PRESS]: ModifierTier.GREAT, [MoveId.HARD_PRESS]: ModifierTier.GREAT,
[MoveId.DRAGON_CHEER]: ModifierTier.COMMON, [MoveId.DRAGON_CHEER]: ModifierTier.COMMON,
[MoveId.ALLURING_VOICE]: ModifierTier.GREAT, [MoveId.ALLURING_VOICE]: ModifierTier.GREAT,
[MoveId.TEMPER_FLARE]: ModifierTier.GREAT, [MoveId.TEMPER_FLARE]: ModifierTier.GREAT,
[MoveId.SUPERCELL_SLAM]: ModifierTier.GREAT, [MoveId.SUPERCELL_SLAM]: ModifierTier.ULTRA,
[MoveId.PSYCHIC_NOISE]: ModifierTier.GREAT, [MoveId.PSYCHIC_NOISE]: ModifierTier.GREAT,
[MoveId.UPPER_HAND]: ModifierTier.COMMON, [MoveId.UPPER_HAND]: ModifierTier.COMMON,
}; };

View File

@ -291,9 +291,17 @@ class AnimTimedSoundEvent extends AnimTimedEvent {
} catch (err) { } catch (err) {
console.error(err); console.error(err);
} }
return Math.ceil((globalScene.sound.get(`battle_anims/${this.resourceName}`).totalDuration * 1000) / 33.33); const sound = globalScene.sound.get(`battle_anims/${this.resourceName}`);
if (!sound) {
return 0;
} }
return Math.ceil((battleAnim.user!.cry(soundConfig).totalDuration * 1000) / 33.33); // TODO: is the bang behind user correct? return Math.ceil((sound.totalDuration * 1000) / 33.33);
}
const cry = battleAnim.user!.cry(soundConfig); // TODO: is the bang behind user correct?
if (!cry) {
return 0;
}
return Math.ceil((cry.totalDuration * 1000) / 33.33);
} }
getEventType(): string { getEventType(): string {
@ -827,7 +835,7 @@ export abstract class BattleAnim {
// biome-ignore lint/complexity/noBannedTypes: callback is used liberally // biome-ignore lint/complexity/noBannedTypes: callback is used liberally
play(onSubstitute?: boolean, callback?: Function) { play(onSubstitute?: boolean, callback?: Function) {
const isOppAnim = this.isOppAnim(); const isOppAnim = this.isOppAnim();
const user = !isOppAnim ? this.user! : this.target!; // TODO: are those bangs correct? const user = !isOppAnim ? this.user! : this.target!; // TODO: These bangs are LITERALLY not correct at all
const target = !isOppAnim ? this.target! : this.user!; const target = !isOppAnim ? this.target! : this.user!;
if (!target?.isOnField() && !this.playRegardlessOfIssues) { if (!target?.isOnField() && !this.playRegardlessOfIssues) {

View File

@ -1058,8 +1058,7 @@ export class SeedTag extends SerializableBattlerTag {
// Check which opponent to restore HP to // Check which opponent to restore HP to
const source = pokemon.getOpponents().find(o => o.getBattlerIndex() === this.sourceIndex); const source = pokemon.getOpponents().find(o => o.getBattlerIndex() === this.sourceIndex);
if (!source) { if (!source) {
console.warn(`Failed to get source Pokemon for SeedTag lapse; id: ${this.sourceId}`); return true;
return false;
} }
const cancelled = new BooleanHolder(false); const cancelled = new BooleanHolder(false);

View File

@ -1,6 +1,6 @@
import type { FixedBattleConfig } from "#app/battle"; import type { FixedBattleConfig } from "#app/battle";
import { getRandomTrainerFunc } from "#app/battle"; import { getRandomTrainerFunc } from "#app/battle";
import { defaultStarterSpecies } from "#app/constants"; import { defaultStarterSpeciesAndEvolutions } from "#balance/pokemon-evolutions";
import { speciesStarterCosts } from "#balance/starters"; import { speciesStarterCosts } from "#balance/starters";
import type { PokemonSpecies } from "#data/pokemon-species"; import type { PokemonSpecies } from "#data/pokemon-species";
import { AbilityAttr } from "#enums/ability-attr"; import { AbilityAttr } from "#enums/ability-attr";
@ -27,7 +27,7 @@ import type { DexEntry } from "#types/dex-data";
import { type BooleanHolder, isBetween, type NumberHolder, randSeedItem } from "#utils/common"; import { type BooleanHolder, isBetween, type NumberHolder, randSeedItem } from "#utils/common";
import { deepCopy } from "#utils/data"; import { deepCopy } from "#utils/data";
import { getPokemonSpecies, getPokemonSpeciesForm } from "#utils/pokemon-utils"; import { getPokemonSpecies, getPokemonSpeciesForm } from "#utils/pokemon-utils";
import { toCamelCase, toSnakeCase } from "#utils/strings"; import { toCamelCase } from "#utils/strings";
import i18next from "i18next"; import i18next from "i18next";
/** A constant for the default max cost of the starting party before a run */ /** A constant for the default max cost of the starting party before a run */
@ -764,7 +764,7 @@ export class SingleTypeChallenge extends Challenge {
} }
getValue(overrideValue: number = this.value): string { getValue(overrideValue: number = this.value): string {
return toSnakeCase(PokemonType[overrideValue - 1]); return PokemonType[overrideValue - 1].toLowerCase();
} }
getDescription(overrideValue: number = this.value): string { getDescription(overrideValue: number = this.value): string {
@ -797,7 +797,7 @@ export class FreshStartChallenge extends Challenge {
} }
applyStarterChoice(pokemon: PokemonSpecies, valid: BooleanHolder): boolean { applyStarterChoice(pokemon: PokemonSpecies, valid: BooleanHolder): boolean {
if (this.value === 1 && !defaultStarterSpecies.includes(pokemon.speciesId)) { if (this.value === 1 && !defaultStarterSpeciesAndEvolutions.includes(pokemon.speciesId)) {
valid.value = false; valid.value = false;
return true; return true;
} }

View File

@ -2325,6 +2325,13 @@ export class HealOnAllyAttr extends HealAttr {
// Don't trigger if not targeting an ally // Don't trigger if not targeting an ally
return target === user.getAlly() && super.canApply(user, target, _move, _args); return target === user.getAlly() && super.canApply(user, target, _move, _args);
} }
override apply(user: Pokemon, target: Pokemon, _move: Move, _args: any[]): boolean {
if (user.isOpponent(target)) {
return false;
}
return super.apply(user, target, _move, _args);
}
} }
/** /**
@ -3270,7 +3277,6 @@ export class DelayedAttackAttr extends OverrideMoveEffectAttr {
) )
) )
user.pushMoveHistory({move: move.id, targets: [target.getBattlerIndex()], result: MoveResult.OTHER, useMode, turn: globalScene.currentBattle.turn})
user.pushMoveHistory({move: move.id, targets: [target.getBattlerIndex()], result: MoveResult.OTHER, useMode, turn: globalScene.currentBattle.turn}) user.pushMoveHistory({move: move.id, targets: [target.getBattlerIndex()], result: MoveResult.OTHER, useMode, turn: globalScene.currentBattle.turn})
// Queue up an attack on the given slot. // Queue up an attack on the given slot.
globalScene.arena.positionalTagManager.addTag<PositionalTagType.DELAYED_ATTACK>({ globalScene.arena.positionalTagManager.addTag<PositionalTagType.DELAYED_ATTACK>({
@ -6850,12 +6856,15 @@ export class CopyBiomeTypeAttr extends MoveEffectAttr {
} }
} }
/**
* Attribute to override the target's current types to the given type.
* Used by {@linkcode MoveId.SOAK} and {@linkcode MoveId.MAGIC_POWDER}.
*/
export class ChangeTypeAttr extends MoveEffectAttr { export class ChangeTypeAttr extends MoveEffectAttr {
private type: PokemonType; private type: PokemonType;
constructor(type: PokemonType) { constructor(type: PokemonType) {
super(false); super(false);
this.type = type; this.type = type;
} }
@ -6863,7 +6872,7 @@ export class ChangeTypeAttr extends MoveEffectAttr {
target.summonData.types = [ this.type ]; target.summonData.types = [ this.type ];
target.updateInfo(); target.updateInfo();
globalScene.phaseManager.queueMessage(i18next.t("moveTriggers:transformedIntoType", { pokemonName: getPokemonNameWithAffix(target), typeName: i18next.t(`pokemonInfo:Type.${PokemonType[this.type]}`) })); globalScene.phaseManager.queueMessage(i18next.t("moveTriggers:transformedIntoType", { pokemonName: getPokemonNameWithAffix(target), typeName: i18next.t(`pokemonInfo:type.${toCamelCase(PokemonType[this.type])}`) }));
return true; return true;
} }
@ -8131,9 +8140,12 @@ const failIfSingleBattle: MoveConditionFunc = (user, target, move) => globalScen
const failIfDampCondition: MoveConditionFunc = (user, target, move) => { const failIfDampCondition: MoveConditionFunc = (user, target, move) => {
const cancelled = new BooleanHolder(false); const cancelled = new BooleanHolder(false);
globalScene.getField(true).map(p=>applyAbAttrs("FieldPreventExplosiveMovesAbAttr", {pokemon: p, cancelled})); // temporary workaround to prevent displaying the message during enemy command phase
// TODO: either move this, or make the move condition func have a `simulated` param
const simulated = globalScene.phaseManager.getCurrentPhase()?.is('EnemyCommandPhase');
globalScene.getField(true).map(p=>applyAbAttrs("FieldPreventExplosiveMovesAbAttr", {pokemon: p, cancelled, simulated}));
// Queue a message if an ability prevented usage of the move // Queue a message if an ability prevented usage of the move
if (cancelled.value) { if (!simulated && cancelled.value) {
globalScene.phaseManager.queueMessage(i18next.t("moveTriggers:cannotUseMove", { pokemonName: getPokemonNameWithAffix(user), moveName: move.name })); globalScene.phaseManager.queueMessage(i18next.t("moveTriggers:cannotUseMove", { pokemonName: getPokemonNameWithAffix(user), moveName: move.name }));
} }
return !cancelled.value; return !cancelled.value;
@ -8155,6 +8167,9 @@ const failIfGhostTypeCondition: MoveConditionFunc = (user: Pokemon, target: Poke
const failIfNoTargetHeldItemsCondition: MoveConditionFunc = (user: Pokemon, target: Pokemon, move: Move) => target.getHeldItems().filter(i => i.isTransferable)?.length > 0; const failIfNoTargetHeldItemsCondition: MoveConditionFunc = (user: Pokemon, target: Pokemon, move: Move) => target.getHeldItems().filter(i => i.isTransferable)?.length > 0;
const attackedByItemMessageFunc = (user: Pokemon, target: Pokemon, move: Move) => { const attackedByItemMessageFunc = (user: Pokemon, target: Pokemon, move: Move) => {
if (isNullOrUndefined(target)) { // Fix bug when used against targets that have both fainted
return "";
}
const heldItems = target.getHeldItems().filter(i => i.isTransferable); const heldItems = target.getHeldItems().filter(i => i.isTransferable);
if (heldItems.length === 0) { if (heldItems.length === 0) {
return ""; return "";

View File

@ -237,7 +237,7 @@ export const BerriesAboundEncounter: MysteryEncounter = MysteryEncounterBuilder.
const config = globalScene.currentBattle.mysteryEncounter!.enemyPartyConfigs[0]; const config = globalScene.currentBattle.mysteryEncounter!.enemyPartyConfigs[0];
config.pokemonConfigs![0].tags = [BattlerTagType.MYSTERY_ENCOUNTER_POST_SUMMON]; config.pokemonConfigs![0].tags = [BattlerTagType.MYSTERY_ENCOUNTER_POST_SUMMON];
config.pokemonConfigs![0].mysteryEncounterBattleEffects = (pokemon: Pokemon) => { config.pokemonConfigs![0].mysteryEncounterBattleEffects = (pokemon: Pokemon) => {
queueEncounterMessage(`${namespace}:option.2.boss_enraged`); queueEncounterMessage(`${namespace}:option.2.bossEnraged`);
globalScene.phaseManager.unshiftNew( globalScene.phaseManager.unshiftNew(
"StatStageChangePhase", "StatStageChangePhase",
pokemon.getBattlerIndex(), pokemon.getBattlerIndex(),

View File

@ -249,7 +249,7 @@ async function tryApplyDigRewardItems() {
await showEncounterText( await showEncounterText(
i18next.t("battle:rewardGainCount", { i18next.t("battle:rewardGainCount", {
modifierName: leftovers.name, modifierName: leftovers.name,
count: 2, count: 1,
}), }),
null, null,
undefined, undefined,

View File

@ -2,6 +2,7 @@ import { globalScene } from "#app/global-scene";
import { allSpecies, modifierTypes } from "#data/data-lists"; import { allSpecies, modifierTypes } from "#data/data-lists";
import { getLevelTotalExp } from "#data/exp"; import { getLevelTotalExp } from "#data/exp";
import type { PokemonSpecies } from "#data/pokemon-species"; import type { PokemonSpecies } from "#data/pokemon-species";
import { AbilityId } from "#enums/ability-id";
import { Challenges } from "#enums/challenges"; import { Challenges } from "#enums/challenges";
import { ModifierTier } from "#enums/modifier-tier"; import { ModifierTier } from "#enums/modifier-tier";
import { MysteryEncounterOptionMode } from "#enums/mystery-encounter-option-mode"; import { MysteryEncounterOptionMode } from "#enums/mystery-encounter-option-mode";
@ -10,8 +11,9 @@ import { MysteryEncounterType } from "#enums/mystery-encounter-type";
import { Nature } from "#enums/nature"; import { Nature } from "#enums/nature";
import { PartyMemberStrength } from "#enums/party-member-strength"; import { PartyMemberStrength } from "#enums/party-member-strength";
import { PlayerGender } from "#enums/player-gender"; import { PlayerGender } from "#enums/player-gender";
import { PokemonType } from "#enums/pokemon-type"; import { MAX_POKEMON_TYPE, PokemonType } from "#enums/pokemon-type";
import { SpeciesId } from "#enums/species-id"; import { SpeciesId } from "#enums/species-id";
import { StatusEffect } from "#enums/status-effect";
import { TrainerType } from "#enums/trainer-type"; import { TrainerType } from "#enums/trainer-type";
import type { PlayerPokemon, Pokemon } from "#field/pokemon"; import type { PlayerPokemon, Pokemon } from "#field/pokemon";
import type { PokemonHeldItemModifier } from "#modifiers/modifier"; import type { PokemonHeldItemModifier } from "#modifiers/modifier";
@ -219,6 +221,7 @@ export const WeirdDreamEncounter: MysteryEncounter = MysteryEncounterBuilder.wit
await showEncounterText(`${namespace}:option.1.dreamComplete`); await showEncounterText(`${namespace}:option.1.dreamComplete`);
await doNewTeamPostProcess(transformations); await doNewTeamPostProcess(transformations);
globalScene.phaseManager.unshiftNew("PartyHealPhase", true);
setEncounterRewards({ setEncounterRewards({
guaranteedModifierTypeFuncs: [ guaranteedModifierTypeFuncs: [
modifierTypes.MEMORY_MUSHROOM, modifierTypes.MEMORY_MUSHROOM,
@ -230,7 +233,7 @@ export const WeirdDreamEncounter: MysteryEncounter = MysteryEncounterBuilder.wit
], ],
fillRemaining: false, fillRemaining: false,
}); });
leaveEncounterWithoutBattle(true); leaveEncounterWithoutBattle(false);
}) })
.build(), .build(),
) )
@ -431,6 +434,8 @@ function getTeamTransformations(): PokemonTransformation[] {
newAbilityIndex, newAbilityIndex,
undefined, undefined,
); );
transformation.newPokemon.teraType = randSeedInt(MAX_POKEMON_TYPE);
} }
return pokemonTransformations; return pokemonTransformations;
@ -440,6 +445,8 @@ async function doNewTeamPostProcess(transformations: PokemonTransformation[]) {
let atLeastOneNewStarter = false; let atLeastOneNewStarter = false;
for (const transformation of transformations) { for (const transformation of transformations) {
const previousPokemon = transformation.previousPokemon; const previousPokemon = transformation.previousPokemon;
const oldHpRatio = previousPokemon.getHpRatio(true);
const oldStatus = previousPokemon.status;
const newPokemon = transformation.newPokemon; const newPokemon = transformation.newPokemon;
const speciesRootForm = newPokemon.species.getRootSpeciesId(); const speciesRootForm = newPokemon.species.getRootSpeciesId();
@ -462,6 +469,19 @@ async function doNewTeamPostProcess(transformations: PokemonTransformation[]) {
} }
newPokemon.calculateStats(); newPokemon.calculateStats();
if (oldHpRatio > 0) {
newPokemon.hp = Math.ceil(oldHpRatio * newPokemon.getMaxHp());
// Assume that the `status` instance can always safely be transferred to the new pokemon
// This is the case (as of version 1.10.4)
// Safeguard against COMATOSE here
if (!newPokemon.hasAbility(AbilityId.COMATOSE, false, true)) {
newPokemon.status = oldStatus;
}
} else {
newPokemon.hp = 0;
newPokemon.doSetStatus(StatusEffect.FAINT);
}
await newPokemon.updateInfo(); await newPokemon.updateInfo();
} }

View File

@ -44,6 +44,34 @@ export abstract class PhasePriorityQueue {
public clear(): void { public clear(): void {
this.queue.splice(0, this.queue.length); 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;
}
} }
/** /**

View File

@ -593,14 +593,14 @@ export abstract class PokemonSpeciesForm {
}); });
} }
cry(soundConfig?: Phaser.Types.Sound.SoundConfig, ignorePlay?: boolean): AnySound { cry(soundConfig?: Phaser.Types.Sound.SoundConfig, ignorePlay?: boolean): AnySound | null {
const cryKey = this.getCryKey(this.formIndex); const cryKey = this.getCryKey(this.formIndex);
let cry: AnySound | null = globalScene.sound.get(cryKey) as AnySound; let cry: AnySound | null = globalScene.sound.get(cryKey) as AnySound;
if (cry?.pendingRemove) { if (cry?.pendingRemove) {
cry = null; cry = null;
} }
cry = globalScene.playSound(cry ?? cryKey, soundConfig); cry = globalScene.playSound(cry ?? cryKey, soundConfig);
if (ignorePlay) { if (cry && ignorePlay) {
cry.stop(); cry.stop();
} }
return cry; return cry;
@ -795,7 +795,7 @@ export class PokemonSpecies extends PokemonSpeciesForm implements Localizable {
return Gender.GENDERLESS; return Gender.GENDERLESS;
} }
if (randSeedFloat() <= this.malePercent) { if (randSeedFloat() * 100 <= this.malePercent) {
return Gender.MALE; return Gender.MALE;
} }
return Gender.FEMALE; return Gender.FEMALE;

View File

@ -11,6 +11,7 @@ import type { MoveId } from "#enums/move-id";
import type { Nature } from "#enums/nature"; import type { Nature } from "#enums/nature";
import type { PokemonType } from "#enums/pokemon-type"; import type { PokemonType } from "#enums/pokemon-type";
import type { SpeciesId } from "#enums/species-id"; import type { SpeciesId } from "#enums/species-id";
import { StatusEffect } from "#enums/status-effect";
import type { AttackMoveResult } from "#types/attack-move-result"; import type { AttackMoveResult } from "#types/attack-move-result";
import type { IllusionData } from "#types/illusion-data"; import type { IllusionData } from "#types/illusion-data";
import type { TurnMove } from "#types/turn-move"; import type { TurnMove } from "#types/turn-move";
@ -326,6 +327,14 @@ export class PokemonTurnData {
public switchedInThisTurn = false; public switchedInThisTurn = false;
public failedRunAway = false; public failedRunAway = false;
public joinedRound = false; public joinedRound = false;
/** Tracker for a pending status effect
*
* @remarks
* Set whenever {@linkcode Pokemon#trySetStatus} succeeds in order to prevent subsequent status effects
* from being applied. Necessary because the status is not actually set until the {@linkcode ObtainStatusEffectPhase} runs,
* which may not happen before another status effect is attempted to be applied.
*/
public pendingStatus: StatusEffect = StatusEffect.NONE;
/** /**
* The amount of times this Pokemon has acted again and used a move in the current turn. * The amount of times this Pokemon has acted again and used a move in the current turn.
* Used to make sure multi-hits occur properly when the user is * Used to make sure multi-hits occur properly when the user is

View File

@ -126,7 +126,9 @@ export class DelayedAttackTag extends PositionalTag implements DelayedAttackArgs
// Silently disappear if either source or target are missing or happen to be the same pokemon // Silently disappear if either source or target are missing or happen to be the same pokemon
// (i.e. targeting oneself) // (i.e. targeting oneself)
// We also need to check for fainted targets as they don't technically leave the field until _after_ the turn ends // We also need to check for fainted targets as they don't technically leave the field until _after_ the turn ends
return !!source && !!target && source !== target && !target.isFainted(); // TODO: Figure out a way to store the target's offensive stat if they faint to allow pending attacks to persist
// TODO: Remove the `?.scene` checks once battle anims are cleaned up - needed to avoid catch+release crash
return !!source?.scene && !!target?.scene && source !== target && !target.isFainted();
} }
} }

View File

@ -20,3 +20,6 @@ export enum PokemonType {
FAIRY, FAIRY,
STELLAR STELLAR
} }
/** The largest legal value for a {@linkcode PokemonType} (includes Stellar) */
export const MAX_POKEMON_TYPE = PokemonType.STELLAR;

View File

@ -454,7 +454,7 @@ export abstract class Pokemon extends Phaser.GameObjects.Container {
getNameToRender(useIllusion = true) { getNameToRender(useIllusion = true) {
const illusion = this.summonData.illusion; const illusion = this.summonData.illusion;
const name = useIllusion ? (illusion?.name ?? this.name) : this.name; const name = useIllusion ? (illusion?.name ?? this.name) : this.name;
const nickname: string | undefined = useIllusion ? illusion?.nickname : this.nickname; const nickname: string | undefined = useIllusion ? (illusion?.nickname ?? this.nickname) : this.nickname;
try { try {
if (nickname) { if (nickname) {
return decodeURIComponent(escape(atob(nickname))); // TODO: Remove `atob` and `escape`... eventually... return decodeURIComponent(escape(atob(nickname))); // TODO: Remove `atob` and `escape`... eventually...
@ -1781,7 +1781,7 @@ export abstract class Pokemon extends Phaser.GameObjects.Container {
* @returns Whether this Pokemon is currently fused with another species. * @returns Whether this Pokemon is currently fused with another species.
*/ */
isFusion(useIllusion = false): boolean { isFusion(useIllusion = false): boolean {
return useIllusion ? !!this.summonData.illusion?.fusionSpecies : !!this.fusionSpecies; return !!(useIllusion ? (this.summonData.illusion?.fusionSpecies ?? this.fusionSpecies) : this.fusionSpecies);
} }
/** /**
@ -2234,8 +2234,16 @@ export abstract class Pokemon extends Phaser.GameObjects.Container {
return this.hasPassive() && (!canApply || this.canApplyAbility(true)) && this.getPassiveAbility().hasAttr(attrType); return this.hasPassive() && (!canApply || this.canApplyAbility(true)) && this.getPassiveAbility().hasAttr(attrType);
} }
public getAbilityPriorities(): [number, number] { /**
return [this.getAbility().postSummonPriority, this.getPassiveAbility().postSummonPriority]; * Return the ability priorities of the pokemon's ability and, if enabled, its passive ability
* @returns A tuple containing the ability priorities of the pokemon
*/
public getAbilityPriorities(): [number] | [activePriority: number, passivePriority: number] {
const abilityPriority = this.getAbility().postSummonPriority;
if (this.hasPassive()) {
return [abilityPriority, this.getPassiveAbility().postSummonPriority];
}
return [abilityPriority];
} }
/** /**
@ -3235,6 +3243,18 @@ export abstract class Pokemon extends Phaser.GameObjects.Container {
rand -= stabMovePool[index++][1]; rand -= stabMovePool[index++][1];
} }
this.moveset.push(new PokemonMove(stabMovePool[index][0])); this.moveset.push(new PokemonMove(stabMovePool[index][0]));
} else {
// If there are no damaging STAB moves, just force a random damaging move
const attackMovePool = baseWeights.filter(m => allMoves[m[0]].category !== MoveCategory.STATUS);
if (attackMovePool.length) {
const totalWeight = attackMovePool.reduce((v, m) => v + m[1], 0);
let rand = randSeedInt(totalWeight);
let index = 0;
while (rand > attackMovePool[index][1]) {
rand -= attackMovePool[index++][1];
}
this.moveset.push(new PokemonMove(attackMovePool[index][0], 0, 0));
}
} }
while (baseWeights.length > this.moveset.length && this.moveset.length < 4) { while (baseWeights.length > this.moveset.length && this.moveset.length < 4) {
@ -4527,28 +4547,36 @@ export abstract class Pokemon extends Phaser.GameObjects.Container {
}); });
} }
cry(soundConfig?: Phaser.Types.Sound.SoundConfig, sceneOverride?: BattleScene): AnySound { cry(soundConfig?: Phaser.Types.Sound.SoundConfig, sceneOverride?: BattleScene): AnySound | null {
const scene = sceneOverride ?? globalScene; // TODO: is `sceneOverride` needed? const scene = sceneOverride ?? globalScene; // TODO: is `sceneOverride` needed?
const cry = this.getSpeciesForm(undefined, true).cry(soundConfig); const cry = this.getSpeciesForm(undefined, true).cry(soundConfig);
if (!cry) {
return cry;
}
let duration = cry.totalDuration * 1000; let duration = cry.totalDuration * 1000;
if (this.fusionSpecies && this.getSpeciesForm(undefined, true) !== this.getFusionSpeciesForm(undefined, true)) { if (this.fusionSpecies && this.getSpeciesForm(undefined, true) !== this.getFusionSpeciesForm(undefined, true)) {
let fusionCry = this.getFusionSpeciesForm(undefined, true).cry(soundConfig, true); const fusionCry = this.getFusionSpeciesForm(undefined, true).cry(soundConfig, true);
if (!fusionCry) {
return cry;
}
duration = Math.min(duration, fusionCry.totalDuration * 1000); duration = Math.min(duration, fusionCry.totalDuration * 1000);
fusionCry.destroy(); fusionCry.destroy();
scene.time.delayedCall(fixedInt(Math.ceil(duration * 0.4)), () => { scene.time.delayedCall(fixedInt(Math.ceil(duration * 0.4)), () => {
try { try {
SoundFade.fadeOut(scene, cry, fixedInt(Math.ceil(duration * 0.2))); SoundFade.fadeOut(scene, cry, fixedInt(Math.ceil(duration * 0.2)));
fusionCry = this.getFusionSpeciesForm(undefined, true).cry({ const fusionCryInner = this.getFusionSpeciesForm(undefined, true).cry({
seek: Math.max(fusionCry.totalDuration * 0.4, 0), seek: Math.max(fusionCry.totalDuration * 0.4, 0),
...soundConfig, ...soundConfig,
}); });
if (fusionCryInner) {
SoundFade.fadeIn( SoundFade.fadeIn(
scene, scene,
fusionCry, fusionCryInner,
fixedInt(Math.ceil(duration * 0.2)), fixedInt(Math.ceil(duration * 0.2)),
scene.masterVolume * scene.fieldVolume, scene.masterVolume * scene.fieldVolume,
0, 0,
); );
}
} catch (err) { } catch (err) {
console.error(err); console.error(err);
} }
@ -4576,14 +4604,14 @@ export abstract class Pokemon extends Phaser.GameObjects.Container {
crySoundConfig.rate = 0.7; crySoundConfig.rate = 0.7;
} }
} }
const cry = globalScene.playSound(key, crySoundConfig) as AnySound; const cry = globalScene.playSound(key, crySoundConfig);
if (!cry || globalScene.fieldVolume === 0) { if (!cry || globalScene.fieldVolume === 0) {
callback(); callback();
return; return;
} }
const sprite = this.getSprite(); const sprite = this.getSprite();
const tintSprite = this.getTintSprite(); const tintSprite = this.getTintSprite();
const delay = Math.max(globalScene.sound.get(key).totalDuration * 50, 25); const delay = Math.max(cry.totalDuration * 50, 25);
let frameProgress = 0; let frameProgress = 0;
let frameThreshold: number; let frameThreshold: number;
@ -4636,20 +4664,20 @@ export abstract class Pokemon extends Phaser.GameObjects.Container {
const key = this.species.getCryKey(this.formIndex); const key = this.species.getCryKey(this.formIndex);
let i = 0; let i = 0;
let rate = 0.85; let rate = 0.85;
const cry = globalScene.playSound(key, { rate: rate }) as AnySound; const cry = globalScene.playSound(key, { rate: rate });
const sprite = this.getSprite(); const sprite = this.getSprite();
const tintSprite = this.getTintSprite(); const tintSprite = this.getTintSprite();
let duration = cry.totalDuration * 1000;
const fusionCryKey = this.fusionSpecies!.getCryKey(this.fusionFormIndex); const fusionCryKey = this.fusionSpecies!.getCryKey(this.fusionFormIndex);
let fusionCry = globalScene.playSound(fusionCryKey, { let fusionCry = globalScene.playSound(fusionCryKey, {
rate: rate, rate: rate,
}) as AnySound; });
if (!cry || !fusionCry || globalScene.fieldVolume === 0) { if (!cry || !fusionCry || globalScene.fieldVolume === 0) {
callback(); callback();
return; return;
} }
fusionCry.stop(); fusionCry.stop();
let duration = cry.totalDuration * 1000;
duration = Math.min(duration, fusionCry.totalDuration * 1000); duration = Math.min(duration, fusionCry.totalDuration * 1000);
fusionCry.destroy(); fusionCry.destroy();
const delay = Math.max(duration * 0.05, 25); const delay = Math.max(duration * 0.05, 25);
@ -4692,9 +4720,12 @@ export abstract class Pokemon extends Phaser.GameObjects.Container {
if (i === transitionIndex && fusionCryKey) { if (i === transitionIndex && fusionCryKey) {
SoundFade.fadeOut(globalScene, cry, fixedInt(Math.ceil((duration / rate) * 0.2))); SoundFade.fadeOut(globalScene, cry, fixedInt(Math.ceil((duration / rate) * 0.2)));
fusionCry = globalScene.playSound(fusionCryKey, { fusionCry = globalScene.playSound(fusionCryKey, {
seek: Math.max(fusionCry.totalDuration * 0.4, 0), // TODO: This bang is correct as this callback can only be called once, but
// this whole block with conditionally reassigning fusionCry needs a second lock.
seek: Math.max(fusionCry!.totalDuration * 0.4, 0),
rate: rate, rate: rate,
}); });
if (fusionCry) {
SoundFade.fadeIn( SoundFade.fadeIn(
globalScene, globalScene,
fusionCry, fusionCry,
@ -4703,6 +4734,7 @@ export abstract class Pokemon extends Phaser.GameObjects.Container {
0, 0,
); );
} }
}
rate *= 0.99; rate *= 0.99;
if (cry && !cry.pendingRemove) { if (cry && !cry.pendingRemove) {
cry.setRate(rate); cry.setRate(rate);
@ -4803,7 +4835,7 @@ export abstract class Pokemon extends Phaser.GameObjects.Container {
if (effect !== StatusEffect.FAINT) { if (effect !== StatusEffect.FAINT) {
// Status-overriding moves (i.e. Rest) fail if their respective status already exists; // Status-overriding moves (i.e. Rest) fail if their respective status already exists;
// all other moves fail if the target already has _any_ status // all other moves fail if the target already has _any_ status
if (overrideStatus ? this.status?.effect === effect : this.status) { if (overrideStatus ? this.status?.effect === effect : this.status || this.turnData.pendingStatus) {
this.queueStatusImmuneMessage(quiet, overrideStatus ? "overlap" : "other"); // having different status displays generic fail message this.queueStatusImmuneMessage(quiet, overrideStatus ? "overlap" : "other"); // having different status displays generic fail message
return false; return false;
} }
@ -4955,6 +4987,8 @@ export abstract class Pokemon extends Phaser.GameObjects.Container {
if (overrideStatus) { if (overrideStatus) {
this.resetStatus(false); this.resetStatus(false);
} else {
this.turnData.pendingStatus = effect;
} }
globalScene.phaseManager.unshiftNew( globalScene.phaseManager.unshiftNew(
@ -4974,6 +5008,8 @@ export abstract class Pokemon extends Phaser.GameObjects.Container {
* Set this Pokemon's {@linkcode status | non-volatile status condition} to the specified effect. * Set this Pokemon's {@linkcode status | non-volatile status condition} to the specified effect.
* @param effect - The {@linkcode StatusEffect} to set * @param effect - The {@linkcode StatusEffect} to set
* @remarks * @remarks
* Clears this pokemon's `pendingStatus` in its {@linkcode Pokemon.turnData | turnData}.
*
* This method does **not** check for feasibility; that is the responsibility of the caller. * This method does **not** check for feasibility; that is the responsibility of the caller.
*/ */
doSetStatus(effect: Exclude<StatusEffect, StatusEffect.SLEEP>): void; doSetStatus(effect: Exclude<StatusEffect, StatusEffect.SLEEP>): void;
@ -4982,6 +5018,8 @@ export abstract class Pokemon extends Phaser.GameObjects.Container {
* @param effect - {@linkcode StatusEffect.SLEEP} * @param effect - {@linkcode StatusEffect.SLEEP}
* @param sleepTurnsRemaining - The number of turns to inflict sleep for; defaults to a random number between 2 and 4 * @param sleepTurnsRemaining - The number of turns to inflict sleep for; defaults to a random number between 2 and 4
* @remarks * @remarks
* Clears this pokemon's `pendingStatus` in its {@linkcode Pokemon#turnData}.
*
* This method does **not** check for feasibility; that is the responsibility of the caller. * This method does **not** check for feasibility; that is the responsibility of the caller.
*/ */
doSetStatus(effect: StatusEffect.SLEEP, sleepTurnsRemaining?: number): void; doSetStatus(effect: StatusEffect.SLEEP, sleepTurnsRemaining?: number): void;
@ -4991,6 +5029,8 @@ export abstract class Pokemon extends Phaser.GameObjects.Container {
* @param sleepTurnsRemaining - The number of turns to inflict sleep for; defaults to a random number between 2 and 4 * @param sleepTurnsRemaining - The number of turns to inflict sleep for; defaults to a random number between 2 and 4
* and is unused for all non-sleep Statuses * and is unused for all non-sleep Statuses
* @remarks * @remarks
* Clears this pokemon's `pendingStatus` in its {@linkcode Pokemon#turnData}.
*
* This method does **not** check for feasibility; that is the responsibility of the caller. * This method does **not** check for feasibility; that is the responsibility of the caller.
*/ */
doSetStatus(effect: StatusEffect, sleepTurnsRemaining?: number): void; doSetStatus(effect: StatusEffect, sleepTurnsRemaining?: number): void;
@ -5000,6 +5040,8 @@ export abstract class Pokemon extends Phaser.GameObjects.Container {
* @param sleepTurnsRemaining - The number of turns to inflict sleep for; defaults to a random number between 2 and 4 * @param sleepTurnsRemaining - The number of turns to inflict sleep for; defaults to a random number between 2 and 4
* and is unused for all non-sleep Statuses * and is unused for all non-sleep Statuses
* @remarks * @remarks
* Clears this pokemon's `pendingStatus` in its {@linkcode Pokemon#turnData}.
*
* This method does **not** check for feasibility; that is the responsibility of the caller. * 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 * @todo Make this and all related fields private and change tests to use a field-based helper or similar
*/ */
@ -5007,6 +5049,8 @@ export abstract class Pokemon extends Phaser.GameObjects.Container {
effect: StatusEffect, effect: StatusEffect,
sleepTurnsRemaining = effect !== StatusEffect.SLEEP ? 0 : this.randBattleSeedIntRange(2, 4), sleepTurnsRemaining = effect !== StatusEffect.SLEEP ? 0 : this.randBattleSeedIntRange(2, 4),
): void { ): void {
// Reset any pending status
this.turnData.pendingStatus = StatusEffect.NONE;
switch (effect) { switch (effect) {
case StatusEffect.POISON: case StatusEffect.POISON:
case StatusEffect.TOXIC: case StatusEffect.TOXIC:
@ -6546,6 +6590,7 @@ export class EnemyPokemon extends Pokemon {
ignoreAllyAbility: !p.getAlly()?.waveData.abilityRevealed, ignoreAllyAbility: !p.getAlly()?.waveData.abilityRevealed,
ignoreSourceAllyAbility: false, ignoreSourceAllyAbility: false,
isCritical, isCritical,
simulated: true,
}).damage >= p.hp }).damage >= p.hp
); );
}) })
@ -6884,7 +6929,7 @@ export class EnemyPokemon extends Pokemon {
const leftoverStats = EFFECTIVE_STATS.filter((s: EffectiveStat) => this.getStatStage(s) < 6); const leftoverStats = EFFECTIVE_STATS.filter((s: EffectiveStat) => this.getStatStage(s) < 6);
const statWeights = leftoverStats.map((s: EffectiveStat) => this.getStat(s, false)); const statWeights = leftoverStats.map((s: EffectiveStat) => this.getStat(s, false));
let boostedStat: EffectiveStat; let boostedStat: EffectiveStat | undefined;
const statThresholds: number[] = []; const statThresholds: number[] = [];
let totalWeight = 0; let totalWeight = 0;
@ -6902,6 +6947,11 @@ export class EnemyPokemon extends Pokemon {
} }
} }
if (boostedStat === undefined) {
this.bossSegmentIndex--;
return;
}
let stages = 1; let stages = 1;
// increase the boost if the boss has at least 3 segments and we passed last shield // increase the boost if the boss has at least 3 segments and we passed last shield
@ -6917,7 +6967,7 @@ export class EnemyPokemon extends Pokemon {
"StatStageChangePhase", "StatStageChangePhase",
this.getBattlerIndex(), this.getBattlerIndex(),
true, true,
[boostedStat!], [boostedStat],
stages, stages,
true, true,
true, true,

View File

@ -90,7 +90,7 @@ export class Trainer extends Phaser.GameObjects.Container {
[this.name, this.partnerName] = this.name.split(" & "); [this.name, this.partnerName] = this.name.split(" & ");
} }
} else { } else {
const partnerGenderKey = i18next.exists(`${classKey}.fenale`) ? ".fenale" : ""; const partnerGenderKey = i18next.exists(`${classKey}.female`) ? ".female" : "";
[this.partnerNameKey, this.partnerName] = getRandomLocaleEntry(`${classKey}${partnerGenderKey}`); [this.partnerNameKey, this.partnerName] = getRandomLocaleEntry(`${classKey}${partnerGenderKey}`);
} }
} }

View File

@ -279,7 +279,7 @@ function initGreatModifierPool() {
new WeightedModifierType(modifierTypes.DIRE_HIT, 4), new WeightedModifierType(modifierTypes.DIRE_HIT, 4),
new WeightedModifierType(modifierTypes.SUPER_LURE, lureWeightFunc(15, 4)), new WeightedModifierType(modifierTypes.SUPER_LURE, lureWeightFunc(15, 4)),
new WeightedModifierType(modifierTypes.NUGGET, skipInLastClassicWaveOrDefault(5)), new WeightedModifierType(modifierTypes.NUGGET, skipInLastClassicWaveOrDefault(5)),
new WeightedModifierType(modifierTypes.SPECIES_STAT_BOOSTER, 4), new WeightedModifierType(modifierTypes.SPECIES_STAT_BOOSTER, 2),
new WeightedModifierType( new WeightedModifierType(
modifierTypes.EVOLUTION_ITEM, modifierTypes.EVOLUTION_ITEM,
() => { () => {

View File

@ -355,14 +355,23 @@ export class PhaseManager {
if (this.phaseQueuePrependSpliceIndex > -1) { if (this.phaseQueuePrependSpliceIndex > -1) {
this.clearPhaseQueueSplice(); this.clearPhaseQueueSplice();
} }
if (this.phaseQueuePrepend.length) { this.phaseQueue.unshift(...this.phaseQueuePrepend);
while (this.phaseQueuePrepend.length) { this.phaseQueuePrepend.splice(0);
const poppedPhase = this.phaseQueuePrepend.pop();
if (poppedPhase) { const unactivatedConditionalPhases: [() => boolean, Phase][] = [];
this.phaseQueue.unshift(poppedPhase); // 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]);
} }
} }
this.conditionalQueue = unactivatedConditionalPhases;
if (!this.phaseQueue.length) { if (!this.phaseQueue.length) {
this.populatePhaseQueue(); this.populatePhaseQueue();
// Clear the conditionalQueue if there are no phases left in the phaseQueue // Clear the conditionalQueue if there are no phases left in the phaseQueue
@ -371,24 +380,6 @@ export class PhaseManager {
this.currentPhase = this.phaseQueue.shift() ?? null; this.currentPhase = this.phaseQueue.shift() ?? null;
const unactivatedConditionalPhases: [() => boolean, Phase][] = [];
// Check if there are any conditional phases queued
while (this.conditionalQueue?.length) {
// Retrieve the first conditional phase from the queue
const conditionalPhase = this.conditionalQueue.shift();
// Evaluate the condition associated with the phase
if (conditionalPhase?.[0]()) {
// If the condition is met, add the phase to the phase queue
this.pushPhase(conditionalPhase[1]);
} else if (conditionalPhase) {
// If the condition is not met, re-add the phase back to the front of the conditional queue
unactivatedConditionalPhases.push(conditionalPhase);
} else {
console.warn("condition phase is undefined/null!", conditionalPhase);
}
}
this.conditionalQueue.push(...unactivatedConditionalPhases);
if (this.currentPhase) { if (this.currentPhase) {
console.log(`%cStart Phase ${this.currentPhase.constructor.name}`, "color:green;"); console.log(`%cStart Phase ${this.currentPhase.constructor.name}`, "color:green;");
this.currentPhase.start(); this.currentPhase.start();
@ -520,6 +511,25 @@ export class PhaseManager {
this.dynamicPhaseQueues[type].push(phase); 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} * Unshifts the top phase from the corresponding dynamic queue onto {@linkcode phaseQueue}
* @param type {@linkcode DynamicPhaseType} The type of dynamic phase to start * @param type {@linkcode DynamicPhaseType} The type of dynamic phase to start

View File

@ -456,7 +456,7 @@ export class CommandPhase extends FieldPhase {
const numBallTypes = 5; const numBallTypes = 5;
if (cursor < numBallTypes) { if (cursor < numBallTypes) {
const targetPokemon = globalScene.getEnemyPokemon(); const targetPokemon = globalScene.getEnemyPokemon(false);
if ( if (
targetPokemon?.isBoss() && targetPokemon?.isBoss() &&
targetPokemon?.bossSegmentIndex >= 1 && targetPokemon?.bossSegmentIndex >= 1 &&

View File

@ -64,7 +64,7 @@ export class EggHatchPhase extends Phase {
private canSkip: boolean; private canSkip: boolean;
private skipped: boolean; private skipped: boolean;
/** The sound effect being played when the egg is hatched */ /** The sound effect being played when the egg is hatched */
private evolutionBgm: AnySound; private evolutionBgm: AnySound | null;
private eggLapsePhase: EggLapsePhase; private eggLapsePhase: EggLapsePhase;
constructor(hatchScene: EggLapsePhase, egg: Egg, eggsToHatchCount: number) { constructor(hatchScene: EggLapsePhase, egg: Egg, eggsToHatchCount: number) {
@ -230,6 +230,7 @@ export class EggHatchPhase extends Phase {
} else { } else {
globalScene.time.delayedCall(250, () => globalScene.setModifiersVisible(true)); globalScene.time.delayedCall(250, () => globalScene.setModifiersVisible(true));
} }
this.pokemon?.destroy();
super.end(); super.end();
} }

View File

@ -39,6 +39,10 @@ export class EggSummaryPhase extends Phase {
} }
end() { end() {
this.eggHatchData.forEach(data => {
data.pokemon?.destroy();
});
this.eggHatchData = [];
globalScene.time.delayedCall(250, () => globalScene.setModifiersVisible(true)); globalScene.time.delayedCall(250, () => globalScene.setModifiersVisible(true));
globalScene.ui.setModeForceTransition(UiMode.MESSAGE).then(() => { globalScene.ui.setModeForceTransition(UiMode.MESSAGE).then(() => {
super.end(); super.end();

View File

@ -28,9 +28,10 @@ export class EvolutionPhase extends Phase {
private evolution: SpeciesFormEvolution | null; private evolution: SpeciesFormEvolution | null;
private fusionSpeciesEvolved: boolean; // Whether the evolution is of the fused species private fusionSpeciesEvolved: boolean; // Whether the evolution is of the fused species
private evolutionBgm: AnySound; private evolutionBgm: AnySound | null;
private evolutionHandler: EvolutionSceneHandler; private evolutionHandler: EvolutionSceneHandler;
/** Container for all assets used by the scene. When the scene is cleared, the children within this are destroyed. */
protected evolutionContainer: Phaser.GameObjects.Container; protected evolutionContainer: Phaser.GameObjects.Container;
protected evolutionBaseBg: Phaser.GameObjects.Image; protected evolutionBaseBg: Phaser.GameObjects.Image;
protected evolutionBg: Phaser.GameObjects.Video; protected evolutionBg: Phaser.GameObjects.Video;
@ -297,8 +298,10 @@ export class EvolutionPhase extends Phase {
this.evolutionBg.setVisible(false); this.evolutionBg.setVisible(false);
}, },
}); });
if (this.evolutionBgm) {
SoundFade.fadeOut(globalScene, this.evolutionBgm, 100); SoundFade.fadeOut(globalScene, this.evolutionBgm, 100);
} }
}
/** /**
* Show the confirmation prompt for pausing evolutions * Show the confirmation prompt for pausing evolutions
@ -377,7 +380,9 @@ export class EvolutionPhase extends Phase {
* Fadeout evolution music, play the cry, show the evolution completed text, and end the phase * Fadeout evolution music, play the cry, show the evolution completed text, and end the phase
*/ */
private onEvolutionComplete(evolvedPokemon: Pokemon) { private onEvolutionComplete(evolvedPokemon: Pokemon) {
if (this.evolutionBgm) {
SoundFade.fadeOut(globalScene, this.evolutionBgm, 100); SoundFade.fadeOut(globalScene, this.evolutionBgm, 100);
}
globalScene.time.delayedCall(250, () => { globalScene.time.delayedCall(250, () => {
this.pokemon.cry(); this.pokemon.cry();
globalScene.time.delayedCall(1250, () => { globalScene.time.delayedCall(1250, () => {
@ -522,6 +527,7 @@ export class EvolutionPhase extends Phase {
return; return;
} }
if (i === lastCycle) { if (i === lastCycle) {
this.pokemonTintSprite.setVisible(false).setActive(false);
this.pokemonEvoTintSprite.setScale(1); this.pokemonEvoTintSprite.setScale(1);
} }
}, },

View File

@ -204,7 +204,7 @@ export class GameOverPhase extends BattlePhase {
} }
this.getRunHistoryEntry().then(runHistoryEntry => { this.getRunHistoryEntry().then(runHistoryEntry => {
globalScene.gameData.saveRunHistory(runHistoryEntry, this.isVictory); globalScene.gameData.saveRunHistory(runHistoryEntry, this.isVictory);
globalScene.phaseManager.pushNew("PostGameOverPhase", endCardPhase); globalScene.phaseManager.pushNew("PostGameOverPhase", globalScene.sessionSlotId, endCardPhase);
this.end(); this.end();
}); });
}; };

View File

@ -400,10 +400,17 @@ export class MoveEffectPhase extends PokemonPhase {
* @param user - The {@linkcode Pokemon} using this phase's invoked move * @param user - The {@linkcode Pokemon} using this phase's invoked move
* @param target - {@linkcode Pokemon} the current target of this phase's invoked move * @param target - {@linkcode Pokemon} the current target of this phase's invoked move
* @param hitResult - The {@linkcode HitResult} of the attempted move * @param hitResult - The {@linkcode HitResult} of the attempted move
* @param damage - The amount of damage dealt to the target in the interaction
* @param wasCritical - `true` if the move was a critical hit * @param wasCritical - `true` if the move was a critical hit
*/ */
protected applyOnGetHitAbEffects(user: Pokemon, target: Pokemon, hitResult: HitResult, wasCritical = false): void { protected applyOnGetHitAbEffects(
const params = { pokemon: target, opponent: user, move: this.move, hitResult }; user: Pokemon,
target: Pokemon,
hitResult: HitResult,
damage: number,
wasCritical = false,
): void {
const params = { pokemon: target, opponent: user, move: this.move, hitResult, damage };
applyAbAttrs("PostDefendAbAttr", params); applyAbAttrs("PostDefendAbAttr", params);
if (wasCritical) { if (wasCritical) {
@ -763,12 +770,12 @@ export class MoveEffectPhase extends PokemonPhase {
this.triggerMoveEffects(MoveEffectTrigger.PRE_APPLY, user, target); this.triggerMoveEffects(MoveEffectTrigger.PRE_APPLY, user, target);
const [hitResult, wasCritical] = this.applyMove(user, target, effectiveness); const [hitResult, wasCritical, dmg] = this.applyMove(user, target, effectiveness);
// Apply effects to the user (always) and the target (if not blocked by substitute). // Apply effects to the user (always) and the target (if not blocked by substitute).
this.triggerMoveEffects(MoveEffectTrigger.POST_APPLY, user, target, firstTarget, true); this.triggerMoveEffects(MoveEffectTrigger.POST_APPLY, user, target, firstTarget, true);
if (!this.move.hitsSubstitute(user, target)) { if (!this.move.hitsSubstitute(user, target)) {
this.applyOnTargetEffects(user, target, hitResult, firstTarget, wasCritical); this.applyOnTargetEffects(user, target, hitResult, firstTarget, dmg, wasCritical);
} }
if (this.lastHit) { if (this.lastHit) {
globalScene.triggerPokemonFormChange(user, SpeciesFormChangePostMoveTrigger); globalScene.triggerPokemonFormChange(user, SpeciesFormChangePostMoveTrigger);
@ -788,9 +795,13 @@ export class MoveEffectPhase extends PokemonPhase {
* @param user - The {@linkcode Pokemon} using this phase's invoked move * @param user - The {@linkcode Pokemon} using this phase's invoked move
* @param target - The {@linkcode Pokemon} targeted by the move * @param target - The {@linkcode Pokemon} targeted by the move
* @param effectiveness - The effectiveness of the move against the target * @param effectiveness - The effectiveness of the move against the target
* @returns The {@linkcode HitResult} of the move against the target and a boolean indicating whether the target was crit * @returns The {@linkcode HitResult} of the move against the target, a boolean indicating whether the target was crit, and the amount of damage dealt
*/ */
protected applyMoveDamage(user: Pokemon, target: Pokemon, effectiveness: TypeDamageMultiplier): [HitResult, boolean] { protected applyMoveDamage(
user: Pokemon,
target: Pokemon,
effectiveness: TypeDamageMultiplier,
): [result: HitResult, critical: boolean, damage: number] {
const isCritical = target.getCriticalHitResult(user, this.move); const isCritical = target.getCriticalHitResult(user, this.move);
/* /*
@ -821,7 +832,7 @@ export class MoveEffectPhase extends PokemonPhase {
const isOneHitKo = result === HitResult.ONE_HIT_KO; const isOneHitKo = result === HitResult.ONE_HIT_KO;
if (!dmg) { if (!dmg) {
return [result, false]; return [result, false, 0];
} }
target.lapseTags(BattlerTagLapseType.HIT); target.lapseTags(BattlerTagLapseType.HIT);
@ -850,7 +861,7 @@ export class MoveEffectPhase extends PokemonPhase {
} }
if (damage <= 0) { if (damage <= 0) {
return [result, isCritical]; return [result, isCritical, damage];
} }
if (user.isPlayer()) { if (user.isPlayer()) {
@ -879,7 +890,7 @@ export class MoveEffectPhase extends PokemonPhase {
globalScene.applyModifiers(DamageMoneyRewardModifier, true, user, new NumberHolder(damage)); globalScene.applyModifiers(DamageMoneyRewardModifier, true, user, new NumberHolder(damage));
} }
return [result, isCritical]; return [result, isCritical, damage];
} }
/** /**
@ -932,12 +943,17 @@ export class MoveEffectPhase extends PokemonPhase {
* @param user - The {@linkcode Pokemon} using this phase's invoked move * @param user - The {@linkcode Pokemon} using this phase's invoked move
* @param target - The {@linkcode Pokemon} struck by the move * @param target - The {@linkcode Pokemon} struck by the move
* @param effectiveness - The effectiveness of the move against the target * @param effectiveness - The effectiveness of the move against the target
* @returns The {@linkcode HitResult} of the move against the target, a boolean indicating whether the target was crit, and the amount of damage dealt
*/ */
protected applyMove(user: Pokemon, target: Pokemon, effectiveness: TypeDamageMultiplier): [HitResult, boolean] { protected applyMove(
user: Pokemon,
target: Pokemon,
effectiveness: TypeDamageMultiplier,
): [HitResult, critical: boolean, damage: number] {
const moveCategory = user.getMoveCategory(target, this.move); const moveCategory = user.getMoveCategory(target, this.move);
if (moveCategory === MoveCategory.STATUS) { if (moveCategory === MoveCategory.STATUS) {
return [HitResult.STATUS, false]; return [HitResult.STATUS, false, 0];
} }
const result = this.applyMoveDamage(user, target, effectiveness); const result = this.applyMoveDamage(user, target, effectiveness);
@ -960,6 +976,7 @@ export class MoveEffectPhase extends PokemonPhase {
* @param target - The {@linkcode Pokemon} targeted by the move * @param target - The {@linkcode Pokemon} targeted by the move
* @param hitResult - The {@linkcode HitResult} obtained from applying the move * @param hitResult - The {@linkcode HitResult} obtained from applying the move
* @param firstTarget - `true` if the target is the first Pokemon hit by the attack * @param firstTarget - `true` if the target is the first Pokemon hit by the attack
* @param damage - The amount of damage dealt to the target in the interaction
* @param wasCritical - `true` if the move was a critical hit * @param wasCritical - `true` if the move was a critical hit
*/ */
protected applyOnTargetEffects( protected applyOnTargetEffects(
@ -967,6 +984,7 @@ export class MoveEffectPhase extends PokemonPhase {
target: Pokemon, target: Pokemon,
hitResult: HitResult, hitResult: HitResult,
firstTarget: boolean, firstTarget: boolean,
damage: number,
wasCritical = false, wasCritical = false,
): void { ): void {
/** Does {@linkcode hitResult} indicate that damage was dealt to the target? */ /** Does {@linkcode hitResult} indicate that damage was dealt to the target? */
@ -979,8 +997,8 @@ export class MoveEffectPhase extends PokemonPhase {
this.triggerMoveEffects(MoveEffectTrigger.POST_APPLY, user, target, firstTarget, false); this.triggerMoveEffects(MoveEffectTrigger.POST_APPLY, user, target, firstTarget, false);
this.applyHeldItemFlinchCheck(user, target, dealsDamage); this.applyHeldItemFlinchCheck(user, target, dealsDamage);
this.applyOnGetHitAbEffects(user, target, hitResult, wasCritical); this.applyOnGetHitAbEffects(user, target, hitResult, damage, wasCritical);
applyAbAttrs("PostAttackAbAttr", { pokemon: user, opponent: target, move: this.move, hitResult }); applyAbAttrs("PostAttackAbAttr", { pokemon: user, opponent: target, move: this.move, hitResult, damage: damage });
// We assume only enemy Pokemon are able to have the EnemyAttackStatusEffectChanceModifier from tokens // We assume only enemy Pokemon are able to have the EnemyAttackStatusEffectChanceModifier from tokens
if (!user.isPlayer() && this.move.is("AttackMove")) { if (!user.isPlayer() && this.move.is("AttackMove")) {

View File

@ -24,6 +24,7 @@ import { applyMoveAttrs } from "#moves/apply-attrs";
import { frenzyMissFunc } from "#moves/move-utils"; import { frenzyMissFunc } from "#moves/move-utils";
import type { PokemonMove } from "#moves/pokemon-move"; import type { PokemonMove } from "#moves/pokemon-move";
import { BattlePhase } from "#phases/battle-phase"; import { BattlePhase } from "#phases/battle-phase";
import type { TurnMove } from "#types/turn-move";
import { NumberHolder } from "#utils/common"; import { NumberHolder } from "#utils/common";
import { enumValueToKey } from "#utils/enums"; import { enumValueToKey } from "#utils/enums";
import i18next from "i18next"; import i18next from "i18next";
@ -41,6 +42,13 @@ export class MovePhase extends BattlePhase {
/** Whether the current move should fail and retain PP. */ /** Whether the current move should fail and retain PP. */
protected cancelled = false; protected cancelled = false;
/** The move history entry object that is pushed to the pokemon's move history
*
* @remarks
* Can be edited _after_ being pushed to the history to adjust the result, targets, etc, for this move phase.
*/
protected moveHistoryEntry: TurnMove;
public get pokemon(): Pokemon { public get pokemon(): Pokemon {
return this._pokemon; return this._pokemon;
} }
@ -82,6 +90,11 @@ export class MovePhase extends BattlePhase {
this.move = move; this.move = move;
this.useMode = useMode; this.useMode = useMode;
this.forcedLast = forcedLast; this.forcedLast = forcedLast;
this.moveHistoryEntry = {
move: MoveId.NONE,
targets,
useMode,
};
} }
/** /**
@ -410,13 +423,9 @@ export class MovePhase extends BattlePhase {
if (showText) { if (showText) {
this.showMoveText(); this.showMoveText();
} }
const moveHistoryEntry = this.moveHistoryEntry;
this.pokemon.pushMoveHistory({ moveHistoryEntry.result = MoveResult.FAIL;
move: this.move.moveId, this.pokemon.pushMoveHistory(moveHistoryEntry);
targets: this.targets,
result: MoveResult.FAIL,
useMode: this.useMode,
});
// Use move-specific failure messages if present before checking terrain/weather blockage // Use move-specific failure messages if present before checking terrain/weather blockage
// and falling back to the classic "But it failed!". // and falling back to the classic "But it failed!".
@ -630,12 +639,9 @@ export class MovePhase extends BattlePhase {
frenzyMissFunc(this.pokemon, this.move.getMove()); frenzyMissFunc(this.pokemon, this.move.getMove());
} }
this.pokemon.pushMoveHistory({ const moveHistoryEntry = this.moveHistoryEntry;
move: MoveId.NONE, moveHistoryEntry.result = MoveResult.FAIL;
result: MoveResult.FAIL, this.pokemon.pushMoveHistory(moveHistoryEntry);
targets: this.targets,
useMode: this.useMode,
});
this.pokemon.lapseTags(BattlerTagLapseType.MOVE_EFFECT); this.pokemon.lapseTags(BattlerTagLapseType.MOVE_EFFECT);
this.pokemon.lapseTags(BattlerTagLapseType.AFTER_MOVE); this.pokemon.lapseTags(BattlerTagLapseType.AFTER_MOVE);
@ -649,13 +655,16 @@ export class MovePhase extends BattlePhase {
* Displays the move's usage text to the player as applicable for the move being used. * Displays the move's usage text to the player as applicable for the move being used.
*/ */
public showMoveText(): void { public showMoveText(): void {
const moveId = this.move.moveId;
if ( if (
this.move.moveId === MoveId.NONE || moveId === MoveId.NONE ||
this.pokemon.getTag(BattlerTagType.RECHARGING) || this.pokemon.getTag(BattlerTagType.RECHARGING) ||
this.pokemon.getTag(BattlerTagType.INTERRUPTED) this.pokemon.getTag(BattlerTagType.INTERRUPTED)
) { ) {
return; return;
} }
// Showing move text always adjusts the move history entry's move id
this.moveHistoryEntry.move = moveId;
// TODO: This should be done by the move... // TODO: This should be done by the move...
globalScene.phaseManager.queueMessage( globalScene.phaseManager.queueMessage(
@ -668,7 +677,7 @@ export class MovePhase extends BattlePhase {
// Moves with pre-use messages (Magnitude, Chilly Reception, Fickle Beam, etc.) always display their messages even on failure // Moves with pre-use messages (Magnitude, Chilly Reception, Fickle Beam, etc.) always display their messages even on failure
// TODO: This assumes single target for message funcs - is this sustainable? // TODO: This assumes single target for message funcs - is this sustainable?
applyMoveAttrs("PreMoveMessageAttr", this.pokemon, this.pokemon.getOpponents(false)[0], this.move.getMove()); applyMoveAttrs("PreMoveMessageAttr", this.pokemon, this.getActiveTargetPokemon()[0], this.move.getMove());
} }
/** /**

View File

@ -38,6 +38,7 @@ export class PartyHealPhase extends BattlePhase {
pokemon.updateInfo(true); pokemon.updateInfo(true);
} }
const healSong = globalScene.playSoundWithoutBgm("heal"); const healSong = globalScene.playSoundWithoutBgm("heal");
if (healSong) {
globalScene.time.delayedCall(fixedInt(healSong.totalDuration * 1000), () => { globalScene.time.delayedCall(fixedInt(healSong.totalDuration * 1000), () => {
healSong.destroy(); healSong.destroy();
if (this.resumeBgm && bgmPlaying) { if (this.resumeBgm && bgmPlaying) {
@ -45,6 +46,7 @@ export class PartyHealPhase extends BattlePhase {
} }
globalScene.ui.fadeIn(500).then(() => this.end()); globalScene.ui.fadeIn(500).then(() => this.end());
}); });
}
}); });
globalScene.arena.playerTerasUsed = 0; globalScene.arena.playerTerasUsed = 0;
} }

View File

@ -5,10 +5,11 @@ import type { EndCardPhase } from "#phases/end-card-phase";
export class PostGameOverPhase extends Phase { export class PostGameOverPhase extends Phase {
public readonly phaseName = "PostGameOverPhase"; public readonly phaseName = "PostGameOverPhase";
private endCardPhase?: EndCardPhase; private endCardPhase?: EndCardPhase;
private slotId: number;
constructor(endCardPhase?: EndCardPhase) { constructor(slotId: number, endCardPhase?: EndCardPhase) {
super(); super();
this.slotId = slotId;
this.endCardPhase = endCardPhase; this.endCardPhase = endCardPhase;
} }
@ -20,9 +21,7 @@ export class PostGameOverPhase extends Phase {
if (!success) { if (!success) {
return globalScene.reset(true); return globalScene.reset(true);
} }
globalScene.gameData globalScene.gameData.tryClearSession(this.slotId).then((success: boolean | [boolean, boolean]) => {
.tryClearSession(globalScene.sessionSlotId)
.then((success: boolean | [boolean, boolean]) => {
if (!success[0]) { if (!success[0]) {
return globalScene.reset(true); return globalScene.reset(true);
} }

View File

@ -177,7 +177,7 @@ export class SelectModifierPhase extends BattlePhase {
this.openModifierMenu(modifierType, cost, modifierSelectCallback); this.openModifierMenu(modifierType, cost, modifierSelectCallback);
} }
} else { } else {
this.applyModifier(modifierType.newModifier()!); this.applyModifier(modifierType.newModifier()!, cost);
} }
return cost === -1; return cost === -1;
} }

View File

@ -1,4 +1,5 @@
import { globalScene } from "#app/global-scene"; import { globalScene } from "#app/global-scene";
import { DynamicPhaseType } from "#enums/dynamic-phase-type";
import { SwitchType } from "#enums/switch-type"; import { SwitchType } from "#enums/switch-type";
import { UiMode } from "#enums/ui-mode"; import { UiMode } from "#enums/ui-mode";
import { BattlePhase } from "#phases/battle-phase"; import { BattlePhase } from "#phases/battle-phase";
@ -75,8 +76,11 @@ export class SwitchPhase extends BattlePhase {
if (slotIndex >= globalScene.currentBattle.getBattlerCount() && slotIndex < 6) { if (slotIndex >= globalScene.currentBattle.getBattlerCount() && slotIndex < 6) {
// Remove any pre-existing PostSummonPhase under the same field index. // 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. // Pre-existing PostSummonPhases may occur when this phase is invoked during a prompt to switch at the start of a wave.
globalScene.phaseManager.tryRemovePhase( // 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, p => p.is("PostSummonPhase") && p.player && p.fieldIndex === this.fieldIndex,
"all",
); );
const switchType = option === PartyOption.PASS_BATON ? SwitchType.BATON_PASS : this.switchType; const switchType = option === PartyOption.PASS_BATON ? SwitchType.BATON_PASS : this.switchType;
globalScene.phaseManager.unshiftNew("SwitchSummonPhase", switchType, fieldIndex, slotIndex, this.doReturn); globalScene.phaseManager.unshiftNew("SwitchSummonPhase", switchType, fieldIndex, slotIndex, this.doReturn);

View File

@ -177,6 +177,9 @@ export class TitlePhase extends Phase {
.then((success: boolean) => { .then((success: boolean) => {
if (success) { if (success) {
this.loaded = true; this.loaded = true;
if (loggedInUser) {
loggedInUser.lastSessionSlot = slotId;
}
globalScene.ui.showText(i18next.t("menu:sessionSuccess"), null, () => this.end()); globalScene.ui.showText(i18next.t("menu:sessionSuccess"), null, () => this.end());
} else { } else {
this.end(); this.end();

View File

@ -179,12 +179,11 @@ export class TurnStartPhase extends FieldPhase {
// https://www.smogon.com/forums/threads/sword-shield-battle-mechanics-research.3655528/page-64#post-9244179 // https://www.smogon.com/forums/threads/sword-shield-battle-mechanics-research.3655528/page-64#post-9244179
phaseManager.pushNew("WeatherEffectPhase"); phaseManager.pushNew("WeatherEffectPhase");
phaseManager.pushNew("PositionalTagPhase");
phaseManager.pushNew("BerryPhase"); phaseManager.pushNew("BerryPhase");
/** Add a new phase to check who should be taking status damage */
phaseManager.pushNew("CheckStatusEffectPhase", moveOrder); phaseManager.pushNew("CheckStatusEffectPhase", moveOrder);
phaseManager.pushNew("PositionalTagPhase");
phaseManager.pushNew("TurnEndPhase"); phaseManager.pushNew("TurnEndPhase");
/* /*

View File

@ -82,6 +82,7 @@ export class PokerogueSessionSavedataApi extends ApiBase {
try { try {
const urlSearchParams = this.toUrlSearchParams(params); const urlSearchParams = this.toUrlSearchParams(params);
const response = await this.doGet(`/savedata/session/delete?${urlSearchParams}`); const response = await this.doGet(`/savedata/session/delete?${urlSearchParams}`);
console.debug("%cSending a request to delete session in slot %d", "color: blue", params.slot);
if (response.ok) { if (response.ok) {
return null; return null;

View File

@ -68,6 +68,7 @@ import { executeIf, fixedInt, isLocal, NumberHolder, randInt, randSeedItem } fro
import { decrypt, encrypt } from "#utils/data"; import { decrypt, encrypt } from "#utils/data";
import { getEnumKeys } from "#utils/enums"; import { getEnumKeys } from "#utils/enums";
import { getPokemonSpecies } from "#utils/pokemon-utils"; import { getPokemonSpecies } from "#utils/pokemon-utils";
import { isBeta } from "#utils/utility-vars";
import { AES, enc } from "crypto-js"; import { AES, enc } from "crypto-js";
import i18next from "i18next"; import i18next from "i18next";
@ -419,7 +420,15 @@ export class GameData {
} }
} }
console.debug(systemData); if (isLocal || isBeta) {
try {
console.debug(
this.parseSystemData(JSON.stringify(systemData, (_, v: any) => (typeof v === "bigint" ? v.toString() : v))),
);
} catch (err) {
console.debug("Attempt to log system data failed:", err);
}
}
localStorage.setItem(`data_${loggedInUser?.username}`, encrypt(systemDataStr, bypassLogin)); localStorage.setItem(`data_${loggedInUser?.username}`, encrypt(systemDataStr, bypassLogin));
@ -945,11 +954,11 @@ export class GameData {
} as SessionSaveData; } as SessionSaveData;
} }
getSession(slotId: number): Promise<SessionSaveData | null> { async getSession(slotId: number): Promise<SessionSaveData | null> {
// biome-ignore lint/suspicious/noAsyncPromiseExecutor: TODO: fix this const { promise, resolve, reject } = Promise.withResolvers<SessionSaveData | null>();
return new Promise(async (resolve, reject) => {
if (slotId < 0) { if (slotId < 0) {
return resolve(null); resolve(null);
return promise;
} }
const handleSessionData = async (sessionDataStr: string) => { const handleSessionData = async (sessionDataStr: string) => {
try { try {
@ -962,10 +971,12 @@ export class GameData {
}; };
if (!bypassLogin && !localStorage.getItem(`sessionData${slotId ? slotId : ""}_${loggedInUser?.username}`)) { if (!bypassLogin && !localStorage.getItem(`sessionData${slotId ? slotId : ""}_${loggedInUser?.username}`)) {
pokerogueApi.savedata.session.get({ slot: slotId, clientSessionId }).then(async response => { const response = await pokerogueApi.savedata.session.get({ slot: slotId, clientSessionId });
if (!response || response?.length === 0 || response?.[0] !== "{") { if (!response || response?.length === 0 || response?.[0] !== "{") {
console.error(response); console.error(response);
return resolve(null); resolve(null);
return promise;
} }
localStorage.setItem( localStorage.setItem(
@ -974,16 +985,15 @@ export class GameData {
); );
await handleSessionData(response); await handleSessionData(response);
}); return promise;
} else { }
const sessionData = localStorage.getItem(`sessionData${slotId ? slotId : ""}_${loggedInUser?.username}`); const sessionData = localStorage.getItem(`sessionData${slotId ? slotId : ""}_${loggedInUser?.username}`);
if (sessionData) { if (sessionData) {
await handleSessionData(decrypt(sessionData, bypassLogin)); await handleSessionData(decrypt(sessionData, bypassLogin));
} else { return promise;
return resolve(null);
} }
} resolve(null);
}); return promise;
} }
async renameSession(slotId: number, newName: string): Promise<boolean> { async renameSession(slotId: number, newName: string): Promise<boolean> {
@ -1028,24 +1038,33 @@ export class GameData {
return !(success !== null && !success); return !(success !== null && !success);
} }
loadSession(slotId: number, sessionData?: SessionSaveData): Promise<boolean> { async loadSession(slotId: number, sessionData?: SessionSaveData): Promise<boolean> {
// biome-ignore lint/suspicious/noAsyncPromiseExecutor: TODO: fix this const { promise, resolve, reject } = Promise.withResolvers<boolean>();
return new Promise(async (resolve, reject) => {
try { try {
const initSessionFromData = async (sessionData: SessionSaveData) => { const initSessionFromData = (fromSession: SessionSaveData) => {
console.debug(sessionData); if (isLocal || isBeta) {
try {
globalScene.gameMode = getGameMode(sessionData.gameMode || GameModes.CLASSIC); console.debug(
if (sessionData.challenges) { this.parseSessionData(
globalScene.gameMode.challenges = sessionData.challenges.map(c => c.toChallenge()); JSON.stringify(fromSession, (_, v: any) => (typeof v === "bigint" ? v.toString() : v)),
),
);
} catch (err) {
console.debug("Attempt to log session data failed:", err);
}
} }
globalScene.setSeed(sessionData.seed || globalScene.game.config.seed[0]); globalScene.gameMode = getGameMode(fromSession.gameMode || GameModes.CLASSIC);
if (fromSession.challenges) {
globalScene.gameMode.challenges = fromSession.challenges.map(c => c.toChallenge());
}
globalScene.setSeed(fromSession.seed || globalScene.game.config.seed[0]);
globalScene.resetSeed(); globalScene.resetSeed();
console.log("Seed:", globalScene.seed); console.log("Seed:", globalScene.seed);
globalScene.sessionPlayTime = sessionData.playTime || 0; globalScene.sessionPlayTime = fromSession.playTime || 0;
globalScene.lastSavePlayTime = 0; globalScene.lastSavePlayTime = 0;
const loadPokemonAssets: Promise<void>[] = []; const loadPokemonAssets: Promise<void>[] = [];
@ -1053,7 +1072,7 @@ export class GameData {
const party = globalScene.getPlayerParty(); const party = globalScene.getPlayerParty();
party.splice(0, party.length); party.splice(0, party.length);
for (const p of sessionData.party) { for (const p of fromSession.party) {
const pokemon = p.toPokemon() as PlayerPokemon; const pokemon = p.toPokemon() as PlayerPokemon;
pokemon.setVisible(false); pokemon.setVisible(false);
loadPokemonAssets.push(pokemon.loadAssets(false)); loadPokemonAssets.push(pokemon.loadAssets(false));
@ -1061,48 +1080,48 @@ export class GameData {
} }
Object.keys(globalScene.pokeballCounts).forEach((key: string) => { Object.keys(globalScene.pokeballCounts).forEach((key: string) => {
globalScene.pokeballCounts[key] = sessionData.pokeballCounts[key] || 0; globalScene.pokeballCounts[key] = fromSession.pokeballCounts[key] || 0;
}); });
if (Overrides.POKEBALL_OVERRIDE.active) { if (Overrides.POKEBALL_OVERRIDE.active) {
globalScene.pokeballCounts = Overrides.POKEBALL_OVERRIDE.pokeballs; globalScene.pokeballCounts = Overrides.POKEBALL_OVERRIDE.pokeballs;
} }
globalScene.money = Math.floor(sessionData.money || 0); globalScene.money = Math.floor(fromSession.money || 0);
globalScene.updateMoneyText(); globalScene.updateMoneyText();
if (globalScene.money > this.gameStats.highestMoney) { if (globalScene.money > this.gameStats.highestMoney) {
this.gameStats.highestMoney = globalScene.money; this.gameStats.highestMoney = globalScene.money;
} }
globalScene.score = sessionData.score; globalScene.score = fromSession.score;
globalScene.updateScoreText(); globalScene.updateScoreText();
globalScene.mysteryEncounterSaveData = new MysteryEncounterSaveData(sessionData.mysteryEncounterSaveData); globalScene.mysteryEncounterSaveData = new MysteryEncounterSaveData(fromSession.mysteryEncounterSaveData);
globalScene.newArena(sessionData.arena.biome, sessionData.playerFaints); globalScene.newArena(fromSession.arena.biome, fromSession.playerFaints);
const battleType = sessionData.battleType || 0; const battleType = fromSession.battleType || 0;
const trainerConfig = sessionData.trainer ? trainerConfigs[sessionData.trainer.trainerType] : null; const trainerConfig = fromSession.trainer ? trainerConfigs[fromSession.trainer.trainerType] : null;
const mysteryEncounterType = const mysteryEncounterType =
sessionData.mysteryEncounterType !== -1 ? sessionData.mysteryEncounterType : undefined; fromSession.mysteryEncounterType !== -1 ? fromSession.mysteryEncounterType : undefined;
const battle = globalScene.newBattle( const battle = globalScene.newBattle(
sessionData.waveIndex, fromSession.waveIndex,
battleType, battleType,
sessionData.trainer, fromSession.trainer,
battleType === BattleType.TRAINER battleType === BattleType.TRAINER
? trainerConfig?.doubleOnly || sessionData.trainer?.variant === TrainerVariant.DOUBLE ? trainerConfig?.doubleOnly || fromSession.trainer?.variant === TrainerVariant.DOUBLE
: sessionData.enemyParty.length > 1, : fromSession.enemyParty.length > 1,
mysteryEncounterType, mysteryEncounterType,
); );
battle.enemyLevels = sessionData.enemyParty.map(p => p.level); battle.enemyLevels = fromSession.enemyParty.map(p => p.level);
globalScene.arena.init(); globalScene.arena.init();
sessionData.enemyParty.forEach((enemyData, e) => { fromSession.enemyParty.forEach((enemyData, e) => {
const enemyPokemon = enemyData.toPokemon( const enemyPokemon = enemyData.toPokemon(
battleType, battleType,
e, e,
sessionData.trainer?.variant === TrainerVariant.DOUBLE, fromSession.trainer?.variant === TrainerVariant.DOUBLE,
) as EnemyPokemon; ) as EnemyPokemon;
battle.enemyParty[e] = enemyPokemon; battle.enemyParty[e] = enemyPokemon;
if (battleType === BattleType.WILD) { if (battleType === BattleType.WILD) {
@ -1112,7 +1131,7 @@ export class GameData {
loadPokemonAssets.push(enemyPokemon.loadAssets()); loadPokemonAssets.push(enemyPokemon.loadAssets());
}); });
globalScene.arena.weather = sessionData.arena.weather; globalScene.arena.weather = fromSession.arena.weather;
globalScene.arena.eventTarget.dispatchEvent( globalScene.arena.eventTarget.dispatchEvent(
new WeatherChangedEvent( new WeatherChangedEvent(
WeatherType.NONE, WeatherType.NONE,
@ -1121,7 +1140,7 @@ export class GameData {
), ),
); // TODO: is this bang correct? ); // TODO: is this bang correct?
globalScene.arena.terrain = sessionData.arena.terrain; globalScene.arena.terrain = fromSession.arena.terrain;
globalScene.arena.eventTarget.dispatchEvent( globalScene.arena.eventTarget.dispatchEvent(
new TerrainChangedEvent( new TerrainChangedEvent(
TerrainType.NONE, TerrainType.NONE,
@ -1130,9 +1149,9 @@ export class GameData {
), ),
); // TODO: is this bang correct? ); // TODO: is this bang correct?
globalScene.arena.playerTerasUsed = sessionData.arena.playerTerasUsed; globalScene.arena.playerTerasUsed = fromSession.arena.playerTerasUsed;
globalScene.arena.tags = sessionData.arena.tags; globalScene.arena.tags = fromSession.arena.tags;
if (globalScene.arena.tags) { if (globalScene.arena.tags) {
for (const tag of globalScene.arena.tags) { for (const tag of globalScene.arena.tags) {
if (tag instanceof EntryHazardTag) { if (tag instanceof EntryHazardTag) {
@ -1146,7 +1165,7 @@ export class GameData {
} }
} }
globalScene.arena.positionalTagManager.tags = sessionData.arena.positionalTags.map(tag => globalScene.arena.positionalTagManager.tags = fromSession.arena.positionalTags.map(tag =>
loadPositionalTag(tag), loadPositionalTag(tag),
); );
@ -1154,7 +1173,7 @@ export class GameData {
console.warn("Existing modifiers not cleared on session load, deleting..."); console.warn("Existing modifiers not cleared on session load, deleting...");
globalScene.modifiers = []; globalScene.modifiers = [];
} }
for (const modifierData of sessionData.modifiers) { for (const modifierData of fromSession.modifiers) {
const modifier = modifierData.toModifier(Modifier[modifierData.className]); const modifier = modifierData.toModifier(Modifier[modifierData.className]);
if (modifier) { if (modifier) {
globalScene.addModifier(modifier, true); globalScene.addModifier(modifier, true);
@ -1162,7 +1181,7 @@ export class GameData {
} }
globalScene.updateModifiers(true); globalScene.updateModifiers(true);
for (const enemyModifierData of sessionData.enemyModifiers) { for (const enemyModifierData of fromSession.enemyModifiers) {
const modifier = enemyModifierData.toModifier(Modifier[enemyModifierData.className]); const modifier = enemyModifierData.toModifier(Modifier[enemyModifierData.className]);
if (modifier) { if (modifier) {
globalScene.addEnemyModifier(modifier, true); globalScene.addEnemyModifier(modifier, true);
@ -1177,7 +1196,9 @@ export class GameData {
initSessionFromData(sessionData); initSessionFromData(sessionData);
} else { } else {
this.getSession(slotId) this.getSession(slotId)
.then(data => data && initSessionFromData(data)) .then(data => {
return data && initSessionFromData(data);
})
.catch(err => { .catch(err => {
reject(err); reject(err);
return; return;
@ -1185,9 +1206,9 @@ export class GameData {
} }
} catch (err) { } catch (err) {
reject(err); reject(err);
return;
} }
});
return promise;
} }
/** /**

View File

@ -61,12 +61,12 @@ export class TouchControl {
* event, removes the keydown state, and removes the 'active' class from the node and the last touched element. * event, removes the keydown state, and removes the 'active' class from the node and the last touched element.
*/ */
bindKey(node: HTMLElement, key: string) { bindKey(node: HTMLElement, key: string) {
node.addEventListener("touchstart", event => { node.addEventListener("pointerdown", event => {
event.preventDefault(); event.preventDefault();
this.touchButtonDown(node, key); this.touchButtonDown(node, key);
}); });
node.addEventListener("touchend", event => { node.addEventListener("pointerup", event => {
event.preventDefault(); event.preventDefault();
this.touchButtonUp(node, key, event.target?.["id"]); this.touchButtonUp(node, key, event.target?.["id"]);
}); });

View File

@ -13,7 +13,7 @@ import { SettingsGamepadUiHandler } from "#ui/settings-gamepad-ui-handler";
import { SettingsKeyboardUiHandler } from "#ui/settings-keyboard-ui-handler"; import { SettingsKeyboardUiHandler } from "#ui/settings-keyboard-ui-handler";
import { SettingsUiHandler } from "#ui/settings-ui-handler"; import { SettingsUiHandler } from "#ui/settings-ui-handler";
import { StarterSelectUiHandler } from "#ui/starter-select-ui-handler"; import { StarterSelectUiHandler } from "#ui/starter-select-ui-handler";
import type Phaser from "phaser"; import Phaser from "phaser";
type ActionKeys = Record<Button, () => void>; type ActionKeys = Record<Button, () => void>;
@ -224,25 +224,26 @@ export class UiInputs {
buttonSpeedChange(up = true): void { buttonSpeedChange(up = true): void {
const settingGameSpeed = settingIndex(SettingKeys.Game_Speed); const settingGameSpeed = settingIndex(SettingKeys.Game_Speed);
const settingOptions = Setting[settingGameSpeed].options;
let currentSetting = settingOptions.findIndex(item => item.value === globalScene.gameSpeed.toString());
// if current setting is -1, then the current game speed is not a valid option, so default to index 5 (3x)
if (currentSetting === -1) {
currentSetting = 5;
}
let direction: number;
if (up && globalScene.gameSpeed < 5) { if (up && globalScene.gameSpeed < 5) {
globalScene.gameData.saveSetting( direction = 1;
SettingKeys.Game_Speed,
Setting[settingGameSpeed].options.findIndex(item => item.label === `${globalScene.gameSpeed}x`) + 1,
);
if (globalScene.ui?.getMode() === UiMode.SETTINGS) {
(globalScene.ui.getHandler() as SettingsUiHandler).show([]);
}
} else if (!up && globalScene.gameSpeed > 1) { } else if (!up && globalScene.gameSpeed > 1) {
direction = -1;
} else {
return;
}
globalScene.gameData.saveSetting( globalScene.gameData.saveSetting(
SettingKeys.Game_Speed, SettingKeys.Game_Speed,
Math.max( Phaser.Math.Clamp(currentSetting + direction, 0, settingOptions.length - 1),
Setting[settingGameSpeed].options.findIndex(item => item.label === `${globalScene.gameSpeed}x`) - 1,
0,
),
); );
if (globalScene.ui?.getMode() === UiMode.SETTINGS) { if (globalScene.ui?.getMode() === UiMode.SETTINGS) {
(globalScene.ui.getHandler() as SettingsUiHandler).show([]); (globalScene.ui.getHandler() as SettingsUiHandler).show([]);
} }
} }
}
} }

View File

@ -287,9 +287,6 @@ export abstract class BattleInfo extends Phaser.GameObjects.Container {
2.5, 2.5,
); );
this.splicedIcon.setVisible(pokemon.isFusion(true)); this.splicedIcon.setVisible(pokemon.isFusion(true));
if (!this.splicedIcon.visible) {
return;
}
this.splicedIcon this.splicedIcon
.on("pointerover", () => .on("pointerover", () =>
globalScene.ui.showTooltip( globalScene.ui.showTooltip(
@ -323,6 +320,10 @@ export abstract class BattleInfo extends Phaser.GameObjects.Container {
.setVisible(pokemon.isShiny()) .setVisible(pokemon.isShiny())
.setTint(getVariantTint(baseVariant)); .setTint(getVariantTint(baseVariant));
this.shinyIcon
.on("pointerover", () => globalScene.ui.showTooltip("", i18next.t("common:shinyOnHover") + shinyDescriptor))
.on("pointerout", () => globalScene.ui.hideTooltip());
if (!this.shinyIcon.visible) { if (!this.shinyIcon.visible) {
return; return;
} }
@ -335,10 +336,6 @@ export abstract class BattleInfo extends Phaser.GameObjects.Container {
} }
shinyDescriptor += ")"; shinyDescriptor += ")";
} }
this.shinyIcon
.on("pointerover", () => globalScene.ui.showTooltip("", i18next.t("common:shinyOnHover") + shinyDescriptor))
.on("pointerout", () => globalScene.ui.hideTooltip());
} }
initInfo(pokemon: Pokemon) { initInfo(pokemon: Pokemon) {

View File

@ -36,7 +36,7 @@ export class EnemyBattleInfo extends BattleInfo {
override constructTypeIcons(): void { override constructTypeIcons(): void {
this.type1Icon = globalScene.add.sprite(-15, -15.5, "pbinfo_enemy_type1").setName("icon_type_1").setOrigin(0); this.type1Icon = globalScene.add.sprite(-15, -15.5, "pbinfo_enemy_type1").setName("icon_type_1").setOrigin(0);
this.type2Icon = globalScene.add.sprite(-15, -2.5, "pbinfo_enemy_type2").setName("icon_type_2").setOrigin(0); this.type2Icon = globalScene.add.sprite(-15, -2.5, "pbinfo_enemy_type2").setName("icon_type_2").setOrigin(0);
this.type3Icon = globalScene.add.sprite(0, 15.5, "pbinfo_enemy_type3").setName("icon_type_3").setOrigin(0); this.type3Icon = globalScene.add.sprite(0, -15.5, "pbinfo_enemy_type").setName("icon_type_3").setOrigin(0);
this.add([this.type1Icon, this.type2Icon, this.type3Icon]); this.add([this.type1Icon, this.type2Icon, this.type3Icon]);
} }
@ -115,6 +115,9 @@ export class EnemyBattleInfo extends BattleInfo {
globalScene.gameData.starterData[pokemon.species.getRootSpeciesId()].classicWinCount > 0 && globalScene.gameData.starterData[pokemon.species.getRootSpeciesId()].classicWinCount > 0 &&
globalScene.gameData.starterData[pokemon.species.getRootSpeciesId(true)].classicWinCount > 0 globalScene.gameData.starterData[pokemon.species.getRootSpeciesId(true)].classicWinCount > 0
) { ) {
// move the ribbon to the left if there is no owned icon
const championRibbonX = this.ownedIcon.visible ? 8 : 0;
this.championRibbon.setPositionRelative(this.nameText, championRibbonX, 11.75);
this.championRibbon.setVisible(true); this.championRibbon.setVisible(true);
} }
@ -180,12 +183,12 @@ export class EnemyBattleInfo extends BattleInfo {
this.ownedIcon, this.ownedIcon,
this.championRibbon, this.championRibbon,
this.statusIndicator, this.statusIndicator,
this.levelContainer,
this.statValuesContainer, this.statValuesContainer,
].map(e => (e.x += 48 * (boss ? -1 : 1))); ].map(e => (e.x += 48 * (boss ? -1 : 1)));
this.hpBar.x += 38 * (boss ? -1 : 1); this.hpBar.x += 38 * (boss ? -1 : 1);
this.hpBar.y += 2 * (this.boss ? -1 : 1); this.hpBar.y += 2 * (this.boss ? -1 : 1);
this.hpBar.setTexture(`overlay_hp${boss ? "_boss" : ""}`); this.hpBar.setTexture(`overlay_hp${boss ? "_boss" : ""}`);
this.levelContainer.x += 2 * (boss ? -1 : 1);
this.box.setTexture(this.getTextureName()); this.box.setTexture(this.getTextureName());
this.statsBox.setTexture(`${this.getTextureName()}_stats`); this.statsBox.setTexture(`${this.getTextureName()}_stats`);
} }

View File

@ -21,7 +21,7 @@ export class PlayerBattleInfo extends BattleInfo {
override constructTypeIcons(): void { override constructTypeIcons(): void {
this.type1Icon = globalScene.add.sprite(-139, -17, "pbinfo_player_type1").setName("icon_type_1").setOrigin(0); this.type1Icon = globalScene.add.sprite(-139, -17, "pbinfo_player_type1").setName("icon_type_1").setOrigin(0);
this.type2Icon = globalScene.add.sprite(-139, -1, "pbinfo_player_type2").setName("icon_type_2").setOrigin(0); this.type2Icon = globalScene.add.sprite(-139, -1, "pbinfo_player_type2").setName("icon_type_2").setOrigin(0);
this.type3Icon = globalScene.add.sprite(-154, -17, "pbinfo_player_type3").setName("icon_type_3").setOrigin(0); this.type3Icon = globalScene.add.sprite(-154, -17, "pbinfo_player_type").setName("icon_type_3").setOrigin(0);
this.add([this.type1Icon, this.type2Icon, this.type3Icon]); this.add([this.type1Icon, this.type2Icon, this.type3Icon]);
} }

View File

@ -743,7 +743,7 @@ export class EggGachaUiHandler extends MessageUiHandler {
if (!freePulls && globalScene.gameData.eggs.length + pulls > 99) { if (!freePulls && globalScene.gameData.eggs.length + pulls > 99) {
errorKey = "egg:tooManyEggs"; errorKey = "egg:tooManyEggs";
} else if (!freePulls && !globalScene.gameData.voucherCounts[voucherType]) { } else if (!freePulls && globalScene.gameData.voucherCounts[voucherType] < vouchersConsumed) {
errorKey = "egg:notEnoughVouchers"; errorKey = "egg:notEnoughVouchers";
} }

View File

@ -136,6 +136,11 @@ export abstract class FormModalUiHandler extends ModalUiHandler {
this.submitAction = config.buttonActions.length ? config.buttonActions[0] : null; this.submitAction = config.buttonActions.length ? config.buttonActions[0] : null;
this.cancelAction = config.buttonActions[1] ?? null; this.cancelAction = config.buttonActions[1] ?? null;
// Auto focus the first input field after a short delay, to prevent accidental inputs
setTimeout(() => {
this.inputs[0].setFocus();
}, 50);
// #region: Override button pointerDown // #region: Override button pointerDown
// Override the pointerDown event for the buttonBgs to call the `submitAction` and `cancelAction` // Override the pointerDown event for the buttonBgs to call the `submitAction` and `cancelAction`
// properties that we set above, allowing their behavior to change after this method terminates // properties that we set above, allowing their behavior to change after this method terminates

View File

@ -563,7 +563,7 @@ export class PartyUiHandler extends MessageUiHandler {
const ui = this.getUi(); const ui = this.getUi();
const option = this.options[this.optionsCursor]; const option = this.options[this.optionsCursor];
if (option === PartyOption.TRANSFER) { if (this.transferMode && option === PartyOption.TRANSFER) {
return this.processTransferOption(); return this.processTransferOption();
} }
@ -613,6 +613,20 @@ export class PartyUiHandler extends MessageUiHandler {
ui.playSelect(); ui.playSelect();
return true; return true;
} }
if (option === PartyOption.SUMMARY) {
return this.processSummaryOption(pokemon);
}
if (option === PartyOption.POKEDEX) {
return this.processPokedexOption(pokemon);
}
if (option === PartyOption.UNPAUSE_EVOLUTION) {
return this.processUnpauseEvolutionOption(pokemon);
}
if (option === PartyOption.RENAME) {
return this.processRenameOption(pokemon);
}
return false; return false;
} }
@ -1021,7 +1035,8 @@ export class PartyUiHandler extends MessageUiHandler {
} }
// Toggle item transfer mode to discard items or vice versa // Toggle item transfer mode to discard items or vice versa
if (this.cursor === 7) { // Prevent changing mode, when currently transfering an item
if (this.cursor === 7 && !this.transferMode) {
switch (this.partyUiMode) { switch (this.partyUiMode) {
case PartyUiMode.DISCARD: case PartyUiMode.DISCARD:
this.partyUiMode = PartyUiMode.MODIFIER_TRANSFER; this.partyUiMode = PartyUiMode.MODIFIER_TRANSFER;
@ -1609,7 +1624,7 @@ export class PartyUiHandler extends MessageUiHandler {
const modifier = formChangeItemModifiers[option - PartyOption.FORM_CHANGE_ITEM]; const modifier = formChangeItemModifiers[option - PartyOption.FORM_CHANGE_ITEM];
optionName = `${modifier.active ? i18next.t("partyUiHandler:deactivate") : i18next.t("partyUiHandler:activate")} ${modifier.type.name}`; optionName = `${modifier.active ? i18next.t("partyUiHandler:deactivate") : i18next.t("partyUiHandler:activate")} ${modifier.type.name}`;
} else if (option === PartyOption.UNPAUSE_EVOLUTION) { } else if (option === PartyOption.UNPAUSE_EVOLUTION) {
optionName = `${pokemon.pauseEvolutions ? i18next.t("partyUiHandler:unpausedEvolution") : i18next.t("partyUiHandler:pauseEvolution")}`; optionName = `${pokemon.pauseEvolutions ? i18next.t("partyUiHandler:unpauseEvolution") : i18next.t("partyUiHandler:pauseEvolution")}`;
} else { } else {
if (this.localizedOptions.includes(option)) { if (this.localizedOptions.includes(option)) {
optionName = i18next.t(`partyUiHandler:${toCamelCase(PartyOption[option])}`); optionName = i18next.t(`partyUiHandler:${toCamelCase(PartyOption[option])}`);
@ -2040,12 +2055,13 @@ class PartySlot extends Phaser.GameObjects.Container {
if (this.pokemon.isShiny()) { if (this.pokemon.isShiny()) {
const doubleShiny = this.pokemon.isDoubleShiny(false); const doubleShiny = this.pokemon.isDoubleShiny(false);
const largeIconTint = doubleShiny ? this.pokemon.getBaseVariant() : this.pokemon.getVariant();
const shinyStar = globalScene.add const shinyStar = globalScene.add
.image(0, 0, `shiny_star_small${doubleShiny ? "_1" : ""}`) .image(0, 0, `shiny_star_small${doubleShiny ? "_1" : ""}`)
.setOrigin(0) .setOrigin(0)
.setPositionRelative(this.slotName, shinyIconToNameOffset.x, shinyIconToNameOffset.y) .setPositionRelative(this.slotName, shinyIconToNameOffset.x, shinyIconToNameOffset.y)
.setTint(getVariantTint(this.pokemon.getBaseVariant())); .setTint(getVariantTint(largeIconTint));
slotInfoContainer.add(shinyStar); slotInfoContainer.add(shinyStar);
if (doubleShiny) { if (doubleShiny) {

View File

@ -106,10 +106,6 @@ export class PokedexScanUiHandler extends FormModalUiHandler {
this.reduceKeys(); this.reduceKeys();
setTimeout(() => {
input.setFocus(); // Focus after a short delay to avoid unwanted input
}, 50);
input.on("keydown", (inputObject, evt: KeyboardEvent) => { input.on("keydown", (inputObject, evt: KeyboardEvent) => {
if ( if (
["escape", "space"].some(v => v === evt.key.toLowerCase() || v === evt.code.toLowerCase()) && ["escape", "space"].some(v => v === evt.key.toLowerCase() || v === evt.code.toLowerCase()) &&

View File

@ -181,7 +181,7 @@ export class SaveSlotSelectUiHandler extends MessageUiHandler {
ui.setOverlayMode( ui.setOverlayMode(
UiMode.CONFIRM, UiMode.CONFIRM,
() => { () => {
globalScene.gameData.tryClearSession(cursor).then(response => { globalScene.gameData.deleteSession(cursor).then(response => {
if (response[0] === false) { if (response[0] === false) {
globalScene.reset(true); globalScene.reset(true);
} else { } else {

View File

@ -9,9 +9,9 @@ export const TOUCH_CONTROL_POSITIONS_PORTRAIT = "touchControlPositionsPortrait";
type ControlPosition = { id: string; x: number; y: number }; type ControlPosition = { id: string; x: number; y: number };
type ConfigurationEventListeners = { type ConfigurationEventListeners = {
touchstart: EventListener[]; pointerdown: EventListener[];
touchmove: EventListener[]; pointermove: EventListener[];
touchend: EventListener[]; pointerup: EventListener[];
}; };
type ToolbarRefs = { type ToolbarRefs = {
@ -39,9 +39,9 @@ export class MoveTouchControlsHandler {
* These are used to remove the event listeners when the configuration mode is disabled. * These are used to remove the event listeners when the configuration mode is disabled.
*/ */
private configurationEventListeners: ConfigurationEventListeners = { private configurationEventListeners: ConfigurationEventListeners = {
touchstart: [], pointerdown: [],
touchmove: [], pointermove: [],
touchend: [], pointerup: [],
}; };
private overlay: Phaser.GameObjects.Container; private overlay: Phaser.GameObjects.Container;
@ -165,34 +165,33 @@ export class MoveTouchControlsHandler {
/** /**
* Start dragging the given button. * Start dragging the given button.
* @param controlGroup The button that is being dragged. * @param controlGroup The button that is being dragged.
* @param touch The touch event that started the drag. * @param event The pointer event that started the drag.
*/ */
private startDrag = (controlGroup: HTMLElement): void => { private startDrag = (controlGroup: HTMLElement): void => {
this.draggingElement = controlGroup; this.draggingElement = controlGroup;
}; };
/** /**
* Drags the currently dragged element to the given touch position. * Drags the currently dragged element to the given pointer position.
* @param touch The touch event that is currently happening. * @param event The pointer event that is currently happening.
* @param isLeft Whether the dragged element is a left button.
*/ */
private drag = (touch: Touch): void => { private drag = (event: PointerEvent): void => {
if (!this.draggingElement) { if (!this.draggingElement) {
return; return;
} }
const rect = this.draggingElement.getBoundingClientRect(); const rect = this.draggingElement.getBoundingClientRect();
// Map the touch position to the center of the dragged element. // Map the pointer position to the center of the dragged element.
const xOffset = this.isLeft(this.draggingElement) const xOffset = this.isLeft(this.draggingElement)
? touch.clientX - rect.width / 2 ? event.clientX - rect.width / 2
: window.innerWidth - touch.clientX - rect.width / 2; : window.innerWidth - event.clientX - rect.width / 2;
const yOffset = window.innerHeight - touch.clientY - rect.height / 2; const yOffset = window.innerHeight - event.clientY - rect.height / 2;
this.setPosition(this.draggingElement, xOffset, yOffset); this.setPosition(this.draggingElement, xOffset, yOffset);
}; };
/** /**
* Stops dragging the currently dragged element. * Stops dragging the currently dragged element.
*/ */
private stopDrag = () => { private stopDrag = (): void => {
this.draggingElement = null; this.draggingElement = null;
}; };
@ -303,19 +302,19 @@ export class MoveTouchControlsHandler {
*/ */
private createConfigurationEventListeners(controlGroups: HTMLDivElement[]): ConfigurationEventListeners { private createConfigurationEventListeners(controlGroups: HTMLDivElement[]): ConfigurationEventListeners {
return { return {
touchstart: controlGroups.map((element: HTMLDivElement) => { pointerdown: controlGroups.map((element: HTMLDivElement) => {
const startDrag = () => this.startDrag(element); const startDrag = () => this.startDrag(element);
element.addEventListener("touchstart", startDrag, { passive: true }); element.addEventListener("pointerdown", startDrag, { passive: true });
return startDrag; return startDrag;
}), }),
touchmove: controlGroups.map(() => { pointermove: controlGroups.map(() => {
const drag = event => this.drag(event.touches[0]); const drag = (event: PointerEvent) => this.drag(event);
window.addEventListener("touchmove", drag, { passive: true }); window.addEventListener("pointermove", drag, { passive: true });
return drag; return drag;
}), }),
touchend: controlGroups.map(() => { pointerup: controlGroups.map(() => {
const stopDrag = () => this.stopDrag(); const stopDrag = () => this.stopDrag();
window.addEventListener("touchend", stopDrag, { passive: true }); window.addEventListener("pointerup", stopDrag, { passive: true });
return stopDrag; return stopDrag;
}), }),
}; };
@ -373,12 +372,12 @@ export class MoveTouchControlsHandler {
this.draggingElement = null; this.draggingElement = null;
// Remove event listeners // Remove event listeners
const { touchstart, touchmove, touchend } = this.configurationEventListeners; const { pointerdown, pointermove, pointerup } = this.configurationEventListeners;
this.getControlGroupElements().forEach((element, index) => this.getControlGroupElements().forEach((element, index) =>
element.removeEventListener("touchstart", touchstart[index]), element.removeEventListener("pointerdown", pointerdown[index]),
); );
touchmove.forEach(listener => window.removeEventListener("touchmove", listener)); pointermove.forEach(listener => window.removeEventListener("pointermove", listener));
touchend.forEach(listener => window.removeEventListener("touchend", listener)); pointerup.forEach(listener => window.removeEventListener("pointerup", listener));
// Remove configuration toolbar // Remove configuration toolbar
const toolbar = document.querySelector("#touchControls #configToolbar"); const toolbar = document.querySelector("#touchControls #configToolbar");

View File

@ -72,7 +72,7 @@ import {
rgbHexToRgba, rgbHexToRgba,
} from "#utils/common"; } from "#utils/common";
import type { StarterPreferences } from "#utils/data"; import type { StarterPreferences } from "#utils/data";
import { loadStarterPreferences, saveStarterPreferences } from "#utils/data"; import { deepCopy, loadStarterPreferences, saveStarterPreferences } from "#utils/data";
import { getPokemonSpeciesForm, getPokerusStarters } from "#utils/pokemon-utils"; import { getPokemonSpeciesForm, getPokerusStarters } from "#utils/pokemon-utils";
import { toCamelCase, toTitleCase } from "#utils/strings"; import { toCamelCase, toTitleCase } from "#utils/strings";
import { argbFromRgba } from "@material/material-color-utilities"; import { argbFromRgba } from "@material/material-color-utilities";
@ -1148,7 +1148,8 @@ export class StarterSelectUiHandler extends MessageUiHandler {
this.starterSelectContainer.setVisible(true); this.starterSelectContainer.setVisible(true);
this.starterPreferences = loadStarterPreferences(); this.starterPreferences = loadStarterPreferences();
this.originalStarterPreferences = loadStarterPreferences(); // Deep copy the JSON (avoid re-loading from disk)
this.originalStarterPreferences = deepCopy(this.starterPreferences);
this.allSpecies.forEach((species, s) => { this.allSpecies.forEach((species, s) => {
const icon = this.starterContainers[s].icon; const icon = this.starterContainers[s].icon;
@ -1212,6 +1213,8 @@ export class StarterSelectUiHandler extends MessageUiHandler {
preferences: StarterPreferences, preferences: StarterPreferences,
ignoreChallenge = false, ignoreChallenge = false,
): StarterAttributes { ): StarterAttributes {
// if preferences for the species is undefined, set it to an empty object
preferences[species.speciesId] ??= {};
const starterAttributes = preferences[species.speciesId]; const starterAttributes = preferences[species.speciesId];
const { dexEntry, starterDataEntry: starterData } = this.getSpeciesData(species.speciesId, !ignoreChallenge); const { dexEntry, starterDataEntry: starterData } = this.getSpeciesData(species.speciesId, !ignoreChallenge);
@ -1828,9 +1831,15 @@ export class StarterSelectUiHandler extends MessageUiHandler {
// The persistent starter data to apply e.g. candy upgrades // The persistent starter data to apply e.g. candy upgrades
const persistentStarterData = globalScene.gameData.starterData[this.lastSpecies.speciesId]; const persistentStarterData = globalScene.gameData.starterData[this.lastSpecies.speciesId];
// The sanitized starter preferences // The sanitized starter preferences
let starterAttributes = this.starterPreferences[this.lastSpecies.speciesId]; if (this.starterPreferences[this.lastSpecies.speciesId] === undefined) {
// The original starter preferences this.starterPreferences[this.lastSpecies.speciesId] = {};
const originalStarterAttributes = this.originalStarterPreferences[this.lastSpecies.speciesId]; }
if (this.originalStarterPreferences[this.lastSpecies.speciesId] === undefined) {
this.originalStarterPreferences[this.lastSpecies.speciesId] = {};
}
// Bangs are safe here due to the above check
const starterAttributes = this.starterPreferences[this.lastSpecies.speciesId]!;
const originalStarterAttributes = this.originalStarterPreferences[this.lastSpecies.speciesId]!;
// this gets the correct pokemon cursor depending on whether you're in the starter screen or the party icons // this gets the correct pokemon cursor depending on whether you're in the starter screen or the party icons
if (!this.starterIconsCursorObj.visible) { if (!this.starterIconsCursorObj.visible) {
@ -2050,10 +2059,6 @@ export class StarterSelectUiHandler extends MessageUiHandler {
const option: OptionSelectItem = { const option: OptionSelectItem = {
label: getNatureName(n, true, true, true, globalScene.uiTheme), label: getNatureName(n, true, true, true, globalScene.uiTheme),
handler: () => { handler: () => {
// update default nature in starter save data
if (!starterAttributes) {
starterAttributes = this.starterPreferences[this.lastSpecies.speciesId] = {};
}
starterAttributes.nature = n; starterAttributes.nature = n;
originalStarterAttributes.nature = starterAttributes.nature; originalStarterAttributes.nature = starterAttributes.nature;
this.clearText(); this.clearText();
@ -2095,28 +2100,22 @@ export class StarterSelectUiHandler extends MessageUiHandler {
const passiveAttr = starterData.passiveAttr; const passiveAttr = starterData.passiveAttr;
if (passiveAttr & PassiveAttr.UNLOCKED) { if (passiveAttr & PassiveAttr.UNLOCKED) {
// this is for enabling and disabling the passive // this is for enabling and disabling the passive
if (!(passiveAttr & PassiveAttr.ENABLED)) { const label = i18next.t(
passiveAttr & PassiveAttr.ENABLED
? "starterSelectUiHandler:disablePassive"
: "starterSelectUiHandler:enablePassive",
);
options.push({ options.push({
label: i18next.t("starterSelectUiHandler:enablePassive"), label,
handler: () => {
starterData.passiveAttr |= PassiveAttr.ENABLED;
ui.setMode(UiMode.STARTER_SELECT);
this.setSpeciesDetails(this.lastSpecies);
return true;
},
});
} else {
options.push({
label: i18next.t("starterSelectUiHandler:disablePassive"),
handler: () => { handler: () => {
starterData.passiveAttr ^= PassiveAttr.ENABLED; starterData.passiveAttr ^= PassiveAttr.ENABLED;
persistentStarterData.passiveAttr ^= PassiveAttr.ENABLED;
ui.setMode(UiMode.STARTER_SELECT); ui.setMode(UiMode.STARTER_SELECT);
this.setSpeciesDetails(this.lastSpecies); this.setSpeciesDetails(this.lastSpecies);
return true; return true;
}, },
}); });
} }
}
// if container.favorite is false, show the favorite option // if container.favorite is false, show the favorite option
const isFavorite = starterAttributes?.favorite ?? false; const isFavorite = starterAttributes?.favorite ?? false;
if (!isFavorite) { if (!isFavorite) {
@ -2391,6 +2390,10 @@ export class StarterSelectUiHandler extends MessageUiHandler {
const newVariant = starterAttributes.variant const newVariant = starterAttributes.variant
? (starterAttributes.variant as Variant) ? (starterAttributes.variant as Variant)
: newProps.variant; : newProps.variant;
starterAttributes.shiny = true;
originalStarterAttributes.shiny = true;
starterAttributes.variant = newVariant;
originalStarterAttributes.variant = newVariant;
this.setSpeciesDetails(this.lastSpecies, { this.setSpeciesDetails(this.lastSpecies, {
shiny: true, shiny: true,
variant: newVariant, variant: newVariant,
@ -2400,9 +2403,6 @@ export class StarterSelectUiHandler extends MessageUiHandler {
// Cycle tint based on current sprite tint // Cycle tint based on current sprite tint
const tint = getVariantTint(newVariant); const tint = getVariantTint(newVariant);
this.pokemonShinyIcon.setFrame(getVariantIcon(newVariant)).setTint(tint).setVisible(true); this.pokemonShinyIcon.setFrame(getVariantIcon(newVariant)).setTint(tint).setVisible(true);
starterAttributes.shiny = true;
originalStarterAttributes.shiny = true;
} else { } else {
// If shiny, we update the variant // If shiny, we update the variant
let newVariant = props.variant; let newVariant = props.variant;
@ -2429,14 +2429,14 @@ export class StarterSelectUiHandler extends MessageUiHandler {
originalStarterAttributes.variant = newVariant; originalStarterAttributes.variant = newVariant;
if (this.speciesStarterDexEntry!.caughtAttr & DexAttr.NON_SHINY && newVariant <= props.variant) { if (this.speciesStarterDexEntry!.caughtAttr & DexAttr.NON_SHINY && newVariant <= props.variant) {
// If we have run out of variants, go back to non shiny // If we have run out of variants, go back to non shiny
starterAttributes.shiny = false;
originalStarterAttributes.shiny = false;
this.setSpeciesDetails(this.lastSpecies, { this.setSpeciesDetails(this.lastSpecies, {
shiny: false, shiny: false,
variant: 0, variant: 0,
}); });
this.pokemonShinyIcon.setVisible(false); this.pokemonShinyIcon.setVisible(false);
success = true; success = true;
starterAttributes.shiny = false;
originalStarterAttributes.shiny = false;
} else { } else {
// If going to a higher variant, or only shiny forms are caught, go to next variant // If going to a higher variant, or only shiny forms are caught, go to next variant
this.setSpeciesDetails(this.lastSpecies, { this.setSpeciesDetails(this.lastSpecies, {
@ -3413,8 +3413,9 @@ export class StarterSelectUiHandler extends MessageUiHandler {
if (species) { if (species) {
const defaultDexAttr = this.getCurrentDexProps(species.speciesId); const defaultDexAttr = this.getCurrentDexProps(species.speciesId);
const defaultProps = globalScene.gameData.getSpeciesDexAttrProps(species, defaultDexAttr); const defaultProps = globalScene.gameData.getSpeciesDexAttrProps(species, defaultDexAttr);
// Bang is correct due to the `?` before variant
const variant = this.starterPreferences[species.speciesId]?.variant const variant = this.starterPreferences[species.speciesId]?.variant
? (this.starterPreferences[species.speciesId].variant as Variant) ? (this.starterPreferences[species.speciesId]!.variant as Variant)
: defaultProps.variant; : defaultProps.variant;
const tint = getVariantTint(variant); const tint = getVariantTint(variant);
this.pokemonShinyIcon.setFrame(getVariantIcon(variant)).setTint(tint); this.pokemonShinyIcon.setFrame(getVariantIcon(variant)).setTint(tint);
@ -3639,7 +3640,9 @@ export class StarterSelectUiHandler extends MessageUiHandler {
if (starterIndex > -1) { if (starterIndex > -1) {
props = globalScene.gameData.getSpeciesDexAttrProps(species, this.starterAttr[starterIndex]); props = globalScene.gameData.getSpeciesDexAttrProps(species, this.starterAttr[starterIndex]);
this.setSpeciesDetails(species, { this.setSpeciesDetails(
species,
{
shiny: props.shiny, shiny: props.shiny,
formIndex: props.formIndex, formIndex: props.formIndex,
female: props.female, female: props.female,
@ -3647,7 +3650,9 @@ export class StarterSelectUiHandler extends MessageUiHandler {
abilityIndex: this.starterAbilityIndexes[starterIndex], abilityIndex: this.starterAbilityIndexes[starterIndex],
natureIndex: this.starterNatures[starterIndex], natureIndex: this.starterNatures[starterIndex],
teraType: this.starterTeras[starterIndex], teraType: this.starterTeras[starterIndex],
}); },
false,
);
} else { } else {
const defaultAbilityIndex = const defaultAbilityIndex =
starterAttributes?.ability ?? globalScene.gameData.getStarterSpeciesDefaultAbilityIndex(species); starterAttributes?.ability ?? globalScene.gameData.getStarterSpeciesDefaultAbilityIndex(species);
@ -3664,7 +3669,9 @@ export class StarterSelectUiHandler extends MessageUiHandler {
props.formIndex = starterAttributes?.form ?? props.formIndex; props.formIndex = starterAttributes?.form ?? props.formIndex;
props.female = starterAttributes?.female ?? props.female; props.female = starterAttributes?.female ?? props.female;
this.setSpeciesDetails(species, { this.setSpeciesDetails(
species,
{
shiny: props.shiny, shiny: props.shiny,
formIndex: props.formIndex, formIndex: props.formIndex,
female: props.female, female: props.female,
@ -3672,7 +3679,9 @@ export class StarterSelectUiHandler extends MessageUiHandler {
abilityIndex: defaultAbilityIndex, abilityIndex: defaultAbilityIndex,
natureIndex: defaultNature, natureIndex: defaultNature,
teraType: starterAttributes?.tera, teraType: starterAttributes?.tera,
}); },
false,
);
} }
if (!isNullOrUndefined(props.formIndex)) { if (!isNullOrUndefined(props.formIndex)) {
@ -3709,7 +3718,9 @@ export class StarterSelectUiHandler extends MessageUiHandler {
const defaultNature = globalScene.gameData.getSpeciesDefaultNature(species); const defaultNature = globalScene.gameData.getSpeciesDefaultNature(species);
const props = globalScene.gameData.getSpeciesDexAttrProps(species, defaultDexAttr); const props = globalScene.gameData.getSpeciesDexAttrProps(species, defaultDexAttr);
this.setSpeciesDetails(species, { this.setSpeciesDetails(
species,
{
shiny: props.shiny, shiny: props.shiny,
formIndex: props.formIndex, formIndex: props.formIndex,
female: props.female, female: props.female,
@ -3717,7 +3728,9 @@ export class StarterSelectUiHandler extends MessageUiHandler {
abilityIndex: defaultAbilityIndex, abilityIndex: defaultAbilityIndex,
natureIndex: defaultNature, natureIndex: defaultNature,
forSeen: true, forSeen: true,
}); },
false,
);
this.pokemonSprite.setTint(0x808080); this.pokemonSprite.setTint(0x808080);
} }
} else { } else {
@ -3739,7 +3752,9 @@ export class StarterSelectUiHandler extends MessageUiHandler {
this.pokemonFormText.setVisible(false); this.pokemonFormText.setVisible(false);
this.teraIcon.setVisible(false); this.teraIcon.setVisible(false);
this.setSpeciesDetails(species!, { this.setSpeciesDetails(
species!,
{
// TODO: is this bang correct? // TODO: is this bang correct?
shiny: false, shiny: false,
formIndex: 0, formIndex: 0,
@ -3747,7 +3762,9 @@ export class StarterSelectUiHandler extends MessageUiHandler {
variant: 0, variant: 0,
abilityIndex: 0, abilityIndex: 0,
natureIndex: 0, natureIndex: 0,
}); },
false,
);
this.pokemonSprite.clearTint(); this.pokemonSprite.clearTint();
} }
} }
@ -3769,7 +3786,7 @@ export class StarterSelectUiHandler extends MessageUiHandler {
return { dexEntry: { ...copiedDexEntry }, starterDataEntry: { ...copiedStarterDataEntry } }; return { dexEntry: { ...copiedDexEntry }, starterDataEntry: { ...copiedStarterDataEntry } };
} }
setSpeciesDetails(species: PokemonSpecies, options: SpeciesDetails = {}): void { setSpeciesDetails(species: PokemonSpecies, options: SpeciesDetails = {}, save = true): void {
let { shiny, formIndex, female, variant, abilityIndex, natureIndex, teraType } = options; let { shiny, formIndex, female, variant, abilityIndex, natureIndex, teraType } = options;
const forSeen: boolean = options.forSeen ?? false; const forSeen: boolean = options.forSeen ?? false;
const oldProps = species ? globalScene.gameData.getSpeciesDexAttrProps(species, this.dexAttrCursor) : null; const oldProps = species ? globalScene.gameData.getSpeciesDexAttrProps(species, this.dexAttrCursor) : null;
@ -4181,8 +4198,10 @@ export class StarterSelectUiHandler extends MessageUiHandler {
this.updateInstructions(); this.updateInstructions();
if (save) {
saveStarterPreferences(this.originalStarterPreferences); saveStarterPreferences(this.originalStarterPreferences);
} }
}
setTypeIcons(type1: PokemonType | null, type2: PokemonType | null): void { setTypeIcons(type1: PokemonType | null, type2: PokemonType | null): void {
if (type1 !== null) { if (type1 !== null) {
@ -4617,6 +4636,8 @@ export class StarterSelectUiHandler extends MessageUiHandler {
clear(): void { clear(): void {
super.clear(); super.clear();
saveStarterPreferences(this.originalStarterPreferences);
this.clearStarterPreferences(); this.clearStarterPreferences();
this.cursor = -1; this.cursor = -1;
this.hideInstructions(); this.hideInstructions();

View File

@ -430,20 +430,21 @@ export class SummaryUiHandler extends UiHandler {
this.friendshipShadow.setCrop(0, 0, 16, 16 - 16 * ((this.pokemon?.friendship || 0) / 255)); this.friendshipShadow.setCrop(0, 0, 16, 16 - 16 * ((this.pokemon?.friendship || 0) / 255));
const doubleShiny = this.pokemon.isDoubleShiny(false); const doubleShiny = this.pokemon.isDoubleShiny(false);
const baseVariant = this.pokemon.getBaseVariant(doubleShiny); const bigIconVariant = doubleShiny ? this.pokemon.getBaseVariant(doubleShiny) : this.pokemon.getVariant();
this.shinyIcon.setPositionRelative( this.shinyIcon.setPositionRelative(
this.nameText, this.nameText,
this.nameText.displayWidth + (this.splicedIcon.visible ? this.splicedIcon.displayWidth + 1 : 0) + 1, this.nameText.displayWidth + (this.splicedIcon.visible ? this.splicedIcon.displayWidth + 1 : 0) + 1,
3, 3,
); );
this.shinyIcon.setTexture(`shiny_star${doubleShiny ? "_1" : ""}`); this.shinyIcon
this.shinyIcon.setVisible(this.pokemon.isShiny(false)); .setTexture(`shiny_star${doubleShiny ? "_1" : ""}`)
this.shinyIcon.setTint(getVariantTint(baseVariant)); .setVisible(this.pokemon.isShiny(false))
.setTint(getVariantTint(bigIconVariant));
if (this.shinyIcon.visible) { if (this.shinyIcon.visible) {
let shinyDescriptor = ""; let shinyDescriptor = "";
if (doubleShiny || baseVariant) { if (doubleShiny || bigIconVariant) {
shinyDescriptor = " (" + getShinyDescriptor(baseVariant); shinyDescriptor = " (" + getShinyDescriptor(bigIconVariant);
if (doubleShiny) { if (doubleShiny) {
shinyDescriptor += "/" + getShinyDescriptor(this.pokemon.fusionVariant); shinyDescriptor += "/" + getShinyDescriptor(this.pokemon.fusionVariant);
} }

View File

@ -349,6 +349,15 @@ export function getTextStyleOptions(
styleOptions.fontSize = defaultFontSize - 42; styleOptions.fontSize = defaultFontSize - 42;
styleOptions.padding = { top: 4 }; styleOptions.padding = { top: 4 };
break; break;
case "ko":
styleOptions.fontSize = defaultFontSize - 38;
styleOptions.padding = { top: 4, left: 6 };
break;
case "zh-CN":
case "zh-TW":
styleOptions.fontSize = defaultFontSize - 42;
styleOptions.padding = { top: 5, left: 14 };
break;
default: default:
styleOptions.fontSize = defaultFontSize - 30; styleOptions.fontSize = defaultFontSize - 30;
styleOptions.padding = { left: 12 }; styleOptions.padding = { left: 12 };

View File

@ -8,7 +8,7 @@ import { AES, enc } from "crypto-js";
* @param values - The object to be deep copied. * @param values - The object to be deep copied.
* @returns A new object that is a deep copy of the input. * @returns A new object that is a deep copy of the input.
*/ */
export function deepCopy(values: object): object { export function deepCopy<T extends object>(values: T): T {
// Convert the object to a JSON string and parse it back to an object to perform a deep copy // Convert the object to a JSON string and parse it back to an object to perform a deep copy
return JSON.parse(JSON.stringify(values)); return JSON.parse(JSON.stringify(values));
} }
@ -58,13 +58,28 @@ export function decrypt(data: string, bypassLogin: boolean): string {
return AES.decrypt(data, saveKey).toString(enc.Utf8); return AES.decrypt(data, saveKey).toString(enc.Utf8);
} }
/**
* Check if an object has no properties of its own (its shape is `{}`). An empty array is considered a bare object.
* @param obj - Object to check
* @returns - Whether the object is bare
*/
export function isBareObject(obj: any): boolean {
if (typeof obj !== "object") {
return false;
}
for (const _ in obj) {
return false;
}
return true;
}
// the latest data saved/loaded for the Starter Preferences. Required to reduce read/writes. Initialize as "{}", since this is the default value and no data needs to be stored if present. // the latest data saved/loaded for the Starter Preferences. Required to reduce read/writes. Initialize as "{}", since this is the default value and no data needs to be stored if present.
// if they ever add private static variables, move this into StarterPrefs // if they ever add private static variables, move this into StarterPrefs
const StarterPrefers_DEFAULT: string = "{}"; const StarterPrefers_DEFAULT: string = "{}";
let StarterPrefers_private_latest: string = StarterPrefers_DEFAULT; let StarterPrefers_private_latest: string = StarterPrefers_DEFAULT;
export interface StarterPreferences { export interface StarterPreferences {
[key: number]: StarterAttributes; [key: number]: StarterAttributes | undefined;
} }
// called on starter selection show once // called on starter selection show once
@ -74,11 +89,17 @@ export function loadStarterPreferences(): StarterPreferences {
localStorage.getItem(`starterPrefs_${loggedInUser?.username}`) || StarterPrefers_DEFAULT), localStorage.getItem(`starterPrefs_${loggedInUser?.username}`) || StarterPrefers_DEFAULT),
); );
} }
// called on starter selection clear, always
export function saveStarterPreferences(prefs: StarterPreferences): void { export function saveStarterPreferences(prefs: StarterPreferences): void {
const pStr: string = JSON.stringify(prefs); // Fastest way to check if an object has any properties (does no allocation)
if (isBareObject(prefs)) {
console.warn("Refusing to save empty starter preferences");
return;
}
// no reason to store `{}` (for starters not customized)
const pStr: string = JSON.stringify(prefs, (_, value) => (isBareObject(value) ? undefined : value));
if (pStr !== StarterPrefers_private_latest) { if (pStr !== StarterPrefers_private_latest) {
console.log("%cSaving starter preferences", "color: blue");
// something changed, store the update // something changed, store the update
localStorage.setItem(`starterPrefs_${loggedInUser?.username}`, pStr); localStorage.setItem(`starterPrefs_${loggedInUser?.username}`, pStr);
// update the latest prefs // update the latest prefs

View File

@ -12,6 +12,6 @@ import i18next from "i18next";
* not supporting arrays in any capacity. * not supporting arrays in any capacity.
*/ */
export function getRandomLocaleEntry(key: string): [key: string, value: string] { export function getRandomLocaleEntry(key: string): [key: string, value: string] {
const keyName = `${key}.${randSeedItem(Object.keys(i18next.t("key", { returnObjects: true })))}`; const keyName = `${key}.${randSeedItem(Object.keys(i18next.t(key, { returnObjects: true })))}`;
return [keyName, i18next.t(keyName)]; return [keyName, i18next.t(keyName)];
} }

View File

@ -35,13 +35,43 @@ describe("Abilities - Intimidate", () => {
it("should lower all opponents' ATK by 1 stage on entry and switch", async () => { it("should lower all opponents' ATK by 1 stage on entry and switch", async () => {
await game.classicMode.startBattle([SpeciesId.MIGHTYENA, SpeciesId.POOCHYENA]); await game.classicMode.startBattle([SpeciesId.MIGHTYENA, SpeciesId.POOCHYENA]);
const [mightyena, poochyena] = game.scene.getPlayerParty();
const enemy = game.field.getEnemyPokemon(); const enemy = game.field.getEnemyPokemon();
expect(enemy.getStatStage(Stat.ATK)).toBe(-1); expect(enemy.getStatStage(Stat.ATK)).toBe(-1);
expect(mightyena).toHaveAbilityApplied(AbilityId.INTIMIDATE);
game.doSwitchPokemon(1); game.doSwitchPokemon(1);
await game.toNextTurn(); await game.toNextTurn();
expect(poochyena.isActive()).toBe(true);
expect(enemy.getStatStage(Stat.ATK)).toBe(-2); expect(enemy.getStatStage(Stat.ATK)).toBe(-2);
expect(poochyena).toHaveAbilityApplied(AbilityId.INTIMIDATE);
});
it("should trigger once on initial switch prompt without cancelling opposing abilities", async () => {
await game.classicMode.runToSummon([SpeciesId.MIGHTYENA, SpeciesId.POOCHYENA]);
await game.classicMode.startBattleWithSwitch(1);
const [poochyena, mightyena] = game.scene.getPlayerParty();
expect(poochyena.species.speciesId).toBe(SpeciesId.POOCHYENA);
const enemy = game.field.getEnemyPokemon();
expect(enemy).toHaveStatStage(Stat.ATK, -1);
expect(poochyena).toHaveStatStage(Stat.ATK, -1);
expect(poochyena).toHaveAbilityApplied(AbilityId.INTIMIDATE);
expect(mightyena).not.toHaveAbilityApplied(AbilityId.INTIMIDATE);
});
it("should activate on reload with single party", async () => {
await game.classicMode.startBattle([SpeciesId.MIGHTYENA]);
expect(game.field.getEnemyPokemon()).toHaveStatStage(Stat.ATK, -1);
await game.reload.reloadSession();
expect(game.field.getEnemyPokemon()).toHaveStatStage(Stat.ATK, -1);
}); });
it("should lower ATK of all opponents in a double battle", async () => { it("should lower ATK of all opponents in a double battle", async () => {

View File

@ -175,4 +175,27 @@ describe("Evolution", () => {
expect(fourForm.evoFormKey).toBe("four"); // meanwhile, according to the pokemon-forms, the evoFormKey for a 4 family maushold is "four" expect(fourForm.evoFormKey).toBe("four"); // meanwhile, according to the pokemon-forms, the evoFormKey for a 4 family maushold is "four"
} }
}); });
it("tyrogue should evolve if move is not in first slot", async () => {
game.override
.moveset([MoveId.TACKLE, MoveId.RAPID_SPIN, MoveId.LOW_KICK])
.enemySpecies(SpeciesId.GOLEM)
.enemyMoveset(MoveId.SPLASH)
.startingWave(41)
.startingLevel(19)
.enemyLevel(30);
await game.classicMode.startBattle([SpeciesId.TYROGUE]);
const tyrogue = game.field.getPlayerPokemon();
const golem = game.field.getEnemyPokemon();
golem.hp = 1;
expect(golem.hp).toBe(1);
game.move.select(MoveId.TACKLE);
await game.phaseInterceptor.to("EndEvolutionPhase");
expect(tyrogue.species.speciesId).toBe(SpeciesId.HITMONTOP);
});
}); });

View File

@ -2,7 +2,6 @@ import { AbilityId } from "#enums/ability-id";
import { MoveId } from "#enums/move-id"; import { MoveId } from "#enums/move-id";
import { SpeciesId } from "#enums/species-id"; import { SpeciesId } from "#enums/species-id";
import { DamageAnimPhase } from "#phases/damage-anim-phase"; import { DamageAnimPhase } from "#phases/damage-anim-phase";
import { TurnEndPhase } from "#phases/turn-end-phase";
import { GameManager } from "#test/test-utils/game-manager"; import { GameManager } from "#test/test-utils/game-manager";
import Phaser from "phaser"; import Phaser from "phaser";
import { afterEach, beforeAll, beforeEach, describe, expect, it } from "vitest"; import { afterEach, beforeAll, beforeEach, describe, expect, it } from "vitest";
@ -54,7 +53,7 @@ describe("Items - Leftovers", () => {
const leadHpAfterDamage = leadPokemon.hp; const leadHpAfterDamage = leadPokemon.hp;
// Check if leftovers heal us // Check if leftovers heal us
await game.phaseInterceptor.to(TurnEndPhase); await game.phaseInterceptor.to("PokemonHealPhase");
expect(leadPokemon.hp).toBeGreaterThan(leadHpAfterDamage); expect(leadPokemon.hp).toBeGreaterThan(leadHpAfterDamage);
}); });
}); });

Some files were not shown because too many files have changed in this diff Show More