Compare commits

...

86 Commits

Author SHA1 Message Date
NightKev
7b56e5b90d
Merge 8f546ce4bb into 8e61b642a3 2025-08-15 19:17:07 +02:00
fabske0
8e61b642a3
[UI/UX Bug] Position runname dynamically (#6271)
Fix runname position

Co-authored-by: Wlowscha <54003515+Wlowscha@users.noreply.github.com>
2025-08-15 13:16:37 -04:00
Wlowscha
8f546ce4bb
Merge branch 'beta' of https://github.com/pagefaultgames/pokerogue into new-modifier-rework 2025-08-15 18:53:31 +02:00
Wlowscha
2bbc7cfdaf
Merge branch 'modifier-rework' of https://github.com/pagefaultgames/pokerogue into new-modifier-rework 2025-08-15 18:39:57 +02:00
fabske0
da7903ab92
[i18n] rename cancel to cancelButton (#6267)
rename cancel to cancelButton
2025-08-15 11:34:54 -04:00
Bertie690
70e7f8b4d4
[Misc] Removed populateAnims script (#6229)
Removed `populateAnims`

Co-authored-by: Wlowscha <54003515+Wlowscha@users.noreply.github.com>
2025-08-15 17:11:37 +02:00
Sirz Benjie
b2990aaa15
[Bug] [Beta] Fix renaming runs (#6268)
Rename run name field, don't encrypt before updating
2025-08-14 16:57:01 -05:00
Bertie690
ee4950633e
[Test] Added toHaveArenaTagMatcher + fixed prior matchers (#6205)
* [Test] Added `toHaveArenaTagMatcher` + fixed prior matchers

* Fixed imports and stuff

* Removed accidental test file addition

* More improvements and minor fixes

* More semantic changes

* Shuffled a few funcs around

* More fixups to strings

* Added `toHavePositionalTag` matcher

* Applied reviews and fixed my godawful penmanship

* Fix vitest.d.ts

* Fix imports in `vitest.d.ts`

---------

Co-authored-by: NightKev <34855794+DayKev@users.noreply.github.com>
Co-authored-by: Sirz Benjie <142067137+SirzBenjie@users.noreply.github.com>
2025-08-14 13:16:23 -07:00
Sirz Benjie
30058ed70e
[Feature] Add per-species tracking for ribbons, show nuzlocke ribbon (#6246)
* Add tracking for nuzlocke completion

* Add ribbon to legacy ui folder

* Add tracking for friendship ribbon

* fix overlapping flag set

* Replace mass getters with a single method

* Add tracking for each generational ribbon

* Add ribbons for each challenge

* Apply Kev's suggestions from code review

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

---------

Co-authored-by: NightKev <34855794+DayKev@users.noreply.github.com>
2025-08-14 13:20:48 -05:00
Wlowscha
140e4ab142
[UI/UX] Party slots refactor (#6199)
* constants for position of discard button

* Moved transfer/discard button up in doubles

* Fixed the various `.setOrigin(0,0)`

* Small clean up

* Added `isBenched` property to slots; x origin of `slotBg` is now 0

* Also set y origin to 0

* Offsets are relevant to the same thing

* Introducing const object to store ui magic numbers

* More magic numbers in const

* Laid out numbers for slot positions

* Added smaller main slots for transfer mode in doubles

* Changed background to fit new slot disposition

* Apply suggestions from code review

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

* Optimized PNGs

* Updated comment

* Removed "magicNumbers" container, added multiple comments

* Update src/ui/party-ui-handler.ts

Co-authored-by: Amani H. <109637146+xsn34kzx@users.noreply.github.com>

* Fainted pkmn slots displaying correctly

---------

Co-authored-by: NightKev <34855794+DayKev@users.noreply.github.com>
Co-authored-by: Bertie690 <136088738+Bertie690@users.noreply.github.com>
Co-authored-by: Adri1 <adrien.grivel@hotmail.fr>
Co-authored-by: Amani H. <109637146+xsn34kzx@users.noreply.github.com>
Co-authored-by: Sirz Benjie <142067137+SirzBenjie@users.noreply.github.com>
2025-08-14 13:10:15 -05:00
fabske0
76d8357d0b
[Dev] Rename OPP_ overrides to ENEMY_ (#6255)
rename `OPP_` to `ENEMY_`
2025-08-14 18:06:24 +00:00
Bertie690
f42237d415
[Refactor] Removed map(x => x) (#6256)
* Enforced a few usages of `toCamelCase`

* Removed `map(x => x)`

* Removed more maps and sufff

* Update test/mystery-encounter/encounters/weird-dream-encounter.test.ts

* Update game-data.ts types to work
2025-08-14 10:25:44 -07:00
fabske0
b44f0a4176
[Refactor] Remove bgm param from arena constructor (#6254) 2025-08-14 16:52:56 +00:00
Sirz Benjie
076ef81691
[Bug] [UI/UX] [Beta] Fix icons not showing in save slot selection (#6262)
Fix icons not showing in save slot selection
2025-08-13 20:49:46 -05:00
fabske0
23271901cf
[Docs] Add locale key naming info to localization.md (#6260) 2025-08-14 01:12:00 +00:00
Inês Simões
1517e0512e
[UI/UX] [Feature] Save Management Tool (Rename/Delete Saves) (#5978)
* Implement Name Run Feat
Modified load session ui component, adding a submenu when selecting a 3
slot. This menu has 4 options:
Load Game -> Behaves as before, allowing the player to continue
progress from the last saved state in the slot.

Rename Run -> Overlays a rename form, allowing the player to type a
name for the run, checking for string validity, with the option to
cancel or confirm (Rename).

Delete Run -> Prompts user confirmation to delete save data, removing
the current save slot from the users save data.

Cancel -> Hides menu overlay.

Modified game data to implement a function to accept and store
runNameText to the users data.

Modified run info ui component, to display the chosen name when
viewing run information.

Example: When loading the game, the user can choose the Load Game
menu option, then select a save slot, prompting the menu, then choose
"Rename Run" and type the name "Monotype Water Run" then confirm,
thus being able to better organize their save files.

Signed-off-by: Matheus Alves <matheus.r.noya.alves@tecnico.ulisboa.pt>
Co-authored-by: Inês Simões <ines.p.simoes@tecnico.ulisboa.pt>

* Implement Rename Input Design and Tests for Name Run Feat
Created a test to verify Name Run Feature behaviour in the
backend (rename_run.test.ts), checking possible errors and
 expected behaviours.

Created a UiHandler RenameRunFormUiHandler
(rename-run-ui-handler.ts), creating a frontend input
overlay for the Name Run Feature.

Signed-off-by: Matheus Alves <matheus.r.noya.alves@tecnico.ulisboa.pt>
Co-authored-by: Inês Simões <ines.p.simoes@tecnico.ulisboa.pt>

* Fixed formating and best practices issues:
Rewrote renameSession to be more inline with other
API call funtions, removed debugging comments and
whitespaces.

Signed-off-by: Matheus Alves <matheus.r.noya.alves@tecnico.ulisboa.pt>
Co-authored-by: Inês Simões <ines.p.simoes@tecnico.ulisboa.pt>

* Minor Sanitization for aesthetics
Deleting the input when closing the overlay for
aesthetics purpose

Signed-off-by: Matheus Alves <matheus.r.noya.alves@tecnico.ulisboa.pt>
Co-authored-by: Inês Simões <ines.p.simoes@tecnico.ulisboa.pt>

* Fixed minor rebase alterations.

Signed-off-by: Matheus Alves matheus.r.noya.alves@tecnico.ulisboa.pt
Co-authored-by: Inês Simões ines.p.simoes@tecnico.ulisboa.pt

* Implemented Default Name Logic
Altered logic in save-slot-select-ui-handler.ts to
support default naming of runs based on the run
game mode with decideFallback function.

In game-data.ts, to prevent inconsistent naming,
added check for unfilled input, ignoring empty
rename requests.

Signed-off-by: Matheus Alves matheus.r.noya.alves@tecnico.ulisboa.pt
Co-authored-by: Inês Simões ines.p.simoes@tecnico.ulisboa.pt

* Replace fallback name logic: use first active challenge instead
of game mode

Previously used game mode as the fallback name, updated to use the
first active challenge instead (e.g. Monogen or Mono Type), which
better reflects the run's theme.
Signed-off-by: Matheus Alves <matheus.r.noya.alves@tecnico.ulisboa.pt>
Co-authored-by: Inês Simões <ines.p.simoes@tecnico.ulisboa.pt>

* Rebasing and conflict resolution

Signed-off-by: Matheus Alves <matheus.r.noya.alves@tecnico.ulisboa.pt>
Co-authored-by: Inês Simões <ines.p.simoes@tecnico.ulisboa.pt>

* Lint fix

Signed-off-by: Matheus Alves <matheus.r.noya.alves@tecnico.ulisboa.pt>
Co-authored-by: Inês Simões <ines.p.simoes@tecnico.ulisboa.pt>

* Minor compile fix

* Dependency resolved

* Format name respected

* Add all active challenges to default challenge session name if possible

If more than 3 challenges are active, only the first 3 are added
to the name (to prevent the text going off-screen)
and then "..." is appended to the end to indicate
there were more challenges active than the ones listed

* Allow deleting malformed sessions

---------

Signed-off-by: Matheus Alves <matheus.r.noya.alves@tecnico.ulisboa.pt>
Signed-off-by: Matheus Alves matheus.r.noya.alves@tecnico.ulisboa.pt
Co-authored-by: Matheus Alves <matheus.r.noya.alves@tecnico.ulisboa.pt>
Co-authored-by: Wlowscha <54003515+Wlowscha@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-13 20:08:12 -05:00
Bertie690
6133e3c39f
Added test utils for held item tests (#6233)
* Added test utils for held item tests

* Update src/items/held-item-manager.ts

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

---------

Co-authored-by: NightKev <34855794+DayKev@users.noreply.github.com>
2025-08-13 21:33:01 +02:00
Bertie690
0da37a0f0c
[Move] Added laser focus locales (#6202)
* Added Laser Focus locales

* Fixed key for locales text

* Added `MessageAttr`; cleaned up a lot of other jank move attrs
2025-08-13 08:16:08 -07:00
Blitzy
907e3c8208
[Balance] Updates to Twins Trainer Class Pool (#6239)
* Update trainer-config.ts

* Update trainer-config.ts

* Update trainer-config.ts

* Update trainer-config.ts

---------

Co-authored-by: damocleas <damocleas25@gmail.com>
2025-08-12 01:10:35 -04:00
Madmadness65
cb3ae4ab87
[Audio] Add new Desert and Fairy Cave biome music (#6257) 2025-08-11 18:09:52 -04:00
Jimmybald1
6c0253ada4
[Misc] Expanded Daily Run custom seeds (#6248)
* Modify custom starters and added boss, biome and luck custom seed overrides

* Added form index to boss custom seed

* Fix circular dependency in daily-run.ts

* Review for PR 6248

- Use early returns

- Update TSDocs

- Use `getEnumValues` instead of `Object.values` for `enum`s

- Add console logging for invalid seeds

---------

Co-authored-by: Jimmybald1 <147992650+IBBCalc@users.noreply.github.com>
Co-authored-by: NightKev <34855794+DayKev@users.noreply.github.com>
2025-08-10 23:43:31 -04:00
Wlowscha
b263ff7ee4
Fixed errors in data/challenge.ts, but will need more fixes to work with new rewards 2025-08-10 19:34:09 +02:00
Wlowscha
e4f236c73a
Fixed more conflicts from merge 2025-08-10 18:14:55 +02:00
Wlowscha
1e8f06da03
Merge branch 'beta' into modifier-rework 2025-08-10 18:02:07 +02:00
Wlowscha
238691c8bf
Merge branch 'beta' of https://github.com/pagefaultgames/pokerogue into new-modifier-rework 2025-08-09 22:35:54 +02:00
Wlowscha
2e3146912a
Added @ts-expect-error to data-lists.ts 2025-08-08 01:54:37 +02:00
Wlowscha
3f3bbf7d85
Removed most uses of allRewards from across the code 2025-08-08 01:50:56 +02:00
Wlowscha
f5d08567c9
Remove leftover unused functions from merge; transfer/discard button 2025-08-08 01:27:52 +02:00
Wlowscha
c6b4f273d0
Using SilentReward for fixed event rewards 2025-08-08 01:07:53 +02:00
Wlowscha
3c8ba17cbf
Updated TextStyle usage in trainer-item.ts 2025-08-08 01:03:09 +02:00
Wlowscha
0d4439630d
Fixed various minor issues 2025-08-08 01:02:21 +02:00
Wlowscha
f83eb054f4
Fixed inputs of restorePokemonHp 2025-08-08 00:58:31 +02:00
Wlowscha
949467f9f4
Fixed phases which award rewards without going through the reward selection screen 2025-08-08 00:52:00 +02:00
Wlowscha
07dad0981d
NoneReward constructed without any parameters; util function to generate reward options never returns null 2025-08-08 00:46:45 +02:00
Wlowscha
6f0fb543ac
NoneReward called with correct arguments 2025-08-08 00:06:58 +02:00
Wlowscha
b0680fd9ad
Removing some mentions of allRewards 2025-08-07 23:54:35 +02:00
Bertie690
a8e4f76a4f Minor nits and todos 2025-08-07 14:16:33 -04:00
Bertie690
6660e33836 Merge remote-tracking branch 'upstream/beta' into modifier 2025-08-07 14:13:39 -04:00
Bertie690
2aca187c66
Reward type safety (#6211)
* Added type safety to reward generators

* Removed duplicate class member so biome won't yell at meeeeeee

* Update all-reward-type.ts

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

* Update all-rewards.ts

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

* Moved `allRewards` to a const object; general cleanup

* Added `noneReward`

* Removed `getPregenArgs`

* Resolved kev's commnet

* Expunged `RewardFunc`

---------

Co-authored-by: NightKev <34855794+DayKev@users.noreply.github.com>
2025-08-07 19:58:53 +02:00
Wlowscha
acc12318d7
Removed unused leftover getPregenArgs methods 2025-08-07 18:14:04 +02:00
Wlowscha
dac9e202a0
[Modifier Refactor] Modernize reward pool (#6208)
* Added missing `export`s to classes

* Created `allRewards` object

* RewardPool now uses ids; function to generate appropriate reward from id (including held item or trainer item)

* Added generateRewardOptionFromId function

* Using `RewardSpecs` for predetermined reward generation

* Removed RewardOverride

* Removed rewardInitObj

* Removed WeightedReward

* Proper initialization of allRewards
2025-08-04 09:04:48 +02:00
Bertie690
ad26adf426 Fixed objectValues issues 2025-08-03 19:07:47 -04:00
Bertie690
5e7a5a7a94 Merge remote-tracking branch 'upstream/beta' into modifier-rework 2025-08-03 19:01:10 -04:00
Bertie690
e50296d14c
Merged upstream/beta into Modifier rework (#6206)
* [Dev] test:silent now passes --silent='passed-only' to Vitest (#6131)

* [Dev] test:silent now passes --silent='passed-only' to Vitest

* Update test shard to actually use `test-silent`

* Removed obselete `project` flag

* [Bug] Unblock priority spread under Psychic Terrain (#6136)

Unblock priority spread under Psychic Terrain

Co-authored-by: Acelynn Zhang <acelynnzhang@Acelynns-MacBook-Pro.local>

* [Dev] Remove `sanitizeOverrides`, consolidate initialization code into 1 file

https://github.com/pagefaultgames/pokerogue/pull/6134

* Removed `sanitizeOverrides`

* Moved initialization code to its own file

* Hopefully fixed test contamination

* Actually listened to people now

* fixed the thingy

* Run stub setup on init because

* Update testFileInitialization.ts

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

---------

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

* [Misc] Sinistea and Poltchageist line alt forms now available (#4989)

* Sinistea and Poltchageist line alt forms now available

* Unmark Poltchageist line as unobtainable, fix sprite key of alt forms

* Correct forms not being marked as starter selectable

* Reduce wild chance for Antique/Masterpiece forms

Instead of being 1/2 chance to get the Antique or Masterpiece forms, it is now only a 1/16 chance to get them.

---------

Co-authored-by: damocleas <damocleas25@gmail.com>

* [Bug] Fix when variable move power is called (#6126)

* Apply variable power attribute before type boost

* Update test/abilities/normal-move-type-change.test.ts

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

* Minor test improvements

---------

Co-authored-by: Sirz Benjie <142067137+SirzBenjie@users.noreply.github.com>
Co-authored-by: Bertie690 <136088738+Bertie690@users.noreply.github.com>
Co-authored-by: NightKev <34855794+DayKev@users.noreply.github.com>

* [UI/UX] [Bug] Fix `ModifierSelectPhase` animation delay (#6121)

* Rework promise handling to ensure no races

* Add delay to ensure pokeball opening animation can be seen

* Remove leftover debug statements.

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

* Add tween bouncing pokeball to tweens that must complete for promise to resolve

* Fix typo in tsdoc

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

---------

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

* [Test] `MoveHelper#changeMoveset` disables moveset overrides (#5915)

Also fix Assist tests and add `expect` for max moveset length

* [Refactor] Minor cleanup of `initExpKeys` (#6127)

* [Bug] Fix Thrash continuing on caught Pokemon (#6144)

Fix Thrash continuing on caught Pokemon

* [UI/UX] Replace 'Neutral' in the Arena Flyout with 'Field' (#6139)

Update arena-flyout.ts for Field > Neutral

* [Dev] Added typedoc deployments for Beta (#6147)

* [Misc] Fix import in decrypt-save.js (#6149)

* [Refactor][Bug] Illusion no longer overwrites data of original Pokemon

https://github.com/pagefaultgames/pokerogue/pull/6140

* [UI/UX] Added "Hide Username" Setting (#6105)

* [UI/UX] Added "Hide Username" Setting

* Mask tid with asterisk instead of hiding completely

---------

Co-authored-by: Sirz Benjie <142067137+SirzBenjie@users.noreply.github.com>

* [Dev] Add `dist/` to `ls-lint` ignore list

* [i18n] Update Locales

* [Beta] [Bug] Fix shiny display issues (#6154)

Fix shininess check

* [Bug][Beta] Make bounce delay use fixed int (#6156)

Make bounce delay use fixed int

* [Refactor] Refactor UI text ts (#5946)

* Add destroy method to pokemon-sprite-sparkle-handler

* Move TextStyle to enums, convert into const object

* Cleanup text.ts file

* Add necessary explicit types for TextStyle let vars

* Fix locales submodule commit

* Fix merge issue

---------

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

* [Test] Added custom equality matchers (#6157)

* Added rudimentary test matchers for `toEqualArrayUnsorted` and `toHaveTypes`

Might port the rest at a later date

* Actually run the effing setup matchers file + fixed ls lint to not be angy

* added dev dep

* Update .ls-lint.yml

* Update .ls-lint.yml

---------

Co-authored-by: Sirz Benjie <142067137+SirzBenjie@users.noreply.github.com>
Co-authored-by: Amani H. <109637146+xsn34kzx@users.noreply.github.com>

* [Misc] Fix import in `vitest.d.ts`

* [Dev] Turned on `checkJs` in TSConfig; fixed script type errors (#6115)

* [Misc] Moved + cleaned up string manipulation functions (#6112)

* Added string utility package to replace util functions

* Changed string manipulation functions fully over to `change-case`

* Fixed missing comma in package.json

trailing commas when :(

* fixed lockfile

* Hopefully re-added all the string utils

* fixed package json

* Fixed remaining cases of regex + code dupliation

* Fixed more bugs and errors

* Moved around functions and hopefully fixed the regex issues

* Minor renaming

* Fixed incorrect casing on setting strings

pascal snake case 💀

* ran biome

* [Refactor] Prevent serialization of full species in pokemon summon data

https://github.com/pagefaultgames/pokerogue/pull/6145

* Prevent serialization of entire species form in pokemon summon data

* Apply suggestions from code review

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

* Apply Kev's suggestions from code review

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

---------

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

* [Docs] Add `@module` modifier tag to `tsdoc.json`

* [Bug] Fix camel case bug in `strings.ts` (#6161)

* [Dev] Change `target` to `ES2023` in `tsconfig.json` (#6160)

* breakup fight and ball commands into their own methods

* Breakup run and pokemon commands

* Breakup commandPhase#start

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

* Minor touchups

* Add overload for handle command

* Fix improperly named computeMoveId method

* Improve `canUse` computation

* Explicitly check against Moves.NONE

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

* Update with Bertie's comments

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

* Fix imports

* Apply kev's suggestions from code review

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

* Improve documentation

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

* [Balance][Challenge] Added expanded Fresh Start options (#6162)

* [Dev] Add `workflow-dispatch` trigger to tests github workflow (#6152)

Add `workflow-dispatch` trigger to github workflow

Co-authored-by: damocleas <damocleas25@gmail.com>

* [Test] Add support for custom boilerplates to `create-test.js` (#6158)

* Added support for custom boilerplates to test:create script

* Added support for custom boilerplates to create-test.js

* Fixed syntax error

* Update create-test.js

Co-authored-by: Amani H. <109637146+xsn34kzx@users.noreply.github.com>

* Fix pluralization error in `create-test.js`

---------

Co-authored-by: Amani H. <109637146+xsn34kzx@users.noreply.github.com>

* [Refactor] Mark nickname in pokemon as optional (#6168)

Mark nickname in pokemon as optional

* [Dev] Update Vite from 6.3.5 to 7.0.6 (#6163)

* [ME] [Bug] Disable trades in GTS ME with only 1 valid party member

https://github.com/pagefaultgames/pokerogue/pull/6167

* [Refactor] Minor refactor of battler tags (#6129)

* Minor refactor battler tags

* Improve documentation

* Update type when loading in pokemon-data constructor for battler tags

* Fix issues in tsdoc comments with Wlowscha's suggestions

Co-authored-by: Wlowscha <54003515+Wlowscha@users.noreply.github.com>

* Apply bertie's suggestions from code review

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

* Remove unnecessary as const from tagType

* Remove missed `as const`

* Apply kev's suggestions from code review

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

* Update src/data/battler-tags.ts

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

* Update src/data/battler-tags.ts

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

---------

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

* [Docs] Adjust `@module` doc comment in `battler-tags.ts`

Note that currently Typedoc is not parsing `@module` docs,
but this comment adjustment would be required if and when
it gets fixed

* [Balance] End of turn triggers won't occur when you end a biome (#6169)

* [Balance] End of turn triggers won't occur when you end a biome

* Add tests

* Move phase manipulation logic into `PhaseManager`

* Rename "biome end" to "interlude"

* Rename `TurnEndPhase#endOfBiome` to `upcomingInterlude`

---------

Co-authored-by: Wlowscha <54003515+Wlowscha@users.noreply.github.com>

* [Test] Add missing single battle override to `CheckInterludePhase` test

* [UI/UX] Fix button and input field overlaps (#6013)

* [Fix] Fix button overlap

* [Fix] Fix input field overlaps

* use getWidth to determine if label should be shortened

---------

Co-authored-by: Sirz Benjie <142067137+SirzBenjie@users.noreply.github.com>
Co-authored-by: damocleas <damocleas25@gmail.com>

* [Dev] Moved type helpers to separate directory; (#6123)

* [Dev] Moved type helpers to separate directory; renamed `EnumValues` to `ObjectValues` and enforced usage

* Update tsconfig.json

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

* Fixed import issue

* Updated documentation slightly

---------

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

* [Misc] Improve type signatures of serialized arena/battler tags (#6180)

* Improve type signatures of serialized arena/battler tags

* Minor adjustments to tsdocs from code review

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

---------

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

* [Balance] Updated SF/Triage interactions for moves (#6179)

* Fixed move flags

* Disabled order up interactionn with sheer force

* Update src/data/moves/move.ts

* Removed order up test that no longer applies

shouldn't have been there in the first place

* [Move Bug] Fully implemented Future Sight, Doom Desire; fixed Wish Double battle oversight  (#5862)

* Mostly implemented Future Sight/Doom Desire

* Fixed a few docs

* Fixed com

* Update magic_guard.test.ts

* Update documentation

* Update documentation on arena-tag.ts

* Update arena-tag.ts docs

* Update arena-tag.ts

* Update turn-end-phase.ts

* Update move.ts documentation

* Fixed tpyo

* Update move.ts documentation

* Add assorted TODO test cases

* Refactored FS to use a positional tag manager

* Added strong typing to the manager, finished save load stufff

* Fixed locales + tests

* Fixed tests and documentation

* sh
Fixed tests for good

* Fixed MEP

* Reverted overrides changse

* Fixed issues with merging

* Fixed locales update & heal block test

* Fixed wish tests

* Fixed test typo

* Fixed wish test flaking out due to speed ties

* Fixed tests fr fr

* actually fixed tests bc i'm stupid

* Fixed tests for real

* Remove locales update

* Update arena-tag.ts

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

* Update move.ts

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

* Update arena-tag.ts

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

* Applied review suggestions and added a _wee_ bit more docs

* fixed wish condition

* Applied kev's reviews

* Minor nits

* Minor formatting change in `heal-block.test.ts`

---------

Co-authored-by: Sirz Benjie <142067137+SirzBenjie@users.noreply.github.com>
Co-authored-by: Dean <69436131+emdeann@users.noreply.github.com>
Co-authored-by: NightKev <34855794+DayKev@users.noreply.github.com>

* [Refactor] Added `PhaseManager.toTitleScreen` (#6114)

* Added `toTitlePhase`; documented phase manager

* Documented phase methods

* Fixed syntax errors + updated signature

* Reverted all the goodies

* Fixed missing shift phase call

GHAG

* Reverted change

* [UI/UX] Implement Discard Button (#5985)

* [feature]Implemented needed parts for discard function from issue #4780:
-TryDiscardFunction in battlescene;
-Created a party discard mode button;
-Updated Transfer button in modifier-select-ui-handler to Manage items;
-Created tests for the discard function in test/ui;
-Added images for the new discard and transfer buttons to loading-scene;
-Created placeholder messages for discard feature in party-ui-handler;

Co-authored-by: Tiago Rodrigues <tiago.n.rodrigues@tecnico.ulisboa.pt>

* [Fix] Updated icon for dynamic messaging

* [Fix] Corrected legacy mode icons and adjusted double-battle button location

* [Fix]Adjusted button positioning and mapping after review. Mapping requires debugging.

* [Fix] Fixed visible pokeball in legacy mode and key mapping

* [Fix] Background fixes,manage menu is the only one affected by changes now

* Implement i18n keys

* [Fix] implemented most code optimizations and callbacks to the modified locales folder

* [Fix] Implemented 3 suggestions

* [Fix]improved/corrected test structure

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

* [Fix] added functionality test for the discard button

* [Fix] added necessary comment

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

* [Fix] Implemented suggested changes in test/discard text prompt

* [Fix] Implemented UI suggestions and removed discard text confirmation

* [Fix] added missing imports

* Fix imports in test file

* [Fix] Implemented suggested cursor behavior and reworked test code

* [Fix] Corrected failed test

* [Fix] atempting to fix the test timeout issue

* [Fix] Undoing latest attempt

* [Fix] Implemented suggestions to fix broken tests

* Reviews

* [Fix] replaced icon images

* [Fix] Updated jsons to match new icons and removed pokeball icon from legacy mode

* Optimized new images

* [Fix] Fixed referenced bug and added similar confirmation box to release

* [Fix] Updated tests to handle the corfirmation box

* [Fix] Added back the accidentally removed changes

* [Fix]updated incorrect import path

* [fix] add description for the manageItemMode function

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

* Update src/ui/party-ui-handler.ts

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

* [Fix] corrected formating issue

---------

Co-authored-by: Mikhail Shueb <mikhail.shueb@tecnico.ulisboa.pt>
Co-authored-by: NightKev <34855794+DayKev@users.noreply.github.com>
Co-authored-by: Bertie690 <136088738+Bertie690@users.noreply.github.com>
Co-authored-by: damocleas <damocleas25@gmail.com>
Co-authored-by: Bertie690 <taylormw163@gmail.com>
Co-authored-by: Adri1 <adrien.grivel@hotmail.fr>

* [Bug] Fix Truant behavior (#6171)

* Update locales

* [i18n] Add Tagalog language support (#6170)

* [Bug] [Beta] Fix serializable battler tags (#6183)

Fix serializable battler tags

* [Misc] [Beta] Fix serializable battler tags (#6184)

Fix serializable battler tags

* [Misc] Disallow using NonFunctionProperties in loadTag methods (#6185)

Disallow using NonFunctionProperties in loadTag methods

* [Dev] `pnpm biome` will now only display errors by default (#6187)

* [Sprite] Move Clauncher, Clawitzer, Skiddo, Fomantis, Lurantis animations to consistent (#6177)

* Move Clauncher, Clawitzer, Skiddo out of exp

* Move Fomantis, Lurantis out of exp

* [Misc] Fix missing types in battler tags and remove DragonCheerTag (#6188)

* Fix missing types in battler tags

* Fix issues with dragon cheer

* [Misc] Set default for crit stage battler tag (#6192)

Set default for crit stage

* [Bug] [Beta] Remove setting currentPhase to null in clearAllPhases (#6193)

Remove setting currentPhase to null in clearAllPhases

* [Test] `game.move.use` overrides summon data moveset if present (#6174)

* [Test] Update `game.move.use` to override summon data moveset if present

* ran biome & fixed errors

* [Beta] Remove non-Legend Fresh Start option, fix starting movesets

https://github.com/pagefaultgames/pokerogue/pull/6196

* Remove non-Legend fresh start option, fix starting movesets

* Move updateInfo call

* Fix relearns counting for starter moves

* Reduce array allocations and function calls

* [Misc] Move asset initialization from `preload` to `launchBattle`

https://github.com/pagefaultgames/pokerogue/pull/6109

* Move asset initialization into `preload` instead of `launchBattle`

* Fix init problems; remove unused `waitUntil` util function

* Fixed missing `clearAllPhases` call

* [Balance] Adjust Cosmog / Cosmoem Evolutions, Lower Buneary Friendship requirements (#6198)

* Lower Buneary Friendship Requirement and Change Cosmog method

* Update biomes.ts - Cosmog/Cosmoem wild evo levels

* fix ?

* fixed now

* Update Cosmog Evolution + Biome Levels

---------

Co-authored-by: damocleas <damocleas25@gmail.com>

* [Test] Port over + augment remaining test matchers from pkty (#6159)

* Partially ported over pkty matchers (WIP)

* Cleaned up some more matchers

* Fiexd up matchers

* Fixed up remaining matchers

* Removed the word "matcher" from the pkty matcher functions

If we want them back we can always undo this commit and convert the other custom ones

* Added wip spite test

* Added `toHaveUsedPP` matcher

* Fixed up docs and tests

* Fixed spite test

* Ran biome

* Apply Biome

* Reverted biome breaking i18next

* Update src/typings/i18next.d.ts comment

* Fixed log message to not be overly verbose

* Added option to check for all PP used in pp matcher + cleaned up grudge tests

* Fixed up tests

* Fixed tests and such

* Fix various TSDocs + missing TSDoc imports

* [Beta] Further Cosmog Evolution Readjustments (#6203)

Update pokemon-evolutions.ts

* [Dev] Move various functions out of `pokemon-species.ts` (#6155)

`initSpecies` moved to its own file,
other functions moved to `pokemon-utils.ts`

* [Balance] Prevent MEs on X1 Waves (#6204)

* [Balance] Prevent MEs on X1 Waves

* Fix Bug-Type Superfan Encounter Test

* Fix Unit Tests

* [Balance] Update and Change Breeder Trainer Class Teams (#6200)

* Update Breeder Trainer Class

* Update trainer-config.ts

* Update trainer-config.ts

* Update whirlwind.test.ts

* Update src/data/trainers/trainer-config.ts

Co-authored-by: AJ Fontaine <36677462+Fontbane@users.noreply.github.com>

* Adjust Party Templates and move Wynaut

* Update trainer-party-template.ts

---------

Co-authored-by: damocleas <damocleas25@gmail.com>
Co-authored-by: AJ Fontaine <36677462+Fontbane@users.noreply.github.com>

---------

Co-authored-by: Acelynn Zhang <102631387+acelynnzhang@users.noreply.github.com>
Co-authored-by: Acelynn Zhang <acelynnzhang@Acelynns-MacBook-Pro.local>
Co-authored-by: NightKev <34855794+DayKev@users.noreply.github.com>
Co-authored-by: Madmadness65 <59298170+Madmadness65@users.noreply.github.com>
Co-authored-by: damocleas <damocleas25@gmail.com>
Co-authored-by: Sirz Benjie <142067137+SirzBenjie@users.noreply.github.com>
Co-authored-by: SmhMyHead <191356399+SmhMyHead@users.noreply.github.com>
Co-authored-by: Amani H. <109637146+xsn34kzx@users.noreply.github.com>
Co-authored-by: AJ Fontaine <36677462+Fontbane@users.noreply.github.com>
Co-authored-by: Wlowscha <54003515+Wlowscha@users.noreply.github.com>
Co-authored-by: fabske0 <192151969+fabske0@users.noreply.github.com>
Co-authored-by: Dean <69436131+emdeann@users.noreply.github.com>
Co-authored-by: Tiago Rodrigues <tiago.n.rodrigues@tecnico.ulisboa.pt>
Co-authored-by: Mikhail Shueb <mikhail.shueb@tecnico.ulisboa.pt>
Co-authored-by: Adri1 <adrien.grivel@hotmail.fr>
Co-authored-by: Blitzy <118096277+Blitz425@users.noreply.github.com>
2025-08-04 00:08:37 +02:00
Wlowscha
466c4aede2
Replace remaining Modifiers with Rewards (#6091)
* Changing remaining Modifiers to Consumables, and renaming ModifierType to Reward

* Renamed modifier files and moved them into items folder

* Using rewards in most places

* Removed consumables in favor of using rewards directly

* Renamed RewardTier to RarityTier

* Reward ids, function to match rewards

* Getting reward tiers from player pool still

* Messing around with parameters of Reward.apply()

* Always requiring player pokemon in rewards

* Fixing some functions in select-reward-phase and battle-scene

* Fixed various post-merge issues

* Fixed most localization strings (accidentally broken by replacing modifierType with reward)

* Fixed tests for select reward phase

* Using Pokemon.hasSpecies()

* Zero weight for trainer items rewards which are already max stack

* Cleaning up SelectRewardPhase, held item rewards behave the same as any PokemonReward

* Cleaned up some functions

* Introduced RewardCategoryId, distributed RewardIds

* Utility `is` functions for rewards

* Minor fixes

* Moved `HeldItemEffect` to its own file

* rmade some todo comments

* Adding a big comment

* Added tsdocs and removed `RewardClass`

* undid breaking changes

* added TODO

* Moved matchingRewards function to reward-utils.ts

* Added RewardGenerator classes for mints and tera shards

* Introducing default rarity tiers for trainer items and rewards

* RewardFunc now can return RewardGenerator

* Moved pool reward functions to their own file, plus other utility files

* Fixed WeightedModifier to work with the new RewardFunc

* Fixed wrong type import

* Shifting trainer item and reward ids to avoid overlaps

* Added some types

* Updated comment in reward.ts

* Added strong typing ot item maps

* added type safety to held item name map

---------

Co-authored-by: Bertie690 <taylormw163@gmail.com>
Co-authored-by: Sirz Benjie <142067137+SirzBenjie@users.noreply.github.com>
2025-07-27 17:09:21 -07:00
Sirz Benjie
d3f2659cdf
Merge branch 'beta' into modifier-rework 2025-07-24 16:00:14 -06:00
Wlowscha
985d352be8
Moved held item manager to item folder, some name changes 2025-07-24 20:39:56 +02:00
Wlowscha
ccd968d5b8
Merge branch 'beta' of https://github.com/pagefaultgames/pokerogue into new-modifier-rework 2025-07-24 20:36:27 +02:00
Wlowscha
e1edb87990
Merge https://github.com/pagefaultgames/pokerogue into new-modifier-rework 2025-07-19 16:30:48 +02:00
Sirz Benjie
315d4cf408
Add parameter destructuring (#6092) 2025-07-14 20:19:24 +02:00
Wlowscha
97e5ab882a
Minor lint 2025-07-13 16:36:17 +02:00
Wlowscha
4ed0fd0384
More minor fixes 2025-07-13 16:12:27 +02:00
Wlowscha
c7a1b0fac5
Reworked getPartyBerries function, fixed harvest test 2025-07-13 15:59:53 +02:00
Wlowscha
b8324e85f7
Moved getPartyBerries() utility function 2025-07-13 13:20:42 +02:00
Wlowscha
e034d7e04e
Added test override for pokeballs 2025-07-13 13:04:46 +02:00
Wlowscha
eaead476e8
Add some type specifications so biome is happy 2025-07-13 12:55:07 +02:00
Wlowscha
e7bb3a7443
formChangeItemName function 2025-07-13 12:45:11 +02:00
Wlowscha
d411702716
Cleaned up more files 2025-07-13 12:34:18 +02:00
NightKev
8118d00a07 Remove relative imports 2025-07-13 02:57:47 -07:00
NightKev
ff5d891c6e Fix some imports 2025-07-13 02:52:02 -07:00
NightKev
882c256933 Create and apply #items import alias 2025-07-13 02:42:22 -07:00
NightKev
4494debaa0 Merge branch 'beta' into modifier-rework 2025-07-13 02:36:31 -07:00
NightKev
c697a47f5b Apply Biome 2025-07-13 02:34:03 -07:00
NightKev
cd2f0b8a6e Merge branch 'beta' into modifier-rework 2025-07-13 02:32:49 -07:00
Wlowscha
414a6b10ea
Cleaned up more tests 2025-07-13 00:42:29 +02:00
Wlowscha
9925b0c358
Some minor fixes 2025-07-12 20:36:13 +02:00
Wlowscha
0d2bae0fcc
Rename SelectModifierPhase to SelectRewardPhase 2025-07-12 20:23:11 +02:00
Wlowscha
5b14a5899c
Renamed various ModifierRewardPhase classes 2025-07-12 17:20:27 +02:00
Wlowscha
2344bdf32a
Renamed modifier-se-ect-ui-handler to reward-select-ui-handler 2025-07-12 17:14:03 +02:00
Wlowscha
88634ca081
Partially fixed delibirdy test 2025-07-12 17:01:15 +02:00
Wlowscha
6fc7db8939
More partial test fixes 2025-07-12 13:22:49 +02:00
Wlowscha
ee2412cafb
Partially fixed absolute avarice, fixed HeldItemRequirement to actually count the items 2025-07-12 13:14:35 +02:00
Wlowscha
e312a4b8f2
Fixed bug-type-superfan encounter; split up HoldingItemRequirement from HeldItemRequirement 2025-07-12 12:56:58 +02:00
Wlowscha
dbfbc24c8a
Fixed fiery fallout encounter and relative test by making sure that isCategoryId works properly 2025-07-12 00:35:42 +02:00
Wlowscha
849550808a
HeldItemManager's .hasItem() now also works on categories 2025-07-11 23:58:49 +02:00
Wlowscha
f2e9ea0e93
Merge branch 'beta' into modifier-rework 2025-07-11 23:29:45 +02:00
Wlowscha
2744567b21
Auxiliary functions to fix type complaints in item managers 2025-07-10 23:21:58 +02:00
Wlowscha
175f2b74e9
Fixed some ME tests 2025-07-10 19:21:40 +02:00
Wlowscha
61cd122223
Removed default value from maxStackCount 2025-07-10 18:50:41 +02:00
NightKev
d0ebb32f9e Add a couple of TODOs 2025-07-10 01:49:20 -07:00
NightKev
fee6320502 Normalize naming 2025-07-10 01:45:28 -07:00
NightKev
d576d66617 Rename HELD_ITEM_EFFECT enum-object to HeldItemEffect 2025-07-10 01:31:55 -07:00
NightKev
79779765e2 Add as const in trainer-item-id.ts 2025-07-10 01:27:54 -07:00
NightKev
e64117d6a6 Fix isItemInRequested 2025-07-10 01:26:00 -07:00
NightKev
1f77eb4dff Add as const to enum-objects in held-item-id.ts 2025-07-10 01:25:02 -07:00
Wlowscha
7e2418b957 Held item refactor (#5656)
* Introducing held items

* Pokemon class can add held items

* Example of applyHeldItemAttrs

* Introducing a PokemonItemManager class

* Moving away from HeldItemAttr

* Attempt at incorporating the new framework in modifier-type

* Changes

* More changes

* Splitting up methods in select-modifier-phase.ts

* Newrefactors of reward-pool-manager.ts

* New refactors of select-modifier-phase.ts

* Extracted logic from modifierSelectCallback

* Moved some files around, added many held item ids

* Introduced HeldItemReward class

* Introduced AttackBoosterReward

* Introduced AttackBoosterRewardGenerator

* Removed unused file

* Select modifier phase tentatively working with new held items

* Working leftovers in new style

* Moved modifier-bar to its own file

* Held items now show up in summary

* Added some suggestions

* Implemented more suggestions

* Splitting up held-item file

* Fixing various imports

* Created items folder

* Shell bell

* Converted White Herb

* HeldItem .apply() methods do not really need the stack as input, just the pokemon

* Made heldItems public in heldItemManager

* Update modifier bar is now called in the apply of consumable held items

* Refactored parameters passed to .apply() methods; introduced generic .applyHeldItems() function; all HeldItems classes specify an ITEM_EFFECT

* Lucky egg and Golden egg

* Converted wild pokemon pool to held item rewards

* Temporary stopgap on maxUpgradeCount to avoid game crashing on modifier select ui handler

* Changed held-items.ts to held-item-id.ts and renamed id object accoridngly

* Added reviver seed

* Simplified HeldItemReward

* Added effect of reviver seed (leveraging consumable logic)

* Remove InstantReviveModifier

* Added Stat Boost items; generic name and description in HeldItem class

* Added Crit Boost held items, King's Rock, Focus Band and Quick Claw

* Added Mystical Rock

* Added Shell Bell, Soul Dew

* Added multi lens and wide lens

* Added Baton and Golden Punch

* Baton switch logic in party ui handler now using held item

* Partial implementation of item steal items

* Using held items in some places

* Using phaseManager

* Tracking forms in held item manager

* Shuckle Juice and Old Gateau

* Using phaseManager

* Removed a bunch of modifiers

* Fixed shell bell in ability.ts (why is it here?)

* Changed BattleScene.removeModifier and pokemon.loseHeldItem

* Making some held items unstealable and unsuppressable

* Refactored most of battle-scene.ts with held items

* Added soundName to HeldItem (possibly useless)

* Reworked various effects that steal items

* Refactored Baton logic

* Reworked most entries in Modifier Types

* pokemon.getHeldItems now uses heldItemManager

* Added Evolution Tracker as held item

* MBE is always untransferable

* Improved item transfer

* Fixed types in held item manager

* Various fixes

* Fixed types in shuckle juice and old gateau

* MBE achievement now tracks held items

* Removed AttackTypeBoosterModifierRequirement for MEs

* Fixed Pickup

* Fixing (most) berry usage

* Using Berry held items in move.ts

* Split up vitamins from the rest of stat boosting items

* Fixed form change trigger after merge conflicts

* Added some utility functions to check if an item fits a list of items/categories, or to filter out which held items fit

* Fixed delibirdy encounter

* Various fixes

* Reworked EnemyPokemonConfig to include a HeldItemProperty object (to feed to the heldItemManager). Updated Dark Deal ME

* More various fixes; introduced isMaxStack(item) method to heldItemManager

* Updated modifier-bar.ts

* Converted Berries Abound encounter

* Converted The Strong Stuff encounter

* Fixed Slumbering Snorlax encounter, overrideItems of heldItemConfiguration can deal with items with 0 stack

* Preliminary changes to some MEs

* Changes to the summary ui handler

* Converting to held item pools

* Fixed evo tracker

* Moved many data types for held items to a dedicated file; introduced held item pools for item generation

* Fixed some MEs

* Added function to assign items from a given configuration

* Fixed held item evolution condition

* Fixed some ui handlers

* Fixed select-modifier-phase

* Some changes to modifier files

* Daily run items are generated within the new system

* Held item generation for enemies follows the new scheme

* Fixed init-modifier-pools.ts

* Held item overrides now use the new system

* Removed unused getOrInferTier function (was only used by thief and covet)

* Fixed shady vitamin ME and some HeldItem files

* Updated various MEs. HeldItemManager can now generate a configuration from its items.

* Converted most MEs, introduced some more utility functions and methods

* Replaced ModifierTier with RewardTier

* Fixed some rogue inputs

* Held item pools take a single pokemon as input, set weights to 0 for max stack items

* Restored various changes to held item pool generation; distinguishing target of the generation from the party

* Pools can now take a HaldItemConfigurationId without weights; set up table of item tiers; used custom pools in Clowning Around ME

* Fixing some phases

* Fixed several bugs related to accessing and visualizing held items

* Fixed position of enemy modifier bar

* Fixed berries, removed some debug messages

* Vitamins max stack temporarily set to 30

* Luck upgrades work correctly again (maybe)

* First steps to port tests to the new system

* Form change items work correctly when selected in battle

* Introducing helper functions for a modifier migrator; PokemonItemMap now uses pokemonId

* Renamed file with item migrator functions

* Restored missing strings

* Shuckle juic and Old gateau are not instance dependent

* Simplified HeldItemData

* Migrator utils include shuckle juice

* Introducing trainer items

* Enemy tokens are now also trainer items

* Converted most of modifierTypes

* Fixed various MEs; removed findModifier from battle-scene

* Removed most uses of globalScene.applyModifiers

* Removed more functions from globalScene; changed lure weight function

* Updated a variety of files

* Split out X_accuracy, fixed trainer-item-manager

* Fixed expert breeder ME

* Replaced updateModifiers with updateItems

* Removed modifiers from saveData

* Trainer item rewards are generating during runs, added properly to the scene

* Items (including held items) are saved and loaded correctly.

* Fixed several trainer item names and descriptions, plus various issues with item generation

* Restored override file

* Added icons to tokens

* Fixed Dire Hit starting stack

* Using Pokemon.getMoveType() in generation of attack type boosters

* Test for dire hit; ensuring that lapsing trainer items are added from config at full stack

* Eviolite does not apply to G-Max forms

* Changes to various item tests

* Some still broken tests

* More changes to tests but still failing

* Fixed evo tracker item

* Moved allHeldItems and allTrainerItems definitions to data-lists

* Renamed modifier-bar and moved to ui files

* Fixed held item when pokemon not passed

* Remove `package-lock.json` added by merge issue

* Add review comments as `TODO`s for now

---------

Co-authored-by: NightKev <34855794+DayKev@users.noreply.github.com>
2025-07-10 01:05:25 -07:00
327 changed files with 14207 additions and 14760 deletions

View File

@ -81,7 +81,7 @@ For example, here is how you could test a scenario where the player Pokemon has
```typescript ```typescript
const overrides = { const overrides = {
ABILITY_OVERRIDE: AbilityId.DROUGHT, ABILITY_OVERRIDE: AbilityId.DROUGHT,
OPP_MOVESET_OVERRIDE: MoveId.WATER_GUN, ENEMY_MOVESET_OVERRIDE: MoveId.WATER_GUN,
} satisfies Partial<InstanceType<typeof DefaultOverrides>>; } satisfies Partial<InstanceType<typeof DefaultOverrides>>;
``` ```

View File

@ -90,9 +90,13 @@ If this feature requires new text, the text should be integrated into the code w
- For any feature pulled from the mainline Pokémon games (e.g. a Move or Ability implementation), it's best practice to include a source link for any added text. - For any feature pulled from the mainline Pokémon games (e.g. a Move or Ability implementation), it's best practice to include a source link for any added text.
[Poké Corpus](https://abcboy101.github.io/poke-corpus/) is a great resource for finding text from the mainline games; otherwise, a video/picture showing the text being displayed should suffice. [Poké Corpus](https://abcboy101.github.io/poke-corpus/) is a great resource for finding text from the mainline games; otherwise, a video/picture showing the text being displayed should suffice.
- You should also [notify the current Head of Translation](#notifying-translation) to ensure a fast response. - You should also [notify the current Head of Translation](#notifying-translation) to ensure a fast response.
3. At this point, you may begin [testing locales integration in your main PR](#documenting-locales-changes). 3. Your locales should use the following format:
4. The Translation Team will approve the locale PR (after corrections, if necessary), then merge it into `pokerogue-locales`. - File names should be in `kebab-case`. Example: `trainer-names.json`
5. The Dev Team will approve your main PR for your feature, then merge it into PokéRogue's beta environment. - Key names should be in `camelCase`. Example: `aceTrainer`
- If you make use of i18next's inbuilt [context support](https://www.i18next.com/translation-function/context), you need to use `snake_case` for the context key. Example: `aceTrainer_male`
4. At this point, you may begin [testing locales integration in your main PR](#documenting-locales-changes).
5. The Translation Team will approve the locale PR (after corrections, if necessary), then merge it into `pokerogue-locales`.
6. The Dev Team will approve your main PR for your feature, then merge it into PokéRogue's beta environment.
[^2]: For those wondering, the reason for choosing English specifically is due to it being the master language set in Pontoon (the program used by the Translation Team to perform locale updates). [^2]: For those wondering, the reason for choosing English specifically is due to it being the master language set in Pontoon (the program used by the Translation Team to perform locale updates).
If a key is present in any language _except_ the master language, it won't appear anywhere else in the translation tool, rendering missing English keys quite a hassle. If a key is present in any language _except_ the master language, it won't appear anywhere else in the translation tool, rendering missing English keys quite a hassle.

View File

@ -183,7 +183,7 @@ input:-internal-autofill-selected {
/* Show #apadStats only in battle and shop */ /* Show #apadStats only in battle and shop */
#touchControls:not([data-ui-mode="COMMAND"]):not([data-ui-mode="FIGHT"]):not( #touchControls:not([data-ui-mode="COMMAND"]):not([data-ui-mode="FIGHT"]):not(
[data-ui-mode="BALL"] [data-ui-mode="BALL"]
):not([data-ui-mode="TARGET_SELECT"]):not([data-ui-mode="MODIFIER_SELECT"]) ):not([data-ui-mode="TARGET_SELECT"]):not([data-ui-mode="REWARD_SELECT"])
#apadStats { #apadStats {
display: none; display: none;
} }

View File

@ -29,6 +29,7 @@
"devDependencies": { "devDependencies": {
"@biomejs/biome": "2.0.0", "@biomejs/biome": "2.0.0",
"@ls-lint/ls-lint": "2.3.1", "@ls-lint/ls-lint": "2.3.1",
"@types/crypto-js": "^4.2.0",
"@types/jsdom": "^21.1.7", "@types/jsdom": "^21.1.7",
"@types/node": "^22.16.5", "@types/node": "^22.16.5",
"@vitest/coverage-istanbul": "^3.2.4", "@vitest/coverage-istanbul": "^3.2.4",

View File

@ -48,6 +48,9 @@ importers:
'@ls-lint/ls-lint': '@ls-lint/ls-lint':
specifier: 2.3.1 specifier: 2.3.1
version: 2.3.1 version: 2.3.1
'@types/crypto-js':
specifier: ^4.2.0
version: 4.2.2
'@types/jsdom': '@types/jsdom':
specifier: ^21.1.7 specifier: ^21.1.7
version: 21.1.7 version: 21.1.7
@ -718,6 +721,9 @@ packages:
'@types/cookie@0.6.0': '@types/cookie@0.6.0':
resolution: {integrity: sha512-4Kh9a6B2bQciAhf7FSuMRRkUWecJgJu9nPnx3yzpsfXX/c50REIqpHY4C82bXP90qrLtXtkDxTZosYO3UpOwlA==} resolution: {integrity: sha512-4Kh9a6B2bQciAhf7FSuMRRkUWecJgJu9nPnx3yzpsfXX/c50REIqpHY4C82bXP90qrLtXtkDxTZosYO3UpOwlA==}
'@types/crypto-js@4.2.2':
resolution: {integrity: sha512-sDOLlVbHhXpAUAL0YHDUUwDZf3iN4Bwi4W6a0W0b+QcAezUbRtH4FVb+9J4h+XFPW7l/gQ9F8qC7P+Ec4k8QVQ==}
'@types/deep-eql@4.0.2': '@types/deep-eql@4.0.2':
resolution: {integrity: sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==} resolution: {integrity: sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==}
@ -2525,6 +2531,8 @@ snapshots:
'@types/cookie@0.6.0': {} '@types/cookie@0.6.0': {}
'@types/crypto-js@4.2.2': {}
'@types/deep-eql@4.0.2': {} '@types/deep-eql@4.0.2': {}
'@types/estree@1.0.8': {} '@types/estree@1.0.8': {}

Binary file not shown.

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 225 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 225 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 837 B

After

Width:  |  Height:  |  Size: 799 B

View File

@ -0,0 +1,146 @@
{
"textures": [
{
"image": "party_slot_main_short.png",
"format": "RGBA8888",
"size": {
"w": 110,
"h": 294
},
"scale": 1,
"frames": [
{
"filename": "party_slot_main_short",
"rotated": false,
"trimmed": false,
"sourceSize": {
"w": 110,
"h": 41
},
"spriteSourceSize": {
"x": 0,
"y": 0,
"w": 110,
"h": 41
},
"frame": {
"x": 0,
"y": 0,
"w": 110,
"h": 41
}
},
{
"filename": "party_slot_main_short_sel",
"rotated": false,
"trimmed": false,
"sourceSize": {
"w": 110,
"h": 41
},
"spriteSourceSize": {
"x": 0,
"y": 0,
"w": 110,
"h": 41
},
"frame": {
"x": 0,
"y": 41,
"w": 110,
"h": 41
}
},
{
"filename": "party_slot_main_short_fnt",
"rotated": false,
"trimmed": false,
"sourceSize": {
"w": 110,
"h": 41
},
"spriteSourceSize": {
"x": 0,
"y": 0,
"w": 110,
"h": 41
},
"frame": {
"x": 0,
"y": 82,
"w": 110,
"h": 41
}
},
{
"filename": "party_slot_main_short_fnt_sel",
"rotated": false,
"trimmed": false,
"sourceSize": {
"w": 110,
"h": 41
},
"spriteSourceSize": {
"x": 0,
"y": 0,
"w": 110,
"h": 41
},
"frame": {
"x": 0,
"y": 123,
"w": 110,
"h": 41
}
},
{
"filename": "party_slot_main_short_swap",
"rotated": false,
"trimmed": false,
"sourceSize": {
"w": 110,
"h": 41
},
"spriteSourceSize": {
"x": 0,
"y": 0,
"w": 110,
"h": 41
},
"frame": {
"x": 0,
"y": 164,
"w": 110,
"h": 41
}
},
{
"filename": "party_slot_main_short_swap_sel",
"rotated": false,
"trimmed": false,
"sourceSize": {
"w": 110,
"h": 41
},
"spriteSourceSize": {
"x": 0,
"y": 0,
"w": 110,
"h": 41
},
"frame": {
"x": 0,
"y": 205,
"w": 110,
"h": 41
}
}
]
}
],
"meta": {
"app": "https://www.codeandweb.com/texturepacker",
"version": "3.0",
"smartupdate": "$TexturePacker:SmartUpdate:29685f2f538901cf5bf7f0ed2ea867c3:a080ea6c8cccd1e03244214053e79796:565f7afc5ca419b6ba8dbce51ea30818$"
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 KiB

View File

@ -0,0 +1,51 @@
import { AbilityId } from "#enums/ability-id";
import { getHeldItemCategory, HeldItemCategoryId } from "#enums/held-item-id";
import { MoveId } from "#enums/move-id";
import { RewardId } from "#enums/reward-id";
import { SpeciesId } from "#enums/species-id";
import { HeldItemReward } from "#items/reward";
import { GameManager } from "#test/test-utils/game-manager";
import { generateRewardForTest } from "#test/test-utils/reward-test-utils";
import Phaser from "phaser";
import { afterEach, beforeAll, beforeEach, describe, expect, it } from "vitest";
describe("{{description}}", () => {
let phaserGame: Phaser.Game;
let game: GameManager;
beforeAll(() => {
phaserGame = new Phaser.Game({
type: Phaser.HEADLESS,
});
});
afterEach(() => {
game.phaseInterceptor.restoreOg();
});
beforeEach(() => {
game = new GameManager(phaserGame);
game.override
.ability(AbilityId.BALL_FETCH)
.battleStyle("single")
.criticalHits(false)
.enemySpecies(SpeciesId.MAGIKARP)
.enemyAbility(AbilityId.BALL_FETCH)
.enemyMoveset(MoveId.SPLASH)
.startingLevel(100)
.enemyLevel(100);
});
it("should do XYZ when applied", async () => {
await game.classicMode.startBattle([SpeciesId.FEEBAS]);
const feebas = game.field.getPlayerPokemon();
const reward = generateRewardForTest(RewardId.BERRY)!;
expect(reward).toBeInstanceOf(HeldItemReward); // Replace with actual reward instance
expect(getHeldItemCategory(reward["itemId"])).toBe(HeldItemCategoryId.BERRY);
game.scene.applyReward(reward, { pokemon: feebas });
expect(feebas).toHaveHeldItem(HeldItemCategoryId.BERRY);
});
});

View File

@ -101,8 +101,8 @@ async function promptFileName(selectedType) {
*/ */
function getBoilerplatePath(choiceType) { function getBoilerplatePath(choiceType) {
switch (choiceType) { switch (choiceType) {
// case "Reward": case "Reward":
// return path.join(__dirname, "boilerplates/reward.ts"); return path.join(__dirname, "boilerplates/rewards/reward.ts");
default: default:
return path.join(__dirname, "boilerplates/default.ts"); return path.join(__dirname, "boilerplates/default.ts");
} }

View File

@ -1,3 +1,5 @@
import type { RibbonData } from "#system/ribbons/ribbon-data";
export interface DexData { export interface DexData {
[key: number]: DexEntry; [key: number]: DexEntry;
} }
@ -10,4 +12,5 @@ export interface DexEntry {
caughtCount: number; caughtCount: number;
hatchedCount: number; hatchedCount: number;
ivs: number[]; ivs: number[];
ribbons: RibbonData;
} }

View File

@ -1,8 +0,0 @@
import type { PokemonHeldItemModifier } from "#modifiers/modifier";
import type { PokemonHeldItemModifierType } from "#modifiers/modifier-type";
export interface HeldModifierConfig {
modifier: PokemonHeldItemModifierType | PokemonHeldItemModifier;
stackCount?: number;
isTransferable?: boolean;
}

View File

@ -103,3 +103,12 @@ export type CoerceNullPropertiesToUndefined<T extends object> = {
* @typeParam T - The type to render partial * @typeParam T - The type to render partial
*/ */
export type AtLeastOne<T extends object> = Partial<T> & ObjectValues<{ [K in keyof T]: Pick<Required<T>, K> }>; export type AtLeastOne<T extends object> = Partial<T> & ObjectValues<{ [K in keyof T]: Pick<Required<T>, K> }>;
/** Type helper that adds a brand to a type, used for nominal typing.
*
* @remarks
* Brands should be either a string or unique symbol. This prevents overlap with other types.
*/
export declare class Brander<B> {
private __brand: B;
}

View File

@ -24,15 +24,15 @@ export interface AbilityTranslationEntries {
[key: string]: AbilityTranslationEntry; [key: string]: AbilityTranslationEntry;
} }
export interface ModifierTypeTranslationEntry { export interface RewardTranslationEntry {
name?: string; name?: string;
description?: string; description?: string;
extra?: SimpleTranslationEntries; extra?: SimpleTranslationEntries;
} }
export interface ModifierTypeTranslationEntries { export interface RewardTranslationEntries {
ModifierType: { [key: string]: ModifierTypeTranslationEntry }; Reward: { [key: string]: RewardTranslationEntry };
SpeciesBoosterItem: { [key: string]: ModifierTypeTranslationEntry }; SpeciesBoosterItem: { [key: string]: RewardTranslationEntry };
AttackTypeBoosterItem: SimpleTranslationEntries; AttackTypeBoosterItem: SimpleTranslationEntries;
TempStatStageBoosterItem: SimpleTranslationEntries; TempStatStageBoosterItem: SimpleTranslationEntries;
BaseStatBoosterItem: SimpleTranslationEntries; BaseStatBoosterItem: SimpleTranslationEntries;

View File

@ -1,32 +0,0 @@
// Intentionally re-exports `ModifierConstructorMap` from `modifier.ts`
import type { Pokemon } from "#field/pokemon";
import type { ModifierConstructorMap } from "#modifiers/modifier";
import type { ModifierType, WeightedModifierType } from "#modifiers/modifier-type";
import type { ObjectValues } from "#types/type-helpers";
export type ModifierTypeFunc = () => ModifierType;
export type WeightedModifierTypeWeightFunc = (party: Pokemon[], rerollCount?: number) => number;
export type { ModifierConstructorMap } from "#modifiers/modifier";
/**
* Map of modifier names to their respective instance types
*/
export type ModifierInstanceMap = {
[K in keyof ModifierConstructorMap]: InstanceType<ModifierConstructorMap[K]>;
};
/**
* Union type of all modifier constructors.
*/
export type ModifierClass = ObjectValues<ModifierConstructorMap>;
/**
* Union type of all modifier names as strings.
*/
export type ModifierString = keyof ModifierConstructorMap;
export type ModifierPool = {
[tier: string]: WeightedModifierType[];
};

View File

@ -1,13 +1,24 @@
import type { Pokemon } from "#field/pokemon";
import type { import type {
AttackMove, AttackMove,
ChargingAttackMove, ChargingAttackMove,
ChargingSelfStatusMove, ChargingSelfStatusMove,
Move,
MoveAttr, MoveAttr,
MoveAttrConstructorMap, MoveAttrConstructorMap,
SelfStatusMove, SelfStatusMove,
StatusMove, StatusMove,
} from "#moves/move"; } from "#moves/move";
/**
* A generic function producing a message during a Move's execution.
* @param user - The {@linkcode Pokemon} using the move
* @param target - The {@linkcode Pokemon} targeted by the move
* @param move - The {@linkcode Move} being used
* @returns a string
*/
export type MoveMessageFunc = (user: Pokemon, target: Pokemon, move: Move) => string;
export type MoveAttrFilter = (attr: MoveAttr) => boolean; export type MoveAttrFilter = (attr: MoveAttr) => boolean;
export type * from "#moves/move"; export type * from "#moves/move";

53
src/@types/rewards.ts Normal file
View File

@ -0,0 +1,53 @@
import type { HeldItemId } from "#enums/held-item-id";
import type { RewardId } from "#enums/reward-id";
import type { TrainerItemId } from "#enums/trainer-item-id";
import type { Pokemon } from "#field/pokemon";
import type { allRewardsType } from "#items/all-rewards";
import type { RewardGenerator } from "#items/reward";
// TODO: Remove party from arguments can be accessed from `globalScene`
export type WeightedRewardWeightFunc = (party: Pokemon[], rerollCount?: number) => number;
export type RewardPoolId = RewardId | HeldItemId | TrainerItemId;
type allRewardGenerators = {
[k in keyof allRewardsType as allRewardsType[k] extends RewardGenerator ? k : never]: allRewardsType[k];
};
type RewardGeneratorArgMap = {
[k in keyof allRewardGenerators]: Exclude<Parameters<allRewardGenerators[k]["generateReward"]>[0], undefined>;
};
/** Union type containing all {@linkcode RewardId}s corresponding to valid {@linkcode RewardGenerator}s. */
type RewardGeneratorId = keyof allRewardGenerators;
type RewardGeneratorSpecs<T extends RewardGeneratorId = RewardGeneratorId> = {
id: T;
args: RewardGeneratorArgMap[T];
};
/** Union type used to specify fixed rewards used in generation. */
export type RewardSpecs<T extends RewardPoolId = RewardPoolId> = T extends RewardGeneratorId
? T | RewardGeneratorSpecs<T>
: T;
export type RewardPoolEntry = {
id: RewardPoolId;
weight: number | WeightedRewardWeightFunc;
maxWeight?: number;
};
export type RewardPool = {
[tier: string]: RewardPoolEntry[];
};
export interface RewardPoolWeights {
[tier: string]: number[];
}
export type SilentReward =
| TrainerItemId
| typeof RewardId.VOUCHER
| typeof RewardId.VOUCHER_PLUS
| typeof RewardId.VOUCHER_PREMIUM
| typeof RewardId.ROGUE_BALL;

View File

@ -1,13 +1,13 @@
import type { PartyMemberStrength } from "#enums/party-member-strength"; import type { PartyMemberStrength } from "#enums/party-member-strength";
import type { SpeciesId } from "#enums/species-id"; import type { SpeciesId } from "#enums/species-id";
import type { EnemyPokemon } from "#field/pokemon"; import type { EnemyPokemon } from "#field/pokemon";
import type { PersistentModifier } from "#modifiers/modifier"; import type { TrainerItemConfiguration } from "#items/trainer-item-data-types";
import type { TrainerConfig } from "#trainers/trainer-config"; import type { TrainerConfig } from "#trainers/trainer-config";
import type { TrainerPartyTemplate } from "#trainers/trainer-party-template"; import type { TrainerPartyTemplate } from "#trainers/trainer-party-template";
export type PartyTemplateFunc = () => TrainerPartyTemplate; export type PartyTemplateFunc = () => TrainerPartyTemplate;
export type PartyMemberFunc = (level: number, strength: PartyMemberStrength) => EnemyPokemon; export type PartyMemberFunc = (level: number, strength: PartyMemberStrength) => EnemyPokemon;
export type GenModifiersFunc = (party: EnemyPokemon[]) => PersistentModifier[]; export type GenTrainerItemsFunc = (party: EnemyPokemon[]) => TrainerItemConfiguration;
export type GenAIFunc = (party: EnemyPokemon[]) => void; export type GenAIFunc = (party: EnemyPokemon[]) => void;
export interface TrainerTierPools { export interface TrainerTierPools {

View File

@ -17,8 +17,7 @@ export function initLoggedInUser(): void {
}; };
} }
export function updateUserInfo(): Promise<[boolean, number]> { export async function updateUserInfo(): Promise<[boolean, number]> {
return new Promise<[boolean, number]>(resolve => {
if (bypassLogin) { if (bypassLogin) {
loggedInUser = { loggedInUser = {
username: "Guest", username: "Guest",
@ -36,7 +35,7 @@ export function updateUserInfo(): Promise<[boolean, number]> {
} }
loggedInUser.lastSessionSlot = lastSessionSlot; loggedInUser.lastSessionSlot = lastSessionSlot;
// Migrate old data from before the username was appended // Migrate old data from before the username was appended
["data", "sessionData", "sessionData1", "sessionData2", "sessionData3", "sessionData4"].map(d => { ["data", "sessionData", "sessionData1", "sessionData2", "sessionData3", "sessionData4"].forEach(d => {
const lsItem = localStorage.getItem(d); const lsItem = localStorage.getItem(d);
if (lsItem && !!loggedInUser?.username) { if (lsItem && !!loggedInUser?.username) {
const lsUserItem = localStorage.getItem(`${d}_${loggedInUser.username}`); const lsUserItem = localStorage.getItem(`${d}_${loggedInUser.username}`);
@ -47,15 +46,13 @@ export function updateUserInfo(): Promise<[boolean, number]> {
localStorage.removeItem(d); localStorage.removeItem(d);
} }
}); });
return resolve([true, 200]); return [true, 200];
} }
pokerogueApi.account.getInfo().then(([accountInfo, status]) => {
const [accountInfo, status] = await pokerogueApi.account.getInfo();
if (!accountInfo) { if (!accountInfo) {
resolve([false, status]); return [false, status];
return;
} }
loggedInUser = accountInfo; loggedInUser = accountInfo;
resolve([true, 200]); return [true, 200];
});
});
} }

File diff suppressed because it is too large Load Diff

View File

@ -5,6 +5,7 @@ import { BattleSpec } from "#enums/battle-spec";
import { BattleType } from "#enums/battle-type"; import { BattleType } from "#enums/battle-type";
import { BattlerIndex } from "#enums/battler-index"; import { BattlerIndex } from "#enums/battler-index";
import type { Command } from "#enums/command"; import type { Command } from "#enums/command";
import type { HeldItemId } from "#enums/held-item-id";
import type { MoveId } from "#enums/move-id"; import type { MoveId } from "#enums/move-id";
import { MysteryEncounterMode } from "#enums/mystery-encounter-mode"; import { MysteryEncounterMode } from "#enums/mystery-encounter-mode";
import type { MysteryEncounterType } from "#enums/mystery-encounter-type"; import type { MysteryEncounterType } from "#enums/mystery-encounter-type";
@ -15,8 +16,8 @@ import { TrainerType } from "#enums/trainer-type";
import { TrainerVariant } from "#enums/trainer-variant"; import { TrainerVariant } from "#enums/trainer-variant";
import type { EnemyPokemon, PlayerPokemon, Pokemon } from "#field/pokemon"; import type { EnemyPokemon, PlayerPokemon, Pokemon } from "#field/pokemon";
import { Trainer } from "#field/trainer"; import { Trainer } from "#field/trainer";
import { MoneyMultiplierModifier, type PokemonHeldItemModifier } from "#modifiers/modifier"; import type { CustomRewardSettings } from "#items/reward-pool-utils";
import type { CustomModifierSettings } from "#modifiers/modifier-type"; import { TrainerItemEffect } from "#items/trainer-item";
import type { MysteryEncounter } from "#mystery-encounters/mystery-encounter"; import type { MysteryEncounter } from "#mystery-encounters/mystery-encounter";
import i18next from "#plugins/i18n"; import i18next from "#plugins/i18n";
import { MusicPreference } from "#system/settings"; import { MusicPreference } from "#system/settings";
@ -68,7 +69,7 @@ export class Battle {
public turnCommands: TurnCommands; public turnCommands: TurnCommands;
public playerParticipantIds: Set<number> = new Set<number>(); public playerParticipantIds: Set<number> = new Set<number>();
public battleScore = 0; public battleScore = 0;
public postBattleLoot: PokemonHeldItemModifier[] = []; public postBattleLoot: HeldItemId[] = [];
public escapeAttempts = 0; public escapeAttempts = 0;
public lastMove: MoveId; public lastMove: MoveId;
public battleSeed: string = randomString(16, true); public battleSeed: string = randomString(16, true);
@ -173,24 +174,12 @@ export class Battle {
} }
addPostBattleLoot(enemyPokemon: EnemyPokemon): void { addPostBattleLoot(enemyPokemon: EnemyPokemon): void {
this.postBattleLoot.push( this.postBattleLoot.push(...enemyPokemon.getHeldItems());
...globalScene
.findModifiers(
m => m.is("PokemonHeldItemModifier") && m.pokemonId === enemyPokemon.id && m.isTransferable,
false,
)
.map(i => {
const ret = i as PokemonHeldItemModifier;
//@ts-expect-error - this is awful to fix/change
ret.pokemonId = null;
return ret;
}),
);
} }
pickUpScatteredMoney(): void { pickUpScatteredMoney(): void {
const moneyAmount = new NumberHolder(globalScene.currentBattle.moneyScattered); const moneyAmount = new NumberHolder(globalScene.currentBattle.moneyScattered);
globalScene.applyModifiers(MoneyMultiplierModifier, true, moneyAmount); globalScene.applyPlayerItems(TrainerItemEffect.MONEY_MULTIPLIER, { numberHolder: moneyAmount });
if (globalScene.arena.getTag(ArenaTagType.HAPPY_HOUR)) { if (globalScene.arena.getTag(ArenaTagType.HAPPY_HOUR)) {
moneyAmount.value *= 2; moneyAmount.value *= 2;
@ -491,7 +480,7 @@ export class FixedBattleConfig {
public getTrainer: GetTrainerFunc; public getTrainer: GetTrainerFunc;
public getEnemyParty: GetEnemyPartyFunc; public getEnemyParty: GetEnemyPartyFunc;
public seedOffsetWaveIndex: number; public seedOffsetWaveIndex: number;
public customModifierRewardSettings?: CustomModifierSettings; public customRewardSettings?: CustomRewardSettings;
setBattleType(battleType: BattleType): FixedBattleConfig { setBattleType(battleType: BattleType): FixedBattleConfig {
this.battleType = battleType; this.battleType = battleType;
@ -518,8 +507,8 @@ export class FixedBattleConfig {
return this; return this;
} }
setCustomModifierRewards(customModifierRewardSettings: CustomModifierSettings) { setCustomRewards(customRewardSettings: CustomRewardSettings) {
this.customModifierRewardSettings = customModifierRewardSettings; this.customRewardSettings = customRewardSettings;
return this; return this;
} }
} }

View File

@ -101,3 +101,9 @@ export const ANTI_VARIANCE_WEIGHT_MODIFIER = 15;
* Default: `10000` (0.01%) * Default: `10000` (0.01%)
*/ */
export const FAKE_TITLE_LOGO_CHANCE = 10000; export const FAKE_TITLE_LOGO_CHANCE = 10000;
/**
* The ceiling on friendship amount that can be reached through the use of rare candies.
* Using rare candies will never increase friendship beyond this value.
*/
export const RARE_CANDY_FRIENDSHIP_CAP = 200;

View File

@ -10,7 +10,7 @@ import type { ArenaTrapTag, SuppressAbilitiesTag } from "#data/arena-tag";
import type { BattlerTag } from "#data/battler-tags"; import type { BattlerTag } from "#data/battler-tags";
import { GroundedTag } from "#data/battler-tags"; import { GroundedTag } from "#data/battler-tags";
import { getBerryEffectFunc } from "#data/berry"; import { getBerryEffectFunc } from "#data/berry";
import { allAbilities, allMoves } from "#data/data-lists"; import { allAbilities, allHeldItems, allMoves } from "#data/data-lists";
import { SpeciesFormChangeAbilityTrigger, SpeciesFormChangeWeatherTrigger } from "#data/form-change-triggers"; import { SpeciesFormChangeAbilityTrigger, SpeciesFormChangeWeatherTrigger } from "#data/form-change-triggers";
import { Gender } from "#data/gender"; import { Gender } from "#data/gender";
import { getPokeballName } from "#data/pokeball"; import { getPokeballName } from "#data/pokeball";
@ -28,6 +28,7 @@ import { BattlerTagLapseType } from "#enums/battler-tag-lapse-type";
import { BattlerTagType } from "#enums/battler-tag-type"; import { BattlerTagType } from "#enums/battler-tag-type";
import type { BerryType } from "#enums/berry-type"; import type { BerryType } from "#enums/berry-type";
import { Command } from "#enums/command"; import { Command } from "#enums/command";
import { HeldItemCategoryId, HeldItemId, isItemInCategory } from "#enums/held-item-id";
import { HitResult } from "#enums/hit-result"; import { HitResult } from "#enums/hit-result";
import { CommonAnim } from "#enums/move-anims-common"; import { CommonAnim } from "#enums/move-anims-common";
import { MoveCategory } from "#enums/move-category"; import { MoveCategory } from "#enums/move-category";
@ -46,8 +47,7 @@ import { SwitchType } from "#enums/switch-type";
import { WeatherType } from "#enums/weather-type"; import { WeatherType } from "#enums/weather-type";
import { BerryUsedEvent } from "#events/battle-scene"; import { BerryUsedEvent } from "#events/battle-scene";
import type { EnemyPokemon, Pokemon } from "#field/pokemon"; import type { EnemyPokemon, Pokemon } from "#field/pokemon";
import { BerryModifier, HitHealModifier, PokemonHeldItemModifier } from "#modifiers/modifier"; import { type BerryHeldItem, berryTypeToHeldItem } from "#items/berry";
import { BerryModifierType } from "#modifiers/modifier-type";
import { applyMoveAttrs } from "#moves/apply-attrs"; import { applyMoveAttrs } from "#moves/apply-attrs";
import { noAbilityTypeOverrideMoves } from "#moves/invalid-moves"; import { noAbilityTypeOverrideMoves } from "#moves/invalid-moves";
import type { Move } from "#moves/move"; import type { Move } from "#moves/move";
@ -74,6 +74,7 @@ import {
randSeedItem, randSeedItem,
toDmgValue, toDmgValue,
} from "#utils/common"; } from "#utils/common";
import { toCamelCase } from "#utils/strings";
import i18next from "i18next"; import i18next from "i18next";
export class Ability implements Localizable { export class Ability implements Localizable {
@ -109,13 +110,9 @@ export class Ability implements Localizable {
} }
localize(): void { localize(): void {
const i18nKey = AbilityId[this.id] const i18nKey = toCamelCase(AbilityId[this.id]);
.split("_")
.filter(f => f)
.map((f, i) => (i ? `${f[0]}${f.slice(1).toLowerCase()}` : f.toLowerCase()))
.join("") as string;
this.name = this.id ? `${i18next.t(`ability:${i18nKey}.name`) as string}${this.nameAppend}` : ""; this.name = this.id ? `${i18next.t(`ability:${i18nKey}.name`)}${this.nameAppend}` : "";
this.description = this.id ? (i18next.t(`ability:${i18nKey}.description`) as string) : ""; this.description = this.id ? (i18next.t(`ability:${i18nKey}.description`) as string) : "";
} }
@ -1670,6 +1667,7 @@ export class MoveTypeChangeAbAttr extends PreAttackAbAttr {
constructor( constructor(
private newType: PokemonType, private newType: PokemonType,
private powerMultiplier: number, private powerMultiplier: number,
// TODO: all moves with this attr solely check the move being used...
private condition?: PokemonAttackCondition, private condition?: PokemonAttackCondition,
) { ) {
super(false); super(false);
@ -2137,7 +2135,7 @@ export abstract class PostAttackAbAttr extends AbAttr {
export class PostAttackStealHeldItemAbAttr extends PostAttackAbAttr { export class PostAttackStealHeldItemAbAttr extends PostAttackAbAttr {
private stealCondition: PokemonAttackCondition | null; private stealCondition: PokemonAttackCondition | null;
private stolenItem?: PokemonHeldItemModifier; private stolenItem?: HeldItemId;
constructor(stealCondition?: PokemonAttackCondition) { constructor(stealCondition?: PokemonAttackCondition) {
super(); super();
@ -2156,11 +2154,11 @@ export class PostAttackStealHeldItemAbAttr extends PostAttackAbAttr {
hitResult < HitResult.NO_EFFECT && hitResult < HitResult.NO_EFFECT &&
(!this.stealCondition || this.stealCondition(pokemon, opponent, move)) (!this.stealCondition || this.stealCondition(pokemon, opponent, move))
) { ) {
const heldItems = this.getTargetHeldItems(opponent).filter(i => i.isTransferable); const heldItems = opponent.heldItemManager.getTransferableHeldItems();
if (heldItems.length) { if (heldItems.length) {
// Ensure that the stolen item in testing is the same as when the effect is applied // Ensure that the stolen item in testing is the same as when the effect is applied
this.stolenItem = heldItems[pokemon.randBattleSeedInt(heldItems.length)]; this.stolenItem = heldItems[pokemon.randBattleSeedInt(heldItems.length)];
if (globalScene.canTransferHeldItemModifier(this.stolenItem, pokemon)) { if (globalScene.canTransferHeldItem(this.stolenItem, opponent, pokemon)) {
return true; return true;
} }
} }
@ -2170,28 +2168,21 @@ export class PostAttackStealHeldItemAbAttr extends PostAttackAbAttr {
} }
override apply({ opponent, pokemon }: PostMoveInteractionAbAttrParams): void { override apply({ opponent, pokemon }: PostMoveInteractionAbAttrParams): void {
const heldItems = this.getTargetHeldItems(opponent).filter(i => i.isTransferable); const heldItems = opponent.heldItemManager.getTransferableHeldItems();
if (!this.stolenItem) { if (!this.stolenItem) {
this.stolenItem = heldItems[pokemon.randBattleSeedInt(heldItems.length)]; this.stolenItem = heldItems[pokemon.randBattleSeedInt(heldItems.length)];
} }
if (globalScene.tryTransferHeldItemModifier(this.stolenItem, pokemon, false)) { if (globalScene.tryTransferHeldItem(this.stolenItem, opponent, pokemon, false)) {
globalScene.phaseManager.queueMessage( globalScene.phaseManager.queueMessage(
i18next.t("abilityTriggers:postAttackStealHeldItem", { i18next.t("abilityTriggers:postAttackStealHeldItem", {
pokemonNameWithAffix: getPokemonNameWithAffix(pokemon), pokemonNameWithAffix: getPokemonNameWithAffix(pokemon),
defenderName: opponent.name, defenderName: opponent.name,
stolenItemType: this.stolenItem.type.name, stolenItemType: allHeldItems[this.stolenItem].name,
}), }),
); );
} }
this.stolenItem = undefined; this.stolenItem = undefined;
} }
getTargetHeldItems(target: Pokemon): PokemonHeldItemModifier[] {
return globalScene.findModifiers(
m => m instanceof PokemonHeldItemModifier && m.pokemonId === target.id,
target.isPlayer(),
) as PokemonHeldItemModifier[];
}
} }
export class PostAttackApplyStatusEffectAbAttr extends PostAttackAbAttr { export class PostAttackApplyStatusEffectAbAttr extends PostAttackAbAttr {
@ -2282,7 +2273,7 @@ export class PostAttackApplyBattlerTagAbAttr extends PostAttackAbAttr {
export class PostDefendStealHeldItemAbAttr extends PostDefendAbAttr { export class PostDefendStealHeldItemAbAttr extends PostDefendAbAttr {
private condition?: PokemonDefendCondition; private condition?: PokemonDefendCondition;
private stolenItem?: PokemonHeldItemModifier; private stolenItem?: HeldItemId;
constructor(condition?: PokemonDefendCondition) { constructor(condition?: PokemonDefendCondition) {
super(); super();
@ -2292,10 +2283,10 @@ export class PostDefendStealHeldItemAbAttr extends PostDefendAbAttr {
override canApply({ simulated, pokemon, opponent, move, hitResult }: PostMoveInteractionAbAttrParams): boolean { override canApply({ simulated, pokemon, opponent, move, hitResult }: PostMoveInteractionAbAttrParams): boolean {
if (!simulated && hitResult < HitResult.NO_EFFECT && (!this.condition || this.condition(pokemon, opponent, move))) { if (!simulated && hitResult < HitResult.NO_EFFECT && (!this.condition || this.condition(pokemon, opponent, move))) {
const heldItems = this.getTargetHeldItems(opponent).filter(i => i.isTransferable); const heldItems = opponent.heldItemManager.getTransferableHeldItems();
if (heldItems.length) { if (heldItems.length) {
this.stolenItem = heldItems[pokemon.randBattleSeedInt(heldItems.length)]; this.stolenItem = heldItems[pokemon.randBattleSeedInt(heldItems.length)];
if (globalScene.canTransferHeldItemModifier(this.stolenItem, pokemon)) { if (globalScene.canTransferHeldItem(this.stolenItem, opponent, pokemon)) {
return true; return true;
} }
} }
@ -2304,28 +2295,21 @@ export class PostDefendStealHeldItemAbAttr extends PostDefendAbAttr {
} }
override apply({ pokemon, opponent }: PostMoveInteractionAbAttrParams): void { override apply({ pokemon, opponent }: PostMoveInteractionAbAttrParams): void {
const heldItems = this.getTargetHeldItems(opponent).filter(i => i.isTransferable); const heldItems = opponent.heldItemManager.getTransferableHeldItems();
if (!this.stolenItem) { if (!this.stolenItem) {
this.stolenItem = heldItems[pokemon.randBattleSeedInt(heldItems.length)]; this.stolenItem = heldItems[pokemon.randBattleSeedInt(heldItems.length)];
} }
if (globalScene.tryTransferHeldItemModifier(this.stolenItem, pokemon, false)) { if (globalScene.tryTransferHeldItem(this.stolenItem, opponent, pokemon, false)) {
globalScene.phaseManager.queueMessage( globalScene.phaseManager.queueMessage(
i18next.t("abilityTriggers:postDefendStealHeldItem", { i18next.t("abilityTriggers:postDefendStealHeldItem", {
pokemonNameWithAffix: getPokemonNameWithAffix(pokemon), pokemonNameWithAffix: getPokemonNameWithAffix(pokemon),
attackerName: opponent.name, attackerName: opponent.name,
stolenItemType: this.stolenItem.type.name, stolenItemType: allHeldItems[this.stolenItem].name,
}), }),
); );
} }
this.stolenItem = undefined; this.stolenItem = undefined;
} }
getTargetHeldItems(target: Pokemon): PokemonHeldItemModifier[] {
return globalScene.findModifiers(
m => m instanceof PokemonHeldItemModifier && m.pokemonId === target.id,
target.isPlayer(),
) as PokemonHeldItemModifier[];
}
} }
/** /**
@ -4661,10 +4645,14 @@ export class PostTurnRestoreBerryAbAttr extends PostTurnAbAttr {
override canApply({ pokemon }: AbAttrBaseParams): boolean { override canApply({ pokemon }: AbAttrBaseParams): boolean {
// Ensure we have at least 1 recoverable berry (at least 1 berry in berriesEaten is not capped) // Ensure we have at least 1 recoverable berry (at least 1 berry in berriesEaten is not capped)
const cappedBerries = new Set( const cappedBerries = new Set(
globalScene pokemon
.getModifiers(BerryModifier, pokemon.isPlayer()) .getHeldItems()
.filter(bm => bm.pokemonId === pokemon.id && bm.getCountUnderMax() < 1) .filter(
.map(bm => bm.berryType), bm =>
isItemInCategory(bm, HeldItemCategoryId.BERRY) &&
pokemon.heldItemManager.getStack(bm) < allHeldItems[bm].maxStackCount,
)
.map(bm => (allHeldItems[bm] as BerryHeldItem).berryType),
); );
this.berriesUnderCap = pokemon.battleData.berriesEaten.filter(bt => !cappedBerries.has(bt)); this.berriesUnderCap = pokemon.battleData.berriesEaten.filter(bt => !cappedBerries.has(bt));
@ -4694,30 +4682,15 @@ export class PostTurnRestoreBerryAbAttr extends PostTurnAbAttr {
const randomIdx = randSeedInt(this.berriesUnderCap.length); const randomIdx = randSeedInt(this.berriesUnderCap.length);
const chosenBerryType = this.berriesUnderCap[randomIdx]; const chosenBerryType = this.berriesUnderCap[randomIdx];
pokemon.battleData.berriesEaten.splice(randomIdx, 1); // Remove berry from memory pokemon.battleData.berriesEaten.splice(randomIdx, 1); // Remove berry from memory
const chosenBerry = new BerryModifierType(chosenBerryType); const chosenBerry = berryTypeToHeldItem[chosenBerryType];
// Add the randomly chosen berry or update the existing one pokemon.heldItemManager.add(chosenBerry);
const berryModifier = globalScene.findModifier(
m => m instanceof BerryModifier && m.berryType === chosenBerryType && m.pokemonId === pokemon.id,
pokemon.isPlayer(),
) as BerryModifier | undefined;
if (berryModifier) { globalScene.updateItems(pokemon.isPlayer());
berryModifier.stackCount++;
} else {
const newBerry = new BerryModifier(chosenBerry, pokemon.id, chosenBerryType, 1);
if (pokemon.isPlayer()) {
globalScene.addModifier(newBerry);
} else {
globalScene.addEnemyModifier(newBerry);
}
}
globalScene.updateModifiers(pokemon.isPlayer());
globalScene.phaseManager.queueMessage( globalScene.phaseManager.queueMessage(
i18next.t("abilityTriggers:postTurnLootCreateEatenBerry", { i18next.t("abilityTriggers:postTurnLootCreateEatenBerry", {
pokemonNameWithAffix: getPokemonNameWithAffix(pokemon), pokemonNameWithAffix: getPokemonNameWithAffix(pokemon),
berryName: chosenBerry.name, berryName: allHeldItems[chosenBerry].name,
}), }),
); );
return true; return true;
@ -4751,8 +4724,7 @@ export class CudChewConsumeBerryAbAttr extends AbAttr {
// This doesn't count as "eating" a berry (for unnerve/stuff cheeks/unburden) as no item is consumed. // This doesn't count as "eating" a berry (for unnerve/stuff cheeks/unburden) as no item is consumed.
for (const berryType of pokemon.summonData.berriesEatenLast) { for (const berryType of pokemon.summonData.berriesEatenLast) {
getBerryEffectFunc(berryType)(pokemon); getBerryEffectFunc(berryType)(pokemon);
const bMod = new BerryModifier(new BerryModifierType(berryType), pokemon.id, berryType, 1); globalScene.eventTarget.dispatchEvent(new BerryUsedEvent(pokemon, berryType)); // trigger message
globalScene.eventTarget.dispatchEvent(new BerryUsedEvent(bMod)); // trigger message
} }
// uncomment to make cheek pouch work with cud chew // uncomment to make cheek pouch work with cud chew
@ -5343,13 +5315,13 @@ export abstract class PostBattleAbAttr extends AbAttr {
} }
export class PostBattleLootAbAttr extends PostBattleAbAttr { export class PostBattleLootAbAttr extends PostBattleAbAttr {
private randItem?: PokemonHeldItemModifier; private randItem?: HeldItemId;
override canApply({ simulated, victory, pokemon }: PostBattleAbAttrParams): boolean { override canApply({ simulated, victory, pokemon }: PostBattleAbAttrParams): boolean {
const postBattleLoot = globalScene.currentBattle.postBattleLoot; const postBattleLoot = globalScene.currentBattle.postBattleLoot;
if (!simulated && postBattleLoot.length && victory) { if (!simulated && postBattleLoot.length && victory) {
this.randItem = randSeedItem(postBattleLoot); this.randItem = randSeedItem(postBattleLoot);
return globalScene.canTransferHeldItemModifier(this.randItem, pokemon, 1); return pokemon.heldItemManager.getStack(this.randItem) < allHeldItems[this.randItem].maxStackCount;
} }
return false; return false;
} }
@ -5360,12 +5332,12 @@ export class PostBattleLootAbAttr extends PostBattleAbAttr {
this.randItem = randSeedItem(postBattleLoot); this.randItem = randSeedItem(postBattleLoot);
} }
if (globalScene.tryTransferHeldItemModifier(this.randItem, pokemon, true, 1, true, undefined, false)) { if (pokemon.heldItemManager.add(this.randItem)) {
postBattleLoot.splice(postBattleLoot.indexOf(this.randItem), 1); postBattleLoot.splice(postBattleLoot.indexOf(this.randItem), 1);
globalScene.phaseManager.queueMessage( globalScene.phaseManager.queueMessage(
i18next.t("abilityTriggers:postBattleLoot", { i18next.t("abilityTriggers:postBattleLoot", {
pokemonNameWithAffix: getPokemonNameWithAffix(pokemon), pokemonNameWithAffix: getPokemonNameWithAffix(pokemon),
itemName: this.randItem.type.name, itemName: allHeldItems[this.randItem].name,
}), }),
); );
} }
@ -6312,8 +6284,6 @@ class ForceSwitchOutHelper {
} }
if (!allyPokemon?.isActive(true)) { if (!allyPokemon?.isActive(true)) {
globalScene.clearEnemyHeldItemModifiers();
if (switchOutTarget.hp) { if (switchOutTarget.hp) {
globalScene.phaseManager.pushNew("BattleEndPhase", false); globalScene.phaseManager.pushNew("BattleEndPhase", false);
@ -6397,11 +6367,9 @@ class ForceSwitchOutHelper {
* @returns The amount of health recovered by Shell Bell. * @returns The amount of health recovered by Shell Bell.
*/ */
function calculateShellBellRecovery(pokemon: Pokemon): number { function calculateShellBellRecovery(pokemon: Pokemon): number {
const shellBellModifier = pokemon.getHeldItems().find(m => m instanceof HitHealModifier); // Returns 0 if no Shell Bell is present
if (shellBellModifier) { const shellBellStack = pokemon.heldItemManager.getStack(HeldItemId.SHELL_BELL);
return toDmgValue(pokemon.turnData.totalDamageDealt / 8) * shellBellModifier.stackCount; return toDmgValue(pokemon.turnData.totalDamageDealt / 8) * shellBellStack;
}
return 0;
} }
export interface PostDamageAbAttrParams extends AbAttrBaseParams { export interface PostDamageAbAttrParams extends AbAttrBaseParams {

View File

@ -1,8 +1,9 @@
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 { allHeldItems, allMoves } from "#data/data-lists";
import { Gender, getGenderSymbol } from "#data/gender"; import { Gender, getGenderSymbol } from "#data/gender";
import { BiomeId } from "#enums/biome-id"; import { BiomeId } from "#enums/biome-id";
import { HeldItemId } from "#enums/held-item-id";
import { MoveId } from "#enums/move-id"; import { MoveId } from "#enums/move-id";
import { Nature } from "#enums/nature"; import { Nature } from "#enums/nature";
import { PokeballType } from "#enums/pokeball"; import { PokeballType } from "#enums/pokeball";
@ -12,7 +13,6 @@ import { SpeciesId } from "#enums/species-id";
import { TimeOfDay } from "#enums/time-of-day"; import { TimeOfDay } from "#enums/time-of-day";
import { WeatherType } from "#enums/weather-type"; import { WeatherType } from "#enums/weather-type";
import type { Pokemon } from "#field/pokemon"; import type { Pokemon } from "#field/pokemon";
import type { SpeciesStatBoosterItem, SpeciesStatBoosterModifierType } from "#modifiers/modifier-type";
import { coerceArray, isNullOrUndefined, randSeedInt } from "#utils/common"; import { coerceArray, isNullOrUndefined, randSeedInt } from "#utils/common";
import { getPokemonSpecies } from "#utils/pokemon-utils"; import { getPokemonSpecies } from "#utils/pokemon-utils";
import i18next from "i18next"; import i18next from "i18next";
@ -99,7 +99,7 @@ const EvoCondKey = {
SPECIES_CAUGHT: 12, SPECIES_CAUGHT: 12,
GENDER: 13, GENDER: 13,
NATURE: 14, NATURE: 14,
HELD_ITEM: 15, // Currently checks only for species stat booster items HELD_ITEM: 15,
} as const; } as const;
type EvolutionConditionData = type EvolutionConditionData =
@ -110,7 +110,7 @@ type EvolutionConditionData =
{key: typeof EvoCondKey.GENDER, gender: Gender} | {key: typeof EvoCondKey.GENDER, gender: Gender} |
{key: typeof EvoCondKey.MOVE_TYPE | typeof EvoCondKey.PARTY_TYPE, pkmnType: PokemonType} | {key: typeof EvoCondKey.MOVE_TYPE | typeof EvoCondKey.PARTY_TYPE, pkmnType: PokemonType} |
{key: typeof EvoCondKey.SPECIES_CAUGHT, speciesCaught: SpeciesId} | {key: typeof EvoCondKey.SPECIES_CAUGHT, speciesCaught: SpeciesId} |
{key: typeof EvoCondKey.HELD_ITEM, itemKey: SpeciesStatBoosterItem} | {key: typeof EvoCondKey.HELD_ITEM, itemKey: HeldItemId} |
{key: typeof EvoCondKey.NATURE, nature: Nature[]} | {key: typeof EvoCondKey.NATURE, nature: Nature[]} |
{key: typeof EvoCondKey.WEATHER, weather: WeatherType[]} | {key: typeof EvoCondKey.WEATHER, weather: WeatherType[]} |
{key: typeof EvoCondKey.TYROGUE, move: TyrogueMove} | {key: typeof EvoCondKey.TYROGUE, move: TyrogueMove} |
@ -177,10 +177,7 @@ export class SpeciesEvolutionCondition {
case EvoCondKey.PARTY_TYPE: case EvoCondKey.PARTY_TYPE:
return globalScene.getPlayerParty().some(p => p.getTypes(false, false, true).includes(cond.pkmnType)) return globalScene.getPlayerParty().some(p => p.getTypes(false, false, true).includes(cond.pkmnType))
case EvoCondKey.EVO_TREASURE_TRACKER: case EvoCondKey.EVO_TREASURE_TRACKER:
return pokemon.getHeldItems().some(m => return allHeldItems[HeldItemId.GIMMIGHOUL_EVO_TRACKER].getStackCount(pokemon) >= cond.value;
m.is("EvoTrackerModifier") &&
m.getStackCount() + pokemon.getPersistentTreasureCount() >= cond.value
);
case EvoCondKey.GENDER: case EvoCondKey.GENDER:
return pokemon.gender === cond.gender; return pokemon.gender === cond.gender;
case EvoCondKey.SHEDINJA: // Shedinja cannot be evolved into directly case EvoCondKey.SHEDINJA: // Shedinja cannot be evolved into directly
@ -201,7 +198,7 @@ export class SpeciesEvolutionCondition {
case EvoCondKey.SPECIES_CAUGHT: case EvoCondKey.SPECIES_CAUGHT:
return !!globalScene.gameData.dexData[cond.speciesCaught].caughtAttr; return !!globalScene.gameData.dexData[cond.speciesCaught].caughtAttr;
case EvoCondKey.HELD_ITEM: case EvoCondKey.HELD_ITEM:
return pokemon.getHeldItems().some(m => m.is("SpeciesStatBoosterModifier") && (m.type as SpeciesStatBoosterModifierType).key === cond.itemKey) return pokemon.heldItemManager.hasItem(cond.itemKey);
} }
}); });
} }
@ -1765,8 +1762,8 @@ export const pokemonEvolutions: PokemonEvolutions = {
new SpeciesEvolution(SpeciesId.DUSKNOIR, 1, EvolutionItem.REAPER_CLOTH, null, SpeciesWildEvolutionDelay.VERY_LONG) new SpeciesEvolution(SpeciesId.DUSKNOIR, 1, EvolutionItem.REAPER_CLOTH, null, SpeciesWildEvolutionDelay.VERY_LONG)
], ],
[SpeciesId.CLAMPERL]: [ [SpeciesId.CLAMPERL]: [
new SpeciesEvolution(SpeciesId.HUNTAIL, 1, EvolutionItem.LINKING_CORD, {key: EvoCondKey.HELD_ITEM, itemKey: "DEEP_SEA_TOOTH"}, SpeciesWildEvolutionDelay.VERY_LONG), new SpeciesEvolution(SpeciesId.HUNTAIL, 1, EvolutionItem.LINKING_CORD, {key: EvoCondKey.HELD_ITEM, itemKey: HeldItemId.DEEP_SEA_TOOTH}, SpeciesWildEvolutionDelay.VERY_LONG),
new SpeciesEvolution(SpeciesId.GOREBYSS, 1, EvolutionItem.LINKING_CORD, {key: EvoCondKey.HELD_ITEM, itemKey: "DEEP_SEA_SCALE"}, SpeciesWildEvolutionDelay.VERY_LONG) new SpeciesEvolution(SpeciesId.GOREBYSS, 1, EvolutionItem.LINKING_CORD, {key: EvoCondKey.HELD_ITEM, itemKey: HeldItemId.DEEP_SEA_SCALE}, SpeciesWildEvolutionDelay.VERY_LONG)
], ],
[SpeciesId.BOLDORE]: [ [SpeciesId.BOLDORE]: [
new SpeciesEvolution(SpeciesId.GIGALITH, 1, EvolutionItem.LINKING_CORD, null, SpeciesWildEvolutionDelay.VERY_LONG) new SpeciesEvolution(SpeciesId.GIGALITH, 1, EvolutionItem.LINKING_CORD, null, SpeciesWildEvolutionDelay.VERY_LONG)
@ -1866,17 +1863,16 @@ interface PokemonPrevolutions {
export const pokemonPrevolutions: PokemonPrevolutions = {}; export const pokemonPrevolutions: PokemonPrevolutions = {};
export function initPokemonPrevolutions(): void { export function initPokemonPrevolutions(): void {
const megaFormKeys = [ SpeciesFormKey.MEGA, "", SpeciesFormKey.MEGA_X, "", SpeciesFormKey.MEGA_Y ].map(sfk => sfk as string); // TODO: Why do we have empty strings in our array?
const prevolutionKeys = Object.keys(pokemonEvolutions); const megaFormKeys = [ SpeciesFormKey.MEGA, "", SpeciesFormKey.MEGA_X, "", SpeciesFormKey.MEGA_Y ];
prevolutionKeys.forEach(pk => { for (const [pk, evolutions] of Object.entries(pokemonEvolutions)) {
const evolutions = pokemonEvolutions[pk];
for (const ev of evolutions) { for (const ev of evolutions) {
if (ev.evoFormKey && megaFormKeys.indexOf(ev.evoFormKey) > -1) { if (ev.evoFormKey && megaFormKeys.indexOf(ev.evoFormKey) > -1) {
continue; continue;
} }
pokemonPrevolutions[ev.speciesId] = Number.parseInt(pk) as SpeciesId; pokemonPrevolutions[ev.speciesId] = Number.parseInt(pk) as SpeciesId;
} }
}); }
} }

View File

@ -1,4 +1,4 @@
import { ModifierTier } from "#enums/modifier-tier"; import { RarityTier } from "#enums/reward-tier";
import { MoveId } from "#enums/move-id"; import { MoveId } from "#enums/move-id";
import { SpeciesId } from "#enums/species-id"; import { SpeciesId } from "#enums/species-id";
@ -68591,324 +68591,324 @@ function transposeTmSpecies(): SpeciesTmMoves {
export const speciesTmMoves: SpeciesTmMoves = transposeTmSpecies(); export const speciesTmMoves: SpeciesTmMoves = transposeTmSpecies();
interface TmPoolTiers { interface TmPoolTiers {
[key: number]: ModifierTier [key: number]: RarityTier
} }
export const tmPoolTiers: TmPoolTiers = { export const tmPoolTiers: TmPoolTiers = {
[MoveId.MEGA_PUNCH]: ModifierTier.GREAT, [MoveId.MEGA_PUNCH]: RarityTier.GREAT,
[MoveId.PAY_DAY]: ModifierTier.ULTRA, [MoveId.PAY_DAY]: RarityTier.ULTRA,
[MoveId.FIRE_PUNCH]: ModifierTier.GREAT, [MoveId.FIRE_PUNCH]: RarityTier.GREAT,
[MoveId.ICE_PUNCH]: ModifierTier.GREAT, [MoveId.ICE_PUNCH]: RarityTier.GREAT,
[MoveId.THUNDER_PUNCH]: ModifierTier.GREAT, [MoveId.THUNDER_PUNCH]: RarityTier.GREAT,
[MoveId.SWORDS_DANCE]: ModifierTier.COMMON, [MoveId.SWORDS_DANCE]: RarityTier.COMMON,
[MoveId.CUT]: ModifierTier.COMMON, [MoveId.CUT]: RarityTier.COMMON,
[MoveId.FLY]: ModifierTier.COMMON, [MoveId.FLY]: RarityTier.COMMON,
[MoveId.MEGA_KICK]: ModifierTier.GREAT, [MoveId.MEGA_KICK]: RarityTier.GREAT,
[MoveId.BODY_SLAM]: ModifierTier.GREAT, [MoveId.BODY_SLAM]: RarityTier.GREAT,
[MoveId.TAKE_DOWN]: ModifierTier.GREAT, [MoveId.TAKE_DOWN]: RarityTier.GREAT,
[MoveId.DOUBLE_EDGE]: ModifierTier.ULTRA, [MoveId.DOUBLE_EDGE]: RarityTier.ULTRA,
[MoveId.PIN_MISSILE]: ModifierTier.COMMON, [MoveId.PIN_MISSILE]: RarityTier.COMMON,
[MoveId.ROAR]: ModifierTier.COMMON, [MoveId.ROAR]: RarityTier.COMMON,
[MoveId.FLAMETHROWER]: ModifierTier.ULTRA, [MoveId.FLAMETHROWER]: RarityTier.ULTRA,
[MoveId.HYDRO_PUMP]: ModifierTier.ULTRA, [MoveId.HYDRO_PUMP]: RarityTier.ULTRA,
[MoveId.SURF]: ModifierTier.ULTRA, [MoveId.SURF]: RarityTier.ULTRA,
[MoveId.ICE_BEAM]: ModifierTier.ULTRA, [MoveId.ICE_BEAM]: RarityTier.ULTRA,
[MoveId.BLIZZARD]: ModifierTier.ULTRA, [MoveId.BLIZZARD]: RarityTier.ULTRA,
[MoveId.PSYBEAM]: ModifierTier.GREAT, [MoveId.PSYBEAM]: RarityTier.GREAT,
[MoveId.HYPER_BEAM]: ModifierTier.ULTRA, [MoveId.HYPER_BEAM]: RarityTier.ULTRA,
[MoveId.LOW_KICK]: ModifierTier.COMMON, [MoveId.LOW_KICK]: RarityTier.COMMON,
[MoveId.COUNTER]: ModifierTier.COMMON, [MoveId.COUNTER]: RarityTier.COMMON,
[MoveId.STRENGTH]: ModifierTier.GREAT, [MoveId.STRENGTH]: RarityTier.GREAT,
[MoveId.SOLAR_BEAM]: ModifierTier.ULTRA, [MoveId.SOLAR_BEAM]: RarityTier.ULTRA,
[MoveId.FIRE_SPIN]: ModifierTier.COMMON, [MoveId.FIRE_SPIN]: RarityTier.COMMON,
[MoveId.THUNDERBOLT]: ModifierTier.ULTRA, [MoveId.THUNDERBOLT]: RarityTier.ULTRA,
[MoveId.THUNDER_WAVE]: ModifierTier.COMMON, [MoveId.THUNDER_WAVE]: RarityTier.COMMON,
[MoveId.THUNDER]: ModifierTier.ULTRA, [MoveId.THUNDER]: RarityTier.ULTRA,
[MoveId.EARTHQUAKE]: ModifierTier.ULTRA, [MoveId.EARTHQUAKE]: RarityTier.ULTRA,
[MoveId.DIG]: ModifierTier.GREAT, [MoveId.DIG]: RarityTier.GREAT,
[MoveId.TOXIC]: ModifierTier.GREAT, [MoveId.TOXIC]: RarityTier.GREAT,
[MoveId.PSYCHIC]: ModifierTier.ULTRA, [MoveId.PSYCHIC]: RarityTier.ULTRA,
[MoveId.AGILITY]: ModifierTier.COMMON, [MoveId.AGILITY]: RarityTier.COMMON,
[MoveId.NIGHT_SHADE]: ModifierTier.COMMON, [MoveId.NIGHT_SHADE]: RarityTier.COMMON,
[MoveId.SCREECH]: ModifierTier.COMMON, [MoveId.SCREECH]: RarityTier.COMMON,
[MoveId.DOUBLE_TEAM]: ModifierTier.COMMON, [MoveId.DOUBLE_TEAM]: RarityTier.COMMON,
[MoveId.CONFUSE_RAY]: ModifierTier.COMMON, [MoveId.CONFUSE_RAY]: RarityTier.COMMON,
[MoveId.LIGHT_SCREEN]: ModifierTier.COMMON, [MoveId.LIGHT_SCREEN]: RarityTier.COMMON,
[MoveId.HAZE]: ModifierTier.COMMON, [MoveId.HAZE]: RarityTier.COMMON,
[MoveId.REFLECT]: ModifierTier.COMMON, [MoveId.REFLECT]: RarityTier.COMMON,
[MoveId.FOCUS_ENERGY]: ModifierTier.COMMON, [MoveId.FOCUS_ENERGY]: RarityTier.COMMON,
[MoveId.METRONOME]: ModifierTier.COMMON, [MoveId.METRONOME]: RarityTier.COMMON,
[MoveId.SELF_DESTRUCT]: ModifierTier.GREAT, [MoveId.SELF_DESTRUCT]: RarityTier.GREAT,
[MoveId.FIRE_BLAST]: ModifierTier.ULTRA, [MoveId.FIRE_BLAST]: RarityTier.ULTRA,
[MoveId.WATERFALL]: ModifierTier.GREAT, [MoveId.WATERFALL]: RarityTier.GREAT,
[MoveId.SWIFT]: ModifierTier.COMMON, [MoveId.SWIFT]: RarityTier.COMMON,
[MoveId.AMNESIA]: ModifierTier.COMMON, [MoveId.AMNESIA]: RarityTier.COMMON,
[MoveId.DREAM_EATER]: ModifierTier.GREAT, [MoveId.DREAM_EATER]: RarityTier.GREAT,
[MoveId.LEECH_LIFE]: ModifierTier.ULTRA, [MoveId.LEECH_LIFE]: RarityTier.ULTRA,
[MoveId.FLASH]: ModifierTier.COMMON, [MoveId.FLASH]: RarityTier.COMMON,
[MoveId.EXPLOSION]: ModifierTier.GREAT, [MoveId.EXPLOSION]: RarityTier.GREAT,
[MoveId.REST]: ModifierTier.COMMON, [MoveId.REST]: RarityTier.COMMON,
[MoveId.ROCK_SLIDE]: ModifierTier.GREAT, [MoveId.ROCK_SLIDE]: RarityTier.GREAT,
[MoveId.TRI_ATTACK]: ModifierTier.ULTRA, [MoveId.TRI_ATTACK]: RarityTier.ULTRA,
[MoveId.SUPER_FANG]: ModifierTier.COMMON, [MoveId.SUPER_FANG]: RarityTier.COMMON,
[MoveId.SUBSTITUTE]: ModifierTier.COMMON, [MoveId.SUBSTITUTE]: RarityTier.COMMON,
[MoveId.THIEF]: ModifierTier.GREAT, [MoveId.THIEF]: RarityTier.GREAT,
[MoveId.SNORE]: ModifierTier.COMMON, [MoveId.SNORE]: RarityTier.COMMON,
[MoveId.CURSE]: ModifierTier.COMMON, [MoveId.CURSE]: RarityTier.COMMON,
[MoveId.REVERSAL]: ModifierTier.COMMON, [MoveId.REVERSAL]: RarityTier.COMMON,
[MoveId.SPITE]: ModifierTier.COMMON, [MoveId.SPITE]: RarityTier.COMMON,
[MoveId.PROTECT]: ModifierTier.COMMON, [MoveId.PROTECT]: RarityTier.COMMON,
[MoveId.SCARY_FACE]: ModifierTier.COMMON, [MoveId.SCARY_FACE]: RarityTier.COMMON,
[MoveId.SLUDGE_BOMB]: ModifierTier.GREAT, [MoveId.SLUDGE_BOMB]: RarityTier.GREAT,
[MoveId.MUD_SLAP]: ModifierTier.COMMON, [MoveId.MUD_SLAP]: RarityTier.COMMON,
[MoveId.SPIKES]: ModifierTier.COMMON, [MoveId.SPIKES]: RarityTier.COMMON,
[MoveId.ICY_WIND]: ModifierTier.GREAT, [MoveId.ICY_WIND]: RarityTier.GREAT,
[MoveId.OUTRAGE]: ModifierTier.ULTRA, [MoveId.OUTRAGE]: RarityTier.ULTRA,
[MoveId.SANDSTORM]: ModifierTier.COMMON, [MoveId.SANDSTORM]: RarityTier.COMMON,
[MoveId.GIGA_DRAIN]: ModifierTier.ULTRA, [MoveId.GIGA_DRAIN]: RarityTier.ULTRA,
[MoveId.ENDURE]: ModifierTier.COMMON, [MoveId.ENDURE]: RarityTier.COMMON,
[MoveId.CHARM]: ModifierTier.COMMON, [MoveId.CHARM]: RarityTier.COMMON,
[MoveId.FALSE_SWIPE]: ModifierTier.COMMON, [MoveId.FALSE_SWIPE]: RarityTier.COMMON,
[MoveId.SWAGGER]: ModifierTier.COMMON, [MoveId.SWAGGER]: RarityTier.COMMON,
[MoveId.STEEL_WING]: ModifierTier.GREAT, [MoveId.STEEL_WING]: RarityTier.GREAT,
[MoveId.ATTRACT]: ModifierTier.COMMON, [MoveId.ATTRACT]: RarityTier.COMMON,
[MoveId.SLEEP_TALK]: ModifierTier.COMMON, [MoveId.SLEEP_TALK]: RarityTier.COMMON,
[MoveId.HEAL_BELL]: ModifierTier.COMMON, [MoveId.HEAL_BELL]: RarityTier.COMMON,
[MoveId.RETURN]: ModifierTier.ULTRA, [MoveId.RETURN]: RarityTier.ULTRA,
[MoveId.FRUSTRATION]: ModifierTier.COMMON, [MoveId.FRUSTRATION]: RarityTier.COMMON,
[MoveId.SAFEGUARD]: ModifierTier.COMMON, [MoveId.SAFEGUARD]: RarityTier.COMMON,
[MoveId.PAIN_SPLIT]: ModifierTier.COMMON, [MoveId.PAIN_SPLIT]: RarityTier.COMMON,
[MoveId.MEGAHORN]: ModifierTier.ULTRA, [MoveId.MEGAHORN]: RarityTier.ULTRA,
[MoveId.BATON_PASS]: ModifierTier.COMMON, [MoveId.BATON_PASS]: RarityTier.COMMON,
[MoveId.ENCORE]: ModifierTier.COMMON, [MoveId.ENCORE]: RarityTier.COMMON,
[MoveId.IRON_TAIL]: ModifierTier.GREAT, [MoveId.IRON_TAIL]: RarityTier.GREAT,
[MoveId.METAL_CLAW]: ModifierTier.COMMON, [MoveId.METAL_CLAW]: RarityTier.COMMON,
[MoveId.SYNTHESIS]: ModifierTier.GREAT, [MoveId.SYNTHESIS]: RarityTier.GREAT,
[MoveId.HIDDEN_POWER]: ModifierTier.GREAT, [MoveId.HIDDEN_POWER]: RarityTier.GREAT,
[MoveId.RAIN_DANCE]: ModifierTier.COMMON, [MoveId.RAIN_DANCE]: RarityTier.COMMON,
[MoveId.SUNNY_DAY]: ModifierTier.COMMON, [MoveId.SUNNY_DAY]: RarityTier.COMMON,
[MoveId.CRUNCH]: ModifierTier.GREAT, [MoveId.CRUNCH]: RarityTier.GREAT,
[MoveId.PSYCH_UP]: ModifierTier.COMMON, [MoveId.PSYCH_UP]: RarityTier.COMMON,
[MoveId.SHADOW_BALL]: ModifierTier.ULTRA, [MoveId.SHADOW_BALL]: RarityTier.ULTRA,
[MoveId.FUTURE_SIGHT]: ModifierTier.GREAT, [MoveId.FUTURE_SIGHT]: RarityTier.GREAT,
[MoveId.ROCK_SMASH]: ModifierTier.COMMON, [MoveId.ROCK_SMASH]: RarityTier.COMMON,
[MoveId.WHIRLPOOL]: ModifierTier.COMMON, [MoveId.WHIRLPOOL]: RarityTier.COMMON,
[MoveId.BEAT_UP]: ModifierTier.COMMON, [MoveId.BEAT_UP]: RarityTier.COMMON,
[MoveId.UPROAR]: ModifierTier.GREAT, [MoveId.UPROAR]: RarityTier.GREAT,
[MoveId.HEAT_WAVE]: ModifierTier.ULTRA, [MoveId.HEAT_WAVE]: RarityTier.ULTRA,
[MoveId.HAIL]: ModifierTier.COMMON, [MoveId.HAIL]: RarityTier.COMMON,
[MoveId.TORMENT]: ModifierTier.COMMON, [MoveId.TORMENT]: RarityTier.COMMON,
[MoveId.WILL_O_WISP]: ModifierTier.COMMON, [MoveId.WILL_O_WISP]: RarityTier.COMMON,
[MoveId.FACADE]: ModifierTier.GREAT, [MoveId.FACADE]: RarityTier.GREAT,
[MoveId.FOCUS_PUNCH]: ModifierTier.COMMON, [MoveId.FOCUS_PUNCH]: RarityTier.COMMON,
[MoveId.NATURE_POWER]: ModifierTier.COMMON, [MoveId.NATURE_POWER]: RarityTier.COMMON,
[MoveId.CHARGE]: ModifierTier.COMMON, [MoveId.CHARGE]: RarityTier.COMMON,
[MoveId.TAUNT]: ModifierTier.COMMON, [MoveId.TAUNT]: RarityTier.COMMON,
[MoveId.HELPING_HAND]: ModifierTier.COMMON, [MoveId.HELPING_HAND]: RarityTier.COMMON,
[MoveId.TRICK]: ModifierTier.COMMON, [MoveId.TRICK]: RarityTier.COMMON,
[MoveId.SUPERPOWER]: ModifierTier.ULTRA, [MoveId.SUPERPOWER]: RarityTier.ULTRA,
[MoveId.RECYCLE]: ModifierTier.COMMON, [MoveId.RECYCLE]: RarityTier.COMMON,
[MoveId.REVENGE]: ModifierTier.GREAT, [MoveId.REVENGE]: RarityTier.GREAT,
[MoveId.BRICK_BREAK]: ModifierTier.GREAT, [MoveId.BRICK_BREAK]: RarityTier.GREAT,
[MoveId.KNOCK_OFF]: ModifierTier.GREAT, [MoveId.KNOCK_OFF]: RarityTier.GREAT,
[MoveId.ENDEAVOR]: ModifierTier.COMMON, [MoveId.ENDEAVOR]: RarityTier.COMMON,
[MoveId.SKILL_SWAP]: ModifierTier.COMMON, [MoveId.SKILL_SWAP]: RarityTier.COMMON,
[MoveId.IMPRISON]: ModifierTier.COMMON, [MoveId.IMPRISON]: RarityTier.COMMON,
[MoveId.SECRET_POWER]: ModifierTier.COMMON, [MoveId.SECRET_POWER]: RarityTier.COMMON,
[MoveId.DIVE]: ModifierTier.GREAT, [MoveId.DIVE]: RarityTier.GREAT,
[MoveId.FEATHER_DANCE]: ModifierTier.COMMON, [MoveId.FEATHER_DANCE]: RarityTier.COMMON,
[MoveId.BLAZE_KICK]: ModifierTier.GREAT, [MoveId.BLAZE_KICK]: RarityTier.GREAT,
[MoveId.HYPER_VOICE]: ModifierTier.ULTRA, [MoveId.HYPER_VOICE]: RarityTier.ULTRA,
[MoveId.BLAST_BURN]: ModifierTier.ULTRA, [MoveId.BLAST_BURN]: RarityTier.ULTRA,
[MoveId.HYDRO_CANNON]: ModifierTier.ULTRA, [MoveId.HYDRO_CANNON]: RarityTier.ULTRA,
[MoveId.WEATHER_BALL]: ModifierTier.COMMON, [MoveId.WEATHER_BALL]: RarityTier.COMMON,
[MoveId.FAKE_TEARS]: ModifierTier.COMMON, [MoveId.FAKE_TEARS]: RarityTier.COMMON,
[MoveId.AIR_CUTTER]: ModifierTier.GREAT, [MoveId.AIR_CUTTER]: RarityTier.GREAT,
[MoveId.OVERHEAT]: ModifierTier.ULTRA, [MoveId.OVERHEAT]: RarityTier.ULTRA,
[MoveId.ROCK_TOMB]: ModifierTier.GREAT, [MoveId.ROCK_TOMB]: RarityTier.GREAT,
[MoveId.METAL_SOUND]: ModifierTier.COMMON, [MoveId.METAL_SOUND]: RarityTier.COMMON,
[MoveId.COSMIC_POWER]: ModifierTier.COMMON, [MoveId.COSMIC_POWER]: RarityTier.COMMON,
[MoveId.SIGNAL_BEAM]: ModifierTier.GREAT, [MoveId.SIGNAL_BEAM]: RarityTier.GREAT,
[MoveId.SAND_TOMB]: ModifierTier.COMMON, [MoveId.SAND_TOMB]: RarityTier.COMMON,
[MoveId.MUDDY_WATER]: ModifierTier.GREAT, [MoveId.MUDDY_WATER]: RarityTier.GREAT,
[MoveId.BULLET_SEED]: ModifierTier.GREAT, [MoveId.BULLET_SEED]: RarityTier.GREAT,
[MoveId.AERIAL_ACE]: ModifierTier.GREAT, [MoveId.AERIAL_ACE]: RarityTier.GREAT,
[MoveId.ICICLE_SPEAR]: ModifierTier.GREAT, [MoveId.ICICLE_SPEAR]: RarityTier.GREAT,
[MoveId.IRON_DEFENSE]: ModifierTier.GREAT, [MoveId.IRON_DEFENSE]: RarityTier.GREAT,
[MoveId.DRAGON_CLAW]: ModifierTier.ULTRA, [MoveId.DRAGON_CLAW]: RarityTier.ULTRA,
[MoveId.FRENZY_PLANT]: ModifierTier.ULTRA, [MoveId.FRENZY_PLANT]: RarityTier.ULTRA,
[MoveId.BULK_UP]: ModifierTier.COMMON, [MoveId.BULK_UP]: RarityTier.COMMON,
[MoveId.BOUNCE]: ModifierTier.GREAT, [MoveId.BOUNCE]: RarityTier.GREAT,
[MoveId.MUD_SHOT]: ModifierTier.GREAT, [MoveId.MUD_SHOT]: RarityTier.GREAT,
[MoveId.POISON_TAIL]: ModifierTier.GREAT, [MoveId.POISON_TAIL]: RarityTier.GREAT,
[MoveId.COVET]: ModifierTier.GREAT, [MoveId.COVET]: RarityTier.GREAT,
[MoveId.MAGICAL_LEAF]: ModifierTier.GREAT, [MoveId.MAGICAL_LEAF]: RarityTier.GREAT,
[MoveId.CALM_MIND]: ModifierTier.GREAT, [MoveId.CALM_MIND]: RarityTier.GREAT,
[MoveId.LEAF_BLADE]: ModifierTier.ULTRA, [MoveId.LEAF_BLADE]: RarityTier.ULTRA,
[MoveId.DRAGON_DANCE]: ModifierTier.GREAT, [MoveId.DRAGON_DANCE]: RarityTier.GREAT,
[MoveId.ROCK_BLAST]: ModifierTier.GREAT, [MoveId.ROCK_BLAST]: RarityTier.GREAT,
[MoveId.WATER_PULSE]: ModifierTier.GREAT, [MoveId.WATER_PULSE]: RarityTier.GREAT,
[MoveId.ROOST]: ModifierTier.GREAT, [MoveId.ROOST]: RarityTier.GREAT,
[MoveId.GRAVITY]: ModifierTier.COMMON, [MoveId.GRAVITY]: RarityTier.COMMON,
[MoveId.GYRO_BALL]: ModifierTier.COMMON, [MoveId.GYRO_BALL]: RarityTier.COMMON,
[MoveId.BRINE]: ModifierTier.GREAT, [MoveId.BRINE]: RarityTier.GREAT,
[MoveId.PLUCK]: ModifierTier.GREAT, [MoveId.PLUCK]: RarityTier.GREAT,
[MoveId.TAILWIND]: ModifierTier.GREAT, [MoveId.TAILWIND]: RarityTier.GREAT,
[MoveId.U_TURN]: ModifierTier.GREAT, [MoveId.U_TURN]: RarityTier.GREAT,
[MoveId.CLOSE_COMBAT]: ModifierTier.ULTRA, [MoveId.CLOSE_COMBAT]: RarityTier.ULTRA,
[MoveId.PAYBACK]: ModifierTier.COMMON, [MoveId.PAYBACK]: RarityTier.COMMON,
[MoveId.ASSURANCE]: ModifierTier.COMMON, [MoveId.ASSURANCE]: RarityTier.COMMON,
[MoveId.EMBARGO]: ModifierTier.COMMON, [MoveId.EMBARGO]: RarityTier.COMMON,
[MoveId.FLING]: ModifierTier.COMMON, [MoveId.FLING]: RarityTier.COMMON,
[MoveId.GASTRO_ACID]: ModifierTier.GREAT, [MoveId.GASTRO_ACID]: RarityTier.GREAT,
[MoveId.POWER_SWAP]: ModifierTier.COMMON, [MoveId.POWER_SWAP]: RarityTier.COMMON,
[MoveId.GUARD_SWAP]: ModifierTier.COMMON, [MoveId.GUARD_SWAP]: RarityTier.COMMON,
[MoveId.WORRY_SEED]: ModifierTier.GREAT, [MoveId.WORRY_SEED]: RarityTier.GREAT,
[MoveId.TOXIC_SPIKES]: ModifierTier.GREAT, [MoveId.TOXIC_SPIKES]: RarityTier.GREAT,
[MoveId.FLARE_BLITZ]: ModifierTier.ULTRA, [MoveId.FLARE_BLITZ]: RarityTier.ULTRA,
[MoveId.AURA_SPHERE]: ModifierTier.GREAT, [MoveId.AURA_SPHERE]: RarityTier.GREAT,
[MoveId.ROCK_POLISH]: ModifierTier.COMMON, [MoveId.ROCK_POLISH]: RarityTier.COMMON,
[MoveId.POISON_JAB]: ModifierTier.GREAT, [MoveId.POISON_JAB]: RarityTier.GREAT,
[MoveId.DARK_PULSE]: ModifierTier.GREAT, [MoveId.DARK_PULSE]: RarityTier.GREAT,
[MoveId.AQUA_TAIL]: ModifierTier.GREAT, [MoveId.AQUA_TAIL]: RarityTier.GREAT,
[MoveId.SEED_BOMB]: ModifierTier.GREAT, [MoveId.SEED_BOMB]: RarityTier.GREAT,
[MoveId.AIR_SLASH]: ModifierTier.GREAT, [MoveId.AIR_SLASH]: RarityTier.GREAT,
[MoveId.X_SCISSOR]: ModifierTier.GREAT, [MoveId.X_SCISSOR]: RarityTier.GREAT,
[MoveId.BUG_BUZZ]: ModifierTier.GREAT, [MoveId.BUG_BUZZ]: RarityTier.GREAT,
[MoveId.DRAGON_PULSE]: ModifierTier.GREAT, [MoveId.DRAGON_PULSE]: RarityTier.GREAT,
[MoveId.POWER_GEM]: ModifierTier.GREAT, [MoveId.POWER_GEM]: RarityTier.GREAT,
[MoveId.DRAIN_PUNCH]: ModifierTier.GREAT, [MoveId.DRAIN_PUNCH]: RarityTier.GREAT,
[MoveId.VACUUM_WAVE]: ModifierTier.COMMON, [MoveId.VACUUM_WAVE]: RarityTier.COMMON,
[MoveId.FOCUS_BLAST]: ModifierTier.GREAT, [MoveId.FOCUS_BLAST]: RarityTier.GREAT,
[MoveId.ENERGY_BALL]: ModifierTier.GREAT, [MoveId.ENERGY_BALL]: RarityTier.GREAT,
[MoveId.BRAVE_BIRD]: ModifierTier.ULTRA, [MoveId.BRAVE_BIRD]: RarityTier.ULTRA,
[MoveId.EARTH_POWER]: ModifierTier.ULTRA, [MoveId.EARTH_POWER]: RarityTier.ULTRA,
[MoveId.GIGA_IMPACT]: ModifierTier.GREAT, [MoveId.GIGA_IMPACT]: RarityTier.GREAT,
[MoveId.NASTY_PLOT]: ModifierTier.COMMON, [MoveId.NASTY_PLOT]: RarityTier.COMMON,
[MoveId.AVALANCHE]: ModifierTier.GREAT, [MoveId.AVALANCHE]: RarityTier.GREAT,
[MoveId.SHADOW_CLAW]: ModifierTier.GREAT, [MoveId.SHADOW_CLAW]: RarityTier.GREAT,
[MoveId.THUNDER_FANG]: ModifierTier.GREAT, [MoveId.THUNDER_FANG]: RarityTier.GREAT,
[MoveId.ICE_FANG]: ModifierTier.GREAT, [MoveId.ICE_FANG]: RarityTier.GREAT,
[MoveId.FIRE_FANG]: ModifierTier.GREAT, [MoveId.FIRE_FANG]: RarityTier.GREAT,
[MoveId.PSYCHO_CUT]: ModifierTier.GREAT, [MoveId.PSYCHO_CUT]: RarityTier.GREAT,
[MoveId.ZEN_HEADBUTT]: ModifierTier.GREAT, [MoveId.ZEN_HEADBUTT]: RarityTier.GREAT,
[MoveId.FLASH_CANNON]: ModifierTier.GREAT, [MoveId.FLASH_CANNON]: RarityTier.GREAT,
[MoveId.ROCK_CLIMB]: ModifierTier.GREAT, [MoveId.ROCK_CLIMB]: RarityTier.GREAT,
[MoveId.DEFOG]: ModifierTier.COMMON, [MoveId.DEFOG]: RarityTier.COMMON,
[MoveId.TRICK_ROOM]: ModifierTier.COMMON, [MoveId.TRICK_ROOM]: RarityTier.COMMON,
[MoveId.DRACO_METEOR]: ModifierTier.ULTRA, [MoveId.DRACO_METEOR]: RarityTier.ULTRA,
[MoveId.LEAF_STORM]: ModifierTier.ULTRA, [MoveId.LEAF_STORM]: RarityTier.ULTRA,
[MoveId.POWER_WHIP]: ModifierTier.ULTRA, [MoveId.POWER_WHIP]: RarityTier.ULTRA,
[MoveId.CROSS_POISON]: ModifierTier.GREAT, [MoveId.CROSS_POISON]: RarityTier.GREAT,
[MoveId.GUNK_SHOT]: ModifierTier.ULTRA, [MoveId.GUNK_SHOT]: RarityTier.ULTRA,
[MoveId.IRON_HEAD]: ModifierTier.GREAT, [MoveId.IRON_HEAD]: RarityTier.GREAT,
[MoveId.STONE_EDGE]: ModifierTier.ULTRA, [MoveId.STONE_EDGE]: RarityTier.ULTRA,
[MoveId.STEALTH_ROCK]: ModifierTier.COMMON, [MoveId.STEALTH_ROCK]: RarityTier.COMMON,
[MoveId.GRASS_KNOT]: ModifierTier.ULTRA, [MoveId.GRASS_KNOT]: RarityTier.ULTRA,
[MoveId.BUG_BITE]: ModifierTier.GREAT, [MoveId.BUG_BITE]: RarityTier.GREAT,
[MoveId.CHARGE_BEAM]: ModifierTier.GREAT, [MoveId.CHARGE_BEAM]: RarityTier.GREAT,
[MoveId.HONE_CLAWS]: ModifierTier.COMMON, [MoveId.HONE_CLAWS]: RarityTier.COMMON,
[MoveId.WONDER_ROOM]: ModifierTier.COMMON, [MoveId.WONDER_ROOM]: RarityTier.COMMON,
[MoveId.PSYSHOCK]: ModifierTier.GREAT, [MoveId.PSYSHOCK]: RarityTier.GREAT,
[MoveId.VENOSHOCK]: ModifierTier.GREAT, [MoveId.VENOSHOCK]: RarityTier.GREAT,
[MoveId.MAGIC_ROOM]: ModifierTier.COMMON, [MoveId.MAGIC_ROOM]: RarityTier.COMMON,
[MoveId.SMACK_DOWN]: ModifierTier.COMMON, [MoveId.SMACK_DOWN]: RarityTier.COMMON,
[MoveId.SLUDGE_WAVE]: ModifierTier.GREAT, [MoveId.SLUDGE_WAVE]: RarityTier.GREAT,
[MoveId.HEAVY_SLAM]: ModifierTier.GREAT, [MoveId.HEAVY_SLAM]: RarityTier.GREAT,
[MoveId.ELECTRO_BALL]: ModifierTier.GREAT, [MoveId.ELECTRO_BALL]: RarityTier.GREAT,
[MoveId.FLAME_CHARGE]: ModifierTier.GREAT, [MoveId.FLAME_CHARGE]: RarityTier.GREAT,
[MoveId.LOW_SWEEP]: ModifierTier.GREAT, [MoveId.LOW_SWEEP]: RarityTier.GREAT,
[MoveId.ACID_SPRAY]: ModifierTier.COMMON, [MoveId.ACID_SPRAY]: RarityTier.COMMON,
[MoveId.FOUL_PLAY]: ModifierTier.ULTRA, [MoveId.FOUL_PLAY]: RarityTier.ULTRA,
[MoveId.ROUND]: ModifierTier.COMMON, [MoveId.ROUND]: RarityTier.COMMON,
[MoveId.ECHOED_VOICE]: ModifierTier.COMMON, [MoveId.ECHOED_VOICE]: RarityTier.COMMON,
[MoveId.STORED_POWER]: ModifierTier.COMMON, [MoveId.STORED_POWER]: RarityTier.COMMON,
[MoveId.ALLY_SWITCH]: ModifierTier.COMMON, [MoveId.ALLY_SWITCH]: RarityTier.COMMON,
[MoveId.SCALD]: ModifierTier.GREAT, [MoveId.SCALD]: RarityTier.GREAT,
[MoveId.HEX]: ModifierTier.GREAT, [MoveId.HEX]: RarityTier.GREAT,
[MoveId.SKY_DROP]: ModifierTier.GREAT, [MoveId.SKY_DROP]: RarityTier.GREAT,
[MoveId.INCINERATE]: ModifierTier.GREAT, [MoveId.INCINERATE]: RarityTier.GREAT,
[MoveId.QUASH]: ModifierTier.COMMON, [MoveId.QUASH]: RarityTier.COMMON,
[MoveId.ACROBATICS]: ModifierTier.GREAT, [MoveId.ACROBATICS]: RarityTier.GREAT,
[MoveId.RETALIATE]: ModifierTier.GREAT, [MoveId.RETALIATE]: RarityTier.GREAT,
[MoveId.WATER_PLEDGE]: ModifierTier.GREAT, [MoveId.WATER_PLEDGE]: RarityTier.GREAT,
[MoveId.FIRE_PLEDGE]: ModifierTier.GREAT, [MoveId.FIRE_PLEDGE]: RarityTier.GREAT,
[MoveId.GRASS_PLEDGE]: ModifierTier.GREAT, [MoveId.GRASS_PLEDGE]: RarityTier.GREAT,
[MoveId.VOLT_SWITCH]: ModifierTier.GREAT, [MoveId.VOLT_SWITCH]: RarityTier.GREAT,
[MoveId.STRUGGLE_BUG]: ModifierTier.COMMON, [MoveId.STRUGGLE_BUG]: RarityTier.COMMON,
[MoveId.BULLDOZE]: ModifierTier.GREAT, [MoveId.BULLDOZE]: RarityTier.GREAT,
[MoveId.FROST_BREATH]: ModifierTier.GREAT, [MoveId.FROST_BREATH]: RarityTier.GREAT,
[MoveId.DRAGON_TAIL]: ModifierTier.GREAT, [MoveId.DRAGON_TAIL]: RarityTier.GREAT,
[MoveId.WORK_UP]: ModifierTier.COMMON, [MoveId.WORK_UP]: RarityTier.COMMON,
[MoveId.ELECTROWEB]: ModifierTier.GREAT, [MoveId.ELECTROWEB]: RarityTier.GREAT,
[MoveId.WILD_CHARGE]: ModifierTier.GREAT, [MoveId.WILD_CHARGE]: RarityTier.GREAT,
[MoveId.DRILL_RUN]: ModifierTier.GREAT, [MoveId.DRILL_RUN]: RarityTier.GREAT,
[MoveId.RAZOR_SHELL]: ModifierTier.GREAT, [MoveId.RAZOR_SHELL]: RarityTier.GREAT,
[MoveId.HEAT_CRASH]: ModifierTier.GREAT, [MoveId.HEAT_CRASH]: RarityTier.GREAT,
[MoveId.TAIL_SLAP]: ModifierTier.GREAT, [MoveId.TAIL_SLAP]: RarityTier.GREAT,
[MoveId.HURRICANE]: ModifierTier.ULTRA, [MoveId.HURRICANE]: RarityTier.ULTRA,
[MoveId.SNARL]: ModifierTier.COMMON, [MoveId.SNARL]: RarityTier.COMMON,
[MoveId.PHANTOM_FORCE]: ModifierTier.ULTRA, [MoveId.PHANTOM_FORCE]: RarityTier.ULTRA,
[MoveId.PETAL_BLIZZARD]: ModifierTier.GREAT, [MoveId.PETAL_BLIZZARD]: RarityTier.GREAT,
[MoveId.DISARMING_VOICE]: ModifierTier.GREAT, [MoveId.DISARMING_VOICE]: RarityTier.GREAT,
[MoveId.DRAINING_KISS]: ModifierTier.GREAT, [MoveId.DRAINING_KISS]: RarityTier.GREAT,
[MoveId.GRASSY_TERRAIN]: ModifierTier.COMMON, [MoveId.GRASSY_TERRAIN]: RarityTier.COMMON,
[MoveId.MISTY_TERRAIN]: ModifierTier.COMMON, [MoveId.MISTY_TERRAIN]: RarityTier.COMMON,
[MoveId.PLAY_ROUGH]: ModifierTier.GREAT, [MoveId.PLAY_ROUGH]: RarityTier.GREAT,
[MoveId.CONFIDE]: ModifierTier.COMMON, [MoveId.CONFIDE]: RarityTier.COMMON,
[MoveId.MYSTICAL_FIRE]: ModifierTier.GREAT, [MoveId.MYSTICAL_FIRE]: RarityTier.GREAT,
[MoveId.EERIE_IMPULSE]: ModifierTier.COMMON, [MoveId.EERIE_IMPULSE]: RarityTier.COMMON,
[MoveId.VENOM_DRENCH]: ModifierTier.COMMON, [MoveId.VENOM_DRENCH]: RarityTier.COMMON,
[MoveId.ELECTRIC_TERRAIN]: ModifierTier.COMMON, [MoveId.ELECTRIC_TERRAIN]: RarityTier.COMMON,
[MoveId.DAZZLING_GLEAM]: ModifierTier.ULTRA, [MoveId.DAZZLING_GLEAM]: RarityTier.ULTRA,
[MoveId.INFESTATION]: ModifierTier.COMMON, [MoveId.INFESTATION]: RarityTier.COMMON,
[MoveId.POWER_UP_PUNCH]: ModifierTier.GREAT, [MoveId.POWER_UP_PUNCH]: RarityTier.GREAT,
[MoveId.DARKEST_LARIAT]: ModifierTier.GREAT, [MoveId.DARKEST_LARIAT]: RarityTier.GREAT,
[MoveId.HIGH_HORSEPOWER]: ModifierTier.ULTRA, [MoveId.HIGH_HORSEPOWER]: RarityTier.ULTRA,
[MoveId.SOLAR_BLADE]: ModifierTier.GREAT, [MoveId.SOLAR_BLADE]: RarityTier.GREAT,
[MoveId.THROAT_CHOP]: ModifierTier.GREAT, [MoveId.THROAT_CHOP]: RarityTier.GREAT,
[MoveId.POLLEN_PUFF]: ModifierTier.GREAT, [MoveId.POLLEN_PUFF]: RarityTier.GREAT,
[MoveId.PSYCHIC_TERRAIN]: ModifierTier.COMMON, [MoveId.PSYCHIC_TERRAIN]: RarityTier.COMMON,
[MoveId.LUNGE]: ModifierTier.GREAT, [MoveId.LUNGE]: RarityTier.GREAT,
[MoveId.SPEED_SWAP]: ModifierTier.COMMON, [MoveId.SPEED_SWAP]: RarityTier.COMMON,
[MoveId.SMART_STRIKE]: ModifierTier.GREAT, [MoveId.SMART_STRIKE]: RarityTier.GREAT,
[MoveId.BRUTAL_SWING]: ModifierTier.GREAT, [MoveId.BRUTAL_SWING]: RarityTier.GREAT,
[MoveId.AURORA_VEIL]: ModifierTier.COMMON, [MoveId.AURORA_VEIL]: RarityTier.COMMON,
[MoveId.PSYCHIC_FANGS]: ModifierTier.GREAT, [MoveId.PSYCHIC_FANGS]: RarityTier.GREAT,
[MoveId.STOMPING_TANTRUM]: ModifierTier.GREAT, [MoveId.STOMPING_TANTRUM]: RarityTier.GREAT,
[MoveId.LIQUIDATION]: ModifierTier.ULTRA, [MoveId.LIQUIDATION]: RarityTier.ULTRA,
[MoveId.BODY_PRESS]: ModifierTier.ULTRA, [MoveId.BODY_PRESS]: RarityTier.ULTRA,
[MoveId.BREAKING_SWIPE]: ModifierTier.GREAT, [MoveId.BREAKING_SWIPE]: RarityTier.GREAT,
[MoveId.STEEL_BEAM]: ModifierTier.ULTRA, [MoveId.STEEL_BEAM]: RarityTier.ULTRA,
[MoveId.EXPANDING_FORCE]: ModifierTier.GREAT, [MoveId.EXPANDING_FORCE]: RarityTier.GREAT,
[MoveId.STEEL_ROLLER]: ModifierTier.COMMON, [MoveId.STEEL_ROLLER]: RarityTier.COMMON,
[MoveId.SCALE_SHOT]: ModifierTier.ULTRA, [MoveId.SCALE_SHOT]: RarityTier.ULTRA,
[MoveId.METEOR_BEAM]: ModifierTier.GREAT, [MoveId.METEOR_BEAM]: RarityTier.GREAT,
[MoveId.MISTY_EXPLOSION]: ModifierTier.COMMON, [MoveId.MISTY_EXPLOSION]: RarityTier.COMMON,
[MoveId.GRASSY_GLIDE]: ModifierTier.COMMON, [MoveId.GRASSY_GLIDE]: RarityTier.COMMON,
[MoveId.RISING_VOLTAGE]: ModifierTier.COMMON, [MoveId.RISING_VOLTAGE]: RarityTier.COMMON,
[MoveId.TERRAIN_PULSE]: ModifierTier.COMMON, [MoveId.TERRAIN_PULSE]: RarityTier.COMMON,
[MoveId.SKITTER_SMACK]: ModifierTier.GREAT, [MoveId.SKITTER_SMACK]: RarityTier.GREAT,
[MoveId.BURNING_JEALOUSY]: ModifierTier.GREAT, [MoveId.BURNING_JEALOUSY]: RarityTier.GREAT,
[MoveId.LASH_OUT]: ModifierTier.GREAT, [MoveId.LASH_OUT]: RarityTier.GREAT,
[MoveId.POLTERGEIST]: ModifierTier.ULTRA, [MoveId.POLTERGEIST]: RarityTier.ULTRA,
[MoveId.CORROSIVE_GAS]: ModifierTier.COMMON, [MoveId.CORROSIVE_GAS]: RarityTier.COMMON,
[MoveId.COACHING]: ModifierTier.COMMON, [MoveId.COACHING]: RarityTier.COMMON,
[MoveId.FLIP_TURN]: ModifierTier.COMMON, [MoveId.FLIP_TURN]: RarityTier.COMMON,
[MoveId.TRIPLE_AXEL]: ModifierTier.COMMON, [MoveId.TRIPLE_AXEL]: RarityTier.COMMON,
[MoveId.DUAL_WINGBEAT]: ModifierTier.COMMON, [MoveId.DUAL_WINGBEAT]: RarityTier.COMMON,
[MoveId.SCORCHING_SANDS]: ModifierTier.GREAT, [MoveId.SCORCHING_SANDS]: RarityTier.GREAT,
[MoveId.TERA_BLAST]: ModifierTier.GREAT, [MoveId.TERA_BLAST]: RarityTier.GREAT,
[MoveId.ICE_SPINNER]: ModifierTier.GREAT, [MoveId.ICE_SPINNER]: RarityTier.GREAT,
[MoveId.SNOWSCAPE]: ModifierTier.COMMON, [MoveId.SNOWSCAPE]: RarityTier.COMMON,
[MoveId.POUNCE]: ModifierTier.COMMON, [MoveId.POUNCE]: RarityTier.COMMON,
[MoveId.TRAILBLAZE]: ModifierTier.COMMON, [MoveId.TRAILBLAZE]: RarityTier.COMMON,
[MoveId.CHILLING_WATER]: ModifierTier.COMMON, [MoveId.CHILLING_WATER]: RarityTier.COMMON,
[MoveId.HARD_PRESS]: ModifierTier.GREAT, [MoveId.HARD_PRESS]: RarityTier.GREAT,
[MoveId.DRAGON_CHEER]: ModifierTier.COMMON, [MoveId.DRAGON_CHEER]: RarityTier.COMMON,
[MoveId.ALLURING_VOICE]: ModifierTier.GREAT, [MoveId.ALLURING_VOICE]: RarityTier.GREAT,
[MoveId.TEMPER_FLARE]: ModifierTier.GREAT, [MoveId.TEMPER_FLARE]: RarityTier.GREAT,
[MoveId.SUPERCELL_SLAM]: ModifierTier.GREAT, [MoveId.SUPERCELL_SLAM]: RarityTier.GREAT,
[MoveId.PSYCHIC_NOISE]: ModifierTier.GREAT, [MoveId.PSYCHIC_NOISE]: RarityTier.GREAT,
[MoveId.UPPER_HAND]: ModifierTier.COMMON, [MoveId.UPPER_HAND]: RarityTier.COMMON,
}; };

View File

@ -404,22 +404,18 @@ export const chargeAnims = new Map<ChargeAnim, AnimConfig | [AnimConfig, AnimCon
export const commonAnims = new Map<CommonAnim, AnimConfig>(); export const commonAnims = new Map<CommonAnim, AnimConfig>();
export const encounterAnims = new Map<EncounterAnim, AnimConfig>(); export const encounterAnims = new Map<EncounterAnim, AnimConfig>();
export function initCommonAnims(): Promise<void> { export async function initCommonAnims(): Promise<void> {
return new Promise(resolve => {
const commonAnimNames = getEnumKeys(CommonAnim);
const commonAnimIds = getEnumValues(CommonAnim);
const commonAnimFetches: Promise<Map<CommonAnim, AnimConfig>>[] = []; const commonAnimFetches: Promise<Map<CommonAnim, AnimConfig>>[] = [];
for (let ca = 0; ca < commonAnimIds.length; ca++) { for (const commonAnimName of getEnumKeys(CommonAnim)) {
const commonAnimId = commonAnimIds[ca]; const commonAnimId = CommonAnim[commonAnimName];
commonAnimFetches.push( commonAnimFetches.push(
globalScene globalScene
.cachedFetch(`./battle-anims/common-${toKebabCase(commonAnimNames[ca])}.json`) .cachedFetch(`./battle-anims/common-${toKebabCase(commonAnimName)}.json`)
.then(response => response.json()) .then(response => response.json())
.then(cas => commonAnims.set(commonAnimId, new AnimConfig(cas))), .then(cas => commonAnims.set(commonAnimId, new AnimConfig(cas))),
); );
} }
Promise.allSettled(commonAnimFetches).then(() => resolve()); await Promise.allSettled(commonAnimFetches);
});
} }
export function initMoveAnim(move: MoveId): Promise<void> { export function initMoveAnim(move: MoveId): Promise<void> {
@ -1396,279 +1392,3 @@ export class EncounterBattleAnim extends BattleAnim {
return this.oppAnim; return this.oppAnim;
} }
} }
export async function populateAnims() {
const commonAnimNames = getEnumKeys(CommonAnim).map(k => k.toLowerCase());
const commonAnimMatchNames = commonAnimNames.map(k => k.replace(/_/g, ""));
const commonAnimIds = getEnumValues(CommonAnim);
const chargeAnimNames = getEnumKeys(ChargeAnim).map(k => k.toLowerCase());
const chargeAnimMatchNames = chargeAnimNames.map(k => k.replace(/_/g, " "));
const chargeAnimIds = getEnumValues(ChargeAnim);
const commonNamePattern = /name: (?:Common:)?(Opp )?(.*)/;
const moveNameToId = {};
// Exclude MoveId.NONE;
for (const move of getEnumValues(MoveId).slice(1)) {
// KARATE_CHOP => KARATECHOP
const moveName = MoveId[move].toUpperCase().replace(/_/g, "");
moveNameToId[moveName] = move;
}
const seNames: string[] = []; //(await fs.readdir('./public/audio/se/battle_anims/')).map(se => se.toString());
const animsData: any[] = []; //battleAnimRawData.split('!ruby/array:PBAnimation').slice(1); // TODO: add a proper type
for (let a = 0; a < animsData.length; a++) {
const fields = animsData[a].split("@").slice(1);
const nameField = fields.find(f => f.startsWith("name: "));
let isOppMove: boolean | undefined;
let commonAnimId: CommonAnim | undefined;
let chargeAnimId: ChargeAnim | undefined;
if (!nameField.startsWith("name: Move:") && !(isOppMove = nameField.startsWith("name: OppMove:"))) {
const nameMatch = commonNamePattern.exec(nameField)!; // TODO: is this bang correct?
const name = nameMatch[2].toLowerCase();
if (commonAnimMatchNames.indexOf(name) > -1) {
commonAnimId = commonAnimIds[commonAnimMatchNames.indexOf(name)];
} else if (chargeAnimMatchNames.indexOf(name) > -1) {
isOppMove = nameField.startsWith("name: Opp ");
chargeAnimId = chargeAnimIds[chargeAnimMatchNames.indexOf(name)];
}
}
const nameIndex = nameField.indexOf(":", 5) + 1;
const animName = nameField.slice(nameIndex, nameField.indexOf("\n", nameIndex));
if (!moveNameToId.hasOwnProperty(animName) && !commonAnimId && !chargeAnimId) {
continue;
}
const anim = commonAnimId || chargeAnimId ? new AnimConfig() : new AnimConfig();
if (anim instanceof AnimConfig) {
(anim as AnimConfig).id = moveNameToId[animName];
}
if (commonAnimId) {
commonAnims.set(commonAnimId, anim);
} else if (chargeAnimId) {
chargeAnims.set(chargeAnimId, !isOppMove ? anim : [chargeAnims.get(chargeAnimId) as AnimConfig, anim]);
} else {
moveAnims.set(
moveNameToId[animName],
!isOppMove ? (anim as AnimConfig) : [moveAnims.get(moveNameToId[animName]) as AnimConfig, anim as AnimConfig],
);
}
for (let f = 0; f < fields.length; f++) {
const field = fields[f];
const fieldName = field.slice(0, field.indexOf(":"));
const fieldData = field.slice(fieldName.length + 1, field.lastIndexOf("\n")).trim();
switch (fieldName) {
case "array": {
const framesData = fieldData.split(" - - - ").slice(1);
for (let fd = 0; fd < framesData.length; fd++) {
anim.frames.push([]);
const frameData = framesData[fd];
const focusFramesData = frameData.split(" - - ");
for (let tf = 0; tf < focusFramesData.length; tf++) {
const values = focusFramesData[tf].replace(/ {6}- /g, "").split("\n");
const targetFrame = new AnimFrame(
Number.parseFloat(values[0]),
Number.parseFloat(values[1]),
Number.parseFloat(values[2]),
Number.parseFloat(values[11]),
Number.parseFloat(values[3]),
Number.parseInt(values[4]) === 1,
Number.parseInt(values[6]) === 1,
Number.parseInt(values[5]),
Number.parseInt(values[7]),
Number.parseInt(values[8]),
Number.parseInt(values[12]),
Number.parseInt(values[13]),
Number.parseInt(values[14]),
Number.parseInt(values[15]),
Number.parseInt(values[16]),
Number.parseInt(values[17]),
Number.parseInt(values[18]),
Number.parseInt(values[19]),
Number.parseInt(values[21]),
Number.parseInt(values[22]),
Number.parseInt(values[23]),
Number.parseInt(values[24]),
Number.parseInt(values[20]) === 1,
Number.parseInt(values[25]),
Number.parseInt(values[26]) as AnimFocus,
);
anim.frames[fd].push(targetFrame);
}
}
break;
}
case "graphic": {
const graphic = fieldData !== "''" ? fieldData : "";
anim.graphic = graphic.indexOf(".") > -1 ? graphic.slice(0, fieldData.indexOf(".")) : graphic;
break;
}
case "timing": {
const timingEntries = fieldData.split("- !ruby/object:PBAnimTiming ").slice(1);
for (let t = 0; t < timingEntries.length; t++) {
const timingData = timingEntries[t]
.replace(/\n/g, " ")
.replace(/[ ]{2,}/g, " ")
.replace(/[a-z]+: ! '', /gi, "")
.replace(/name: (.*?),/, 'name: "$1",')
.replace(
/flashColor: !ruby\/object:Color { alpha: ([\d.]+), blue: ([\d.]+), green: ([\d.]+), red: ([\d.]+)}/,
"flashRed: $4, flashGreen: $3, flashBlue: $2, flashAlpha: $1",
);
const frameIndex = Number.parseInt(/frame: (\d+)/.exec(timingData)![1]); // TODO: is the bang correct?
let resourceName = /name: "(.*?)"/.exec(timingData)![1].replace("''", ""); // TODO: is the bang correct?
const timingType = Number.parseInt(/timingType: (\d)/.exec(timingData)![1]); // TODO: is the bang correct?
let timedEvent: AnimTimedEvent | undefined;
switch (timingType) {
case 0:
if (resourceName && resourceName.indexOf(".") === -1) {
let ext: string | undefined;
["wav", "mp3", "m4a"].every(e => {
if (seNames.indexOf(`${resourceName}.${e}`) > -1) {
ext = e;
return false;
}
return true;
});
if (!ext) {
ext = ".wav";
}
resourceName += `.${ext}`;
}
timedEvent = new AnimTimedSoundEvent(frameIndex, resourceName);
break;
case 1:
timedEvent = new AnimTimedAddBgEvent(frameIndex, resourceName.slice(0, resourceName.indexOf(".")));
break;
case 2:
timedEvent = new AnimTimedUpdateBgEvent(frameIndex, resourceName.slice(0, resourceName.indexOf(".")));
break;
}
if (!timedEvent) {
continue;
}
const propPattern = /([a-z]+): (.*?)(?:,|\})/gi;
let propMatch: RegExpExecArray;
while ((propMatch = propPattern.exec(timingData)!)) {
// TODO: is this bang correct?
const prop = propMatch[1];
let value: any = propMatch[2];
switch (prop) {
case "bgX":
case "bgY":
value = Number.parseFloat(value);
break;
case "volume":
case "pitch":
case "opacity":
case "colorRed":
case "colorGreen":
case "colorBlue":
case "colorAlpha":
case "duration":
case "flashScope":
case "flashRed":
case "flashGreen":
case "flashBlue":
case "flashAlpha":
case "flashDuration":
value = Number.parseInt(value);
break;
}
if (timedEvent.hasOwnProperty(prop)) {
timedEvent[prop] = value;
}
}
if (!anim.frameTimedEvents.has(frameIndex)) {
anim.frameTimedEvents.set(frameIndex, []);
}
anim.frameTimedEvents.get(frameIndex)!.push(timedEvent); // TODO: is this bang correct?
}
break;
}
case "position":
anim.position = Number.parseInt(fieldData);
break;
case "hue":
anim.hue = Number.parseInt(fieldData);
break;
}
}
}
// biome-ignore lint/correctness/noUnusedVariables: used in commented code
const animReplacer = (k, v) => {
if (k === "id" && !v) {
return undefined;
}
if (v instanceof Map) {
return Object.fromEntries(v);
}
if (v instanceof AnimTimedEvent) {
v["eventType"] = v.getEventType();
}
return v;
};
const animConfigProps = ["id", "graphic", "frames", "frameTimedEvents", "position", "hue"];
const animFrameProps = [
"x",
"y",
"zoomX",
"zoomY",
"angle",
"mirror",
"visible",
"blendType",
"target",
"graphicFrame",
"opacity",
"color",
"tone",
"flash",
"locked",
"priority",
"focus",
];
const propSets = [animConfigProps, animFrameProps];
// biome-ignore lint/correctness/noUnusedVariables: used in commented code
const animComparator = (a: Element, b: Element) => {
let props: string[];
for (let p = 0; p < propSets.length; p++) {
props = propSets[p];
// @ts-expect-error TODO
const ai = props.indexOf(a.key);
if (ai === -1) {
continue;
}
// @ts-expect-error TODO
const bi = props.indexOf(b.key);
return ai < bi ? -1 : ai > bi ? 1 : 0;
}
return 0;
};
/*for (let ma of moveAnims.keys()) {
const data = moveAnims.get(ma);
(async () => {
await fs.writeFile(`../public/battle-anims/${Moves[ma].toLowerCase().replace(/\_/g, '-')}.json`, stringify(data, { replacer: animReplacer, cmp: animComparator, space: ' ' }));
})();
}
for (let ca of chargeAnims.keys()) {
const data = chargeAnims.get(ca);
(async () => {
await fs.writeFile(`../public/battle-anims/${chargeAnimNames[chargeAnimIds.indexOf(ca)].replace(/\_/g, '-')}.json`, stringify(data, { replacer: animReplacer, cmp: animComparator, space: ' ' }));
})();
}
for (let cma of commonAnims.keys()) {
const data = commonAnims.get(cma);
(async () => {
await fs.writeFile(`../public/battle-anims/common-${commonAnimNames[commonAnimIds.indexOf(cma)].replace(/\_/g, '-')}.json`, stringify(data, { replacer: animReplacer, cmp: animComparator, space: ' ' }));
})();
}*/
}

View File

@ -7,19 +7,20 @@ import { BattleType } from "#enums/battle-type";
import { Challenges } from "#enums/challenges"; import { Challenges } from "#enums/challenges";
import { TypeColor, TypeShadow } from "#enums/color"; import { TypeColor, TypeShadow } from "#enums/color";
import { ClassicFixedBossWaves } from "#enums/fixed-boss-waves"; import { ClassicFixedBossWaves } from "#enums/fixed-boss-waves";
import { ModifierTier } from "#enums/modifier-tier";
import { MoveId } from "#enums/move-id"; import { MoveId } from "#enums/move-id";
import type { MoveSourceType } from "#enums/move-source-type"; import type { MoveSourceType } from "#enums/move-source-type";
import { Nature } from "#enums/nature"; import { Nature } from "#enums/nature";
import { PokemonType } from "#enums/pokemon-type"; import { PokemonType } from "#enums/pokemon-type";
import { RarityTier } from "#enums/reward-tier";
import { SpeciesId } from "#enums/species-id"; import { SpeciesId } from "#enums/species-id";
import { TrainerType } from "#enums/trainer-type"; import { TrainerType } from "#enums/trainer-type";
import { TrainerVariant } from "#enums/trainer-variant"; import { TrainerVariant } from "#enums/trainer-variant";
import type { EnemyPokemon, PlayerPokemon, Pokemon } from "#field/pokemon"; import type { EnemyPokemon, PlayerPokemon, Pokemon } from "#field/pokemon";
import { Trainer } from "#field/trainer"; import { Trainer } from "#field/trainer";
import type { ModifierTypeOption } from "#modifiers/modifier-type"; import type { RewardOption } from "#items/reward";
import { PokemonMove } from "#moves/pokemon-move"; import { PokemonMove } from "#moves/pokemon-move";
import type { DexAttrProps, GameData } from "#system/game-data"; import type { DexAttrProps, GameData } from "#system/game-data";
import { RibbonData, type RibbonFlag } from "#system/ribbons/ribbon-data";
import { type BooleanHolder, isBetween, type NumberHolder, randSeedItem } from "#utils/common"; import { type BooleanHolder, isBetween, type NumberHolder, randSeedItem } from "#utils/common";
import { deepCopy } from "#utils/data"; import { deepCopy } from "#utils/data";
import { getPokemonSpecies, getPokemonSpeciesForm } from "#utils/pokemon-utils"; import { getPokemonSpecies, getPokemonSpeciesForm } from "#utils/pokemon-utils";
@ -42,6 +43,15 @@ export abstract class Challenge {
public conditions: ChallengeCondition[]; public conditions: ChallengeCondition[];
/**
* The Ribbon awarded on challenge completion, or 0 if the challenge has no ribbon or is not enabled
*
* @defaultValue 0
*/
public get ribbonAwarded(): RibbonFlag {
return 0 as RibbonFlag;
}
/** /**
* @param id {@link Challenges} The enum value for the challenge * @param id {@link Challenges} The enum value for the challenge
*/ */
@ -393,7 +403,7 @@ export abstract class Challenge {
* @param _status - Whether the item should be added to the shop or not * @param _status - Whether the item should be added to the shop or not
* @returns Whether this function did anything * @returns Whether this function did anything
*/ */
applyShopItem(_shopItem: ModifierTypeOption | null, _status: BooleanHolder): boolean { applyShopItem(_shopItem: RewardOption | null, _status: BooleanHolder): boolean {
return false; return false;
} }
@ -403,7 +413,7 @@ export abstract class Challenge {
* @param _status - Whether the reward should be added to the reward options or not * @param _status - Whether the reward should be added to the reward options or not
* @returns Whether this function did anything * @returns Whether this function did anything
*/ */
applyWaveReward(_reward: ModifierTypeOption | null, _status: BooleanHolder): boolean { applyWaveReward(_reward: RewardOption | null, _status: BooleanHolder): boolean {
return false; return false;
} }
@ -423,6 +433,12 @@ type ChallengeCondition = (data: GameData) => boolean;
* Implements a mono generation challenge. * Implements a mono generation challenge.
*/ */
export class SingleGenerationChallenge extends Challenge { export class SingleGenerationChallenge extends Challenge {
public override get ribbonAwarded(): RibbonFlag {
// NOTE: This logic will not work for the eventual mono gen 10 ribbon, as
// as its flag will not be in sequence with the other mono gen ribbons.
return this.value ? ((RibbonData.MONO_GEN_1 << (this.value - 1)) as RibbonFlag) : 0;
}
constructor() { constructor() {
super(Challenges.SINGLE_GENERATION, 9); super(Challenges.SINGLE_GENERATION, 9);
} }
@ -527,13 +543,13 @@ export class SingleGenerationChallenge extends Challenge {
.setBattleType(BattleType.TRAINER) .setBattleType(BattleType.TRAINER)
.setSeedOffsetWave(ClassicFixedBossWaves.EVIL_GRUNT_1) .setSeedOffsetWave(ClassicFixedBossWaves.EVIL_GRUNT_1)
.setGetTrainerFunc(getRandomTrainerFunc(trainerTypes, true)) .setGetTrainerFunc(getRandomTrainerFunc(trainerTypes, true))
.setCustomModifierRewards({ .setCustomRewards({
guaranteedModifierTiers: [ guaranteedRarityTiers: [
ModifierTier.ROGUE, RarityTier.ROGUE,
ModifierTier.ROGUE, RarityTier.ROGUE,
ModifierTier.ULTRA, RarityTier.ULTRA,
ModifierTier.ULTRA, RarityTier.ULTRA,
ModifierTier.ULTRA, RarityTier.ULTRA,
], ],
allowLuckUpgrades: false, allowLuckUpgrades: false,
}); });
@ -544,14 +560,14 @@ export class SingleGenerationChallenge extends Challenge {
.setBattleType(BattleType.TRAINER) .setBattleType(BattleType.TRAINER)
.setSeedOffsetWave(ClassicFixedBossWaves.EVIL_GRUNT_1) .setSeedOffsetWave(ClassicFixedBossWaves.EVIL_GRUNT_1)
.setGetTrainerFunc(getRandomTrainerFunc(trainerTypes, true)) .setGetTrainerFunc(getRandomTrainerFunc(trainerTypes, true))
.setCustomModifierRewards({ .setCustomRewards({
guaranteedModifierTiers: [ guaranteedRarityTiers: [
ModifierTier.ROGUE, RarityTier.ROGUE,
ModifierTier.ROGUE, RarityTier.ROGUE,
ModifierTier.ULTRA, RarityTier.ULTRA,
ModifierTier.ULTRA, RarityTier.ULTRA,
ModifierTier.ULTRA, RarityTier.ULTRA,
ModifierTier.ULTRA, RarityTier.ULTRA,
], ],
allowLuckUpgrades: false, allowLuckUpgrades: false,
}); });
@ -686,6 +702,12 @@ interface monotypeOverride {
* Implements a mono type challenge. * Implements a mono type challenge.
*/ */
export class SingleTypeChallenge extends Challenge { export class SingleTypeChallenge extends Challenge {
public override get ribbonAwarded(): RibbonFlag {
// `this.value` represents the 1-based index of pokemon type
// `RibbonData.MONO_NORMAL` starts the flag position for the types,
// and we shift it by 1 for the specific type.
return this.value ? ((RibbonData.MONO_NORMAL << (this.value - 1)) as RibbonFlag) : 0;
}
private static TYPE_OVERRIDES: monotypeOverride[] = [ private static TYPE_OVERRIDES: monotypeOverride[] = [
{ species: SpeciesId.CASTFORM, type: PokemonType.NORMAL, fusion: false }, { species: SpeciesId.CASTFORM, type: PokemonType.NORMAL, fusion: false },
]; ];
@ -755,6 +777,9 @@ export class SingleTypeChallenge extends Challenge {
* Implements a fresh start challenge. * Implements a fresh start challenge.
*/ */
export class FreshStartChallenge extends Challenge { export class FreshStartChallenge extends Challenge {
public override get ribbonAwarded(): RibbonFlag {
return this.value ? RibbonData.FRESH_START : 0;
}
constructor() { constructor() {
super(Challenges.FRESH_START, 2); super(Challenges.FRESH_START, 2);
} }
@ -828,6 +853,9 @@ export class FreshStartChallenge extends Challenge {
* Implements an inverse battle challenge. * Implements an inverse battle challenge.
*/ */
export class InverseBattleChallenge extends Challenge { export class InverseBattleChallenge extends Challenge {
public override get ribbonAwarded(): RibbonFlag {
return this.value ? RibbonData.INVERSE : 0;
}
constructor() { constructor() {
super(Challenges.INVERSE_BATTLE, 1); super(Challenges.INVERSE_BATTLE, 1);
} }
@ -861,6 +889,9 @@ export class InverseBattleChallenge extends Challenge {
* Implements a flip stat challenge. * Implements a flip stat challenge.
*/ */
export class FlipStatChallenge extends Challenge { export class FlipStatChallenge extends Challenge {
public override get ribbonAwarded(): RibbonFlag {
return this.value ? RibbonData.FLIP_STATS : 0;
}
constructor() { constructor() {
super(Challenges.FLIP_STAT, 1); super(Challenges.FLIP_STAT, 1);
} }
@ -941,6 +972,9 @@ export class LowerStarterPointsChallenge extends Challenge {
* Implements a No Support challenge * Implements a No Support challenge
*/ */
export class LimitedSupportChallenge extends Challenge { export class LimitedSupportChallenge extends Challenge {
public override get ribbonAwarded(): RibbonFlag {
return this.value ? ((RibbonData.NO_HEAL << (this.value - 1)) as RibbonFlag) : 0;
}
constructor() { constructor() {
super(Challenges.LIMITED_SUPPORT, 3); super(Challenges.LIMITED_SUPPORT, 3);
} }
@ -973,6 +1007,9 @@ export class LimitedSupportChallenge extends Challenge {
* Implements a Limited Catch challenge * Implements a Limited Catch challenge
*/ */
export class LimitedCatchChallenge extends Challenge { export class LimitedCatchChallenge extends Challenge {
public override get ribbonAwarded(): RibbonFlag {
return this.value ? RibbonData.LIMITED_CATCH : 0;
}
constructor() { constructor() {
super(Challenges.LIMITED_CATCH, 1); super(Challenges.LIMITED_CATCH, 1);
} }
@ -997,6 +1034,9 @@ export class LimitedCatchChallenge extends Challenge {
* Implements a Permanent Faint challenge * Implements a Permanent Faint challenge
*/ */
export class HardcoreChallenge extends Challenge { export class HardcoreChallenge extends Challenge {
public override get ribbonAwarded(): RibbonFlag {
return this.value ? RibbonData.HARDCORE : 0;
}
constructor() { constructor() {
super(Challenges.HARDCORE, 1); super(Challenges.HARDCORE, 1);
} }
@ -1009,12 +1049,12 @@ export class HardcoreChallenge extends Challenge {
return false; return false;
} }
override applyShopItem(shopItem: ModifierTypeOption | null, status: BooleanHolder): boolean { override applyShopItem(shopItem: RewardOption | null, status: BooleanHolder): boolean {
status.value = shopItem?.type.group !== "revive"; status.value = shopItem?.type.group !== "revive";
return true; return true;
} }
override applyWaveReward(reward: ModifierTypeOption | null, status: BooleanHolder): boolean { override applyWaveReward(reward: RewardOption | null, status: BooleanHolder): boolean {
return this.applyShopItem(reward, status); return this.applyShopItem(reward, status);
} }

View File

@ -5,10 +5,9 @@ import type { PokemonSpeciesForm } from "#data/pokemon-species";
import { PokemonSpecies } from "#data/pokemon-species"; import { PokemonSpecies } from "#data/pokemon-species";
import { BiomeId } from "#enums/biome-id"; import { BiomeId } from "#enums/biome-id";
import { PartyMemberStrength } from "#enums/party-member-strength"; import { PartyMemberStrength } from "#enums/party-member-strength";
import type { SpeciesId } from "#enums/species-id"; import { SpeciesId } from "#enums/species-id";
import { PlayerPokemon } from "#field/pokemon";
import type { Starter } from "#ui/starter-select-ui-handler"; import type { Starter } from "#ui/starter-select-ui-handler";
import { randSeedGauss, randSeedInt, randSeedItem } from "#utils/common"; import { isNullOrUndefined, randSeedGauss, randSeedInt, randSeedItem } from "#utils/common";
import { getEnumValues } from "#utils/enums"; import { getEnumValues } from "#utils/enums";
import { getPokemonSpecies, getPokemonSpeciesForm } from "#utils/pokemon-utils"; import { getPokemonSpecies, getPokemonSpeciesForm } from "#utils/pokemon-utils";
@ -32,15 +31,9 @@ export function getDailyRunStarters(seed: string): Starter[] {
() => { () => {
const startingLevel = globalScene.gameMode.getStartingLevel(); const startingLevel = globalScene.gameMode.getStartingLevel();
if (/\d{18}$/.test(seed)) { const eventStarters = getDailyEventSeedStarters(seed);
for (let s = 0; s < 3; s++) { if (!isNullOrUndefined(eventStarters)) {
const offset = 6 + s * 6; starters.push(...eventStarters);
const starterSpeciesForm = getPokemonSpeciesForm(
Number.parseInt(seed.slice(offset, offset + 4)) as SpeciesId,
Number.parseInt(seed.slice(offset + 4, offset + 6)),
);
starters.push(getDailyRunStarter(starterSpeciesForm, startingLevel));
}
return; return;
} }
@ -72,18 +65,7 @@ function getDailyRunStarter(starterSpeciesForm: PokemonSpeciesForm, startingLeve
const starterSpecies = const starterSpecies =
starterSpeciesForm instanceof PokemonSpecies ? starterSpeciesForm : getPokemonSpecies(starterSpeciesForm.speciesId); starterSpeciesForm instanceof PokemonSpecies ? starterSpeciesForm : getPokemonSpecies(starterSpeciesForm.speciesId);
const formIndex = starterSpeciesForm instanceof PokemonSpecies ? undefined : starterSpeciesForm.formIndex; const formIndex = starterSpeciesForm instanceof PokemonSpecies ? undefined : starterSpeciesForm.formIndex;
const pokemon = new PlayerPokemon( const pokemon = globalScene.addPlayerPokemon(starterSpecies, startingLevel, undefined, formIndex);
starterSpecies,
startingLevel,
undefined,
formIndex,
undefined,
undefined,
undefined,
undefined,
undefined,
undefined,
);
const starter: Starter = { const starter: Starter = {
species: starterSpecies, species: starterSpecies,
dexAttr: pokemon.getDexAttr(), dexAttr: pokemon.getDexAttr(),
@ -145,6 +127,11 @@ const dailyBiomeWeights: BiomeWeights = {
}; };
export function getDailyStartingBiome(): BiomeId { export function getDailyStartingBiome(): BiomeId {
const eventBiome = getDailyEventSeedBiome(globalScene.seed);
if (!isNullOrUndefined(eventBiome)) {
return eventBiome;
}
const biomes = getEnumValues(BiomeId).filter(b => b !== BiomeId.TOWN && b !== BiomeId.END); const biomes = getEnumValues(BiomeId).filter(b => b !== BiomeId.TOWN && b !== BiomeId.END);
let totalWeight = 0; let totalWeight = 0;
@ -169,3 +156,126 @@ export function getDailyStartingBiome(): BiomeId {
// TODO: should this use `randSeedItem`? // TODO: should this use `randSeedItem`?
return biomes[randSeedInt(biomes.length)]; return biomes[randSeedInt(biomes.length)];
} }
/**
* If this is Daily Mode and the seed is longer than a default seed
* then it has been modified and could contain a custom event seed. \
* Default seeds are always exactly 24 characters.
* @returns `true` if it is a Daily Event Seed.
*/
export function isDailyEventSeed(seed: string): boolean {
return globalScene.gameMode.isDaily && seed.length > 24;
}
/**
* Expects the seed to contain `/starters\d{18}/`
* where the digits alternate between 4 digits for the species ID and 2 digits for the form index
* (left padded with `0`s as necessary).
* @returns An array of {@linkcode Starter}s, or `null` if no valid match.
*/
export function getDailyEventSeedStarters(seed: string): Starter[] | null {
if (!isDailyEventSeed(seed)) {
return null;
}
const starters: Starter[] = [];
const match = /starters(\d{4})(\d{2})(\d{4})(\d{2})(\d{4})(\d{2})/g.exec(seed);
if (!match || match.length !== 7) {
return null;
}
for (let i = 1; i < match.length; i += 2) {
const speciesId = Number.parseInt(match[i]) as SpeciesId;
const formIndex = Number.parseInt(match[i + 1]);
if (!getEnumValues(SpeciesId).includes(speciesId)) {
console.warn("Invalid species ID used for custom daily run seed starter:", speciesId);
return null;
}
const starterForm = getPokemonSpeciesForm(speciesId, formIndex);
const startingLevel = globalScene.gameMode.getStartingLevel();
const starter = getDailyRunStarter(starterForm, startingLevel);
starters.push(starter);
}
return starters;
}
/**
* Expects the seed to contain `/boss\d{4}\d{2}/`
* where the first 4 digits are the species ID and the next 2 digits are the form index
* (left padded with `0`s as necessary).
* @returns A {@linkcode PokemonSpeciesForm} to be used for the boss, or `null` if no valid match.
*/
export function getDailyEventSeedBoss(seed: string): PokemonSpeciesForm | null {
if (!isDailyEventSeed(seed)) {
return null;
}
const match = /boss(\d{4})(\d{2})/g.exec(seed);
if (!match || match.length !== 3) {
return null;
}
const speciesId = Number.parseInt(match[1]) as SpeciesId;
const formIndex = Number.parseInt(match[2]);
if (!getEnumValues(SpeciesId).includes(speciesId)) {
console.warn("Invalid species ID used for custom daily run seed boss:", speciesId);
return null;
}
const starterForm = getPokemonSpeciesForm(speciesId, formIndex);
return starterForm;
}
/**
* Expects the seed to contain `/biome\d{2}/` where the 2 digits are a biome ID (left padded with `0` if necessary).
* @returns The biome to use or `null` if no valid match.
*/
export function getDailyEventSeedBiome(seed: string): BiomeId | null {
if (!isDailyEventSeed(seed)) {
return null;
}
const match = /biome(\d{2})/g.exec(seed);
if (!match || match.length !== 2) {
return null;
}
const startingBiome = Number.parseInt(match[1]) as BiomeId;
if (!getEnumValues(BiomeId).includes(startingBiome)) {
console.warn("Invalid biome ID used for custom daily run seed:", startingBiome);
return null;
}
return startingBiome;
}
/**
* Expects the seed to contain `/luck\d{2}/` where the 2 digits are a number between `0` and `14`
* (left padded with `0` if necessary).
* @returns The custom luck value or `null` if no valid match.
*/
export function getDailyEventSeedLuck(seed: string): number | null {
if (!isDailyEventSeed(seed)) {
return null;
}
const match = /luck(\d{2})/g.exec(seed);
if (!match || match.length !== 2) {
return null;
}
const luck = Number.parseInt(match[1]);
if (luck < 0 || luck > 14) {
console.warn("Invalid luck value used for custom daily run seed:", luck);
return null;
}
return luck;
}

View File

@ -1,11 +1,16 @@
import type { Ability } from "#abilities/ability"; import type { Ability } from "#abilities/ability";
import type { PokemonSpecies } from "#data/pokemon-species"; import type { PokemonSpecies } from "#data/pokemon-species";
import type { ModifierTypes } from "#modifiers/modifier-type"; import type { HeldItemId } from "#enums/held-item-id";
import type { TrainerItemId } from "#enums/trainer-item-id";
import type { HeldItem } from "#items/held-item";
import type { TrainerItem } from "#items/trainer-item";
import type { Move } from "#moves/move"; import type { Move } from "#moves/move";
export const allAbilities: Ability[] = []; export const allAbilities: Ability[] = [];
export const allMoves: Move[] = []; export const allMoves: Move[] = [];
export const allSpecies: PokemonSpecies[] = []; export const allSpecies: PokemonSpecies[] = [];
// TODO: Figure out what this is used for and provide an appropriate tsdoc comment //@ts-expect-error
export const modifierTypes = {} as ModifierTypes; export const allHeldItems: Record<HeldItemId, HeldItem> = {};
//@ts-expect-error
export const allTrainerItems: Record<TrainerItemId, TrainerItem> = {};

View File

@ -47,6 +47,7 @@ export class EggHatchData {
caughtCount: currDexEntry.caughtCount, caughtCount: currDexEntry.caughtCount,
hatchedCount: currDexEntry.hatchedCount, hatchedCount: currDexEntry.hatchedCount,
ivs: [...currDexEntry.ivs], ivs: [...currDexEntry.ivs],
ribbons: currDexEntry.ribbons,
}; };
this.starterDataEntryBeforeUpdate = { this.starterDataEntryBeforeUpdate = {
moveset: currStarterDataEntry.moveset, moveset: currStarterDataEntry.moveset,

View File

@ -22,7 +22,7 @@ import {
TypeBoostTag, TypeBoostTag,
} from "#data/battler-tags"; } from "#data/battler-tags";
import { getBerryEffectFunc } from "#data/berry"; import { getBerryEffectFunc } from "#data/berry";
import { allAbilities, allMoves } from "#data/data-lists"; import { allAbilities, allHeldItems, allMoves } from "#data/data-lists";
import { SpeciesFormChangeRevertWeatherFormTrigger } from "#data/form-change-triggers"; import { SpeciesFormChangeRevertWeatherFormTrigger } from "#data/form-change-triggers";
import { DelayedAttackTag } from "#data/positional-tags/positional-tag"; import { DelayedAttackTag } from "#data/positional-tags/positional-tag";
import { import {
@ -42,8 +42,8 @@ import { BiomeId } from "#enums/biome-id";
import { ChallengeType } from "#enums/challenge-type"; import { ChallengeType } from "#enums/challenge-type";
import { Command } from "#enums/command"; import { Command } from "#enums/command";
import { FieldPosition } from "#enums/field-position"; import { FieldPosition } from "#enums/field-position";
import { HeldItemCategoryId, HeldItemId, isItemInCategory } from "#enums/held-item-id";
import { HitResult } from "#enums/hit-result"; import { HitResult } from "#enums/hit-result";
import { ModifierPoolType } from "#enums/modifier-pool-type";
import { ChargeAnim } from "#enums/move-anims-common"; import { ChargeAnim } from "#enums/move-anims-common";
import { MoveId } from "#enums/move-id"; import { MoveId } from "#enums/move-id";
import { MoveResult } from "#enums/move-result"; import { MoveResult } from "#enums/move-result";
@ -68,14 +68,10 @@ import { SwitchType } from "#enums/switch-type";
import { WeatherType } from "#enums/weather-type"; import { WeatherType } from "#enums/weather-type";
import { MoveUsedEvent } from "#events/battle-scene"; import { MoveUsedEvent } from "#events/battle-scene";
import type { EnemyPokemon, Pokemon } from "#field/pokemon"; import type { EnemyPokemon, Pokemon } from "#field/pokemon";
import { import { applyHeldItems } from "#items/all-held-items";
AttackTypeBoosterModifier, import { BerryHeldItem, berryTypeToHeldItem } from "#items/berry";
BerryModifier, import { HeldItemEffect } from "#enums/held-item-effect";
PokemonHeldItemModifier, import { TrainerItemEffect } from "#items/trainer-item";
PokemonMoveAccuracyBoosterModifier,
PokemonMultiHitModifier,
PreserveBerryModifier,
} from "#modifiers/modifier";
import { applyMoveAttrs } from "#moves/apply-attrs"; import { applyMoveAttrs } from "#moves/apply-attrs";
import { invalidAssistMoves, invalidCopycatMoves, invalidMetronomeMoves, invalidMirrorMoveMoves, invalidSketchMoves, invalidSleepTalkMoves } from "#moves/invalid-moves"; import { invalidAssistMoves, invalidCopycatMoves, invalidMetronomeMoves, invalidMirrorMoveMoves, invalidSketchMoves, invalidSleepTalkMoves } from "#moves/invalid-moves";
import { frenzyMissFunc, getMoveTargets } from "#moves/move-utils"; import { frenzyMissFunc, getMoveTargets } from "#moves/move-utils";
@ -86,11 +82,11 @@ import { PokemonHealPhase } from "#phases/pokemon-heal-phase";
import { SwitchSummonPhase } from "#phases/switch-summon-phase"; import { SwitchSummonPhase } from "#phases/switch-summon-phase";
import type { AttackMoveResult } from "#types/attack-move-result"; import type { AttackMoveResult } from "#types/attack-move-result";
import type { Localizable } from "#types/locales"; import type { Localizable } from "#types/locales";
import type { ChargingMove, MoveAttrMap, MoveAttrString, MoveClassMap, MoveKindString } from "#types/move-types"; import type { ChargingMove, MoveAttrMap, MoveAttrString, MoveClassMap, MoveKindString, MoveMessageFunc } from "#types/move-types";
import type { TurnMove } from "#types/turn-move"; import type { TurnMove } from "#types/turn-move";
import { BooleanHolder, type Constructor, isNullOrUndefined, NumberHolder, randSeedFloat, randSeedInt, randSeedItem, toDmgValue } from "#utils/common"; import { BooleanHolder, type Constructor, isNullOrUndefined, NumberHolder, randSeedFloat, randSeedInt, randSeedItem, toDmgValue } from "#utils/common";
import { getEnumValues } from "#utils/enums"; import { getEnumValues } from "#utils/enums";
import { toTitleCase } from "#utils/strings"; import { toCamelCase, toTitleCase } from "#utils/strings";
import i18next from "i18next"; import i18next from "i18next";
import { applyChallenges } from "#utils/challenge-utils"; import { applyChallenges } from "#utils/challenge-utils";
@ -162,10 +158,16 @@ export abstract class Move implements Localizable {
} }
localize(): void { localize(): void {
const i18nKey = MoveId[this.id].split("_").filter(f => f).map((f, i) => i ? `${f[0]}${f.slice(1).toLowerCase()}` : f.toLowerCase()).join("") as unknown as string; const i18nKey = toCamelCase(MoveId[this.id])
this.name = this.id ? `${i18next.t(`move:${i18nKey}.name`)}${this.nameAppend}` : ""; if (this.id === MoveId.NONE) {
this.effect = this.id ? `${i18next.t(`move:${i18nKey}.effect`)}${this.nameAppend}` : ""; this.name = "";
this.effect = ""
return;
}
this.name = `${i18next.t(`move:${i18nKey}.name`)}${this.nameAppend}`;
this.effect = `${i18next.t(`move:${i18nKey}.effect`)}${this.nameAppend}`;
} }
/** /**
@ -779,7 +781,7 @@ export abstract class Move implements Localizable {
const isOhko = this.hasAttr("OneHitKOAccuracyAttr"); const isOhko = this.hasAttr("OneHitKOAccuracyAttr");
if (!isOhko) { if (!isOhko) {
globalScene.applyModifiers(PokemonMoveAccuracyBoosterModifier, user.isPlayer(), user, moveAccuracy); applyHeldItems(HeldItemEffect.ACCURACY_BOOSTER, { pokemon: user, moveAccuracy: moveAccuracy });
} }
if (globalScene.arena.weather?.weatherType === WeatherType.FOG) { if (globalScene.arena.weather?.weatherType === WeatherType.FOG) {
@ -833,9 +835,15 @@ export abstract class Move implements Localizable {
} }
// Non-priority, single-hit moves of the user's Tera Type are always a bare minimum of 60 power // Non-priority, single-hit moves of the user's Tera Type are always a bare minimum of 60 power
const sourceTeraType = source.getTeraType(); const sourceTeraType = source.getTeraType();
if (source.isTerastallized && sourceTeraType === this.type && power.value < 60 && this.priority <= 0 && !this.hasAttr("MultiHitAttr") && !globalScene.findModifier(m => m instanceof PokemonMultiHitModifier && m.pokemonId === source.id)) { if (
source.isTerastallized
&& sourceTeraType === this.type
&& power.value < 60
&& this.priority <= 0
&& !this.hasAttr("MultiHitAttr")
&& !source.heldItemManager.hasItem(HeldItemId.MULTI_LENS)
) {
power.value = 60; power.value = 60;
} }
@ -865,7 +873,11 @@ export abstract class Move implements Localizable {
if (!this.hasAttr("TypelessAttr")) { if (!this.hasAttr("TypelessAttr")) {
globalScene.arena.applyTags(WeakenMoveTypeTag, simulated, typeChangeHolder.value, power); globalScene.arena.applyTags(WeakenMoveTypeTag, simulated, typeChangeHolder.value, power);
globalScene.applyModifiers(AttackTypeBoosterModifier, source.isPlayer(), source, typeChangeHolder.value, power); applyHeldItems(HeldItemEffect.ATTACK_TYPE_BOOST, {
pokemon: source,
moveType: typeChangeHolder.value,
movePower: power,
});
} }
if (source.getTag(HelpingHandTag)) { if (source.getTag(HelpingHandTag)) {
@ -928,7 +940,7 @@ export abstract class Move implements Localizable {
* Returns `true` if this move can be given additional strikes * Returns `true` if this move can be given additional strikes
* by enhancing effects. * by enhancing effects.
* Currently used for {@link https://bulbapedia.bulbagarden.net/wiki/Parental_Bond_(Ability) | Parental Bond} * Currently used for {@link https://bulbapedia.bulbagarden.net/wiki/Parental_Bond_(Ability) | Parental Bond}
* and {@linkcode PokemonMultiHitModifier | Multi-Lens}. * and {@linkcode MultiHitHeldItem | Multi-Lens}.
* @param user The {@linkcode Pokemon} using the move * @param user The {@linkcode Pokemon} using the move
* @param restrictSpread `true` if the enhancing effect * @param restrictSpread `true` if the enhancing effect
* should not affect multi-target moves (default `false`) * should not affect multi-target moves (default `false`)
@ -1357,20 +1369,20 @@ export class MoveHeaderAttr extends MoveAttr {
/** /**
* Header attribute to queue a message at the beginning of a turn. * Header attribute to queue a message at the beginning of a turn.
* @see {@link MoveHeaderAttr}
*/ */
export class MessageHeaderAttr extends MoveHeaderAttr { export class MessageHeaderAttr extends MoveHeaderAttr {
private message: string | ((user: Pokemon, move: Move) => string); /** The message to display, or a function producing one. */
private message: string | MoveMessageFunc;
constructor(message: string | ((user: Pokemon, move: Move) => string)) { constructor(message: string | MoveMessageFunc) {
super(); super();
this.message = message; this.message = message;
} }
apply(user: Pokemon, target: Pokemon, move: Move, args: any[]): boolean { apply(user: Pokemon, target: Pokemon, move: Move): boolean {
const message = typeof this.message === "string" const message = typeof this.message === "string"
? this.message ? this.message
: this.message(user, move); : this.message(user, target, move);
if (message) { if (message) {
globalScene.phaseManager.queueMessage(message); globalScene.phaseManager.queueMessage(message);
@ -1418,21 +1430,21 @@ export class BeakBlastHeaderAttr extends AddBattlerTagHeaderAttr {
*/ */
export class PreMoveMessageAttr extends MoveAttr { export class PreMoveMessageAttr extends MoveAttr {
/** The message to display or a function returning one */ /** The message to display or a function returning one */
private message: string | ((user: Pokemon, target: Pokemon, move: Move) => string | undefined); private message: string | MoveMessageFunc;
/** /**
* Create a new {@linkcode PreMoveMessageAttr} to display a message before move execution. * Create a new {@linkcode PreMoveMessageAttr} to display a message before move execution.
* @param message - The message to display before move use, either as a string or a function producing one. * @param message - The message to display before move use, either` a literal string or a function producing one.
* @remarks * @remarks
* If {@linkcode message} evaluates to an empty string (`''`), no message will be displayed * If {@linkcode message} evaluates to an empty string (`""`), no message will be displayed
* (though the move will still succeed). * (though the move will still succeed).
*/ */
constructor(message: string | ((user: Pokemon, target: Pokemon, move: Move) => string)) { constructor(message: string | MoveMessageFunc) {
super(); super();
this.message = message; this.message = message;
} }
apply(user: Pokemon, target: Pokemon, move: Move, _args: any[]): boolean { apply(user: Pokemon, target: Pokemon, move: Move): boolean {
const message = typeof this.message === "function" const message = typeof this.message === "function"
? this.message(user, target, move) ? this.message(user, target, move)
: this.message; : this.message;
@ -1453,18 +1465,17 @@ export class PreMoveMessageAttr extends MoveAttr {
* @extends MoveAttr * @extends MoveAttr
*/ */
export class PreUseInterruptAttr extends MoveAttr { export class PreUseInterruptAttr extends MoveAttr {
protected message?: string | ((user: Pokemon, target: Pokemon, move: Move) => string); protected message: string | MoveMessageFunc;
protected overridesFailedMessage: boolean;
protected conditionFunc: MoveConditionFunc; protected conditionFunc: MoveConditionFunc;
/** /**
* Create a new MoveInterruptedMessageAttr. * Create a new MoveInterruptedMessageAttr.
* @param message The message to display when the move is interrupted, or a function that formats the message based on the user, target, and move. * @param message The message to display when the move is interrupted, or a function that formats the message based on the user, target, and move.
*/ */
constructor(message?: string | ((user: Pokemon, target: Pokemon, move: Move) => string), conditionFunc?: MoveConditionFunc) { constructor(message: string | MoveMessageFunc, conditionFunc: MoveConditionFunc) {
super(); super();
this.message = message; this.message = message;
this.conditionFunc = conditionFunc ?? (() => true); this.conditionFunc = conditionFunc;
} }
/** /**
@ -1485,11 +1496,9 @@ export class PreUseInterruptAttr extends MoveAttr {
*/ */
override getFailedText(user: Pokemon, target: Pokemon, move: Move): string | undefined { override getFailedText(user: Pokemon, target: Pokemon, move: Move): string | undefined {
if (this.message && this.conditionFunc(user, target, move)) { if (this.message && this.conditionFunc(user, target, move)) {
const message = return typeof this.message === "string"
typeof this.message === "string" ? this.message
? (this.message as string)
: this.message(user, target, move); : this.message(user, target, move);
return message;
} }
} }
} }
@ -1577,7 +1586,7 @@ export class TargetHalfHpDamageAttr extends FixedDamageAttr {
apply(user: Pokemon, target: Pokemon, move: Move, args: any[]): boolean { apply(user: Pokemon, target: Pokemon, move: Move, args: any[]): boolean {
// first, determine if the hit is coming from multi lens or not // first, determine if the hit is coming from multi lens or not
const lensCount = user.getHeldItems().find(i => i instanceof PokemonMultiHitModifier)?.getStackCount() ?? 0; const lensCount = user.heldItemManager.getStack(HeldItemId.MULTI_LENS);
if (lensCount <= 0) { if (lensCount <= 0) {
// no multi lenses; we can just halve the target's hp and call it a day // no multi lenses; we can just halve the target's hp and call it a day
(args[0] as NumberHolder).value = toDmgValue(target.hp / 2); (args[0] as NumberHolder).value = toDmgValue(target.hp / 2);
@ -1694,17 +1703,30 @@ export class SurviveDamageAttr extends ModifiedDamageAttr {
} }
} }
export class SplashAttr extends MoveEffectAttr { /**
apply(user: Pokemon, target: Pokemon, move: Move, args: any[]): boolean { * Move attribute to display arbitrary text during a move's execution.
globalScene.phaseManager.queueMessage(i18next.t("moveTriggers:splash")); */
export class MessageAttr extends MoveEffectAttr {
/** The message to display, either as a string or a function returning one. */
private message: string | MoveMessageFunc;
constructor(message: string | MoveMessageFunc, options?: MoveEffectAttrOptions) {
// TODO: Do we need to respect `selfTarget` if we're just displaying text?
super(false, options)
this.message = message;
}
override apply(user: Pokemon, target: Pokemon, move: Move): boolean {
const message = typeof this.message === "function"
? this.message(user, target, move)
: this.message;
// TODO: Consider changing if/when MoveAttr `apply` return values become significant
if (message) {
globalScene.phaseManager.queueMessage(message, 500);
return true; return true;
} }
} return false;
export class CelebrateAttr extends MoveEffectAttr {
apply(user: Pokemon, target: Pokemon, move: Move, args: any[]): boolean {
globalScene.phaseManager.queueMessage(i18next.t("moveTriggers:celebrate", { playerName: loggedInUser?.username }));
return true;
} }
} }
@ -2636,35 +2658,33 @@ export class StealHeldItemChanceAttr extends MoveEffectAttr {
return false; return false;
} }
const heldItems = this.getTargetHeldItems(target).filter((i) => i.isTransferable); const heldItems = target.heldItemManager.getTransferableHeldItems();
if (!heldItems.length) { if (!heldItems.length) {
return false; return false;
} }
const poolType = target.isPlayer() ? ModifierPoolType.PLAYER : target.hasTrainer() ? ModifierPoolType.TRAINER : ModifierPoolType.WILD; const stolenItem = heldItems[user.randBattleSeedInt(heldItems.length)];
const highestItemTier = heldItems.map((m) => m.type.getOrInferTier(poolType)).reduce((highestTier, tier) => Math.max(tier!, highestTier), 0); // TODO: is the bang after tier correct?
const tierHeldItems = heldItems.filter((m) => m.type.getOrInferTier(poolType) === highestItemTier); if (!globalScene.tryTransferHeldItem(stolenItem, target, user, false)) {
const stolenItem = tierHeldItems[user.randBattleSeedInt(tierHeldItems.length)];
if (!globalScene.tryTransferHeldItemModifier(stolenItem, user, false)) {
return false; return false;
} }
globalScene.phaseManager.queueMessage(i18next.t("moveTriggers:stoleItem", { pokemonName: getPokemonNameWithAffix(user), targetName: getPokemonNameWithAffix(target), itemName: stolenItem.type.name })); globalScene.phaseManager.queueMessage(i18next.t("moveTriggers:stoleItem",
{ pokemonName: getPokemonNameWithAffix(user),
targetName: getPokemonNameWithAffix(target),
itemName: allHeldItems[stolenItem].name
}
));
return true; return true;
} }
getTargetHeldItems(target: Pokemon): PokemonHeldItemModifier[] {
return globalScene.findModifiers(m => m instanceof PokemonHeldItemModifier
&& m.pokemonId === target.id, target.isPlayer()) as PokemonHeldItemModifier[];
}
getUserBenefitScore(user: Pokemon, target: Pokemon, move: Move): number { getUserBenefitScore(user: Pokemon, target: Pokemon, move: Move): number {
const heldItems = this.getTargetHeldItems(target); const heldItems = target.heldItemManager.getTransferableHeldItems();
return heldItems.length ? 5 : 0; return heldItems.length ? 5 : 0;
} }
getTargetBenefitScore(user: Pokemon, target: Pokemon, move: Move): number { getTargetBenefitScore(user: Pokemon, target: Pokemon, move: Move): number {
const heldItems = this.getTargetHeldItems(target); const heldItems = target.heldItemManager.getTransferableHeldItems();
return heldItems.length ? -5 : 0; return heldItems.length ? -5 : 0;
} }
} }
@ -2710,10 +2730,10 @@ export class RemoveHeldItemAttr extends MoveEffectAttr {
// Considers entire transferrable item pool by default (Knock Off). // Considers entire transferrable item pool by default (Knock Off).
// Otherwise only consider berries (Incinerate). // Otherwise only consider berries (Incinerate).
let heldItems = this.getTargetHeldItems(target).filter(i => i.isTransferable); let heldItems = target.heldItemManager.getTransferableHeldItems();
if (this.berriesOnly) { if (this.berriesOnly) {
heldItems = heldItems.filter(m => m instanceof BerryModifier && m.pokemonId === target.id, target.isPlayer()); heldItems = heldItems.filter(m => m in Object.values(berryTypeToHeldItem));
} }
if (!heldItems.length) { if (!heldItems.length) {
@ -2724,29 +2744,26 @@ export class RemoveHeldItemAttr extends MoveEffectAttr {
// Decrease item amount and update icon // Decrease item amount and update icon
target.loseHeldItem(removedItem); target.loseHeldItem(removedItem);
globalScene.updateModifiers(target.isPlayer()); globalScene.updateItems(target.isPlayer());
if (this.berriesOnly) { if (this.berriesOnly) {
globalScene.phaseManager.queueMessage(i18next.t("moveTriggers:incineratedItem", { pokemonName: getPokemonNameWithAffix(user), targetName: getPokemonNameWithAffix(target), itemName: removedItem.type.name })); globalScene.phaseManager.queueMessage(i18next.t("moveTriggers:incineratedItem",
{ pokemonName: getPokemonNameWithAffix(user), targetName: getPokemonNameWithAffix(target), itemName: allHeldItems[removedItem].name }));
} else { } else {
globalScene.phaseManager.queueMessage(i18next.t("moveTriggers:knockedOffItem", { pokemonName: getPokemonNameWithAffix(user), targetName: getPokemonNameWithAffix(target), itemName: removedItem.type.name })); globalScene.phaseManager.queueMessage(i18next.t("moveTriggers:knockedOffItem",
{ pokemonName: getPokemonNameWithAffix(user), targetName: getPokemonNameWithAffix(target), itemName: allHeldItems[removedItem].name }));
} }
return true; return true;
} }
getTargetHeldItems(target: Pokemon): PokemonHeldItemModifier[] {
return globalScene.findModifiers(m => m instanceof PokemonHeldItemModifier
&& m.pokemonId === target.id, target.isPlayer()) as PokemonHeldItemModifier[];
}
getUserBenefitScore(user: Pokemon, target: Pokemon, move: Move): number { getUserBenefitScore(user: Pokemon, target: Pokemon, move: Move): number {
const heldItems = this.getTargetHeldItems(target); const heldItems = target.getHeldItems();
return heldItems.length ? 5 : 0; return heldItems.length ? 5 : 0;
} }
getTargetBenefitScore(user: Pokemon, target: Pokemon, move: Move): number { getTargetBenefitScore(user: Pokemon, target: Pokemon, move: Move): number {
const heldItems = this.getTargetHeldItems(target); const heldItems = target.getHeldItems();
return heldItems.length ? -5 : 0; return heldItems.length ? -5 : 0;
} }
} }
@ -2755,7 +2772,7 @@ export class RemoveHeldItemAttr extends MoveEffectAttr {
* Attribute that causes targets of the move to eat a berry. Used for Teatime, Stuff Cheeks * Attribute that causes targets of the move to eat a berry. Used for Teatime, Stuff Cheeks
*/ */
export class EatBerryAttr extends MoveEffectAttr { export class EatBerryAttr extends MoveEffectAttr {
protected chosenBerry: BerryModifier; protected chosenBerry: HeldItemId;
constructor(selfTarget: boolean) { constructor(selfTarget: boolean) {
super(selfTarget); super(selfTarget);
} }
@ -2784,9 +2801,9 @@ export class EatBerryAttr extends MoveEffectAttr {
this.chosenBerry = heldBerries[user.randBattleSeedInt(heldBerries.length)]; this.chosenBerry = heldBerries[user.randBattleSeedInt(heldBerries.length)];
const preserve = new BooleanHolder(false); const preserve = new BooleanHolder(false);
// check for berry pouch preservation // check for berry pouch preservation
globalScene.applyModifiers(PreserveBerryModifier, pokemon.isPlayer(), pokemon, preserve); globalScene.applyPlayerItems(TrainerItemEffect.PRESERVE_BERRY, {pokemon: pokemon, doPreserve: preserve});
if (!preserve.value) { if (!preserve.value) {
this.reduceBerryModifier(pokemon); this.reduceBerryItem(pokemon);
} }
// Don't update harvest for berries preserved via Berry pouch (no item dupes lol) // Don't update harvest for berries preserved via Berry pouch (no item dupes lol)
@ -2795,16 +2812,15 @@ export class EatBerryAttr extends MoveEffectAttr {
return true; return true;
} }
getTargetHeldBerries(target: Pokemon): BerryModifier[] { getTargetHeldBerries(target: Pokemon): HeldItemId[] {
return globalScene.findModifiers(m => m instanceof BerryModifier return target.getHeldItems().filter(m => isItemInCategory(m, HeldItemCategoryId.BERRY));
&& (m as BerryModifier).pokemonId === target.id, target.isPlayer()) as BerryModifier[];
} }
reduceBerryModifier(target: Pokemon) { reduceBerryItem(target: Pokemon) {
if (this.chosenBerry) { if (this.chosenBerry) {
target.loseHeldItem(this.chosenBerry); target.loseHeldItem(this.chosenBerry);
} }
globalScene.updateModifiers(target.isPlayer()); globalScene.updateItems(target.isPlayer());
} }
@ -2818,10 +2834,10 @@ export class EatBerryAttr extends MoveEffectAttr {
*/ */
protected eatBerry(consumer: Pokemon, berryOwner: Pokemon = consumer, updateHarvest = consumer === berryOwner) { protected eatBerry(consumer: Pokemon, berryOwner: Pokemon = consumer, updateHarvest = consumer === berryOwner) {
// consumer eats berry, owner triggers unburden and similar effects // consumer eats berry, owner triggers unburden and similar effects
getBerryEffectFunc(this.chosenBerry.berryType)(consumer); getBerryEffectFunc((allHeldItems[this.chosenBerry] as BerryHeldItem).berryType)(consumer);
applyAbAttrs("PostItemLostAbAttr", {pokemon: berryOwner}); applyAbAttrs("PostItemLostAbAttr", {pokemon: berryOwner});
applyAbAttrs("HealFromBerryUseAbAttr", {pokemon: consumer}); applyAbAttrs("HealFromBerryUseAbAttr", {pokemon: consumer});
consumer.recordEatenBerry(this.chosenBerry.berryType, updateHarvest); consumer.recordEatenBerry((allHeldItems[this.chosenBerry] as BerryHeldItem).berryType, updateHarvest);
} }
} }
@ -2860,9 +2876,9 @@ export class StealEatBerryAttr extends EatBerryAttr {
// pick a random berry and eat it // pick a random berry and eat it
this.chosenBerry = heldBerries[user.randBattleSeedInt(heldBerries.length)]; this.chosenBerry = heldBerries[user.randBattleSeedInt(heldBerries.length)];
applyAbAttrs("PostItemLostAbAttr", {pokemon: target}); applyAbAttrs("PostItemLostAbAttr", {pokemon: target});
const message = i18next.t("battle:stealEatBerry", { pokemonName: user.name, targetName: target.name, berryName: this.chosenBerry.type.name }); const message = i18next.t("battle:stealEatBerry", { pokemonName: user.name, targetName: target.name, berryName: allHeldItems[this.chosenBerry].name });
globalScene.phaseManager.queueMessage(message); globalScene.phaseManager.queueMessage(message);
this.reduceBerryModifier(target); this.reduceBerryItem(target);
this.eatBerry(user, target); this.eatBerry(user, target);
return true; return true;
@ -5931,38 +5947,6 @@ export class ProtectAttr extends AddBattlerTagAttr {
} }
} }
export class IgnoreAccuracyAttr extends AddBattlerTagAttr {
constructor() {
super(BattlerTagType.IGNORE_ACCURACY, true, false, 2);
}
apply(user: Pokemon, target: Pokemon, move: Move, args: any[]): boolean {
if (!super.apply(user, target, move, args)) {
return false;
}
globalScene.phaseManager.queueMessage(i18next.t("moveTriggers:tookAimAtTarget", { pokemonName: getPokemonNameWithAffix(user), targetName: getPokemonNameWithAffix(target) }));
return true;
}
}
export class FaintCountdownAttr extends AddBattlerTagAttr {
constructor() {
super(BattlerTagType.PERISH_SONG, false, true, 4);
}
apply(user: Pokemon, target: Pokemon, move: Move, args: any[]): boolean {
if (!super.apply(user, target, move, args)) {
return false;
}
globalScene.phaseManager.queueMessage(i18next.t("moveTriggers:faintCountdown", { pokemonName: getPokemonNameWithAffix(target), turnCount: this.turnCountMin - 1 }));
return true;
}
}
/** /**
* Attribute to remove all Substitutes from the field. * Attribute to remove all Substitutes from the field.
* @extends MoveEffectAttr * @extends MoveEffectAttr
@ -6480,9 +6464,6 @@ export class ForceSwitchOutAttr extends MoveEffectAttr {
} }
} }
// clear out enemy held item modifiers of the switch out target
globalScene.clearEnemyHeldItemModifiers(switchOutTarget);
if (!allyPokemon?.isActive(true) && switchOutTarget.hp) { if (!allyPokemon?.isActive(true) && switchOutTarget.hp) {
globalScene.phaseManager.pushNew("BattleEndPhase", false); globalScene.phaseManager.pushNew("BattleEndPhase", false);
@ -6603,8 +6584,10 @@ export class ChillyReceptionAttr extends ForceSwitchOutAttr {
return (user, target, move) => globalScene.arena.weather?.weatherType !== WeatherType.SNOW || super.getSwitchOutCondition()(user, target, move); return (user, target, move) => globalScene.arena.weather?.weatherType !== WeatherType.SNOW || super.getSwitchOutCondition()(user, target, move);
} }
} }
export class RemoveTypeAttr extends MoveEffectAttr { export class RemoveTypeAttr extends MoveEffectAttr {
// TODO: Remove the message callback
private removedType: PokemonType; private removedType: PokemonType;
private messageCallback: ((user: Pokemon) => void) | undefined; private messageCallback: ((user: Pokemon) => void) | undefined;
@ -8083,14 +8066,14 @@ const failIfLastInPartyCondition: MoveConditionFunc = (user: Pokemon, target: Po
const failIfGhostTypeCondition: MoveConditionFunc = (user: Pokemon, target: Pokemon, move: Move) => !target.isOfType(PokemonType.GHOST); const failIfGhostTypeCondition: MoveConditionFunc = (user: Pokemon, target: Pokemon, move: Move) => !target.isOfType(PokemonType.GHOST);
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.heldItemManager.getTransferableHeldItems().length > 0;
const attackedByItemMessageFunc = (user: Pokemon, target: Pokemon, move: Move) => { const attackedByItemMessageFunc = (user: Pokemon, target: Pokemon, move: Move) => {
const heldItems = target.getHeldItems().filter(i => i.isTransferable); const heldItems = target.heldItemManager.getTransferableHeldItems();
if (heldItems.length === 0) { if (heldItems.length === 0) {
return ""; return "";
} }
const itemName = heldItems[0]?.type?.name ?? "item"; const itemName = allHeldItems[heldItems[0]].name ?? "item";
const message: string = i18next.t("moveTriggers:attackedByItem", { pokemonName: getPokemonNameWithAffix(target), itemName: itemName }); const message: string = i18next.t("moveTriggers:attackedByItem", { pokemonName: getPokemonNameWithAffix(target), itemName: itemName });
return message; return message;
}; };
@ -8299,8 +8282,6 @@ const MoveAttrs = Object.freeze({
RandomLevelDamageAttr, RandomLevelDamageAttr,
ModifiedDamageAttr, ModifiedDamageAttr,
SurviveDamageAttr, SurviveDamageAttr,
SplashAttr,
CelebrateAttr,
RecoilAttr, RecoilAttr,
SacrificialAttr, SacrificialAttr,
SacrificialAttrOnHit, SacrificialAttrOnHit,
@ -8443,8 +8424,7 @@ const MoveAttrs = Object.freeze({
RechargeAttr, RechargeAttr,
TrapAttr, TrapAttr,
ProtectAttr, ProtectAttr,
IgnoreAccuracyAttr, MessageAttr,
FaintCountdownAttr,
RemoveAllSubstitutesAttr, RemoveAllSubstitutesAttr,
HitsTagAttr, HitsTagAttr,
HitsTagForDoubleDamageAttr, HitsTagForDoubleDamageAttr,
@ -8938,7 +8918,7 @@ export function initMoves() {
new AttackMove(MoveId.PSYWAVE, PokemonType.PSYCHIC, MoveCategory.SPECIAL, -1, 100, 15, -1, 0, 1) new AttackMove(MoveId.PSYWAVE, PokemonType.PSYCHIC, MoveCategory.SPECIAL, -1, 100, 15, -1, 0, 1)
.attr(RandomLevelDamageAttr), .attr(RandomLevelDamageAttr),
new SelfStatusMove(MoveId.SPLASH, PokemonType.NORMAL, -1, 40, -1, 0, 1) new SelfStatusMove(MoveId.SPLASH, PokemonType.NORMAL, -1, 40, -1, 0, 1)
.attr(SplashAttr) .attr(MessageAttr, i18next.t("moveTriggers:splash"))
.condition(failOnGravityCondition), .condition(failOnGravityCondition),
new SelfStatusMove(MoveId.ACID_ARMOR, PokemonType.POISON, -1, 20, -1, 0, 1) new SelfStatusMove(MoveId.ACID_ARMOR, PokemonType.POISON, -1, 20, -1, 0, 1)
.attr(StatStageChangeAttr, [ Stat.DEF ], 2, true), .attr(StatStageChangeAttr, [ Stat.DEF ], 2, true),
@ -9000,7 +8980,10 @@ export function initMoves() {
.attr(AddBattlerTagAttr, BattlerTagType.TRAPPED, false, true, 1) .attr(AddBattlerTagAttr, BattlerTagType.TRAPPED, false, true, 1)
.reflectable(), .reflectable(),
new StatusMove(MoveId.MIND_READER, PokemonType.NORMAL, -1, 5, -1, 0, 2) new StatusMove(MoveId.MIND_READER, PokemonType.NORMAL, -1, 5, -1, 0, 2)
.attr(IgnoreAccuracyAttr), .attr(AddBattlerTagAttr, BattlerTagType.IGNORE_ACCURACY, true, false, 2)
.attr(MessageAttr, (user, target) =>
i18next.t("moveTriggers:tookAimAtTarget", { pokemonName: getPokemonNameWithAffix(user), targetName: getPokemonNameWithAffix(target) })
),
new StatusMove(MoveId.NIGHTMARE, PokemonType.GHOST, 100, 15, -1, 0, 2) new StatusMove(MoveId.NIGHTMARE, PokemonType.GHOST, 100, 15, -1, 0, 2)
.attr(AddBattlerTagAttr, BattlerTagType.NIGHTMARE) .attr(AddBattlerTagAttr, BattlerTagType.NIGHTMARE)
.condition(targetSleptOrComatoseCondition), .condition(targetSleptOrComatoseCondition),
@ -9088,7 +9071,9 @@ export function initMoves() {
return lastTurnMove.length === 0 || lastTurnMove[0].move !== move.id || lastTurnMove[0].result !== MoveResult.SUCCESS; return lastTurnMove.length === 0 || lastTurnMove[0].move !== move.id || lastTurnMove[0].result !== MoveResult.SUCCESS;
}), }),
new StatusMove(MoveId.PERISH_SONG, PokemonType.NORMAL, -1, 5, -1, 0, 2) new StatusMove(MoveId.PERISH_SONG, PokemonType.NORMAL, -1, 5, -1, 0, 2)
.attr(FaintCountdownAttr) .attr(AddBattlerTagAttr, BattlerTagType.PERISH_SONG, false, true, 4)
.attr(MessageAttr, (_user, target) =>
i18next.t("moveTriggers:faintCountdown", { pokemonName: getPokemonNameWithAffix(target), turnCount: 3 }))
.ignoresProtect() .ignoresProtect()
.soundBased() .soundBased()
.condition(failOnBossCondition) .condition(failOnBossCondition)
@ -9104,7 +9089,10 @@ export function initMoves() {
.attr(MultiHitAttr) .attr(MultiHitAttr)
.makesContact(false), .makesContact(false),
new StatusMove(MoveId.LOCK_ON, PokemonType.NORMAL, -1, 5, -1, 0, 2) new StatusMove(MoveId.LOCK_ON, PokemonType.NORMAL, -1, 5, -1, 0, 2)
.attr(IgnoreAccuracyAttr), .attr(AddBattlerTagAttr, BattlerTagType.IGNORE_ACCURACY, true, false, 2)
.attr(MessageAttr, (user, target) =>
i18next.t("moveTriggers:tookAimAtTarget", { pokemonName: getPokemonNameWithAffix(user), targetName: getPokemonNameWithAffix(target) })
),
new AttackMove(MoveId.OUTRAGE, PokemonType.DRAGON, MoveCategory.PHYSICAL, 120, 100, 10, -1, 0, 2) new AttackMove(MoveId.OUTRAGE, PokemonType.DRAGON, MoveCategory.PHYSICAL, 120, 100, 10, -1, 0, 2)
.attr(FrenzyAttr) .attr(FrenzyAttr)
.attr(MissEffectAttr, frenzyMissFunc) .attr(MissEffectAttr, frenzyMissFunc)
@ -9331,8 +9319,8 @@ export function initMoves() {
&& (user.status.effect === StatusEffect.BURN || user.status.effect === StatusEffect.POISON || user.status.effect === StatusEffect.TOXIC || user.status.effect === StatusEffect.PARALYSIS) ? 2 : 1) && (user.status.effect === StatusEffect.BURN || user.status.effect === StatusEffect.POISON || user.status.effect === StatusEffect.TOXIC || user.status.effect === StatusEffect.PARALYSIS) ? 2 : 1)
.attr(BypassBurnDamageReductionAttr), .attr(BypassBurnDamageReductionAttr),
new AttackMove(MoveId.FOCUS_PUNCH, PokemonType.FIGHTING, MoveCategory.PHYSICAL, 150, 100, 20, -1, -3, 3) new AttackMove(MoveId.FOCUS_PUNCH, PokemonType.FIGHTING, MoveCategory.PHYSICAL, 150, 100, 20, -1, -3, 3)
.attr(MessageHeaderAttr, (user, move) => i18next.t("moveTriggers:isTighteningFocus", { pokemonName: getPokemonNameWithAffix(user) })) .attr(MessageHeaderAttr, (user) => i18next.t("moveTriggers:isTighteningFocus", { pokemonName: getPokemonNameWithAffix(user) }))
.attr(PreUseInterruptAttr, (user, target, move) => i18next.t("moveTriggers:lostFocus", { pokemonName: getPokemonNameWithAffix(user) }), user => !!user.turnData.attacksReceived.find(r => r.damage)) .attr(PreUseInterruptAttr, (user) => i18next.t("moveTriggers:lostFocus", { pokemonName: getPokemonNameWithAffix(user) }), user => user.turnData.attacksReceived.some(r => r.damage > 0))
.punchingMove(), .punchingMove(),
new AttackMove(MoveId.SMELLING_SALTS, PokemonType.NORMAL, MoveCategory.PHYSICAL, 70, 100, 10, -1, 0, 3) new AttackMove(MoveId.SMELLING_SALTS, PokemonType.NORMAL, MoveCategory.PHYSICAL, 70, 100, 10, -1, 0, 3)
.attr(MovePowerMultiplierAttr, (user, target, move) => target.status?.effect === StatusEffect.PARALYSIS ? 2 : 1) .attr(MovePowerMultiplierAttr, (user, target, move) => target.status?.effect === StatusEffect.PARALYSIS ? 2 : 1)
@ -9387,7 +9375,7 @@ export function initMoves() {
.condition((user, target, move) => !target.status && !target.isSafeguarded(user)) .condition((user, target, move) => !target.status && !target.isSafeguarded(user))
.reflectable(), .reflectable(),
new AttackMove(MoveId.KNOCK_OFF, PokemonType.DARK, MoveCategory.PHYSICAL, 65, 100, 20, -1, 0, 3) new AttackMove(MoveId.KNOCK_OFF, PokemonType.DARK, MoveCategory.PHYSICAL, 65, 100, 20, -1, 0, 3)
.attr(MovePowerMultiplierAttr, (user, target, move) => target.getHeldItems().filter(i => i.isTransferable).length > 0 ? 1.5 : 1) .attr(MovePowerMultiplierAttr, (user, target, move) => target.heldItemManager.getTransferableHeldItems().length > 0 ? 1.5 : 1)
.attr(RemoveHeldItemAttr, false) .attr(RemoveHeldItemAttr, false)
.edgeCase(), .edgeCase(),
// Should not be able to remove held item if user faints due to Rough Skin, Iron Barbs, etc. // Should not be able to remove held item if user faints due to Rough Skin, Iron Barbs, etc.
@ -10113,7 +10101,7 @@ export function initMoves() {
.condition((user, target, move) => !target.turnData.acted) .condition((user, target, move) => !target.turnData.acted)
.attr(ForceLastAttr), .attr(ForceLastAttr),
new AttackMove(MoveId.ACROBATICS, PokemonType.FLYING, MoveCategory.PHYSICAL, 55, 100, 15, -1, 0, 5) new AttackMove(MoveId.ACROBATICS, PokemonType.FLYING, MoveCategory.PHYSICAL, 55, 100, 15, -1, 0, 5)
.attr(MovePowerMultiplierAttr, (user, target, move) => Math.max(1, 2 - 0.2 * user.getHeldItems().filter(i => i.isTransferable).reduce((v, m) => v + m.stackCount, 0))), .attr(MovePowerMultiplierAttr, (user, target, move) => Math.max(1, 2 - 0.2 * user.heldItemManager.getTransferableHeldItems().reduce((v, m) => v + user.heldItemManager.getStack(m), 0))),
new StatusMove(MoveId.REFLECT_TYPE, PokemonType.NORMAL, -1, 15, -1, 0, 5) new StatusMove(MoveId.REFLECT_TYPE, PokemonType.NORMAL, -1, 15, -1, 0, 5)
.ignoresSubstitute() .ignoresSubstitute()
.attr(CopyTypeAttr), .attr(CopyTypeAttr),
@ -10433,7 +10421,8 @@ export function initMoves() {
new AttackMove(MoveId.DAZZLING_GLEAM, PokemonType.FAIRY, MoveCategory.SPECIAL, 80, 100, 10, -1, 0, 6) new AttackMove(MoveId.DAZZLING_GLEAM, PokemonType.FAIRY, MoveCategory.SPECIAL, 80, 100, 10, -1, 0, 6)
.target(MoveTarget.ALL_NEAR_ENEMIES), .target(MoveTarget.ALL_NEAR_ENEMIES),
new SelfStatusMove(MoveId.CELEBRATE, PokemonType.NORMAL, -1, 40, -1, 0, 6) new SelfStatusMove(MoveId.CELEBRATE, PokemonType.NORMAL, -1, 40, -1, 0, 6)
.attr(CelebrateAttr), // NB: This needs a lambda function as the user will not be logged in by the time the moves are initialized
.attr(MessageAttr, () => i18next.t("moveTriggers:celebrate", { playerName: loggedInUser?.username })),
new StatusMove(MoveId.HOLD_HANDS, PokemonType.NORMAL, -1, 40, -1, 0, 6) new StatusMove(MoveId.HOLD_HANDS, PokemonType.NORMAL, -1, 40, -1, 0, 6)
.ignoresSubstitute() .ignoresSubstitute()
.target(MoveTarget.NEAR_ALLY), .target(MoveTarget.NEAR_ALLY),
@ -10608,7 +10597,12 @@ export function initMoves() {
.attr(StatStageChangeAttr, [ Stat.SPD ], -1) .attr(StatStageChangeAttr, [ Stat.SPD ], -1)
.reflectable(), .reflectable(),
new SelfStatusMove(MoveId.LASER_FOCUS, PokemonType.NORMAL, -1, 30, -1, 0, 7) new SelfStatusMove(MoveId.LASER_FOCUS, PokemonType.NORMAL, -1, 30, -1, 0, 7)
.attr(AddBattlerTagAttr, BattlerTagType.ALWAYS_CRIT, true, false), .attr(AddBattlerTagAttr, BattlerTagType.ALWAYS_CRIT, true, false)
.attr(MessageAttr, (user) =>
i18next.t("battlerTags:laserFocusOnAdd", {
pokemonNameWithAffix: getPokemonNameWithAffix(user),
}),
),
new StatusMove(MoveId.GEAR_UP, PokemonType.STEEL, -1, 20, -1, 0, 7) new StatusMove(MoveId.GEAR_UP, PokemonType.STEEL, -1, 20, -1, 0, 7)
.attr(StatStageChangeAttr, [ Stat.ATK, Stat.SPATK ], 1, false, { condition: (user, target, move) => !![ AbilityId.PLUS, AbilityId.MINUS ].find(a => target.hasAbility(a, false)) }) .attr(StatStageChangeAttr, [ Stat.ATK, Stat.SPATK ], 1, false, { condition: (user, target, move) => !![ AbilityId.PLUS, AbilityId.MINUS ].find(a => target.hasAbility(a, false)) })
.ignoresSubstitute() .ignoresSubstitute()
@ -10862,7 +10856,7 @@ export function initMoves() {
.attr(EatBerryAttr, true) .attr(EatBerryAttr, true)
.attr(StatStageChangeAttr, [ Stat.DEF ], 2, true) .attr(StatStageChangeAttr, [ Stat.DEF ], 2, true)
.condition((user) => { .condition((user) => {
const userBerries = globalScene.findModifiers(m => m instanceof BerryModifier, user.isPlayer()); const userBerries = user.getHeldItems().filter(m => isItemInCategory(m, HeldItemCategoryId.BERRY));
return userBerries.length > 0; return userBerries.length > 0;
}) })
.edgeCase(), // Stuff Cheeks should not be selectable when the user does not have a berry, see wiki .edgeCase(), // Stuff Cheeks should not be selectable when the user does not have a berry, see wiki

View File

@ -11,7 +11,7 @@ import { BooleanHolder, toDmgValue } from "#utils/common";
* These are the moves assigned to a {@linkcode Pokemon} object. * These are the moves assigned to a {@linkcode Pokemon} object.
* It links to {@linkcode Move} class via the move ID. * It links to {@linkcode Move} class via the move ID.
* Compared to {@linkcode Move}, this class also tracks things like * Compared to {@linkcode Move}, this class also tracks things like
* PP Ups recieved, PP used, etc. * PP Ups received, PP used, etc.
* @see {@linkcode isUsable} - checks if move is restricted, out of PP, or not implemented. * @see {@linkcode isUsable} - checks if move is restricted, out of PP, or not implemented.
* @see {@linkcode getMove} - returns {@linkcode Move} object by looking it up via ID. * @see {@linkcode getMove} - returns {@linkcode Move} object by looking it up via ID.
* @see {@linkcode usePp} - removes a point of PP from the move. * @see {@linkcode usePp} - removes a point of PP from the move.

View File

@ -1,12 +1,12 @@
import { CLASSIC_MODE_MYSTERY_ENCOUNTER_WAVES } from "#app/constants"; import { CLASSIC_MODE_MYSTERY_ENCOUNTER_WAVES } from "#app/constants";
import { globalScene } from "#app/global-scene"; import { globalScene } from "#app/global-scene";
import { modifierTypes } from "#data/data-lists";
import type { IEggOptions } from "#data/egg"; import type { IEggOptions } from "#data/egg";
import { EggSourceType } from "#enums/egg-source-types"; import { EggSourceType } from "#enums/egg-source-types";
import { EggTier } from "#enums/egg-type"; import { EggTier } from "#enums/egg-type";
import { ModifierTier } from "#enums/modifier-tier";
import { MysteryEncounterTier } from "#enums/mystery-encounter-tier"; import { MysteryEncounterTier } from "#enums/mystery-encounter-tier";
import { MysteryEncounterType } from "#enums/mystery-encounter-type"; import { MysteryEncounterType } from "#enums/mystery-encounter-type";
import { RewardId } from "#enums/reward-id";
import { RarityTier } from "#enums/reward-tier";
import { SpeciesId } from "#enums/species-id"; import { SpeciesId } from "#enums/species-id";
import { TrainerType } from "#enums/trainer-type"; import { TrainerType } from "#enums/trainer-type";
import type { EnemyPartyConfig } from "#mystery-encounters/encounter-phase-utils"; import type { EnemyPartyConfig } from "#mystery-encounters/encounter-phase-utils";
@ -164,8 +164,8 @@ export const ATrainersTestEncounter: MysteryEncounter = MysteryEncounterBuilder.
encounter.setDialogueToken("eggType", i18next.t(`${namespace}:eggTypes.epic`)); encounter.setDialogueToken("eggType", i18next.t(`${namespace}:eggTypes.epic`));
setEncounterRewards( setEncounterRewards(
{ {
guaranteedModifierTypeFuncs: [modifierTypes.RELIC_GOLD], guaranteedRewardSpecs: [RewardId.RELIC_GOLD],
guaranteedModifierTiers: [ModifierTier.ROGUE, ModifierTier.ROGUE], guaranteedRarityTiers: [RarityTier.ROGUE, RarityTier.ROGUE],
fillRemaining: true, fillRemaining: true,
}, },
[eggOptions], [eggOptions],

View File

@ -1,8 +1,8 @@
import { globalScene } from "#app/global-scene"; import { globalScene } from "#app/global-scene";
import { modifierTypes } from "#data/data-lists"; import { allHeldItems } from "#data/data-lists";
import { BattlerIndex } from "#enums/battler-index"; import { BattlerIndex } from "#enums/battler-index";
import { BattlerTagType } from "#enums/battler-tag-type"; import { BattlerTagType } from "#enums/battler-tag-type";
import type { BerryType } from "#enums/berry-type"; import { HeldItemCategoryId, HeldItemId } from "#enums/held-item-id";
import { MoveId } from "#enums/move-id"; import { MoveId } from "#enums/move-id";
import { MoveUseMode } from "#enums/move-use-mode"; import { MoveUseMode } from "#enums/move-use-mode";
import { MysteryEncounterOptionMode } from "#enums/mystery-encounter-option-mode"; import { MysteryEncounterOptionMode } from "#enums/mystery-encounter-option-mode";
@ -12,37 +12,44 @@ import { PokeballType } from "#enums/pokeball";
import { SpeciesId } from "#enums/species-id"; import { SpeciesId } from "#enums/species-id";
import { Stat } from "#enums/stat"; import { Stat } from "#enums/stat";
import { TrainerSlot } from "#enums/trainer-slot"; import { TrainerSlot } from "#enums/trainer-slot";
import type { MysteryEncounterSpriteConfig } from "#field/mystery-encounter-intro";
import type { Pokemon } from "#field/pokemon"; import type { Pokemon } from "#field/pokemon";
import { EnemyPokemon } from "#field/pokemon"; import { EnemyPokemon } from "#field/pokemon";
import { BerryModifier, PokemonInstantReviveModifier } from "#modifiers/modifier"; import type { HeldItemConfiguration, HeldItemSpecs, PokemonItemMap } from "#items/held-item-data-types";
import type { BerryModifierType, PokemonHeldItemModifierType } from "#modifiers/modifier-type"; import { getPartyBerries } from "#items/item-utility";
import { PokemonMove } from "#moves/pokemon-move"; import { PokemonMove } from "#moves/pokemon-move";
import { queueEncounterMessage } from "#mystery-encounters/encounter-dialogue-utils"; import { queueEncounterMessage } from "#mystery-encounters/encounter-dialogue-utils";
import type { EnemyPartyConfig } from "#mystery-encounters/encounter-phase-utils"; import type { EnemyPartyConfig } from "#mystery-encounters/encounter-phase-utils";
import { import {
generateModifierType,
initBattleWithEnemyConfig, initBattleWithEnemyConfig,
leaveEncounterWithoutBattle, leaveEncounterWithoutBattle,
setEncounterRewards, setEncounterRewards,
transitionMysteryEncounterIntroVisuals, transitionMysteryEncounterIntroVisuals,
} from "#mystery-encounters/encounter-phase-utils"; } from "#mystery-encounters/encounter-phase-utils";
import { import { catchPokemon, getHighestLevelPlayerPokemon } from "#mystery-encounters/encounter-pokemon-utils";
applyModifierTypeToPlayerPokemon,
catchPokemon,
getHighestLevelPlayerPokemon,
} from "#mystery-encounters/encounter-pokemon-utils";
import type { MysteryEncounter } from "#mystery-encounters/mystery-encounter"; import type { MysteryEncounter } from "#mystery-encounters/mystery-encounter";
import { MysteryEncounterBuilder } from "#mystery-encounters/mystery-encounter"; import { MysteryEncounterBuilder } from "#mystery-encounters/mystery-encounter";
import { MysteryEncounterOptionBuilder } from "#mystery-encounters/mystery-encounter-option"; import { MysteryEncounterOptionBuilder } from "#mystery-encounters/mystery-encounter-option";
import { PersistentModifierRequirement } from "#mystery-encounters/mystery-encounter-requirements"; import { HeldItemRequirement } from "#mystery-encounters/mystery-encounter-requirements";
import type { HeldModifierConfig } from "#types/held-modifier-config"; import { pickWeightedIndex, randInt } from "#utils/common";
import { randInt } from "#utils/common";
import { getPokemonSpecies } from "#utils/pokemon-utils"; import { getPokemonSpecies } from "#utils/pokemon-utils";
import i18next from "i18next"; import i18next from "i18next";
/** the i18n namespace for this encounter */ /** the i18n namespace for this encounter */
const namespace = "mysteryEncounters/absoluteAvarice"; const namespace = "mysteryEncounters/absoluteAvarice";
function berrySprite(spriteKey: string, x: number, y: number): MysteryEncounterSpriteConfig {
return {
spriteKey: spriteKey,
fileRoot: "items",
isItem: true,
x: x,
y: y,
hidden: true,
disableAnimation: true,
};
}
/** /**
* Absolute Avarice encounter. * Absolute Avarice encounter.
* @see {@link https://github.com/pagefaultgames/pokerogue/issues/3805 | GitHub Issue #3805} * @see {@link https://github.com/pagefaultgames/pokerogue/issues/3805 | GitHub Issue #3805}
@ -53,7 +60,7 @@ export const AbsoluteAvariceEncounter: MysteryEncounter = MysteryEncounterBuilde
) )
.withEncounterTier(MysteryEncounterTier.GREAT) .withEncounterTier(MysteryEncounterTier.GREAT)
.withSceneWaveRangeRequirement(20, 180) .withSceneWaveRangeRequirement(20, 180)
.withSceneRequirement(new PersistentModifierRequirement("BerryModifier", 6)) // Must have at least 6 berries to spawn .withSceneRequirement(new HeldItemRequirement(HeldItemCategoryId.BERRY, 6)) // Must have at least 6 berries to spawn
.withFleeAllowed(false) .withFleeAllowed(false)
.withIntroSpriteConfigs([ .withIntroSpriteConfigs([
{ {
@ -74,105 +81,17 @@ export const AbsoluteAvariceEncounter: MysteryEncounter = MysteryEncounterBuilde
repeat: true, repeat: true,
x: -5, x: -5,
}, },
{ berrySprite("lum_berry", 7, -14),
spriteKey: "lum_berry", berrySprite("salac_berry", 2, 4),
fileRoot: "items", berrySprite("lansat_berry", 32, 5),
isItem: true, berrySprite("liechi_berry", 6, -5),
x: 7, berrySprite("sitrus_berry", 7, 8),
y: -14, berrySprite("enigma_berry", 26, -4),
hidden: true, berrySprite("leppa_berry", 16, -27),
disableAnimation: true, berrySprite("petaya_berry", 30, -17),
}, berrySprite("ganlon_berry", 16, -11),
{ berrySprite("apicot_berry", 14, -2),
spriteKey: "salac_berry", berrySprite("starf_berry", 18, 9),
fileRoot: "items",
isItem: true,
x: 2,
y: 4,
hidden: true,
disableAnimation: true,
},
{
spriteKey: "lansat_berry",
fileRoot: "items",
isItem: true,
x: 32,
y: 5,
hidden: true,
disableAnimation: true,
},
{
spriteKey: "liechi_berry",
fileRoot: "items",
isItem: true,
x: 6,
y: -5,
hidden: true,
disableAnimation: true,
},
{
spriteKey: "sitrus_berry",
fileRoot: "items",
isItem: true,
x: 7,
y: 8,
hidden: true,
disableAnimation: true,
},
{
spriteKey: "enigma_berry",
fileRoot: "items",
isItem: true,
x: 26,
y: -4,
hidden: true,
disableAnimation: true,
},
{
spriteKey: "leppa_berry",
fileRoot: "items",
isItem: true,
x: 16,
y: -27,
hidden: true,
disableAnimation: true,
},
{
spriteKey: "petaya_berry",
fileRoot: "items",
isItem: true,
x: 30,
y: -17,
hidden: true,
disableAnimation: true,
},
{
spriteKey: "ganlon_berry",
fileRoot: "items",
isItem: true,
x: 16,
y: -11,
hidden: true,
disableAnimation: true,
},
{
spriteKey: "apicot_berry",
fileRoot: "items",
isItem: true,
x: 14,
y: -2,
hidden: true,
disableAnimation: true,
},
{
spriteKey: "starf_berry",
fileRoot: "items",
isItem: true,
x: 18,
y: 9,
hidden: true,
disableAnimation: true,
},
]) ])
.withHideWildIntroMessage(true) .withHideWildIntroMessage(true)
.withAutoHideIntroVisuals(false) .withAutoHideIntroVisuals(false)
@ -191,35 +110,17 @@ export const AbsoluteAvariceEncounter: MysteryEncounter = MysteryEncounterBuilde
globalScene.loadSe("PRSFX- Bug Bite", "battle_anims", "PRSFX- Bug Bite.wav"); globalScene.loadSe("PRSFX- Bug Bite", "battle_anims", "PRSFX- Bug Bite.wav");
globalScene.loadSe("Follow Me", "battle_anims", "Follow Me.mp3"); globalScene.loadSe("Follow Me", "battle_anims", "Follow Me.mp3");
// Get all player berry items, remove from party, and store reference // Get all berries in party, with references to the pokemon
const berryItems = globalScene.findModifiers(m => m instanceof BerryModifier) as BerryModifier[]; const berryItems = getPartyBerries();
// Sort berries by party member ID to more easily re-add later if necessary encounter.misc = { berryItemsMap: berryItems };
const berryItemsMap = new Map<number, BerryModifier[]>();
globalScene.getPlayerParty().forEach(pokemon => { // Adds stolen berries to the Greedent item configuration
const pokemonBerries = berryItems.filter(b => b.pokemonId === pokemon.id); const bossHeldItemConfig: HeldItemConfiguration = [];
if (pokemonBerries?.length > 0) { berryItems.forEach(map => {
berryItemsMap.set(pokemon.id, pokemonBerries); bossHeldItemConfig.push({ entry: map.item, count: 1 });
}
}); });
encounter.misc = { berryItemsMap };
// Generates copies of the stolen berries to put on the Greedent
const bossModifierConfigs: HeldModifierConfig[] = [];
berryItems.forEach(berryMod => {
// Can't define stack count on a ModifierType, have to just create separate instances for each stack
// Overflow berries will be "lost" on the boss, but it's un-catchable anyway
for (let i = 0; i < berryMod.stackCount; i++) {
const modifierType = generateModifierType(modifierTypes.BERRY, [
berryMod.berryType,
]) as PokemonHeldItemModifierType;
bossModifierConfigs.push({ modifier: modifierType });
}
});
// Do NOT remove the real berries yet or else it will be persisted in the session data
// +1 SpDef below wave 50, SpDef and Speed otherwise // +1 SpDef below wave 50, SpDef and Speed otherwise
const statChangesForBattle: (Stat.ATK | Stat.DEF | Stat.SPATK | Stat.SPDEF | Stat.SPD | Stat.ACC | Stat.EVA)[] = const statChangesForBattle: (Stat.ATK | Stat.DEF | Stat.SPATK | Stat.SPDEF | Stat.SPD | Stat.ACC | Stat.EVA)[] =
globalScene.currentBattle.waveIndex < 50 ? [Stat.SPDEF] : [Stat.SPDEF, Stat.SPD]; globalScene.currentBattle.waveIndex < 50 ? [Stat.SPDEF] : [Stat.SPDEF, Stat.SPD];
@ -234,7 +135,7 @@ export const AbsoluteAvariceEncounter: MysteryEncounter = MysteryEncounterBuilde
bossSegments: 3, bossSegments: 3,
shiny: false, // Shiny lock because of consistency issues between the different options shiny: false, // Shiny lock because of consistency issues between the different options
moveSet: [MoveId.THRASH, MoveId.CRUNCH, MoveId.BODY_PRESS, MoveId.SLACK_OFF], moveSet: [MoveId.THRASH, MoveId.CRUNCH, MoveId.BODY_PRESS, MoveId.SLACK_OFF],
modifierConfigs: bossModifierConfigs, heldItemConfig: bossHeldItemConfig,
tags: [BattlerTagType.MYSTERY_ENCOUNTER_POST_SUMMON], tags: [BattlerTagType.MYSTERY_ENCOUNTER_POST_SUMMON],
mysteryEncounterBattleEffects: (pokemon: Pokemon) => { mysteryEncounterBattleEffects: (pokemon: Pokemon) => {
queueEncounterMessage(`${namespace}:option.1.boss_enraged`); queueEncounterMessage(`${namespace}:option.1.boss_enraged`);
@ -261,12 +162,12 @@ export const AbsoluteAvariceEncounter: MysteryEncounter = MysteryEncounterBuilde
// Remove the berries from the party // Remove the berries from the party
// Session has been safely saved at this point, so data won't be lost // Session has been safely saved at this point, so data won't be lost
const berryItems = globalScene.findModifiers(m => m instanceof BerryModifier) as BerryModifier[]; const berryItems = getPartyBerries();
berryItems.forEach(berryMod => { berryItems.forEach(map => {
globalScene.removeModifier(berryMod); globalScene.getPokemonById(map.pokemonId)?.heldItemManager.remove(map.item.id as HeldItemId);
}); });
globalScene.updateModifiers(true); globalScene.updateItems(true);
return true; return true;
}) })
@ -286,19 +187,14 @@ export const AbsoluteAvariceEncounter: MysteryEncounter = MysteryEncounterBuilde
const encounter = globalScene.currentBattle.mysteryEncounter!; const encounter = globalScene.currentBattle.mysteryEncounter!;
// Provides 1x Reviver Seed to each party member at end of battle // Provides 1x Reviver Seed to each party member at end of battle
const revSeed = generateModifierType(modifierTypes.REVIVER_SEED);
encounter.setDialogueToken( encounter.setDialogueToken(
"foodReward", "foodReward",
revSeed?.name ?? i18next.t("modifierType:ModifierType.REVIVER_SEED.name"), allHeldItems[HeldItemId.REVIVER_SEED].name ?? i18next.t("modifierType:ModifierType.REVIVER_SEED.name"),
); );
const givePartyPokemonReviverSeeds = () => { const givePartyPokemonReviverSeeds = () => {
const party = globalScene.getPlayerParty(); const party = globalScene.getPlayerParty();
party.forEach(p => { party.forEach(p => {
const heldItems = p.getHeldItems(); p.heldItemManager.add(HeldItemId.REVIVER_SEED);
if (revSeed && !heldItems.some(item => item instanceof PokemonInstantReviveModifier)) {
const seedModifier = revSeed.newModifier(p);
globalScene.addModifier(seedModifier, false, false, false, true);
}
}); });
queueEncounterMessage(`${namespace}:option.1.food_stash`); queueEncounterMessage(`${namespace}:option.1.food_stash`);
}; };
@ -329,28 +225,27 @@ export const AbsoluteAvariceEncounter: MysteryEncounter = MysteryEncounterBuilde
}) })
.withOptionPhase(async () => { .withOptionPhase(async () => {
const encounter = globalScene.currentBattle.mysteryEncounter!; const encounter = globalScene.currentBattle.mysteryEncounter!;
const berryMap = encounter.misc.berryItemsMap; const berryMap = encounter.misc.berryItemsMap as PokemonItemMap[];
// Returns 2/5 of the berries stolen to each Pokemon // Returns 2/5 of the berries stolen to each Pokemon
const party = globalScene.getPlayerParty(); const party = globalScene.getPlayerParty();
party.forEach(pokemon => { party.forEach(pokemon => {
const stolenBerries: BerryModifier[] = berryMap.get(pokemon.id); const stolenBerries = berryMap.filter(map => map.pokemonId === pokemon.id);
const berryTypesAsArray: BerryType[] = []; const stolenBerryCount = stolenBerries.reduce((a, b) => a + (b.item as HeldItemSpecs).stack, 0);
stolenBerries?.forEach(bMod => berryTypesAsArray.push(...new Array(bMod.stackCount).fill(bMod.berryType))); const returnedBerryCount = Math.floor(((stolenBerryCount ?? 0) * 2) / 5);
const returnedBerryCount = Math.floor(((berryTypesAsArray.length ?? 0) * 2) / 5);
if (returnedBerryCount > 0) { if (returnedBerryCount > 0) {
for (let i = 0; i < returnedBerryCount; i++) { for (let i = 0; i < returnedBerryCount; i++) {
// Shuffle remaining berry types and pop // Shuffle remaining berry types and pop
Phaser.Math.RND.shuffle(berryTypesAsArray); const berryWeights = stolenBerries.map(b => (b.item as HeldItemSpecs).stack);
const randBerryType = berryTypesAsArray.pop(); const which = pickWeightedIndex(berryWeights) ?? 0;
const randBerry = stolenBerries[which];
const berryModType = generateModifierType(modifierTypes.BERRY, [randBerryType]) as BerryModifierType; pokemon.heldItemManager.add(randBerry.item.id as HeldItemId);
applyModifierTypeToPlayerPokemon(pokemon, berryModType); (randBerry.item as HeldItemSpecs).stack -= 1;
} }
} }
}); });
await globalScene.updateModifiers(true); await globalScene.updateItems(true);
await transitionMysteryEncounterIntroVisuals(true, true, 500); await transitionMysteryEncounterIntroVisuals(true, true, 500);
leaveEncounterWithoutBattle(true); leaveEncounterWithoutBattle(true);

View File

@ -1,13 +1,13 @@
import { CLASSIC_MODE_MYSTERY_ENCOUNTER_WAVES } from "#app/constants"; import { CLASSIC_MODE_MYSTERY_ENCOUNTER_WAVES } 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 { modifierTypes } from "#data/data-lists"; import { allTrainerItems } from "#data/data-lists";
import { MysteryEncounterOptionMode } from "#enums/mystery-encounter-option-mode"; import { MysteryEncounterOptionMode } from "#enums/mystery-encounter-option-mode";
import { MysteryEncounterTier } from "#enums/mystery-encounter-tier"; import { MysteryEncounterTier } from "#enums/mystery-encounter-tier";
import { MysteryEncounterType } from "#enums/mystery-encounter-type"; import { MysteryEncounterType } from "#enums/mystery-encounter-type";
import { SpeciesId } from "#enums/species-id"; import { SpeciesId } from "#enums/species-id";
import { TrainerItemId } from "#enums/trainer-item-id";
import { import {
generateModifierType,
leaveEncounterWithoutBattle, leaveEncounterWithoutBattle,
setEncounterExp, setEncounterExp,
updatePlayerMoney, updatePlayerMoney,
@ -109,8 +109,8 @@ export const AnOfferYouCantRefuseEncounter: MysteryEncounter = MysteryEncounterB
} }
} }
const shinyCharm = generateModifierType(modifierTypes.SHINY_CHARM); const name = allTrainerItems[TrainerItemId.SHINY_CHARM].name;
encounter.setDialogueToken("itemName", shinyCharm?.name ?? i18next.t("modifierType:ModifierType.SHINY_CHARM.name")); encounter.setDialogueToken("itemName", name ?? i18next.t("modifierType:ModifierType.SHINY_CHARM.name"));
encounter.setDialogueToken("liepardName", getPokemonSpecies(SpeciesId.LIEPARD).getName()); encounter.setDialogueToken("liepardName", getPokemonSpecies(SpeciesId.LIEPARD).getName());
return true; return true;
@ -136,7 +136,7 @@ export const AnOfferYouCantRefuseEncounter: MysteryEncounter = MysteryEncounterB
}) })
.withOptionPhase(async () => { .withOptionPhase(async () => {
// Give the player a Shiny Charm // Give the player a Shiny Charm
globalScene.phaseManager.unshiftNew("ModifierRewardPhase", modifierTypes.SHINY_CHARM); globalScene.phaseManager.unshiftNew("RewardPhase", TrainerItemId.SHINY_CHARM);
leaveEncounterWithoutBattle(true); leaveEncounterWithoutBattle(true);
}) })
.build(), .build(),

View File

@ -1,23 +1,22 @@
import { CLASSIC_MODE_MYSTERY_ENCOUNTER_WAVES } from "#app/constants"; import { CLASSIC_MODE_MYSTERY_ENCOUNTER_WAVES } from "#app/constants";
import { globalScene } from "#app/global-scene"; import { globalScene } from "#app/global-scene";
import { getPokemonNameWithAffix } from "#app/messages"; import { getPokemonNameWithAffix } from "#app/messages";
import { modifierTypes } from "#data/data-lists";
import { BattlerTagType } from "#enums/battler-tag-type"; import { BattlerTagType } from "#enums/battler-tag-type";
import { BerryType } from "#enums/berry-type"; import { BerryType } from "#enums/berry-type";
import { ModifierPoolType } from "#enums/modifier-pool-type";
import { MysteryEncounterOptionMode } from "#enums/mystery-encounter-option-mode"; import { MysteryEncounterOptionMode } from "#enums/mystery-encounter-option-mode";
import { MysteryEncounterTier } from "#enums/mystery-encounter-tier"; import { MysteryEncounterTier } from "#enums/mystery-encounter-tier";
import { MysteryEncounterType } from "#enums/mystery-encounter-type"; import { MysteryEncounterType } from "#enums/mystery-encounter-type";
import { RewardId } from "#enums/reward-id";
import { RewardPoolType } from "#enums/reward-pool-type";
import { PERMANENT_STATS, Stat } from "#enums/stat"; import { PERMANENT_STATS, Stat } from "#enums/stat";
import type { PlayerPokemon, Pokemon } from "#field/pokemon"; import type { PlayerPokemon, Pokemon } from "#field/pokemon";
import { BerryModifier } from "#modifiers/modifier"; import { berryTypeToHeldItem } from "#items/berry";
import type { BerryModifierType, ModifierTypeOption } from "#modifiers/modifier-type"; import type { RewardOption } from "#items/reward";
import { regenerateModifierPoolThresholds } from "#modifiers/modifier-type"; import { generateRewardPoolWeights, getRewardPoolForType } from "#items/reward-pool-utils";
import { generateRewardOptionFromId } from "#items/reward-utils";
import { queueEncounterMessage, showEncounterText } from "#mystery-encounters/encounter-dialogue-utils"; import { queueEncounterMessage, showEncounterText } from "#mystery-encounters/encounter-dialogue-utils";
import type { EnemyPartyConfig } from "#mystery-encounters/encounter-phase-utils"; import type { EnemyPartyConfig } from "#mystery-encounters/encounter-phase-utils";
import { import {
generateModifierType,
generateModifierTypeOption,
getRandomEncounterSpecies, getRandomEncounterSpecies,
initBattleWithEnemyConfig, initBattleWithEnemyConfig,
leaveEncounterWithoutBattle, leaveEncounterWithoutBattle,
@ -25,7 +24,6 @@ import {
setEncounterRewards, setEncounterRewards,
} from "#mystery-encounters/encounter-phase-utils"; } from "#mystery-encounters/encounter-phase-utils";
import { import {
applyModifierTypeToPlayerPokemon,
getEncounterPokemonLevelForWave, getEncounterPokemonLevelForWave,
getHighestStatPlayerPokemon, getHighestStatPlayerPokemon,
getSpriteKeysFromPokemon, getSpriteKeysFromPokemon,
@ -90,7 +88,7 @@ export const BerriesAboundEncounter: MysteryEncounter = MysteryEncounterBuilder.
: globalScene.currentBattle.waveIndex > 40 : globalScene.currentBattle.waveIndex > 40
? 4 ? 4
: 2; : 2;
regenerateModifierPoolThresholds(globalScene.getPlayerParty(), ModifierPoolType.PLAYER, 0); generateRewardPoolWeights(getRewardPoolForType(RewardPoolType.PLAYER), globalScene.getPlayerParty(), 0);
encounter.misc = { numBerries }; encounter.misc = { numBerries };
const { spriteKey, fileRoot } = getSpriteKeysFromPokemon(bossPokemon); const { spriteKey, fileRoot } = getSpriteKeysFromPokemon(bossPokemon);
@ -161,20 +159,16 @@ export const BerriesAboundEncounter: MysteryEncounter = MysteryEncounterBuilder.
} }
}; };
const shopOptions: ModifierTypeOption[] = []; const shopOptions: RewardOption[] = [];
for (let i = 0; i < 5; i++) { for (let i = 0; i < 5; i++) {
// Generate shop berries // Generate shop berries
const mod = generateModifierTypeOption(modifierTypes.BERRY); const mod = generateRewardOptionFromId(RewardId.BERRY);
if (mod) { if (mod) {
shopOptions.push(mod); shopOptions.push(mod);
} }
} }
setEncounterRewards( setEncounterRewards({ guaranteedRewardOptions: shopOptions, fillRemaining: false }, undefined, doBerryRewards);
{ guaranteedModifierTypeOptions: shopOptions, fillRemaining: false },
undefined,
doBerryRewards,
);
await initBattleWithEnemyConfig(globalScene.currentBattle.mysteryEncounter!.enemyPartyConfigs[0]); await initBattleWithEnemyConfig(globalScene.currentBattle.mysteryEncounter!.enemyPartyConfigs[0]);
}, },
) )
@ -192,10 +186,10 @@ export const BerriesAboundEncounter: MysteryEncounter = MysteryEncounterBuilder.
const speedDiff = fastestPokemon.getStat(Stat.SPD) / (enemySpeed * 1.1); const speedDiff = fastestPokemon.getStat(Stat.SPD) / (enemySpeed * 1.1);
const numBerries: number = encounter.misc.numBerries; const numBerries: number = encounter.misc.numBerries;
const shopOptions: ModifierTypeOption[] = []; const shopOptions: RewardOption[] = [];
for (let i = 0; i < 5; i++) { for (let i = 0; i < 5; i++) {
// Generate shop berries // Generate shop berries
const mod = generateModifierTypeOption(modifierTypes.BERRY); const mod = generateRewardOptionFromId(RewardId.BERRY);
if (mod) { if (mod) {
shopOptions.push(mod); shopOptions.push(mod);
} }
@ -248,7 +242,7 @@ export const BerriesAboundEncounter: MysteryEncounter = MysteryEncounterBuilder.
}; };
setEncounterRewards( setEncounterRewards(
{ {
guaranteedModifierTypeOptions: shopOptions, guaranteedRewardOptions: shopOptions,
fillRemaining: false, fillRemaining: false,
}, },
undefined, undefined,
@ -281,7 +275,7 @@ export const BerriesAboundEncounter: MysteryEncounter = MysteryEncounterBuilder.
setEncounterExp(fastestPokemon.id, encounter.enemyPartyConfigs[0].pokemonConfigs![0].species.baseExp); setEncounterExp(fastestPokemon.id, encounter.enemyPartyConfigs[0].pokemonConfigs![0].species.baseExp);
setEncounterRewards( setEncounterRewards(
{ {
guaranteedModifierTypeOptions: shopOptions, guaranteedRewardOptions: shopOptions,
fillRemaining: false, fillRemaining: false,
}, },
undefined, undefined,
@ -312,35 +306,17 @@ export const BerriesAboundEncounter: MysteryEncounter = MysteryEncounterBuilder.
function tryGiveBerry(prioritizedPokemon?: PlayerPokemon) { function tryGiveBerry(prioritizedPokemon?: PlayerPokemon) {
const berryType = randSeedItem(getEnumValues(BerryType)); const berryType = randSeedItem(getEnumValues(BerryType));
const berry = generateModifierType(modifierTypes.BERRY, [berryType]) as BerryModifierType; const berry = berryTypeToHeldItem[berryType];
const party = globalScene.getPlayerParty(); const party = globalScene.getPlayerParty();
// Will try to apply to prioritized pokemon first, then do normal application method if it fails // Will give the berry to a Pokemon, starting from the prioritized one
if (prioritizedPokemon) { if (prioritizedPokemon?.heldItemManager.add(berry)) {
const heldBerriesOfType = globalScene.findModifier(
m =>
m instanceof BerryModifier &&
m.pokemonId === prioritizedPokemon.id &&
(m as BerryModifier).berryType === berryType,
true,
) as BerryModifier;
if (!heldBerriesOfType || heldBerriesOfType.getStackCount() < heldBerriesOfType.getMaxStackCount()) {
applyModifierTypeToPlayerPokemon(prioritizedPokemon, berry);
return; return;
} }
}
// Iterate over the party until berry was successfully given
for (const pokemon of party) { for (const pokemon of party) {
const heldBerriesOfType = globalScene.findModifier( if (pokemon.heldItemManager.add(berry)) {
m => m instanceof BerryModifier && m.pokemonId === pokemon.id && (m as BerryModifier).berryType === berryType,
true,
) as BerryModifier;
if (!heldBerriesOfType || heldBerriesOfType.getStackCount() < heldBerriesOfType.getMaxStackCount()) {
applyModifierTypeToPlayerPokemon(pokemon, berry);
return; return;
} }
} }

View File

@ -1,32 +1,26 @@
import { CLASSIC_MODE_MYSTERY_ENCOUNTER_WAVES } from "#app/constants"; import { CLASSIC_MODE_MYSTERY_ENCOUNTER_WAVES } from "#app/constants";
import { globalScene } from "#app/global-scene"; import { globalScene } from "#app/global-scene";
import { allMoves, modifierTypes } from "#data/data-lists"; import { allHeldItems, allMoves } from "#data/data-lists";
import { ModifierTier } from "#enums/modifier-tier"; import { HeldItemId } from "#enums/held-item-id";
import { MoveId } from "#enums/move-id"; import { MoveId } from "#enums/move-id";
import { MysteryEncounterOptionMode } from "#enums/mystery-encounter-option-mode"; import { MysteryEncounterOptionMode } from "#enums/mystery-encounter-option-mode";
import { MysteryEncounterTier } from "#enums/mystery-encounter-tier"; import { MysteryEncounterTier } from "#enums/mystery-encounter-tier";
import { MysteryEncounterType } from "#enums/mystery-encounter-type"; import { MysteryEncounterType } from "#enums/mystery-encounter-type";
import { PartyMemberStrength } from "#enums/party-member-strength"; import { PartyMemberStrength } from "#enums/party-member-strength";
import { PokemonType } from "#enums/pokemon-type"; import { PokemonType } from "#enums/pokemon-type";
import { RewardId } from "#enums/reward-id";
import { RarityTier } from "#enums/reward-tier";
import { SpeciesId } from "#enums/species-id"; import { SpeciesId } from "#enums/species-id";
import { TrainerItemId } from "#enums/trainer-item-id";
import { TrainerSlot } from "#enums/trainer-slot"; import { TrainerSlot } from "#enums/trainer-slot";
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 { RewardOption } from "#items/reward";
import { import { generateRewardOptionFromId } from "#items/reward-utils";
AttackTypeBoosterModifier,
BypassSpeedChanceModifier,
ContactHeldItemTransferChanceModifier,
GigantamaxAccessModifier,
MegaEvolutionAccessModifier,
} from "#modifiers/modifier";
import type { AttackTypeBoosterModifierType, ModifierTypeOption } from "#modifiers/modifier-type";
import { PokemonMove } from "#moves/pokemon-move"; import { PokemonMove } from "#moves/pokemon-move";
import { getEncounterText, showEncounterDialogue } from "#mystery-encounters/encounter-dialogue-utils"; import { getEncounterText, showEncounterDialogue } from "#mystery-encounters/encounter-dialogue-utils";
import type { EnemyPartyConfig } from "#mystery-encounters/encounter-phase-utils"; import type { EnemyPartyConfig } from "#mystery-encounters/encounter-phase-utils";
import { import {
generateModifierType,
generateModifierTypeOption,
initBattleWithEnemyConfig, initBattleWithEnemyConfig,
leaveEncounterWithoutBattle, leaveEncounterWithoutBattle,
selectOptionThenPokemon, selectOptionThenPokemon,
@ -39,9 +33,8 @@ import type { MysteryEncounter } from "#mystery-encounters/mystery-encounter";
import { MysteryEncounterBuilder } from "#mystery-encounters/mystery-encounter"; import { MysteryEncounterBuilder } from "#mystery-encounters/mystery-encounter";
import { MysteryEncounterOptionBuilder } from "#mystery-encounters/mystery-encounter-option"; import { MysteryEncounterOptionBuilder } from "#mystery-encounters/mystery-encounter-option";
import { import {
AttackTypeBoosterHeldItemTypeRequirement,
CombinationPokemonRequirement, CombinationPokemonRequirement,
HeldItemRequirement, HoldingItemRequirement,
TypeRequirement, TypeRequirement,
} from "#mystery-encounters/mystery-encounter-requirements"; } from "#mystery-encounters/mystery-encounter-requirements";
import { getRandomPartyMemberFunc, trainerConfigs } from "#trainers/trainer-config"; import { getRandomPartyMemberFunc, trainerConfigs } from "#trainers/trainer-config";
@ -140,6 +133,8 @@ const POOL_3_POKEMON: { species: SpeciesId; formIndex?: number }[] = [
const POOL_4_POKEMON = [SpeciesId.GENESECT, SpeciesId.SLITHER_WING, SpeciesId.BUZZWOLE, SpeciesId.PHEROMOSA]; const POOL_4_POKEMON = [SpeciesId.GENESECT, SpeciesId.SLITHER_WING, SpeciesId.BUZZWOLE, SpeciesId.PHEROMOSA];
const REQUIRED_ITEMS = [HeldItemId.QUICK_CLAW, HeldItemId.GRIP_CLAW, HeldItemId.SILVER_POWDER];
const PHYSICAL_TUTOR_MOVES = [ const PHYSICAL_TUTOR_MOVES = [
MoveId.MEGAHORN, MoveId.MEGAHORN,
MoveId.ATTACK_ORDER, MoveId.ATTACK_ORDER,
@ -183,8 +178,7 @@ export const BugTypeSuperfanEncounter: MysteryEncounter = MysteryEncounterBuilde
.withPrimaryPokemonRequirement( .withPrimaryPokemonRequirement(
CombinationPokemonRequirement.Some( CombinationPokemonRequirement.Some(
// Must have at least 1 Bug type on team, OR have a bug item somewhere on the team // Must have at least 1 Bug type on team, OR have a bug item somewhere on the team
new HeldItemRequirement(["BypassSpeedChanceModifier", "ContactHeldItemTransferChanceModifier"], 1), new HoldingItemRequirement(REQUIRED_ITEMS, 1),
new AttackTypeBoosterHeldItemTypeRequirement(PokemonType.BUG, 1),
new TypeRequirement(PokemonType.BUG, false, 1), new TypeRequirement(PokemonType.BUG, false, 1),
), ),
) )
@ -256,13 +250,7 @@ export const BugTypeSuperfanEncounter: MysteryEncounter = MysteryEncounterBuilde
}, },
]; ];
const requiredItems = [ const requiredItemString = REQUIRED_ITEMS.map(m => allHeldItems[m].name ?? "unknown").join("/");
generateModifierType(modifierTypes.QUICK_CLAW),
generateModifierType(modifierTypes.GRIP_CLAW),
generateModifierType(modifierTypes.ATTACK_TYPE_BOOSTER, [PokemonType.BUG]),
];
const requiredItemString = requiredItems.map(m => m?.name ?? "unknown").join("/");
encounter.setDialogueToken("requiredBugItems", requiredItemString); encounter.setDialogueToken("requiredBugItems", requiredItemString);
return true; return true;
@ -298,7 +286,7 @@ export const BugTypeSuperfanEncounter: MysteryEncounter = MysteryEncounterBuilde
moveTutorOptions, moveTutorOptions,
}; };
// Assigns callback that teaches move before continuing to rewards // Assigns callback that teaches move before continuing to RewardId
encounter.onRewards = doBugTypeMoveTutor; encounter.onRewards = doBugTypeMoveTutor;
setEncounterRewards({ fillRemaining: true }); setEncounterRewards({ fillRemaining: true });
@ -318,7 +306,7 @@ export const BugTypeSuperfanEncounter: MysteryEncounter = MysteryEncounterBuilde
// Player shows off their bug types // Player shows off their bug types
const encounter = globalScene.currentBattle.mysteryEncounter!; const encounter = globalScene.currentBattle.mysteryEncounter!;
// Player gets different rewards depending on the number of bug types they have // Player gets different RewardId depending on the number of bug types they have
const numBugTypes = globalScene.getPlayerParty().filter(p => p.isOfType(PokemonType.BUG, true)).length; const numBugTypes = globalScene.getPlayerParty().filter(p => p.isOfType(PokemonType.BUG, true)).length;
const numBugTypesText = i18next.t(`${namespace}:numBugTypes`, { const numBugTypesText = i18next.t(`${namespace}:numBugTypes`, {
count: numBugTypes, count: numBugTypes,
@ -327,7 +315,7 @@ export const BugTypeSuperfanEncounter: MysteryEncounter = MysteryEncounterBuilde
if (numBugTypes < 2) { if (numBugTypes < 2) {
setEncounterRewards({ setEncounterRewards({
guaranteedModifierTypeFuncs: [modifierTypes.SUPER_LURE, modifierTypes.GREAT_BALL], guaranteedRewardSpecs: [RewardId.SUPER_LURE, RewardId.GREAT_BALL],
fillRemaining: false, fillRemaining: false,
}); });
encounter.selectedOption!.dialogue!.selected = [ encounter.selectedOption!.dialogue!.selected = [
@ -338,7 +326,7 @@ export const BugTypeSuperfanEncounter: MysteryEncounter = MysteryEncounterBuilde
]; ];
} else if (numBugTypes < 4) { } else if (numBugTypes < 4) {
setEncounterRewards({ setEncounterRewards({
guaranteedModifierTypeFuncs: [modifierTypes.QUICK_CLAW, modifierTypes.MAX_LURE, modifierTypes.ULTRA_BALL], guaranteedRewardSpecs: [HeldItemId.QUICK_CLAW, RewardId.MAX_LURE, RewardId.ULTRA_BALL],
fillRemaining: false, fillRemaining: false,
}); });
encounter.selectedOption!.dialogue!.selected = [ encounter.selectedOption!.dialogue!.selected = [
@ -349,7 +337,7 @@ export const BugTypeSuperfanEncounter: MysteryEncounter = MysteryEncounterBuilde
]; ];
} else if (numBugTypes < 6) { } else if (numBugTypes < 6) {
setEncounterRewards({ setEncounterRewards({
guaranteedModifierTypeFuncs: [modifierTypes.GRIP_CLAW, modifierTypes.MAX_LURE, modifierTypes.ROGUE_BALL], guaranteedRewardSpecs: [HeldItemId.GRIP_CLAW, RewardId.MAX_LURE, RewardId.ROGUE_BALL],
fillRemaining: false, fillRemaining: false,
}); });
encounter.selectedOption!.dialogue!.selected = [ encounter.selectedOption!.dialogue!.selected = [
@ -361,38 +349,38 @@ export const BugTypeSuperfanEncounter: MysteryEncounter = MysteryEncounterBuilde
} else { } else {
// If the player has any evolution/form change items that are valid for their party, // If the player has any evolution/form change items that are valid for their party,
// spawn one of those items in addition to Dynamax Band, Mega Band, and Master Ball // spawn one of those items in addition to Dynamax Band, Mega Band, and Master Ball
const modifierOptions: ModifierTypeOption[] = [generateModifierTypeOption(modifierTypes.MASTER_BALL)!]; const rewardOptions: RewardOption[] = [generateRewardOptionFromId(RewardId.MASTER_BALL)!];
const specialOptions: ModifierTypeOption[] = []; const specialOptions: RewardOption[] = [];
if (!globalScene.findModifier(m => m instanceof MegaEvolutionAccessModifier)) { if (!globalScene.trainerItems.hasItem(TrainerItemId.MEGA_BRACELET)) {
modifierOptions.push(generateModifierTypeOption(modifierTypes.MEGA_BRACELET)!); rewardOptions.push(generateRewardOptionFromId(TrainerItemId.MEGA_BRACELET)!);
} }
if (!globalScene.findModifier(m => m instanceof GigantamaxAccessModifier)) { if (!globalScene.trainerItems.hasItem(TrainerItemId.DYNAMAX_BAND)) {
modifierOptions.push(generateModifierTypeOption(modifierTypes.DYNAMAX_BAND)!); rewardOptions.push(generateRewardOptionFromId(TrainerItemId.DYNAMAX_BAND)!);
} }
const nonRareEvolutionModifier = generateModifierTypeOption(modifierTypes.EVOLUTION_ITEM); const nonRareEvolutionReward = generateRewardOptionFromId(RewardId.EVOLUTION_ITEM);
if (nonRareEvolutionModifier) { if (nonRareEvolutionReward) {
specialOptions.push(nonRareEvolutionModifier); specialOptions.push(nonRareEvolutionReward);
} }
const rareEvolutionModifier = generateModifierTypeOption(modifierTypes.RARE_EVOLUTION_ITEM); const rareEvolutionReward = generateRewardOptionFromId(RewardId.RARE_EVOLUTION_ITEM);
if (rareEvolutionModifier) { if (rareEvolutionReward) {
specialOptions.push(rareEvolutionModifier); specialOptions.push(rareEvolutionReward);
} }
const formChangeModifier = generateModifierTypeOption(modifierTypes.FORM_CHANGE_ITEM); const formChangeReward = generateRewardOptionFromId(RewardId.FORM_CHANGE_ITEM);
if (formChangeModifier) { if (formChangeReward) {
specialOptions.push(formChangeModifier); specialOptions.push(formChangeReward);
} }
const rareFormChangeModifier = generateModifierTypeOption(modifierTypes.RARE_FORM_CHANGE_ITEM); const rareFormChangeReward = generateRewardOptionFromId(RewardId.RARE_FORM_CHANGE_ITEM);
if (rareFormChangeModifier) { if (rareFormChangeReward) {
specialOptions.push(rareFormChangeModifier); specialOptions.push(rareFormChangeReward);
} }
if (specialOptions.length > 0) { if (specialOptions.length > 0) {
// TODO: should this use `randSeedItem`? // TODO: should this use `randSeedItem`?
modifierOptions.push(specialOptions[randSeedInt(specialOptions.length)]); rewardOptions.push(specialOptions[randSeedInt(specialOptions.length)]);
} }
setEncounterRewards({ setEncounterRewards({
guaranteedModifierTypeOptions: modifierOptions, guaranteedRewardOptions: rewardOptions,
fillRemaining: false, fillRemaining: false,
}); });
encounter.selectedOption!.dialogue!.selected = [ encounter.selectedOption!.dialogue!.selected = [
@ -414,8 +402,7 @@ export const BugTypeSuperfanEncounter: MysteryEncounter = MysteryEncounterBuilde
.withPrimaryPokemonRequirement( .withPrimaryPokemonRequirement(
CombinationPokemonRequirement.Some( CombinationPokemonRequirement.Some(
// Meets one or both of the below reqs // Meets one or both of the below reqs
new HeldItemRequirement(["BypassSpeedChanceModifier", "ContactHeldItemTransferChanceModifier"], 1), new HoldingItemRequirement(REQUIRED_ITEMS, 1),
new AttackTypeBoosterHeldItemTypeRequirement(PokemonType.BUG, 1),
), ),
) )
.withDialogue({ .withDialogue({
@ -438,25 +425,19 @@ export const BugTypeSuperfanEncounter: MysteryEncounter = MysteryEncounterBuilde
const onPokemonSelected = (pokemon: PlayerPokemon) => { const onPokemonSelected = (pokemon: PlayerPokemon) => {
// Get Pokemon held items and filter for valid ones // Get Pokemon held items and filter for valid ones
const validItems = pokemon.getHeldItems().filter(item => { const validItems = pokemon.heldItemManager
return ( .getTransferableHeldItems()
(item instanceof BypassSpeedChanceModifier || .filter(item => REQUIRED_ITEMS.some(i => i === item));
item instanceof ContactHeldItemTransferChanceModifier ||
(item instanceof AttackTypeBoosterModifier &&
(item.type as AttackTypeBoosterModifierType).moveType === PokemonType.BUG)) &&
item.isTransferable
);
});
return validItems.map((modifier: PokemonHeldItemModifier) => { return validItems.map((item: HeldItemId) => {
const option: OptionSelectItem = { const option: OptionSelectItem = {
label: modifier.type.name, label: allHeldItems[item].name,
handler: () => { handler: () => {
// Pokemon and item selected // Pokemon and item selected
encounter.setDialogueToken("selectedItem", modifier.type.name); encounter.setDialogueToken("selectedItem", allHeldItems[item].name);
encounter.misc = { encounter.misc = {
chosenPokemon: pokemon, chosenPokemon: pokemon,
chosenModifier: modifier, chosenItem: item,
}; };
return true; return true;
}, },
@ -467,14 +448,7 @@ export const BugTypeSuperfanEncounter: MysteryEncounter = MysteryEncounterBuilde
const selectableFilter = (pokemon: Pokemon) => { const selectableFilter = (pokemon: Pokemon) => {
// If pokemon has valid item, it can be selected // If pokemon has valid item, it can be selected
const hasValidItem = pokemon.getHeldItems().some(item => { const hasValidItem = pokemon.getHeldItems().some(item => REQUIRED_ITEMS.some(i => i === item));
return (
item instanceof BypassSpeedChanceModifier ||
item instanceof ContactHeldItemTransferChanceModifier ||
(item instanceof AttackTypeBoosterModifier &&
(item.type as AttackTypeBoosterModifierType).moveType === PokemonType.BUG)
);
});
if (!hasValidItem) { if (!hasValidItem) {
return getEncounterText(`${namespace}:option.3.invalid_selection`) ?? null; return getEncounterText(`${namespace}:option.3.invalid_selection`) ?? null;
} }
@ -486,18 +460,18 @@ export const BugTypeSuperfanEncounter: MysteryEncounter = MysteryEncounterBuilde
}) })
.withOptionPhase(async () => { .withOptionPhase(async () => {
const encounter = globalScene.currentBattle.mysteryEncounter!; const encounter = globalScene.currentBattle.mysteryEncounter!;
const modifier = encounter.misc.chosenModifier; const lostItem = encounter.misc.chosenItem;
const chosenPokemon: PlayerPokemon = encounter.misc.chosenPokemon; const chosenPokemon: PlayerPokemon = encounter.misc.chosenPokemon;
chosenPokemon.loseHeldItem(modifier, false); chosenPokemon.loseHeldItem(lostItem, false);
globalScene.updateModifiers(true, true); globalScene.updateItems(true);
const bugNet = generateModifierTypeOption(modifierTypes.MYSTERY_ENCOUNTER_GOLDEN_BUG_NET)!; const bugNet = generateRewardOptionFromId(TrainerItemId.GOLDEN_BUG_NET)!;
bugNet.type.tier = ModifierTier.ROGUE; bugNet.type.tier = RarityTier.ROGUE;
setEncounterRewards({ setEncounterRewards({
guaranteedModifierTypeOptions: [bugNet], guaranteedRewardOptions: [bugNet],
guaranteedModifierTypeFuncs: [modifierTypes.REVIVER_SEED], guaranteedRewardSpecs: [HeldItemId.REVIVER_SEED],
fillRemaining: false, fillRemaining: false,
}); });
leaveEncounterWithoutBattle(true); leaveEncounterWithoutBattle(true);
@ -769,7 +743,7 @@ function doBugTypeMoveTutor(): Promise<void> {
); );
} }
// Complete battle and go to rewards // Complete battle and go to RewardId
resolve(); resolve();
}); });
} }

View File

@ -1,15 +1,13 @@
import { CLASSIC_MODE_MYSTERY_ENCOUNTER_WAVES } from "#app/constants"; import { CLASSIC_MODE_MYSTERY_ENCOUNTER_WAVES } from "#app/constants";
import { globalScene } from "#app/global-scene"; import { globalScene } from "#app/global-scene";
import { EncounterBattleAnim } from "#data/battle-anims"; import { EncounterBattleAnim } from "#data/battle-anims";
import { allAbilities, modifierTypes } from "#data/data-lists"; import { allAbilities } from "#data/data-lists";
import { CustomPokemonData } from "#data/pokemon-data"; import { CustomPokemonData } from "#data/pokemon-data";
import { AbilityId } from "#enums/ability-id"; import { AbilityId } from "#enums/ability-id";
import { BattlerIndex } from "#enums/battler-index"; import { BattlerIndex } from "#enums/battler-index";
import { BerryType } from "#enums/berry-type";
import { Challenges } from "#enums/challenges"; import { Challenges } from "#enums/challenges";
import { EncounterAnim } from "#enums/encounter-anims"; import { EncounterAnim } from "#enums/encounter-anims";
import { ModifierPoolType } from "#enums/modifier-pool-type"; import { HeldItemCategoryId, HeldItemId, isItemInCategory } from "#enums/held-item-id";
import { ModifierTier } from "#enums/modifier-tier";
import { MoveCategory } from "#enums/move-category"; import { MoveCategory } from "#enums/move-category";
import { MoveId } from "#enums/move-id"; import { MoveId } from "#enums/move-id";
import { MoveUseMode } from "#enums/move-use-mode"; import { MoveUseMode } from "#enums/move-use-mode";
@ -18,17 +16,17 @@ import { MysteryEncounterTier } from "#enums/mystery-encounter-tier";
import { MysteryEncounterType } from "#enums/mystery-encounter-type"; import { MysteryEncounterType } from "#enums/mystery-encounter-type";
import { PartyMemberStrength } from "#enums/party-member-strength"; import { PartyMemberStrength } from "#enums/party-member-strength";
import { PokemonType } from "#enums/pokemon-type"; import { PokemonType } from "#enums/pokemon-type";
import { RarityTier } from "#enums/reward-tier";
import { SpeciesId } from "#enums/species-id"; import { SpeciesId } from "#enums/species-id";
import { TrainerType } from "#enums/trainer-type"; import { TrainerType } from "#enums/trainer-type";
import { UiMode } from "#enums/ui-mode"; import { UiMode } from "#enums/ui-mode";
import type { PlayerPokemon } from "#field/pokemon"; import type { PlayerPokemon } from "#field/pokemon";
import { BerryModifier } from "#modifiers/modifier"; import { getHeldItemTier } from "#items/held-item-default-tiers";
import type { PokemonHeldItemModifierType } from "#modifiers/modifier-type"; import { assignItemsFromConfiguration } from "#items/held-item-pool";
import { PokemonMove } from "#moves/pokemon-move"; import { PokemonMove } from "#moves/pokemon-move";
import { showEncounterDialogue, showEncounterText } from "#mystery-encounters/encounter-dialogue-utils"; import { showEncounterDialogue, showEncounterText } from "#mystery-encounters/encounter-dialogue-utils";
import type { EnemyPartyConfig } from "#mystery-encounters/encounter-phase-utils"; import type { EnemyPartyConfig } from "#mystery-encounters/encounter-phase-utils";
import { import {
generateModifierType,
initBattleWithEnemyConfig, initBattleWithEnemyConfig,
leaveEncounterWithoutBattle, leaveEncounterWithoutBattle,
loadCustomMovesForEncounter, loadCustomMovesForEncounter,
@ -36,10 +34,7 @@ import {
setEncounterRewards, setEncounterRewards,
transitionMysteryEncounterIntroVisuals, transitionMysteryEncounterIntroVisuals,
} from "#mystery-encounters/encounter-phase-utils"; } from "#mystery-encounters/encounter-phase-utils";
import { import { applyAbilityOverrideToPokemon } from "#mystery-encounters/encounter-pokemon-utils";
applyAbilityOverrideToPokemon,
applyModifierTypeToPlayerPokemon,
} from "#mystery-encounters/encounter-pokemon-utils";
import type { MysteryEncounter } from "#mystery-encounters/mystery-encounter"; import type { MysteryEncounter } from "#mystery-encounters/mystery-encounter";
import { MysteryEncounterBuilder } from "#mystery-encounters/mystery-encounter"; import { MysteryEncounterBuilder } from "#mystery-encounters/mystery-encounter";
import { MysteryEncounterOptionBuilder } from "#mystery-encounters/mystery-encounter-option"; import { MysteryEncounterOptionBuilder } from "#mystery-encounters/mystery-encounter-option";
@ -283,16 +278,16 @@ export const ClowningAroundEncounter: MysteryEncounter = MysteryEncounterBuilder
const party = globalScene.getPlayerParty(); const party = globalScene.getPlayerParty();
let mostHeldItemsPokemon = party[0]; let mostHeldItemsPokemon = party[0];
let count = mostHeldItemsPokemon let count = mostHeldItemsPokemon.heldItemManager
.getHeldItems() .getTransferableHeldItems()
.filter(m => m.isTransferable && !(m instanceof BerryModifier)) .filter(m => !isItemInCategory(m, HeldItemCategoryId.BERRY))
.reduce((v, m) => v + m.stackCount, 0); .reduce((v, m) => v + mostHeldItemsPokemon.heldItemManager.getStack(m), 0);
for (const pokemon of party) { for (const pokemon of party) {
const nextCount = pokemon const nextCount = pokemon.heldItemManager
.getHeldItems() .getTransferableHeldItems()
.filter(m => m.isTransferable && !(m instanceof BerryModifier)) .filter(m => !isItemInCategory(m, HeldItemCategoryId.BERRY))
.reduce((v, m) => v + m.stackCount, 0); .reduce((v, m) => v + pokemon.heldItemManager.getStack(m), 0);
if (nextCount > count) { if (nextCount > count) {
mostHeldItemsPokemon = pokemon; mostHeldItemsPokemon = pokemon;
count = nextCount; count = nextCount;
@ -301,16 +296,31 @@ export const ClowningAroundEncounter: MysteryEncounter = MysteryEncounterBuilder
encounter.setDialogueToken("switchPokemon", mostHeldItemsPokemon.getNameToRender()); encounter.setDialogueToken("switchPokemon", mostHeldItemsPokemon.getNameToRender());
const items = mostHeldItemsPokemon.getHeldItems(); const items = mostHeldItemsPokemon.heldItemManager
.getTransferableHeldItems()
.filter(m => !isItemInCategory(m, HeldItemCategoryId.BERRY));
// Shuffles Berries (if they have any) // Shuffles Berries (if they have any)
const oldBerries = mostHeldItemsPokemon.heldItemManager
.getHeldItems()
.filter(m => isItemInCategory(m, HeldItemCategoryId.BERRY));
let numBerries = 0; let numBerries = 0;
for (const m of items.filter(m => m instanceof BerryModifier)) { for (const berry of oldBerries) {
numBerries += m.stackCount; const stack = mostHeldItemsPokemon.heldItemManager.getStack(berry);
globalScene.removeModifier(m); numBerries += stack;
mostHeldItemsPokemon.heldItemManager.remove(berry, stack);
} }
generateItemsOfTier(mostHeldItemsPokemon, numBerries, "Berries"); assignItemsFromConfiguration(
[
{
entry: HeldItemCategoryId.BERRY,
count: numBerries,
},
],
mostHeldItemsPokemon,
);
// Shuffle Transferable held items in the same tier (only shuffles Ultra and Rogue atm) // Shuffle Transferable held items in the same tier (only shuffles Ultra and Rogue atm)
// For the purpose of this ME, Soothe Bells and Lucky Eggs are counted as Ultra tier // For the purpose of this ME, Soothe Bells and Lucky Eggs are counted as Ultra tier
@ -318,20 +328,36 @@ export const ClowningAroundEncounter: MysteryEncounter = MysteryEncounterBuilder
let numUltra = 0; let numUltra = 0;
let numRogue = 0; let numRogue = 0;
for (const m of items.filter(m => m.isTransferable && !(m instanceof BerryModifier))) { for (const m of items) {
const type = m.type.withTierFromPool(ModifierPoolType.PLAYER, party); const tier = getHeldItemTier(m) ?? RarityTier.ULTRA;
const tier = type.tier ?? ModifierTier.ULTRA; const stack = mostHeldItemsPokemon.heldItemManager.getStack(m);
if (type.id === "GOLDEN_EGG" || tier === ModifierTier.ROGUE) { if (tier === RarityTier.ROGUE) {
numRogue += m.stackCount; numRogue += stack;
globalScene.removeModifier(m); } else if (tier === RarityTier.ULTRA) {
} else if (type.id === "LUCKY_EGG" || type.id === "SOOTHE_BELL" || tier === ModifierTier.ULTRA) { numUltra += stack;
numUltra += m.stackCount;
globalScene.removeModifier(m);
} }
mostHeldItemsPokemon.heldItemManager.remove(m, stack);
} }
generateItemsOfTier(mostHeldItemsPokemon, numUltra, ModifierTier.ULTRA); assignItemsFromConfiguration(
generateItemsOfTier(mostHeldItemsPokemon, numRogue, ModifierTier.ROGUE); [
{
entry: ultraPool,
count: numUltra,
},
],
mostHeldItemsPokemon,
);
assignItemsFromConfiguration(
[
{
entry: roguePool,
count: numRogue,
},
],
mostHeldItemsPokemon,
);
}) })
.withOptionPhase(async () => { .withOptionPhase(async () => {
leaveEncounterWithoutBattle(true); leaveEncounterWithoutBattle(true);
@ -487,68 +513,21 @@ function onYesAbilitySwap(resolve) {
selectPokemonForOption(onPokemonSelected, onPokemonNotSelected); selectPokemonForOption(onPokemonSelected, onPokemonNotSelected);
} }
function generateItemsOfTier(pokemon: PlayerPokemon, numItems: number, tier: ModifierTier | "Berries") { const ultraPool = [
// These pools have to be defined at runtime so that modifierTypes exist { entry: HeldItemCategoryId.TYPE_ATTACK_BOOSTER, weight: 1 },
// Pools have instances of the modifier type equal to the max stacks that modifier can be applied to any one pokemon { entry: HeldItemId.REVIVER_SEED, weight: 1 },
// This is to prevent "over-generating" a random item of a certain type during item swaps { entry: HeldItemId.GOLDEN_PUNCH, weight: 1 },
const ultraPool = [ { entry: HeldItemId.QUICK_CLAW, weight: 1 },
[modifierTypes.REVIVER_SEED, 1], { entry: HeldItemId.WIDE_LENS, weight: 1 },
[modifierTypes.GOLDEN_PUNCH, 5], ];
[modifierTypes.ATTACK_TYPE_BOOSTER, 99],
[modifierTypes.QUICK_CLAW, 3],
[modifierTypes.WIDE_LENS, 3],
];
const roguePool = [ const roguePool = [
[modifierTypes.LEFTOVERS, 4], { entry: HeldItemId.LEFTOVERS, weight: 1 },
[modifierTypes.SHELL_BELL, 4], { entry: HeldItemId.SHELL_BELL, weight: 1 },
[modifierTypes.SOUL_DEW, 10], { entry: HeldItemId.SOUL_DEW, weight: 1 },
[modifierTypes.SCOPE_LENS, 1], { entry: HeldItemId.SCOPE_LENS, weight: 1 },
[modifierTypes.BATON, 1], { entry: HeldItemId.BATON, weight: 1 },
[modifierTypes.FOCUS_BAND, 5], { entry: HeldItemId.FOCUS_BAND, weight: 1 },
[modifierTypes.KINGS_ROCK, 3], { entry: HeldItemId.KINGS_ROCK, weight: 1 },
[modifierTypes.GRIP_CLAW, 5], { entry: HeldItemId.GRIP_CLAW, weight: 1 },
]; ];
const berryPool = [
[BerryType.APICOT, 3],
[BerryType.ENIGMA, 2],
[BerryType.GANLON, 3],
[BerryType.LANSAT, 3],
[BerryType.LEPPA, 2],
[BerryType.LIECHI, 3],
[BerryType.LUM, 2],
[BerryType.PETAYA, 3],
[BerryType.SALAC, 2],
[BerryType.SITRUS, 2],
[BerryType.STARF, 3],
];
let pool: any[];
if (tier === "Berries") {
pool = berryPool;
} else {
pool = tier === ModifierTier.ULTRA ? ultraPool : roguePool;
}
for (let i = 0; i < numItems; i++) {
if (pool.length === 0) {
// Stop generating new items if somehow runs out of items to spawn
return;
}
const randIndex = randSeedInt(pool.length);
const newItemType = pool[randIndex];
let newMod: PokemonHeldItemModifierType;
if (tier === "Berries") {
newMod = generateModifierType(modifierTypes.BERRY, [newItemType[0]]) as PokemonHeldItemModifierType;
} else {
newMod = generateModifierType(newItemType[0]) as PokemonHeldItemModifierType;
}
applyModifierTypeToPlayerPokemon(pokemon, newMod);
// Decrement max stacks and remove from pool if at max
newItemType[1]--;
if (newItemType[1] <= 0) {
pool.splice(randIndex, 1);
}
}
}

View File

@ -1,11 +1,11 @@
import { CLASSIC_MODE_MYSTERY_ENCOUNTER_WAVES } from "#app/constants"; import { CLASSIC_MODE_MYSTERY_ENCOUNTER_WAVES } from "#app/constants";
import { globalScene } from "#app/global-scene"; import { globalScene } from "#app/global-scene";
import { EncounterBattleAnim } from "#data/battle-anims"; import { EncounterBattleAnim } from "#data/battle-anims";
import { modifierTypes } from "#data/data-lists";
import { BattlerIndex } from "#enums/battler-index"; import { BattlerIndex } from "#enums/battler-index";
import { BattlerTagType } from "#enums/battler-tag-type"; import { BattlerTagType } from "#enums/battler-tag-type";
import { BiomeId } from "#enums/biome-id"; import { BiomeId } from "#enums/biome-id";
import { EncounterAnim } from "#enums/encounter-anims"; import { EncounterAnim } from "#enums/encounter-anims";
import { HeldItemId } from "#enums/held-item-id";
import { MoveId } from "#enums/move-id"; import { MoveId } from "#enums/move-id";
import { MoveUseMode } from "#enums/move-use-mode"; import { MoveUseMode } from "#enums/move-use-mode";
import { MysteryEncounterOptionMode } from "#enums/mystery-encounter-option-mode"; import { MysteryEncounterOptionMode } from "#enums/mystery-encounter-option-mode";
@ -153,7 +153,7 @@ export const DancingLessonsEncounter: MysteryEncounter = MysteryEncounterBuilder
} }
const oricorioData = new PokemonData(enemyPokemon); const oricorioData = new PokemonData(enemyPokemon);
const oricorio = globalScene.addEnemyPokemon(species, level, TrainerSlot.NONE, false, false, oricorioData); const oricorio = globalScene.addEnemyPokemon(species, level, TrainerSlot.NONE, false, false, [], oricorioData);
// Adds a real Pokemon sprite to the field (required for the animation) // Adds a real Pokemon sprite to the field (required for the animation)
for (const enemyPokemon of globalScene.getEnemyParty()) { for (const enemyPokemon of globalScene.getEnemyParty()) {
@ -219,7 +219,7 @@ export const DancingLessonsEncounter: MysteryEncounter = MysteryEncounterBuilder
await hideOricorioPokemon(); await hideOricorioPokemon();
setEncounterRewards({ setEncounterRewards({
guaranteedModifierTypeFuncs: [modifierTypes.BATON], guaranteedRewardSpecs: [HeldItemId.BATON],
fillRemaining: true, fillRemaining: true,
}); });
await initBattleWithEnemyConfig(encounter.enemyPartyConfigs[0]); await initBattleWithEnemyConfig(encounter.enemyPartyConfigs[0]);

View File

@ -1,14 +1,13 @@
import { CLASSIC_MODE_MYSTERY_ENCOUNTER_WAVES } from "#app/constants"; import { CLASSIC_MODE_MYSTERY_ENCOUNTER_WAVES } from "#app/constants";
import { globalScene } from "#app/global-scene"; import { globalScene } from "#app/global-scene";
import { modifierTypes } from "#data/data-lists";
import { Challenges } from "#enums/challenges"; import { Challenges } from "#enums/challenges";
import { MysteryEncounterOptionMode } from "#enums/mystery-encounter-option-mode"; import { MysteryEncounterOptionMode } from "#enums/mystery-encounter-option-mode";
import { MysteryEncounterTier } from "#enums/mystery-encounter-tier"; import { MysteryEncounterTier } from "#enums/mystery-encounter-tier";
import { MysteryEncounterType } from "#enums/mystery-encounter-type"; import { MysteryEncounterType } from "#enums/mystery-encounter-type";
import type { PokemonType } from "#enums/pokemon-type"; import type { PokemonType } from "#enums/pokemon-type";
import { RewardId } from "#enums/reward-id";
import { SpeciesId } from "#enums/species-id"; import { SpeciesId } from "#enums/species-id";
import type { PokemonHeldItemModifier } from "#modifiers/modifier"; import type { HeldItemConfiguration } from "#items/held-item-data-types";
import { PokemonFormChangeItemModifier } from "#modifiers/modifier";
import type { EnemyPartyConfig, EnemyPokemonConfig } from "#mystery-encounters/encounter-phase-utils"; import type { EnemyPartyConfig, EnemyPokemonConfig } from "#mystery-encounters/encounter-phase-utils";
import { initBattleWithEnemyConfig, leaveEncounterWithoutBattle } from "#mystery-encounters/encounter-phase-utils"; import { initBattleWithEnemyConfig, leaveEncounterWithoutBattle } from "#mystery-encounters/encounter-phase-utils";
import { getRandomPlayerPokemon, getRandomSpeciesByStarterCost } from "#mystery-encounters/encounter-pokemon-utils"; import { getRandomPlayerPokemon, getRandomSpeciesByStarterCost } from "#mystery-encounters/encounter-pokemon-utils";
@ -149,7 +148,7 @@ export const DarkDealEncounter: MysteryEncounter = MysteryEncounterBuilder.withE
const removedPokemon = getRandomPlayerPokemon(true, false, true); const removedPokemon = getRandomPlayerPokemon(true, false, true);
// Get all the pokemon's held items // Get all the pokemon's held items
const modifiers = removedPokemon.getHeldItems().filter(m => !(m instanceof PokemonFormChangeItemModifier)); const itemConfig = removedPokemon.heldItemManager.generateHeldItemConfiguration();
globalScene.removePokemonFromPlayerParty(removedPokemon); globalScene.removePokemonFromPlayerParty(removedPokemon);
const encounter = globalScene.currentBattle.mysteryEncounter!; const encounter = globalScene.currentBattle.mysteryEncounter!;
@ -158,13 +157,13 @@ export const DarkDealEncounter: MysteryEncounter = MysteryEncounterBuilder.withE
// Store removed pokemon types // Store removed pokemon types
encounter.misc = { encounter.misc = {
removedTypes: removedPokemon.getTypes(), removedTypes: removedPokemon.getTypes(),
modifiers, itemConfig: itemConfig,
}; };
}) })
.withOptionPhase(async () => { .withOptionPhase(async () => {
// Give the player 5 Rogue Balls // Give the player 5 Rogue Balls
const encounter = globalScene.currentBattle.mysteryEncounter!; const encounter = globalScene.currentBattle.mysteryEncounter!;
globalScene.phaseManager.unshiftNew("ModifierRewardPhase", modifierTypes.ROGUE_BALL); globalScene.phaseManager.unshiftNew("RewardPhase", RewardId.ROGUE_BALL);
// Start encounter with random legendary (7-10 starter strength) that has level additive // Start encounter with random legendary (7-10 starter strength) that has level additive
// If this is a mono-type challenge, always ensure the required type is filtered for // If this is a mono-type challenge, always ensure the required type is filtered for
@ -176,7 +175,7 @@ export const DarkDealEncounter: MysteryEncounter = MysteryEncounterBuilder.withE
bossTypes = singleTypeChallenges.map(c => (c.value - 1) as PokemonType); bossTypes = singleTypeChallenges.map(c => (c.value - 1) as PokemonType);
} }
const bossModifiers: PokemonHeldItemModifier[] = encounter.misc.modifiers; const bossItemConfig: HeldItemConfiguration = encounter.misc.itemConfig;
// Starter egg tier, 35/50/10/5 %odds for tiers 6/7/8/9+ // Starter egg tier, 35/50/10/5 %odds for tiers 6/7/8/9+
const roll = randSeedInt(100); const roll = randSeedInt(100);
const starterTier: number | [number, number] = roll >= 65 ? 6 : roll >= 15 ? 7 : roll >= 5 ? 8 : [9, 10]; const starterTier: number | [number, number] = roll >= 65 ? 6 : roll >= 15 ? 7 : roll >= 5 ? 8 : [9, 10];
@ -184,12 +183,7 @@ export const DarkDealEncounter: MysteryEncounter = MysteryEncounterBuilder.withE
const pokemonConfig: EnemyPokemonConfig = { const pokemonConfig: EnemyPokemonConfig = {
species: bossSpecies, species: bossSpecies,
isBoss: true, isBoss: true,
modifierConfigs: bossModifiers.map(m => { heldItemConfig: bossItemConfig,
return {
modifier: m,
stackCount: m.getStackCount(),
};
}),
}; };
if (!isNullOrUndefined(bossSpecies.forms) && bossSpecies.forms.length > 0) { if (!isNullOrUndefined(bossSpecies.forms) && bossSpecies.forms.length > 0) {
pokemonConfig.formIndex = 0; pokemonConfig.formIndex = 0;

View File

@ -1,35 +1,27 @@
import { CLASSIC_MODE_MYSTERY_ENCOUNTER_WAVES } from "#app/constants"; import { CLASSIC_MODE_MYSTERY_ENCOUNTER_WAVES } from "#app/constants";
import { timedEventManager } from "#app/global-event-manager"; import { timedEventManager } from "#app/global-event-manager";
import { globalScene } from "#app/global-scene"; import { globalScene } from "#app/global-scene";
import { modifierTypes } from "#data/data-lists"; import { allHeldItems } from "#data/data-lists";
import { HeldItemCategoryId, HeldItemId, isItemInCategory } from "#enums/held-item-id";
import { MysteryEncounterOptionMode } from "#enums/mystery-encounter-option-mode"; import { MysteryEncounterOptionMode } from "#enums/mystery-encounter-option-mode";
import { MysteryEncounterTier } from "#enums/mystery-encounter-tier"; import { MysteryEncounterTier } from "#enums/mystery-encounter-tier";
import { MysteryEncounterType } from "#enums/mystery-encounter-type"; import { MysteryEncounterType } from "#enums/mystery-encounter-type";
import { RewardId } from "#enums/reward-id";
import { SpeciesId } from "#enums/species-id"; import { SpeciesId } from "#enums/species-id";
import { TrainerItemId } from "#enums/trainer-item-id";
import type { PlayerPokemon, Pokemon } from "#field/pokemon"; import type { PlayerPokemon, Pokemon } from "#field/pokemon";
import type { PokemonHeldItemModifier, PokemonInstantReviveModifier } from "#modifiers/modifier";
import {
BerryModifier,
HealingBoosterModifier,
LevelIncrementBoosterModifier,
MoneyMultiplierModifier,
PreserveBerryModifier,
} from "#modifiers/modifier";
import type { PokemonHeldItemModifierType } from "#modifiers/modifier-type";
import { getEncounterText, showEncounterText } from "#mystery-encounters/encounter-dialogue-utils"; import { getEncounterText, showEncounterText } from "#mystery-encounters/encounter-dialogue-utils";
import { import {
generateModifierType,
leaveEncounterWithoutBattle, leaveEncounterWithoutBattle,
selectPokemonForOption, selectPokemonForOption,
updatePlayerMoney, updatePlayerMoney,
} from "#mystery-encounters/encounter-phase-utils"; } from "#mystery-encounters/encounter-phase-utils";
import { applyModifierTypeToPlayerPokemon } from "#mystery-encounters/encounter-pokemon-utils";
import type { MysteryEncounter } from "#mystery-encounters/mystery-encounter"; import type { MysteryEncounter } from "#mystery-encounters/mystery-encounter";
import { MysteryEncounterBuilder } from "#mystery-encounters/mystery-encounter"; import { MysteryEncounterBuilder } from "#mystery-encounters/mystery-encounter";
import { MysteryEncounterOptionBuilder } from "#mystery-encounters/mystery-encounter-option"; import { MysteryEncounterOptionBuilder } from "#mystery-encounters/mystery-encounter-option";
import { import {
CombinationPokemonRequirement, CombinationPokemonRequirement,
HeldItemRequirement, HoldingItemRequirement,
MoneyRequirement, MoneyRequirement,
} from "#mystery-encounters/mystery-encounter-requirements"; } from "#mystery-encounters/mystery-encounter-requirements";
import i18next from "#plugins/i18n"; import i18next from "#plugins/i18n";
@ -41,32 +33,39 @@ import { getPokemonSpecies } from "#utils/pokemon-utils";
const namespace = "mysteryEncounters/delibirdy"; const namespace = "mysteryEncounters/delibirdy";
/** Berries only */ /** Berries only */
const OPTION_2_ALLOWED_MODIFIERS = ["BerryModifier", "PokemonInstantReviveModifier"]; const OPTION_2_ALLOWED_HELD_ITEMS = [HeldItemCategoryId.BERRY, HeldItemId.REVIVER_SEED];
/** Disallowed items are berries, Reviver Seeds, and Vitamins (form change items and fusion items are not PokemonHeldItemModifiers) */ /** Disallowed items are berries, Reviver Seeds, and Vitamins */
const OPTION_3_DISALLOWED_MODIFIERS = [ const OPTION_3_DISALLOWED_HELD_ITEMS = [HeldItemCategoryId.BERRY, HeldItemId.REVIVER_SEED];
"BerryModifier",
"PokemonInstantReviveModifier",
"TerastallizeModifier",
"PokemonBaseStatModifier",
"PokemonBaseStatTotalModifier",
];
const DELIBIRDY_MONEY_PRICE_MULTIPLIER = 2; const DELIBIRDY_MONEY_PRICE_MULTIPLIER = 2;
async function backupOption() {
globalScene.getPlayerPokemon()?.heldItemManager.add(HeldItemId.SHELL_BELL);
globalScene.playSound("item_fanfare");
await showEncounterText(
i18next.t("battle:rewardGain", {
modifierName: allHeldItems[HeldItemId.SHELL_BELL].name,
}),
null,
undefined,
true,
);
doEventReward();
}
const doEventReward = () => { const doEventReward = () => {
const event_buff = timedEventManager.getDelibirdyBuff(); const event_buff = timedEventManager.getDelibirdyBuff();
if (event_buff.length > 0) { if (event_buff.length > 0) {
const candidates = event_buff.filter(c => { const candidates = event_buff.filter(c => {
const mtype = generateModifierType(modifierTypes[c]); const fullStack = globalScene.trainerItems.isMaxStack(c);
const existingCharm = globalScene.findModifier(m => m.type.id === mtype?.id); return !fullStack;
return !(existingCharm && existingCharm.getStackCount() >= existingCharm.getMaxStackCount());
}); });
if (candidates.length > 0) { if (candidates.length > 0) {
globalScene.phaseManager.unshiftNew("ModifierRewardPhase", modifierTypes[randSeedItem(candidates)]); globalScene.phaseManager.unshiftNew("RewardPhase", randSeedItem(candidates));
} else { } else {
// At max stacks, give a Voucher instead // At max stacks, give a Voucher instead
globalScene.phaseManager.unshiftNew("ModifierRewardPhase", modifierTypes.VOUCHER); globalScene.phaseManager.unshiftNew("RewardPhase", RewardId.VOUCHER);
} }
} }
}; };
@ -85,8 +84,8 @@ export const DelibirdyEncounter: MysteryEncounter = MysteryEncounterBuilder.with
.withPrimaryPokemonRequirement( .withPrimaryPokemonRequirement(
CombinationPokemonRequirement.Some( CombinationPokemonRequirement.Some(
// Must also have either option 2 or 3 available to spawn // Must also have either option 2 or 3 available to spawn
new HeldItemRequirement(OPTION_2_ALLOWED_MODIFIERS), new HoldingItemRequirement(OPTION_2_ALLOWED_HELD_ITEMS),
new HeldItemRequirement(OPTION_3_DISALLOWED_MODIFIERS, 1, true), new HoldingItemRequirement(OPTION_3_DISALLOWED_HELD_ITEMS, 1, true),
), ),
) )
.withIntroSpriteConfigs([ .withIntroSpriteConfigs([
@ -164,22 +163,13 @@ export const DelibirdyEncounter: MysteryEncounter = MysteryEncounterBuilder.with
.withOptionPhase(async () => { .withOptionPhase(async () => {
// Give the player an Amulet Coin // Give the player an Amulet Coin
// Check if the player has max stacks of that item already // Check if the player has max stacks of that item already
const existing = globalScene.findModifier(m => m instanceof MoneyMultiplierModifier) as MoneyMultiplierModifier; const fullStack = globalScene.trainerItems.isMaxStack(TrainerItemId.AMULET_COIN);
if (existing && existing.getStackCount() >= existing.getMaxStackCount()) { if (fullStack) {
// At max stacks, give the first party pokemon a Shell Bell instead // At max stacks, give the first party pokemon a Shell Bell instead
const shellBell = generateModifierType(modifierTypes.SHELL_BELL) as PokemonHeldItemModifierType; backupOption();
await applyModifierTypeToPlayerPokemon(globalScene.getPlayerPokemon()!, shellBell);
globalScene.playSound("item_fanfare");
await showEncounterText(
i18next.t("battle:rewardGain", { modifierName: shellBell.name }),
null,
undefined,
true,
);
doEventReward();
} else { } else {
globalScene.phaseManager.unshiftNew("ModifierRewardPhase", modifierTypes.AMULET_COIN); globalScene.phaseManager.unshiftNew("RewardPhase", TrainerItemId.AMULET_COIN);
doEventReward(); doEventReward();
} }
@ -189,7 +179,7 @@ export const DelibirdyEncounter: MysteryEncounter = MysteryEncounterBuilder.with
) )
.withOption( .withOption(
MysteryEncounterOptionBuilder.newOptionWithMode(MysteryEncounterOptionMode.DISABLED_OR_DEFAULT) MysteryEncounterOptionBuilder.newOptionWithMode(MysteryEncounterOptionMode.DISABLED_OR_DEFAULT)
.withPrimaryPokemonRequirement(new HeldItemRequirement(OPTION_2_ALLOWED_MODIFIERS)) .withPrimaryPokemonRequirement(new HoldingItemRequirement(OPTION_2_ALLOWED_HELD_ITEMS))
.withDialogue({ .withDialogue({
buttonLabel: `${namespace}:option.2.label`, buttonLabel: `${namespace}:option.2.label`,
buttonTooltip: `${namespace}:option.2.tooltip`, buttonTooltip: `${namespace}:option.2.tooltip`,
@ -204,19 +194,17 @@ export const DelibirdyEncounter: MysteryEncounter = MysteryEncounterBuilder.with
const encounter = globalScene.currentBattle.mysteryEncounter!; const encounter = globalScene.currentBattle.mysteryEncounter!;
const onPokemonSelected = (pokemon: PlayerPokemon) => { const onPokemonSelected = (pokemon: PlayerPokemon) => {
// Get Pokemon held items and filter for valid ones // Get Pokemon held items and filter for valid ones
const validItems = pokemon.getHeldItems().filter(it => { const validItems = pokemon.heldItemManager.filterRequestedItems(OPTION_2_ALLOWED_HELD_ITEMS, true);
return OPTION_2_ALLOWED_MODIFIERS.some(heldItem => it.constructor.name === heldItem) && it.isTransferable;
});
return validItems.map((modifier: PokemonHeldItemModifier) => { return validItems.map((item: HeldItemId) => {
const option: OptionSelectItem = { const option: OptionSelectItem = {
label: modifier.type.name, label: allHeldItems[item].name,
handler: () => { handler: () => {
// Pokemon and item selected // Pokemon and item selected
encounter.setDialogueToken("chosenItem", modifier.type.name); encounter.setDialogueToken("chosenItem", allHeldItems[item].name);
encounter.misc = { encounter.misc = {
chosenPokemon: pokemon, chosenPokemon: pokemon,
chosenModifier: modifier, chosenItem: item,
}; };
return true; return true;
}, },
@ -239,59 +227,35 @@ export const DelibirdyEncounter: MysteryEncounter = MysteryEncounterBuilder.with
}) })
.withOptionPhase(async () => { .withOptionPhase(async () => {
const encounter = globalScene.currentBattle.mysteryEncounter!; const encounter = globalScene.currentBattle.mysteryEncounter!;
const modifier: BerryModifier | PokemonInstantReviveModifier = encounter.misc.chosenModifier; const chosenItem: HeldItemId = encounter.misc.chosenItem;
const chosenPokemon: PlayerPokemon = encounter.misc.chosenPokemon; const chosenPokemon: PlayerPokemon = encounter.misc.chosenPokemon;
// Give the player a Candy Jar if they gave a Berry, and a Berry Pouch for Reviver Seed // Give the player a Candy Jar if they gave a Berry, and a Berry Pouch for Reviver Seed
if (modifier instanceof BerryModifier) { if (isItemInCategory(chosenItem, HeldItemCategoryId.BERRY)) {
// Check if the player has max stacks of that Candy Jar already // Check if the player has max stacks of that Candy Jar already
const existing = globalScene.findModifier( const fullStack = globalScene.trainerItems.isMaxStack(TrainerItemId.CANDY_JAR);
m => m instanceof LevelIncrementBoosterModifier,
) as LevelIncrementBoosterModifier;
if (existing && existing.getStackCount() >= existing.getMaxStackCount()) { if (fullStack) {
// At max stacks, give the first party pokemon a Shell Bell instead // At max stacks, give the first party pokemon a Shell Bell instead
const shellBell = generateModifierType(modifierTypes.SHELL_BELL) as PokemonHeldItemModifierType; backupOption();
await applyModifierTypeToPlayerPokemon(globalScene.getPlayerPokemon()!, shellBell);
globalScene.playSound("item_fanfare");
await showEncounterText(
i18next.t("battle:rewardGain", {
modifierName: shellBell.name,
}),
null,
undefined,
true,
);
doEventReward();
} else { } else {
globalScene.phaseManager.unshiftNew("ModifierRewardPhase", modifierTypes.CANDY_JAR); globalScene.phaseManager.unshiftNew("RewardPhase", TrainerItemId.CANDY_JAR);
doEventReward(); doEventReward();
} }
} else { } else {
// Check if the player has max stacks of that Berry Pouch already // Check if the player has max stacks of that Berry Pouch already
const existing = globalScene.findModifier(m => m instanceof PreserveBerryModifier) as PreserveBerryModifier; const fullStack = globalScene.trainerItems.isMaxStack(TrainerItemId.BERRY_POUCH);
if (existing && existing.getStackCount() >= existing.getMaxStackCount()) { if (fullStack) {
// At max stacks, give the first party pokemon a Shell Bell instead // At max stacks, give the first party pokemon a Shell Bell instead
const shellBell = generateModifierType(modifierTypes.SHELL_BELL) as PokemonHeldItemModifierType; backupOption();
await applyModifierTypeToPlayerPokemon(globalScene.getPlayerPokemon()!, shellBell);
globalScene.playSound("item_fanfare");
await showEncounterText(
i18next.t("battle:rewardGain", {
modifierName: shellBell.name,
}),
null,
undefined,
true,
);
doEventReward();
} else { } else {
globalScene.phaseManager.unshiftNew("ModifierRewardPhase", modifierTypes.BERRY_POUCH); globalScene.phaseManager.unshiftNew("RewardPhase", TrainerItemId.BERRY_POUCH);
doEventReward(); doEventReward();
} }
} }
chosenPokemon.loseHeldItem(modifier, false); chosenPokemon.loseHeldItem(chosenItem, false);
leaveEncounterWithoutBattle(true); leaveEncounterWithoutBattle(true);
}) })
@ -299,7 +263,7 @@ export const DelibirdyEncounter: MysteryEncounter = MysteryEncounterBuilder.with
) )
.withOption( .withOption(
MysteryEncounterOptionBuilder.newOptionWithMode(MysteryEncounterOptionMode.DISABLED_OR_DEFAULT) MysteryEncounterOptionBuilder.newOptionWithMode(MysteryEncounterOptionMode.DISABLED_OR_DEFAULT)
.withPrimaryPokemonRequirement(new HeldItemRequirement(OPTION_3_DISALLOWED_MODIFIERS, 1, true)) .withPrimaryPokemonRequirement(new HoldingItemRequirement(OPTION_3_DISALLOWED_HELD_ITEMS, 1, true))
.withDialogue({ .withDialogue({
buttonLabel: `${namespace}:option.3.label`, buttonLabel: `${namespace}:option.3.label`,
buttonTooltip: `${namespace}:option.3.tooltip`, buttonTooltip: `${namespace}:option.3.tooltip`,
@ -314,21 +278,17 @@ export const DelibirdyEncounter: MysteryEncounter = MysteryEncounterBuilder.with
const encounter = globalScene.currentBattle.mysteryEncounter!; const encounter = globalScene.currentBattle.mysteryEncounter!;
const onPokemonSelected = (pokemon: PlayerPokemon) => { const onPokemonSelected = (pokemon: PlayerPokemon) => {
// Get Pokemon held items and filter for valid ones // Get Pokemon held items and filter for valid ones
const validItems = pokemon.getHeldItems().filter(it => { const validItems = pokemon.heldItemManager.filterRequestedItems(OPTION_3_DISALLOWED_HELD_ITEMS, true, true);
return (
!OPTION_3_DISALLOWED_MODIFIERS.some(heldItem => it.constructor.name === heldItem) && it.isTransferable
);
});
return validItems.map((modifier: PokemonHeldItemModifier) => { return validItems.map((item: HeldItemId) => {
const option: OptionSelectItem = { const option: OptionSelectItem = {
label: modifier.type.name, label: allHeldItems[item].name,
handler: () => { handler: () => {
// Pokemon and item selected // Pokemon and item selected
encounter.setDialogueToken("chosenItem", modifier.type.name); encounter.setDialogueToken("chosenItem", allHeldItems[item].name);
encounter.misc = { encounter.misc = {
chosenPokemon: pokemon, chosenPokemon: pokemon,
chosenModifier: modifier, chosenItem: item,
}; };
return true; return true;
}, },
@ -351,30 +311,22 @@ export const DelibirdyEncounter: MysteryEncounter = MysteryEncounterBuilder.with
}) })
.withOptionPhase(async () => { .withOptionPhase(async () => {
const encounter = globalScene.currentBattle.mysteryEncounter!; const encounter = globalScene.currentBattle.mysteryEncounter!;
const modifier = encounter.misc.chosenModifier; const chosenItem = encounter.misc.chosenItem;
const chosenPokemon: PlayerPokemon = encounter.misc.chosenPokemon; const chosenPokemon: PlayerPokemon = encounter.misc.chosenPokemon;
// Check if the player has max stacks of Healing Charm already // Check if the player has max stacks of Healing Charm already
const existing = globalScene.findModifier(m => m instanceof HealingBoosterModifier) as HealingBoosterModifier;
if (existing && existing.getStackCount() >= existing.getMaxStackCount()) { const fullStack = globalScene.trainerItems.isMaxStack(TrainerItemId.HEALING_CHARM);
if (fullStack) {
// At max stacks, give the first party pokemon a Shell Bell instead // At max stacks, give the first party pokemon a Shell Bell instead
const shellBell = generateModifierType(modifierTypes.SHELL_BELL) as PokemonHeldItemModifierType; backupOption();
await applyModifierTypeToPlayerPokemon(globalScene.getPlayerParty()[0], shellBell);
globalScene.playSound("item_fanfare");
await showEncounterText(
i18next.t("battle:rewardGain", { modifierName: shellBell.name }),
null,
undefined,
true,
);
doEventReward();
} else { } else {
globalScene.phaseManager.unshiftNew("ModifierRewardPhase", modifierTypes.HEALING_CHARM); globalScene.phaseManager.unshiftNew("RewardPhase", TrainerItemId.HEALING_CHARM);
doEventReward(); doEventReward();
} }
chosenPokemon.loseHeldItem(modifier, false); chosenPokemon.loseHeldItem(chosenItem, false);
leaveEncounterWithoutBattle(true); leaveEncounterWithoutBattle(true);
}) })

View File

@ -1,12 +1,12 @@
import { CLASSIC_MODE_MYSTERY_ENCOUNTER_WAVES } from "#app/constants"; import { CLASSIC_MODE_MYSTERY_ENCOUNTER_WAVES } from "#app/constants";
import { modifierTypes } from "#data/data-lists";
import { MysteryEncounterTier } from "#enums/mystery-encounter-tier"; import { MysteryEncounterTier } from "#enums/mystery-encounter-tier";
import { MysteryEncounterType } from "#enums/mystery-encounter-type"; import { MysteryEncounterType } from "#enums/mystery-encounter-type";
import { RewardId } from "#enums/reward-id";
import { SpeciesId } from "#enums/species-id"; import { SpeciesId } from "#enums/species-id";
import { leaveEncounterWithoutBattle, setEncounterRewards } from "#mystery-encounters/encounter-phase-utils"; import { leaveEncounterWithoutBattle, setEncounterRewards } from "#mystery-encounters/encounter-phase-utils";
import type { MysteryEncounter } from "#mystery-encounters/mystery-encounter"; import type { MysteryEncounter } from "#mystery-encounters/mystery-encounter";
import { MysteryEncounterBuilder } from "#mystery-encounters/mystery-encounter"; import { MysteryEncounterBuilder } from "#mystery-encounters/mystery-encounter";
import type { ModifierTypeFunc } from "#types/modifier-types"; import type { RewardSpecs } from "#types/rewards";
import { randSeedInt } from "#utils/common"; import { randSeedInt } from "#utils/common";
/** i18n namespace for encounter */ /** i18n namespace for encounter */
@ -59,23 +59,23 @@ export const DepartmentStoreSaleEncounter: MysteryEncounter = MysteryEncounterBu
}, },
async () => { async () => {
// Choose TMs // Choose TMs
const modifiers: ModifierTypeFunc[] = []; const rewards: RewardSpecs[] = [];
let i = 0; let i = 0;
while (i < 5) { while (i < 5) {
// 2/2/1 weight on TM rarity // 2/2/1 weight on TM rarity
const roll = randSeedInt(5); const roll = randSeedInt(5);
if (roll < 2) { if (roll < 2) {
modifiers.push(modifierTypes.TM_COMMON); rewards.push(RewardId.TM_COMMON);
} else if (roll < 4) { } else if (roll < 4) {
modifiers.push(modifierTypes.TM_GREAT); rewards.push(RewardId.TM_GREAT);
} else { } else {
modifiers.push(modifierTypes.TM_ULTRA); rewards.push(RewardId.TM_ULTRA);
} }
i++; i++;
} }
setEncounterRewards({ setEncounterRewards({
guaranteedModifierTypeFuncs: modifiers, guaranteedRewardSpecs: rewards,
fillRemaining: false, fillRemaining: false,
}); });
leaveEncounterWithoutBattle(); leaveEncounterWithoutBattle();
@ -88,21 +88,21 @@ export const DepartmentStoreSaleEncounter: MysteryEncounter = MysteryEncounterBu
}, },
async () => { async () => {
// Choose Vitamins // Choose Vitamins
const modifiers: ModifierTypeFunc[] = []; const rewards: RewardSpecs[] = [];
let i = 0; let i = 0;
while (i < 3) { while (i < 3) {
// 2/1 weight on base stat booster vs PP Up // 2/1 weight on base stat booster vs PP Up
const roll = randSeedInt(3); const roll = randSeedInt(3);
if (roll === 0) { if (roll === 0) {
modifiers.push(modifierTypes.PP_UP); rewards.push(RewardId.PP_UP);
} else { } else {
modifiers.push(modifierTypes.BASE_STAT_BOOSTER); rewards.push(RewardId.BASE_STAT_BOOSTER);
} }
i++; i++;
} }
setEncounterRewards({ setEncounterRewards({
guaranteedModifierTypeFuncs: modifiers, guaranteedRewardSpecs: rewards,
fillRemaining: false, fillRemaining: false,
}); });
leaveEncounterWithoutBattle(); leaveEncounterWithoutBattle();
@ -115,21 +115,21 @@ export const DepartmentStoreSaleEncounter: MysteryEncounter = MysteryEncounterBu
}, },
async () => { async () => {
// Choose X Items // Choose X Items
const modifiers: ModifierTypeFunc[] = []; const rewards: RewardSpecs[] = [];
let i = 0; let i = 0;
while (i < 5) { while (i < 5) {
// 4/1 weight on base stat booster vs Dire Hit // 4/1 weight on base stat booster vs Dire Hit
const roll = randSeedInt(5); const roll = randSeedInt(5);
if (roll === 0) { if (roll === 0) {
modifiers.push(modifierTypes.DIRE_HIT); rewards.push(RewardId.DIRE_HIT);
} else { } else {
modifiers.push(modifierTypes.TEMP_STAT_STAGE_BOOSTER); rewards.push(RewardId.TEMP_STAT_STAGE_BOOSTER);
} }
i++; i++;
} }
setEncounterRewards({ setEncounterRewards({
guaranteedModifierTypeFuncs: modifiers, guaranteedRewardSpecs: rewards,
fillRemaining: false, fillRemaining: false,
}); });
leaveEncounterWithoutBattle(); leaveEncounterWithoutBattle();
@ -142,25 +142,25 @@ export const DepartmentStoreSaleEncounter: MysteryEncounter = MysteryEncounterBu
}, },
async () => { async () => {
// Choose Pokeballs // Choose Pokeballs
const modifiers: ModifierTypeFunc[] = []; const rewards: RewardSpecs[] = [];
let i = 0; let i = 0;
while (i < 4) { while (i < 4) {
// 10/30/20/5 weight on pokeballs // 10/30/20/5 weight on pokeballs
const roll = randSeedInt(65); const roll = randSeedInt(65);
if (roll < 10) { if (roll < 10) {
modifiers.push(modifierTypes.POKEBALL); rewards.push(RewardId.POKEBALL);
} else if (roll < 40) { } else if (roll < 40) {
modifiers.push(modifierTypes.GREAT_BALL); rewards.push(RewardId.GREAT_BALL);
} else if (roll < 60) { } else if (roll < 60) {
modifiers.push(modifierTypes.ULTRA_BALL); rewards.push(RewardId.ULTRA_BALL);
} else { } else {
modifiers.push(modifierTypes.ROGUE_BALL); rewards.push(RewardId.ROGUE_BALL);
} }
i++; i++;
} }
setEncounterRewards({ setEncounterRewards({
guaranteedModifierTypeFuncs: modifiers, guaranteedRewardSpecs: rewards,
fillRemaining: false, fillRemaining: false,
}); });
leaveEncounterWithoutBattle(); leaveEncounterWithoutBattle();

View File

@ -1,15 +1,16 @@
import { CLASSIC_MODE_MYSTERY_ENCOUNTER_WAVES } from "#app/constants"; import { CLASSIC_MODE_MYSTERY_ENCOUNTER_WAVES } from "#app/constants";
import { globalScene } from "#app/global-scene"; import { globalScene } from "#app/global-scene";
import { modifierTypes } from "#data/data-lists";
import { MoveCategory } from "#enums/move-category"; import { MoveCategory } from "#enums/move-category";
import { MysteryEncounterOptionMode } from "#enums/mystery-encounter-option-mode"; import { MysteryEncounterOptionMode } from "#enums/mystery-encounter-option-mode";
import { MysteryEncounterTier } from "#enums/mystery-encounter-tier"; import { MysteryEncounterTier } from "#enums/mystery-encounter-tier";
import { MysteryEncounterType } from "#enums/mystery-encounter-type"; import { MysteryEncounterType } from "#enums/mystery-encounter-type";
import { RewardId } from "#enums/reward-id";
import { Stat } from "#enums/stat"; import { Stat } from "#enums/stat";
import { TrainerItemId } from "#enums/trainer-item-id";
import type { PlayerPokemon } from "#field/pokemon"; import type { PlayerPokemon } from "#field/pokemon";
import { generateRewardOptionFromId } from "#items/reward-utils";
import type { PokemonMove } from "#moves/pokemon-move"; import type { PokemonMove } from "#moves/pokemon-move";
import { import {
generateModifierTypeOption,
leaveEncounterWithoutBattle, leaveEncounterWithoutBattle,
selectPokemonForOption, selectPokemonForOption,
setEncounterExp, setEncounterExp,
@ -95,16 +96,16 @@ export const FieldTripEncounter: MysteryEncounter = MysteryEncounterBuilder.with
.withOptionPhase(async () => { .withOptionPhase(async () => {
const encounter = globalScene.currentBattle.mysteryEncounter!; const encounter = globalScene.currentBattle.mysteryEncounter!;
if (encounter.misc.correctMove) { if (encounter.misc.correctMove) {
const modifiers = [ const rewards = [
generateModifierTypeOption(modifierTypes.TEMP_STAT_STAGE_BOOSTER, [Stat.ATK])!, generateRewardOptionFromId({ id: RewardId.TEMP_STAT_STAGE_BOOSTER, args: Stat.ATK })!,
generateModifierTypeOption(modifierTypes.TEMP_STAT_STAGE_BOOSTER, [Stat.DEF])!, generateRewardOptionFromId({ id: RewardId.TEMP_STAT_STAGE_BOOSTER, args: Stat.DEF })!,
generateModifierTypeOption(modifierTypes.TEMP_STAT_STAGE_BOOSTER, [Stat.SPD])!, generateRewardOptionFromId({ id: RewardId.TEMP_STAT_STAGE_BOOSTER, args: Stat.SPD })!,
generateModifierTypeOption(modifierTypes.DIRE_HIT)!, generateRewardOptionFromId(RewardId.DIRE_HIT)!,
generateModifierTypeOption(modifierTypes.RARER_CANDY)!, generateRewardOptionFromId(RewardId.RARER_CANDY)!,
]; ];
setEncounterRewards({ setEncounterRewards({
guaranteedModifierTypeOptions: modifiers, guaranteedRewardOptions: rewards,
fillRemaining: false, fillRemaining: false,
}); });
} }
@ -143,16 +144,16 @@ export const FieldTripEncounter: MysteryEncounter = MysteryEncounterBuilder.with
.withOptionPhase(async () => { .withOptionPhase(async () => {
const encounter = globalScene.currentBattle.mysteryEncounter!; const encounter = globalScene.currentBattle.mysteryEncounter!;
if (encounter.misc.correctMove) { if (encounter.misc.correctMove) {
const modifiers = [ const rewards = [
generateModifierTypeOption(modifierTypes.TEMP_STAT_STAGE_BOOSTER, [Stat.SPATK])!, generateRewardOptionFromId({ id: RewardId.TEMP_STAT_STAGE_BOOSTER, args: Stat.SPATK })!,
generateModifierTypeOption(modifierTypes.TEMP_STAT_STAGE_BOOSTER, [Stat.SPDEF])!, generateRewardOptionFromId({ id: RewardId.TEMP_STAT_STAGE_BOOSTER, args: Stat.SPDEF })!,
generateModifierTypeOption(modifierTypes.TEMP_STAT_STAGE_BOOSTER, [Stat.SPD])!, generateRewardOptionFromId({ id: RewardId.TEMP_STAT_STAGE_BOOSTER, args: Stat.SPD })!,
generateModifierTypeOption(modifierTypes.DIRE_HIT)!, generateRewardOptionFromId(RewardId.DIRE_HIT)!,
generateModifierTypeOption(modifierTypes.RARER_CANDY)!, generateRewardOptionFromId(RewardId.RARER_CANDY)!,
]; ];
setEncounterRewards({ setEncounterRewards({
guaranteedModifierTypeOptions: modifiers, guaranteedRewardOptions: rewards,
fillRemaining: false, fillRemaining: false,
}); });
} }
@ -191,16 +192,16 @@ export const FieldTripEncounter: MysteryEncounter = MysteryEncounterBuilder.with
.withOptionPhase(async () => { .withOptionPhase(async () => {
const encounter = globalScene.currentBattle.mysteryEncounter!; const encounter = globalScene.currentBattle.mysteryEncounter!;
if (encounter.misc.correctMove) { if (encounter.misc.correctMove) {
const modifiers = [ const rewards = [
generateModifierTypeOption(modifierTypes.TEMP_STAT_STAGE_BOOSTER, [Stat.ACC])!, generateRewardOptionFromId({ id: RewardId.TEMP_STAT_STAGE_BOOSTER, args: Stat.ACC })!,
generateModifierTypeOption(modifierTypes.TEMP_STAT_STAGE_BOOSTER, [Stat.SPD])!, generateRewardOptionFromId({ id: RewardId.TEMP_STAT_STAGE_BOOSTER, args: Stat.SPD })!,
generateModifierTypeOption(modifierTypes.GREAT_BALL)!, generateRewardOptionFromId(RewardId.GREAT_BALL)!,
generateModifierTypeOption(modifierTypes.IV_SCANNER)!, generateRewardOptionFromId(TrainerItemId.IV_SCANNER)!,
generateModifierTypeOption(modifierTypes.RARER_CANDY)!, generateRewardOptionFromId(RewardId.RARER_CANDY)!,
]; ];
setEncounterRewards({ setEncounterRewards({
guaranteedModifierTypeOptions: modifiers, guaranteedRewardOptions: rewards,
fillRemaining: false, fillRemaining: false,
}); });
} }

View File

@ -1,12 +1,13 @@
import { CLASSIC_MODE_MYSTERY_ENCOUNTER_WAVES } from "#app/constants"; import { CLASSIC_MODE_MYSTERY_ENCOUNTER_WAVES } from "#app/constants";
import { globalScene } from "#app/global-scene"; import { globalScene } from "#app/global-scene";
import { EncounterBattleAnim } from "#data/battle-anims"; import { EncounterBattleAnim } from "#data/battle-anims";
import { allAbilities, modifierTypes } from "#data/data-lists"; import { allAbilities, allHeldItems } from "#data/data-lists";
import { Gender } from "#data/gender"; import { Gender } from "#data/gender";
import { AbilityId } from "#enums/ability-id"; import { AbilityId } from "#enums/ability-id";
import { BattlerIndex } from "#enums/battler-index"; import { BattlerIndex } from "#enums/battler-index";
import { BattlerTagType } from "#enums/battler-tag-type"; import { BattlerTagType } from "#enums/battler-tag-type";
import { EncounterAnim } from "#enums/encounter-anims"; import { EncounterAnim } from "#enums/encounter-anims";
import { HeldItemCategoryId, HeldItemId } from "#enums/held-item-id";
import { MoveId } from "#enums/move-id"; import { MoveId } from "#enums/move-id";
import { MoveUseMode } from "#enums/move-use-mode"; import { MoveUseMode } from "#enums/move-use-mode";
import { MysteryEncounterOptionMode } from "#enums/mystery-encounter-option-mode"; import { MysteryEncounterOptionMode } from "#enums/mystery-encounter-option-mode";
@ -18,12 +19,11 @@ import { Stat } from "#enums/stat";
import { StatusEffect } from "#enums/status-effect"; import { StatusEffect } from "#enums/status-effect";
import { WeatherType } from "#enums/weather-type"; import { WeatherType } from "#enums/weather-type";
import type { Pokemon } from "#field/pokemon"; import type { Pokemon } from "#field/pokemon";
import type { AttackTypeBoosterModifierType } from "#modifiers/modifier-type"; import { getNewHeldItemFromCategory } from "#items/held-item-pool";
import { PokemonMove } from "#moves/pokemon-move"; import { PokemonMove } from "#moves/pokemon-move";
import { queueEncounterMessage } from "#mystery-encounters/encounter-dialogue-utils"; import { queueEncounterMessage } from "#mystery-encounters/encounter-dialogue-utils";
import type { EnemyPartyConfig } from "#mystery-encounters/encounter-phase-utils"; import type { EnemyPartyConfig } from "#mystery-encounters/encounter-phase-utils";
import { import {
generateModifierType,
initBattleWithEnemyConfig, initBattleWithEnemyConfig,
leaveEncounterWithoutBattle, leaveEncounterWithoutBattle,
loadCustomMovesForEncounter, loadCustomMovesForEncounter,
@ -31,11 +31,7 @@ import {
setEncounterRewards, setEncounterRewards,
transitionMysteryEncounterIntroVisuals, transitionMysteryEncounterIntroVisuals,
} from "#mystery-encounters/encounter-phase-utils"; } from "#mystery-encounters/encounter-phase-utils";
import { import { applyAbilityOverrideToPokemon, applyDamageToPokemon } from "#mystery-encounters/encounter-pokemon-utils";
applyAbilityOverrideToPokemon,
applyDamageToPokemon,
applyModifierTypeToPlayerPokemon,
} from "#mystery-encounters/encounter-pokemon-utils";
import type { MysteryEncounter } from "#mystery-encounters/mystery-encounter"; import type { MysteryEncounter } from "#mystery-encounters/mystery-encounter";
import { MysteryEncounterBuilder } from "#mystery-encounters/mystery-encounter"; import { MysteryEncounterBuilder } from "#mystery-encounters/mystery-encounter";
import { MysteryEncounterOptionBuilder } from "#mystery-encounters/mystery-encounter-option"; import { MysteryEncounterOptionBuilder } from "#mystery-encounters/mystery-encounter-option";
@ -254,7 +250,6 @@ export const FieryFalloutEncounter: MysteryEncounter = MysteryEncounterBuilder.w
} }
} }
// No rewards
leaveEncounterWithoutBattle(true); leaveEncounterWithoutBattle(true);
}, },
) )
@ -299,19 +294,17 @@ export const FieryFalloutEncounter: MysteryEncounter = MysteryEncounterBuilder.w
function giveLeadPokemonAttackTypeBoostItem() { function giveLeadPokemonAttackTypeBoostItem() {
// Give first party pokemon attack type boost item for free at end of battle // Give first party pokemon attack type boost item for free at end of battle
const leadPokemon = globalScene.getPlayerParty()?.[0]; const leadPokemon = globalScene.getPlayerParty()[0];
if (leadPokemon) { if (leadPokemon) {
// Generate type booster held item, default to Charcoal if item fails to generate // Generate type booster held item, default to Charcoal if item fails to generate
let boosterModifierType = generateModifierType(modifierTypes.ATTACK_TYPE_BOOSTER) as AttackTypeBoosterModifierType; let item = getNewHeldItemFromCategory(HeldItemCategoryId.TYPE_ATTACK_BOOSTER, leadPokemon);
if (!boosterModifierType) { if (!item) {
boosterModifierType = generateModifierType(modifierTypes.ATTACK_TYPE_BOOSTER, [ item = HeldItemId.CHARCOAL;
PokemonType.FIRE,
]) as AttackTypeBoosterModifierType;
} }
applyModifierTypeToPlayerPokemon(leadPokemon, boosterModifierType); leadPokemon.heldItemManager.add(item);
const encounter = globalScene.currentBattle.mysteryEncounter!; const encounter = globalScene.currentBattle.mysteryEncounter!;
encounter.setDialogueToken("itemName", boosterModifierType.name); encounter.setDialogueToken("itemName", allHeldItems[item].name);
encounter.setDialogueToken("leadPokemon", leadPokemon.getNameToRender()); encounter.setDialogueToken("leadPokemon", leadPokemon.getNameToRender());
queueEncounterMessage(`${namespace}:found_item`); queueEncounterMessage(`${namespace}:found_item`);
} }

View File

@ -1,14 +1,16 @@
import { CLASSIC_MODE_MYSTERY_ENCOUNTER_WAVES } from "#app/constants"; import { CLASSIC_MODE_MYSTERY_ENCOUNTER_WAVES } from "#app/constants";
import { globalScene } from "#app/global-scene"; import { globalScene } from "#app/global-scene";
import { BattlerTagType } from "#enums/battler-tag-type"; import { BattlerTagType } from "#enums/battler-tag-type";
import { ModifierPoolType } from "#enums/modifier-pool-type";
import { ModifierTier } from "#enums/modifier-tier";
import { MysteryEncounterOptionMode } from "#enums/mystery-encounter-option-mode"; import { MysteryEncounterOptionMode } from "#enums/mystery-encounter-option-mode";
import { MysteryEncounterTier } from "#enums/mystery-encounter-tier"; import { MysteryEncounterTier } from "#enums/mystery-encounter-tier";
import { MysteryEncounterType } from "#enums/mystery-encounter-type"; import { MysteryEncounterType } from "#enums/mystery-encounter-type";
import { RewardPoolType } from "#enums/reward-pool-type";
import { RarityTier } from "#enums/reward-tier";
import { TrainerItemId } from "#enums/trainer-item-id";
import type { Pokemon } from "#field/pokemon"; import type { Pokemon } from "#field/pokemon";
import type { ModifierTypeOption } from "#modifiers/modifier-type"; import type { RewardOption, TrainerItemReward } from "#items/reward";
import { getPlayerModifierTypeOptions, regenerateModifierPoolThresholds } from "#modifiers/modifier-type"; import { generatePlayerRewardOptions, generateRewardPoolWeights, getRewardPoolForType } from "#items/reward-pool-utils";
import { isTmReward } from "#items/reward-utils";
import { queueEncounterMessage } from "#mystery-encounters/encounter-dialogue-utils"; import { queueEncounterMessage } from "#mystery-encounters/encounter-dialogue-utils";
import type { EnemyPartyConfig } from "#mystery-encounters/encounter-phase-utils"; import type { EnemyPartyConfig } from "#mystery-encounters/encounter-phase-utils";
import { import {
@ -89,18 +91,18 @@ export const FightOrFlightEncounter: MysteryEncounter = MysteryEncounterBuilder.
// Waves 10-40 GREAT, 60-120 ULTRA, 120-160 ROGUE, 160-180 MASTER // Waves 10-40 GREAT, 60-120 ULTRA, 120-160 ROGUE, 160-180 MASTER
const tier = const tier =
globalScene.currentBattle.waveIndex > 160 globalScene.currentBattle.waveIndex > 160
? ModifierTier.MASTER ? RarityTier.MASTER
: globalScene.currentBattle.waveIndex > 120 : globalScene.currentBattle.waveIndex > 120
? ModifierTier.ROGUE ? RarityTier.ROGUE
: globalScene.currentBattle.waveIndex > 40 : globalScene.currentBattle.waveIndex > 40
? ModifierTier.ULTRA ? RarityTier.ULTRA
: ModifierTier.GREAT; : RarityTier.GREAT;
regenerateModifierPoolThresholds(globalScene.getPlayerParty(), ModifierPoolType.PLAYER, 0); generateRewardPoolWeights(getRewardPoolForType(RewardPoolType.PLAYER), globalScene.getPlayerParty(), 0);
let item: ModifierTypeOption | null = null; let item: RewardOption | null = null;
// TMs and Candy Jar excluded from possible rewards as they're too swingy in value for a singular item reward // TMs and Candy Jar excluded from possible rewards as they're too swingy in value for a singular item reward
while (!item || item.type.id.includes("TM_") || item.type.id === "CANDY_JAR") { while (!item || isTmReward(item.type) || (item.type as TrainerItemReward).itemId === TrainerItemId.CANDY_JAR) {
item = getPlayerModifierTypeOptions(1, globalScene.getPlayerParty(), [], { item = generatePlayerRewardOptions(1, globalScene.getPlayerParty(), [], {
guaranteedModifierTiers: [tier], guaranteedRarityTiers: [tier],
allowLuckUpgrades: false, allowLuckUpgrades: false,
})[0]; })[0];
} }
@ -151,9 +153,9 @@ export const FightOrFlightEncounter: MysteryEncounter = MysteryEncounterBuilder.
async () => { async () => {
// Pick battle // Pick battle
// Pokemon will randomly boost 1 stat by 2 stages // Pokemon will randomly boost 1 stat by 2 stages
const item = globalScene.currentBattle.mysteryEncounter!.misc as ModifierTypeOption; const item = globalScene.currentBattle.mysteryEncounter!.misc as RewardOption;
setEncounterRewards({ setEncounterRewards({
guaranteedModifierTypeOptions: [item], guaranteedRewardOptions: [item],
fillRemaining: false, fillRemaining: false,
}); });
await initBattleWithEnemyConfig(globalScene.currentBattle.mysteryEncounter!.enemyPartyConfigs[0]); await initBattleWithEnemyConfig(globalScene.currentBattle.mysteryEncounter!.enemyPartyConfigs[0]);
@ -175,9 +177,9 @@ export const FightOrFlightEncounter: MysteryEncounter = MysteryEncounterBuilder.
.withOptionPhase(async () => { .withOptionPhase(async () => {
// Pick steal // Pick steal
const encounter = globalScene.currentBattle.mysteryEncounter!; const encounter = globalScene.currentBattle.mysteryEncounter!;
const item = globalScene.currentBattle.mysteryEncounter!.misc as ModifierTypeOption; const item = globalScene.currentBattle.mysteryEncounter!.misc as RewardOption;
setEncounterRewards({ setEncounterRewards({
guaranteedModifierTypeOptions: [item], guaranteedRewardOptions: [item],
fillRemaining: false, fillRemaining: false,
}); });

View File

@ -1,10 +1,10 @@
import { CLASSIC_MODE_MYSTERY_ENCOUNTER_WAVES } from "#app/constants"; import { CLASSIC_MODE_MYSTERY_ENCOUNTER_WAVES } from "#app/constants";
import { globalScene } from "#app/global-scene"; import { globalScene } from "#app/global-scene";
import { getPokemonNameWithAffix } from "#app/messages"; import { getPokemonNameWithAffix } from "#app/messages";
import { modifierTypes } from "#data/data-lists";
import { SpeciesFormChangeActiveTrigger } from "#data/form-change-triggers"; import { SpeciesFormChangeActiveTrigger } from "#data/form-change-triggers";
import { getPokeballAtlasKey, getPokeballTintColor } from "#data/pokeball"; import { getPokeballAtlasKey, getPokeballTintColor } from "#data/pokeball";
import { FieldPosition } from "#enums/field-position"; import { FieldPosition } from "#enums/field-position";
import { HeldItemId } from "#enums/held-item-id";
import { MysteryEncounterOptionMode } from "#enums/mystery-encounter-option-mode"; import { MysteryEncounterOptionMode } from "#enums/mystery-encounter-option-mode";
import { MysteryEncounterTier } from "#enums/mystery-encounter-tier"; import { MysteryEncounterTier } from "#enums/mystery-encounter-tier";
import { MysteryEncounterType } from "#enums/mystery-encounter-type"; import { MysteryEncounterType } from "#enums/mystery-encounter-type";
@ -160,7 +160,7 @@ export const FunAndGamesEncounter: MysteryEncounter = MysteryEncounterBuilder.wi
], ],
}, },
async () => { async () => {
// Leave encounter with no rewards or exp // Leave encounter with no RewardId or exp
await transitionMysteryEncounterIntroVisuals(true, true); await transitionMysteryEncounterIntroVisuals(true, true);
leaveEncounterWithoutBattle(true); leaveEncounterWithoutBattle(true);
return true; return true;
@ -281,21 +281,21 @@ function handleNextTurn() {
if (healthRatio < 0.03) { if (healthRatio < 0.03) {
// Grand prize // Grand prize
setEncounterRewards({ setEncounterRewards({
guaranteedModifierTypeFuncs: [modifierTypes.MULTI_LENS], guaranteedRewardSpecs: [HeldItemId.MULTI_LENS],
fillRemaining: false, fillRemaining: false,
}); });
resultMessageKey = `${namespace}:best_result`; resultMessageKey = `${namespace}:best_result`;
} else if (healthRatio < 0.15) { } else if (healthRatio < 0.15) {
// 2nd prize // 2nd prize
setEncounterRewards({ setEncounterRewards({
guaranteedModifierTypeFuncs: [modifierTypes.SCOPE_LENS], guaranteedRewardSpecs: [HeldItemId.SCOPE_LENS],
fillRemaining: false, fillRemaining: false,
}); });
resultMessageKey = `${namespace}:great_result`; resultMessageKey = `${namespace}:great_result`;
} else if (healthRatio < 0.33) { } else if (healthRatio < 0.33) {
// 3rd prize // 3rd prize
setEncounterRewards({ setEncounterRewards({
guaranteedModifierTypeFuncs: [modifierTypes.WIDE_LENS], guaranteedRewardSpecs: [HeldItemId.WIDE_LENS],
fillRemaining: false, fillRemaining: false,
}); });
resultMessageKey = `${namespace}:good_result`; resultMessageKey = `${namespace}:good_result`;
@ -387,7 +387,7 @@ function summonPlayerPokemonAnimation(pokemon: PlayerPokemon): Promise<void> {
globalScene.add.existing(pokemon); globalScene.add.existing(pokemon);
globalScene.field.add(pokemon); globalScene.field.add(pokemon);
addPokeballOpenParticles(pokemon.x, pokemon.y - 16, pokemon.pokeball); addPokeballOpenParticles(pokemon.x, pokemon.y - 16, pokemon.pokeball);
globalScene.updateModifiers(true); globalScene.updateItems(true);
globalScene.updateFieldScale(); globalScene.updateFieldScale();
pokemon.showInfo(); pokemon.showInfo();
pokemon.playAnim(); pokemon.playAnim();

View File

@ -1,33 +1,30 @@
import { CLASSIC_MODE_MYSTERY_ENCOUNTER_WAVES } from "#app/constants"; import { CLASSIC_MODE_MYSTERY_ENCOUNTER_WAVES } from "#app/constants";
import { timedEventManager } from "#app/global-event-manager"; import { timedEventManager } from "#app/global-event-manager";
import { globalScene } from "#app/global-scene"; import { globalScene } from "#app/global-scene";
import { allSpecies } from "#data/data-lists"; import { allHeldItems, allSpecies } from "#data/data-lists";
import { Gender, getGenderSymbol } from "#data/gender"; import { Gender, getGenderSymbol } from "#data/gender";
import { getNatureName } from "#data/nature"; import { getNatureName } from "#data/nature";
import { getPokeballAtlasKey, getPokeballTintColor } from "#data/pokeball"; import { getPokeballAtlasKey, getPokeballTintColor } from "#data/pokeball";
import type { PokemonSpecies } from "#data/pokemon-species"; import type { PokemonSpecies } from "#data/pokemon-species";
import { getTypeRgb } from "#data/type"; import { getTypeRgb } from "#data/type";
import { ModifierPoolType } from "#enums/modifier-pool-type"; import { HeldItemCategoryId, type HeldItemId, isItemInCategory } from "#enums/held-item-id";
import { ModifierTier } from "#enums/modifier-tier";
import { MysteryEncounterOptionMode } from "#enums/mystery-encounter-option-mode"; import { MysteryEncounterOptionMode } from "#enums/mystery-encounter-option-mode";
import { MysteryEncounterTier } from "#enums/mystery-encounter-tier"; import { MysteryEncounterTier } from "#enums/mystery-encounter-tier";
import { MysteryEncounterType } from "#enums/mystery-encounter-type"; import { MysteryEncounterType } from "#enums/mystery-encounter-type";
import type { PokeballType } from "#enums/pokeball"; import type { PokeballType } from "#enums/pokeball";
import { RewardPoolType } from "#enums/reward-pool-type";
import { RarityTier } from "#enums/reward-tier";
import { SpeciesId } from "#enums/species-id"; import { SpeciesId } from "#enums/species-id";
import { TrainerSlot } from "#enums/trainer-slot"; import { TrainerSlot } from "#enums/trainer-slot";
import { TrainerType } from "#enums/trainer-type"; import { TrainerType } from "#enums/trainer-type";
import { doShinySparkleAnim } from "#field/anims"; import { doShinySparkleAnim } from "#field/anims";
import type { PlayerPokemon, Pokemon } from "#field/pokemon"; import type { PlayerPokemon, Pokemon } from "#field/pokemon";
import { EnemyPokemon } from "#field/pokemon"; import { EnemyPokemon } from "#field/pokemon";
import type { PokemonHeldItemModifier } from "#modifiers/modifier"; import { getHeldItemTier } from "#items/held-item-default-tiers";
import { import type { RewardOption } from "#items/reward";
HiddenAbilityRateBoosterModifier, import { generatePlayerRewardOptions, generateRewardPoolWeights, getRewardPoolForType } from "#items/reward-pool-utils";
PokemonFormChangeItemModifier, import { isTmReward } from "#items/reward-utils";
ShinyRateBoosterModifier, import { TrainerItemEffect } from "#items/trainer-item";
SpeciesStatBoosterModifier,
} from "#modifiers/modifier";
import type { ModifierTypeOption } from "#modifiers/modifier-type";
import { getPlayerModifierTypeOptions, regenerateModifierPoolThresholds } from "#modifiers/modifier-type";
import { PokemonMove } from "#moves/pokemon-move"; import { PokemonMove } from "#moves/pokemon-move";
import { getEncounterText, showEncounterText } from "#mystery-encounters/encounter-dialogue-utils"; import { getEncounterText, showEncounterText } from "#mystery-encounters/encounter-dialogue-utils";
import { import {
@ -209,9 +206,9 @@ export const GlobalTradeSystemEncounter: MysteryEncounter = MysteryEncounterBuil
const encounter = globalScene.currentBattle.mysteryEncounter!; const encounter = globalScene.currentBattle.mysteryEncounter!;
const tradedPokemon: PlayerPokemon = encounter.misc.tradedPokemon; const tradedPokemon: PlayerPokemon = encounter.misc.tradedPokemon;
const receivedPokemonData: EnemyPokemon = encounter.misc.receivedPokemon; const receivedPokemonData: EnemyPokemon = encounter.misc.receivedPokemon;
const modifiers = tradedPokemon const heldItemConfig = tradedPokemon.heldItemManager
.getHeldItems() .generateHeldItemConfiguration()
.filter(m => !(m instanceof PokemonFormChangeItemModifier) && !(m instanceof SpeciesStatBoosterModifier)); .filter(ic => !isItemInCategory(ic.entry as HeldItemId, HeldItemCategoryId.SPECIES_STAT_BOOSTER));
// Generate a trainer name // Generate a trainer name
const traderName = generateRandomTraderName(); const traderName = generateRandomTraderName();
@ -235,16 +232,12 @@ export const GlobalTradeSystemEncounter: MysteryEncounter = MysteryEncounterBuil
dataSource.variant, dataSource.variant,
dataSource.ivs, dataSource.ivs,
dataSource.nature, dataSource.nature,
heldItemConfig,
dataSource, dataSource,
); );
globalScene.getPlayerParty().push(newPlayerPokemon); globalScene.getPlayerParty().push(newPlayerPokemon);
await newPlayerPokemon.loadAssets(); await newPlayerPokemon.loadAssets();
for (const mod of modifiers) {
mod.pokemonId = newPlayerPokemon.id;
globalScene.addModifier(mod, true, false, false, true);
}
// Show the trade animation // Show the trade animation
await showTradeBackground(); await showTradeBackground();
await doPokemonTradeSequence(tradedPokemon, newPlayerPokemon); await doPokemonTradeSequence(tradedPokemon, newPlayerPokemon);
@ -278,7 +271,7 @@ export const GlobalTradeSystemEncounter: MysteryEncounter = MysteryEncounterBuil
if (timedEventManager.isEventActive()) { if (timedEventManager.isEventActive()) {
shinyThreshold.value *= timedEventManager.getShinyMultiplier(); shinyThreshold.value *= timedEventManager.getShinyMultiplier();
} }
globalScene.applyModifiers(ShinyRateBoosterModifier, true, shinyThreshold); globalScene.applyPlayerItems(TrainerItemEffect.SHINY_RATE_BOOSTER, { numberHolder: shinyThreshold });
// Base shiny chance of 512/65536 -> 1/128, affected by events and Shiny Charms // Base shiny chance of 512/65536 -> 1/128, affected by events and Shiny Charms
// Maximum shiny chance of 4096/65536 -> 1/16, cannot improve further after that // Maximum shiny chance of 4096/65536 -> 1/16, cannot improve further after that
@ -292,7 +285,9 @@ export const GlobalTradeSystemEncounter: MysteryEncounter = MysteryEncounterBuil
if (tradePokemon.species.abilityHidden) { if (tradePokemon.species.abilityHidden) {
if (tradePokemon.abilityIndex < hiddenIndex) { if (tradePokemon.abilityIndex < hiddenIndex) {
const hiddenAbilityChance = new NumberHolder(64); const hiddenAbilityChance = new NumberHolder(64);
globalScene.applyModifiers(HiddenAbilityRateBoosterModifier, true, hiddenAbilityChance); globalScene.applyPlayerItems(TrainerItemEffect.HIDDEN_ABILITY_CHANCE_BOOSTER, {
numberHolder: hiddenAbilityChance,
});
const hasHiddenAbility = !randSeedInt(hiddenAbilityChance.value); const hasHiddenAbility = !randSeedInt(hiddenAbilityChance.value);
@ -331,9 +326,9 @@ export const GlobalTradeSystemEncounter: MysteryEncounter = MysteryEncounterBuil
const encounter = globalScene.currentBattle.mysteryEncounter!; const encounter = globalScene.currentBattle.mysteryEncounter!;
const tradedPokemon: PlayerPokemon = encounter.misc.tradedPokemon; const tradedPokemon: PlayerPokemon = encounter.misc.tradedPokemon;
const receivedPokemonData: EnemyPokemon = encounter.misc.receivedPokemon; const receivedPokemonData: EnemyPokemon = encounter.misc.receivedPokemon;
const modifiers = tradedPokemon const heldItemConfig = tradedPokemon.heldItemManager
.getHeldItems() .generateHeldItemConfiguration()
.filter(m => !(m instanceof PokemonFormChangeItemModifier) && !(m instanceof SpeciesStatBoosterModifier)); .filter(ic => !isItemInCategory(ic.entry as HeldItemId, HeldItemCategoryId.SPECIES_STAT_BOOSTER));
// Generate a trainer name // Generate a trainer name
const traderName = generateRandomTraderName(); const traderName = generateRandomTraderName();
@ -356,16 +351,12 @@ export const GlobalTradeSystemEncounter: MysteryEncounter = MysteryEncounterBuil
dataSource.variant, dataSource.variant,
dataSource.ivs, dataSource.ivs,
dataSource.nature, dataSource.nature,
heldItemConfig,
dataSource, dataSource,
); );
globalScene.getPlayerParty().push(newPlayerPokemon); globalScene.getPlayerParty().push(newPlayerPokemon);
await newPlayerPokemon.loadAssets(); await newPlayerPokemon.loadAssets();
for (const mod of modifiers) {
mod.pokemonId = newPlayerPokemon.id;
globalScene.addModifier(mod, true, false, false, true);
}
// Show the trade animation // Show the trade animation
await showTradeBackground(); await showTradeBackground();
await doPokemonTradeSequence(tradedPokemon, newPlayerPokemon); await doPokemonTradeSequence(tradedPokemon, newPlayerPokemon);
@ -390,17 +381,15 @@ export const GlobalTradeSystemEncounter: MysteryEncounter = MysteryEncounterBuil
const encounter = globalScene.currentBattle.mysteryEncounter!; const encounter = globalScene.currentBattle.mysteryEncounter!;
const onPokemonSelected = (pokemon: PlayerPokemon) => { const onPokemonSelected = (pokemon: PlayerPokemon) => {
// Get Pokemon held items and filter for valid ones // Get Pokemon held items and filter for valid ones
const validItems = pokemon.getHeldItems().filter(it => { const validItems = pokemon.heldItemManager.getTransferableHeldItems();
return it.isTransferable;
});
return validItems.map((modifier: PokemonHeldItemModifier) => { return validItems.map((id: HeldItemId) => {
const option: OptionSelectItem = { const option: OptionSelectItem = {
label: modifier.type.name, label: allHeldItems[id].name,
handler: () => { handler: () => {
// Pokemon and item selected // Pokemon and item selected
encounter.setDialogueToken("chosenItem", modifier.type.name); encounter.setDialogueToken("chosenItem", allHeldItems[id].name);
encounter.misc.chosenModifier = modifier; encounter.misc.chosenHeldItem = id;
encounter.misc.chosenPokemon = pokemon; encounter.misc.chosenPokemon = pokemon;
return true; return true;
}, },
@ -411,10 +400,7 @@ export const GlobalTradeSystemEncounter: MysteryEncounter = MysteryEncounterBuil
const selectableFilter = (pokemon: Pokemon) => { const selectableFilter = (pokemon: Pokemon) => {
// If pokemon has items to trade // If pokemon has items to trade
const meetsReqs = const meetsReqs = pokemon.heldItemManager.getTransferableHeldItems().length > 0;
pokemon.getHeldItems().filter(it => {
return it.isTransferable;
}).length > 0;
if (!meetsReqs) { if (!meetsReqs) {
return getEncounterText(`${namespace}:option.3.invalid_selection`) ?? null; return getEncounterText(`${namespace}:option.3.invalid_selection`) ?? null;
} }
@ -426,44 +412,36 @@ export const GlobalTradeSystemEncounter: MysteryEncounter = MysteryEncounterBuil
}) })
.withOptionPhase(async () => { .withOptionPhase(async () => {
const encounter = globalScene.currentBattle.mysteryEncounter!; const encounter = globalScene.currentBattle.mysteryEncounter!;
const modifier = encounter.misc.chosenModifier as PokemonHeldItemModifier; const heldItemId = encounter.misc.chosenHeldItem as HeldItemId;
const party = globalScene.getPlayerParty(); const party = globalScene.getPlayerParty();
const chosenPokemon: PlayerPokemon = encounter.misc.chosenPokemon; const chosenPokemon: PlayerPokemon = encounter.misc.chosenPokemon;
// Check tier of the traded item, the received item will be one tier up // Check tier of the traded item, the received item will be one tier up
const type = modifier.type.withTierFromPool(ModifierPoolType.PLAYER, party); let tier = getHeldItemTier(heldItemId) ?? RarityTier.GREAT;
let tier = type.tier ?? ModifierTier.GREAT;
// Eggs and White Herb are not in the pool
if (type.id === "WHITE_HERB") {
tier = ModifierTier.GREAT;
} else if (type.id === "LUCKY_EGG") {
tier = ModifierTier.ULTRA;
} else if (type.id === "GOLDEN_EGG") {
tier = ModifierTier.ROGUE;
}
// Increment tier by 1 // Increment tier by 1
if (tier < ModifierTier.MASTER) { if (tier < RarityTier.MASTER) {
tier++; tier++;
} }
regenerateModifierPoolThresholds(party, ModifierPoolType.PLAYER, 0); generateRewardPoolWeights(getRewardPoolForType(RewardPoolType.PLAYER), party, 0);
let item: ModifierTypeOption | null = null; let item: RewardOption | null = null;
// TMs excluded from possible rewards // TMs excluded from possible rewards
while (!item || item.type.id.includes("TM_")) { while (!item || isTmReward(item.type)) {
item = getPlayerModifierTypeOptions(1, party, [], { item = generatePlayerRewardOptions(1, party, [], {
guaranteedModifierTiers: [tier], guaranteedRarityTiers: [tier],
allowLuckUpgrades: false, allowLuckUpgrades: false,
})[0]; })[0];
} }
encounter.setDialogueToken("itemName", item.type.name); encounter.setDialogueToken("itemName", item.type.name);
setEncounterRewards({ setEncounterRewards({
guaranteedModifierTypeOptions: [item], guaranteedRewardOptions: [item],
fillRemaining: false, fillRemaining: false,
}); });
chosenPokemon.loseHeldItem(modifier, false); chosenPokemon.heldItemManager.remove(heldItemId);
await globalScene.updateModifiers(true, true); await globalScene.updateItems(true);
// Generate a trainer name // Generate a trainer name
const traderName = generateRandomTraderName(); const traderName = generateRandomTraderName();

View File

@ -1,10 +1,10 @@
import { CLASSIC_MODE_MYSTERY_ENCOUNTER_WAVES } from "#app/constants"; import { CLASSIC_MODE_MYSTERY_ENCOUNTER_WAVES } from "#app/constants";
import { globalScene } from "#app/global-scene"; import { globalScene } from "#app/global-scene";
import { modifierTypes } from "#data/data-lists";
import { ModifierTier } from "#enums/modifier-tier";
import { MysteryEncounterTier } from "#enums/mystery-encounter-tier"; import { MysteryEncounterTier } from "#enums/mystery-encounter-tier";
import { MysteryEncounterType } from "#enums/mystery-encounter-type"; import { MysteryEncounterType } from "#enums/mystery-encounter-type";
import { PartyMemberStrength } from "#enums/party-member-strength"; import { PartyMemberStrength } from "#enums/party-member-strength";
import { RewardId } from "#enums/reward-id";
import { RarityTier } from "#enums/reward-tier";
import type { EnemyPartyConfig } from "#mystery-encounters/encounter-phase-utils"; import type { EnemyPartyConfig } from "#mystery-encounters/encounter-phase-utils";
import { initBattleWithEnemyConfig, setEncounterRewards } from "#mystery-encounters/encounter-phase-utils"; import { initBattleWithEnemyConfig, setEncounterRewards } from "#mystery-encounters/encounter-phase-utils";
import type { MysteryEncounter } from "#mystery-encounters/mystery-encounter"; import type { MysteryEncounter } from "#mystery-encounters/mystery-encounter";
@ -147,7 +147,7 @@ export const MysteriousChallengersEncounter: MysteryEncounter = MysteryEncounter
const config: EnemyPartyConfig = encounter.enemyPartyConfigs[0]; const config: EnemyPartyConfig = encounter.enemyPartyConfigs[0];
setEncounterRewards({ setEncounterRewards({
guaranteedModifierTypeFuncs: [modifierTypes.TM_COMMON, modifierTypes.TM_GREAT, modifierTypes.MEMORY_MUSHROOM], guaranteedRewardSpecs: [RewardId.TM_COMMON, RewardId.TM_GREAT, RewardId.MEMORY_MUSHROOM],
fillRemaining: true, fillRemaining: true,
}); });
@ -175,7 +175,7 @@ export const MysteriousChallengersEncounter: MysteryEncounter = MysteryEncounter
const config: EnemyPartyConfig = encounter.enemyPartyConfigs[1]; const config: EnemyPartyConfig = encounter.enemyPartyConfigs[1];
setEncounterRewards({ setEncounterRewards({
guaranteedModifierTiers: [ModifierTier.ULTRA, ModifierTier.ULTRA, ModifierTier.GREAT, ModifierTier.GREAT], guaranteedRarityTiers: [RarityTier.ULTRA, RarityTier.ULTRA, RarityTier.GREAT, RarityTier.GREAT],
fillRemaining: true, fillRemaining: true,
}); });
@ -206,7 +206,7 @@ export const MysteriousChallengersEncounter: MysteryEncounter = MysteryEncounter
encounter.expMultiplier = 0.9; encounter.expMultiplier = 0.9;
setEncounterRewards({ setEncounterRewards({
guaranteedModifierTiers: [ModifierTier.ROGUE, ModifierTier.ROGUE, ModifierTier.ULTRA, ModifierTier.GREAT], guaranteedRarityTiers: [RarityTier.ROGUE, RarityTier.ROGUE, RarityTier.ULTRA, RarityTier.GREAT],
fillRemaining: true, fillRemaining: true,
}); });

View File

@ -1,10 +1,10 @@
import { CLASSIC_MODE_MYSTERY_ENCOUNTER_WAVES } from "#app/constants"; import { CLASSIC_MODE_MYSTERY_ENCOUNTER_WAVES } from "#app/constants";
import { globalScene } from "#app/global-scene"; import { globalScene } from "#app/global-scene";
import { ModifierTier } from "#enums/modifier-tier";
import { MoveId } from "#enums/move-id"; import { MoveId } from "#enums/move-id";
import { MysteryEncounterOptionMode } from "#enums/mystery-encounter-option-mode"; import { MysteryEncounterOptionMode } from "#enums/mystery-encounter-option-mode";
import { MysteryEncounterTier } from "#enums/mystery-encounter-tier"; import { MysteryEncounterTier } from "#enums/mystery-encounter-tier";
import { MysteryEncounterType } from "#enums/mystery-encounter-type"; import { MysteryEncounterType } from "#enums/mystery-encounter-type";
import { RarityTier } from "#enums/reward-tier";
import { SpeciesId } from "#enums/species-id"; import { SpeciesId } from "#enums/species-id";
import { queueEncounterMessage, showEncounterText } from "#mystery-encounters/encounter-dialogue-utils"; import { queueEncounterMessage, showEncounterText } from "#mystery-encounters/encounter-dialogue-utils";
import type { EnemyPartyConfig } from "#mystery-encounters/encounter-phase-utils"; import type { EnemyPartyConfig } from "#mystery-encounters/encounter-phase-utils";
@ -141,7 +141,7 @@ export const MysteriousChestEncounter: MysteryEncounter = MysteryEncounterBuilde
if (roll >= RAND_LENGTH - COMMON_REWARDS_PERCENT) { if (roll >= RAND_LENGTH - COMMON_REWARDS_PERCENT) {
// Choose between 2 COMMON / 2 GREAT tier items (20%) // Choose between 2 COMMON / 2 GREAT tier items (20%)
setEncounterRewards({ setEncounterRewards({
guaranteedModifierTiers: [ModifierTier.COMMON, ModifierTier.COMMON, ModifierTier.GREAT, ModifierTier.GREAT], guaranteedRarityTiers: [RarityTier.COMMON, RarityTier.COMMON, RarityTier.GREAT, RarityTier.GREAT],
}); });
// Display result message then proceed to rewards // Display result message then proceed to rewards
queueEncounterMessage(`${namespace}:option.1.normal`); queueEncounterMessage(`${namespace}:option.1.normal`);
@ -149,7 +149,7 @@ export const MysteriousChestEncounter: MysteryEncounter = MysteryEncounterBuilde
} else if (roll >= RAND_LENGTH - COMMON_REWARDS_PERCENT - ULTRA_REWARDS_PERCENT) { } else if (roll >= RAND_LENGTH - COMMON_REWARDS_PERCENT - ULTRA_REWARDS_PERCENT) {
// Choose between 3 ULTRA tier items (30%) // Choose between 3 ULTRA tier items (30%)
setEncounterRewards({ setEncounterRewards({
guaranteedModifierTiers: [ModifierTier.ULTRA, ModifierTier.ULTRA, ModifierTier.ULTRA], guaranteedRarityTiers: [RarityTier.ULTRA, RarityTier.ULTRA, RarityTier.ULTRA],
}); });
// Display result message then proceed to rewards // Display result message then proceed to rewards
queueEncounterMessage(`${namespace}:option.1.good`); queueEncounterMessage(`${namespace}:option.1.good`);
@ -157,7 +157,7 @@ export const MysteriousChestEncounter: MysteryEncounter = MysteryEncounterBuilde
} else if (roll >= RAND_LENGTH - COMMON_REWARDS_PERCENT - ULTRA_REWARDS_PERCENT - ROGUE_REWARDS_PERCENT) { } else if (roll >= RAND_LENGTH - COMMON_REWARDS_PERCENT - ULTRA_REWARDS_PERCENT - ROGUE_REWARDS_PERCENT) {
// Choose between 2 ROGUE tier items (10%) // Choose between 2 ROGUE tier items (10%)
setEncounterRewards({ setEncounterRewards({
guaranteedModifierTiers: [ModifierTier.ROGUE, ModifierTier.ROGUE], guaranteedRarityTiers: [RarityTier.ROGUE, RarityTier.ROGUE],
}); });
// Display result message then proceed to rewards // Display result message then proceed to rewards
queueEncounterMessage(`${namespace}:option.1.great`); queueEncounterMessage(`${namespace}:option.1.great`);
@ -168,7 +168,7 @@ export const MysteriousChestEncounter: MysteryEncounter = MysteryEncounterBuilde
) { ) {
// Choose 1 MASTER tier item (5%) // Choose 1 MASTER tier item (5%)
setEncounterRewards({ setEncounterRewards({
guaranteedModifierTiers: [ModifierTier.MASTER], guaranteedRarityTiers: [RarityTier.MASTER],
}); });
// Display result message then proceed to rewards // Display result message then proceed to rewards
queueEncounterMessage(`${namespace}:option.1.amazing`); queueEncounterMessage(`${namespace}:option.1.amazing`);

View File

@ -8,9 +8,10 @@ import { MysteryEncounterTier } from "#enums/mystery-encounter-tier";
import { MysteryEncounterType } from "#enums/mystery-encounter-type"; import { MysteryEncounterType } from "#enums/mystery-encounter-type";
import { PlayerGender } from "#enums/player-gender"; import { PlayerGender } from "#enums/player-gender";
import { PokeballType } from "#enums/pokeball"; import { PokeballType } from "#enums/pokeball";
import { TrainerItemId } from "#enums/trainer-item-id";
import { TrainerSlot } from "#enums/trainer-slot"; import { TrainerSlot } from "#enums/trainer-slot";
import type { EnemyPokemon } from "#field/pokemon"; import type { EnemyPokemon } from "#field/pokemon";
import { HiddenAbilityRateBoosterModifier, IvScannerModifier } from "#modifiers/modifier"; import { TrainerItemEffect } from "#items/trainer-item";
import { getEncounterText, showEncounterText } from "#mystery-encounters/encounter-dialogue-utils"; import { getEncounterText, showEncounterText } from "#mystery-encounters/encounter-dialogue-utils";
import { import {
initSubsequentOptionSelect, initSubsequentOptionSelect,
@ -297,7 +298,9 @@ async function summonSafariPokemon() {
const hiddenIndex = pokemon.species.ability2 ? 2 : 1; const hiddenIndex = pokemon.species.ability2 ? 2 : 1;
if (pokemon.abilityIndex < hiddenIndex) { if (pokemon.abilityIndex < hiddenIndex) {
const hiddenAbilityChance = new NumberHolder(256); const hiddenAbilityChance = new NumberHolder(256);
globalScene.applyModifiers(HiddenAbilityRateBoosterModifier, true, hiddenAbilityChance); globalScene.applyPlayerItems(TrainerItemEffect.HIDDEN_ABILITY_CHANCE_BOOSTER, {
numberHolder: hiddenAbilityChance,
});
const hasHiddenAbility = !randSeedInt(hiddenAbilityChance.value); const hasHiddenAbility = !randSeedInt(hiddenAbilityChance.value);
@ -332,8 +335,7 @@ async function summonSafariPokemon() {
// shows up and the IV scanner breaks. For now, we place the IV scanner code // shows up and the IV scanner breaks. For now, we place the IV scanner code
// separately so that at least the IV scanner works. // separately so that at least the IV scanner works.
const ivScannerModifier = globalScene.findModifier(m => m instanceof IvScannerModifier); if (globalScene.trainerItems.hasItem(TrainerItemId.IV_SCANNER)) {
if (ivScannerModifier) {
globalScene.phaseManager.pushNew("ScanIvsPhase", pokemon.getBattlerIndex()); globalScene.phaseManager.pushNew("ScanIvsPhase", pokemon.getBattlerIndex());
} }
} }

View File

@ -1,6 +1,6 @@
import { CLASSIC_MODE_MYSTERY_ENCOUNTER_WAVES } from "#app/constants"; import { CLASSIC_MODE_MYSTERY_ENCOUNTER_WAVES } from "#app/constants";
import { globalScene } from "#app/global-scene"; import { globalScene } from "#app/global-scene";
import { modifierTypes } from "#data/data-lists"; import { allHeldItems } from "#data/data-lists";
import { getNatureName } from "#data/nature"; import { getNatureName } from "#data/nature";
import { MysteryEncounterOptionMode } from "#enums/mystery-encounter-option-mode"; import { MysteryEncounterOptionMode } from "#enums/mystery-encounter-option-mode";
import { MysteryEncounterTier } from "#enums/mystery-encounter-tier"; import { MysteryEncounterTier } from "#enums/mystery-encounter-tier";
@ -8,9 +8,9 @@ import { MysteryEncounterType } from "#enums/mystery-encounter-type";
import type { Nature } from "#enums/nature"; import type { Nature } from "#enums/nature";
import { SpeciesId } from "#enums/species-id"; import { SpeciesId } from "#enums/species-id";
import type { PlayerPokemon, Pokemon } from "#field/pokemon"; import type { PlayerPokemon, Pokemon } from "#field/pokemon";
import { getNewVitaminHeldItem } from "#items/held-item-pool";
import { getEncounterText, queueEncounterMessage } from "#mystery-encounters/encounter-dialogue-utils"; import { getEncounterText, queueEncounterMessage } from "#mystery-encounters/encounter-dialogue-utils";
import { import {
generateModifierType,
leaveEncounterWithoutBattle, leaveEncounterWithoutBattle,
selectPokemonForOption, selectPokemonForOption,
setEncounterExp, setEncounterExp,
@ -18,7 +18,6 @@ import {
} from "#mystery-encounters/encounter-phase-utils"; } from "#mystery-encounters/encounter-phase-utils";
import { import {
applyDamageToPokemon, applyDamageToPokemon,
applyModifierTypeToPlayerPokemon,
isPokemonValidForEncounterOptionSelection, isPokemonValidForEncounterOptionSelection,
} from "#mystery-encounters/encounter-pokemon-utils"; } from "#mystery-encounters/encounter-pokemon-utils";
import type { MysteryEncounter } from "#mystery-encounters/mystery-encounter"; import type { MysteryEncounter } from "#mystery-encounters/mystery-encounter";
@ -96,15 +95,12 @@ export const ShadyVitaminDealerEncounter: MysteryEncounter = MysteryEncounterBui
// Update money // Update money
updatePlayerMoney(-(encounter.options[0].requirements[0] as MoneyRequirement).requiredMoney); updatePlayerMoney(-(encounter.options[0].requirements[0] as MoneyRequirement).requiredMoney);
// Calculate modifiers and dialogue tokens // Calculate modifiers and dialogue tokens
const modifiers = [ const items = [getNewVitaminHeldItem(), getNewVitaminHeldItem()];
generateModifierType(modifierTypes.BASE_STAT_BOOSTER)!, encounter.setDialogueToken("boost1", allHeldItems[items[0]].name);
generateModifierType(modifierTypes.BASE_STAT_BOOSTER)!, encounter.setDialogueToken("boost2", allHeldItems[items[1]].name);
];
encounter.setDialogueToken("boost1", modifiers[0].name);
encounter.setDialogueToken("boost2", modifiers[1].name);
encounter.misc = { encounter.misc = {
chosenPokemon: pokemon, chosenPokemon: pokemon,
modifiers: modifiers, items: items,
}; };
}; };
@ -131,10 +127,10 @@ export const ShadyVitaminDealerEncounter: MysteryEncounter = MysteryEncounterBui
// Choose Cheap Option // Choose Cheap Option
const encounter = globalScene.currentBattle.mysteryEncounter!; const encounter = globalScene.currentBattle.mysteryEncounter!;
const chosenPokemon = encounter.misc.chosenPokemon; const chosenPokemon = encounter.misc.chosenPokemon;
const modifiers = encounter.misc.modifiers; const items = encounter.misc.items;
for (const modType of modifiers) { for (const item of items) {
await applyModifierTypeToPlayerPokemon(chosenPokemon, modType); chosenPokemon.heldItemManager.add(item);
} }
leaveEncounterWithoutBattle(true); leaveEncounterWithoutBattle(true);
@ -179,15 +175,12 @@ export const ShadyVitaminDealerEncounter: MysteryEncounter = MysteryEncounterBui
// Update money // Update money
updatePlayerMoney(-(encounter.options[1].requirements[0] as MoneyRequirement).requiredMoney); updatePlayerMoney(-(encounter.options[1].requirements[0] as MoneyRequirement).requiredMoney);
// Calculate modifiers and dialogue tokens // Calculate modifiers and dialogue tokens
const modifiers = [ const items = [getNewVitaminHeldItem(), getNewVitaminHeldItem()];
generateModifierType(modifierTypes.BASE_STAT_BOOSTER)!, encounter.setDialogueToken("boost1", allHeldItems[items[0]].name);
generateModifierType(modifierTypes.BASE_STAT_BOOSTER)!, encounter.setDialogueToken("boost2", allHeldItems[items[1]].name);
];
encounter.setDialogueToken("boost1", modifiers[0].name);
encounter.setDialogueToken("boost2", modifiers[1].name);
encounter.misc = { encounter.misc = {
chosenPokemon: pokemon, chosenPokemon: pokemon,
modifiers: modifiers, items: items,
}; };
}; };
@ -202,10 +195,10 @@ export const ShadyVitaminDealerEncounter: MysteryEncounter = MysteryEncounterBui
// Choose Expensive Option // Choose Expensive Option
const encounter = globalScene.currentBattle.mysteryEncounter!; const encounter = globalScene.currentBattle.mysteryEncounter!;
const chosenPokemon = encounter.misc.chosenPokemon; const chosenPokemon = encounter.misc.chosenPokemon;
const modifiers = encounter.misc.modifiers; const items = encounter.misc.items;
for (const modType of modifiers) { for (const item of items) {
await applyModifierTypeToPlayerPokemon(chosenPokemon, modType); chosenPokemon.heldItemManager.add(item);
} }
leaveEncounterWithoutBattle(true); leaveEncounterWithoutBattle(true);

View File

@ -1,9 +1,8 @@
import { globalScene } from "#app/global-scene"; import { globalScene } from "#app/global-scene";
import { modifierTypes } from "#data/data-lists";
import { CustomPokemonData } from "#data/pokemon-data"; import { CustomPokemonData } from "#data/pokemon-data";
import { AiType } from "#enums/ai-type"; import { AiType } from "#enums/ai-type";
import { BattlerIndex } from "#enums/battler-index"; import { BattlerIndex } from "#enums/battler-index";
import { BerryType } from "#enums/berry-type"; import { HeldItemId } from "#enums/held-item-id";
import { MoveId } from "#enums/move-id"; import { MoveId } from "#enums/move-id";
import { MoveUseMode } from "#enums/move-use-mode"; import { MoveUseMode } from "#enums/move-use-mode";
import { MysteryEncounterOptionMode } from "#enums/mystery-encounter-option-mode"; import { MysteryEncounterOptionMode } from "#enums/mystery-encounter-option-mode";
@ -11,14 +10,11 @@ import { MysteryEncounterTier } from "#enums/mystery-encounter-tier";
import { MysteryEncounterType } from "#enums/mystery-encounter-type"; import { MysteryEncounterType } from "#enums/mystery-encounter-type";
import { Nature } from "#enums/nature"; import { Nature } from "#enums/nature";
import { SpeciesId } from "#enums/species-id"; import { SpeciesId } from "#enums/species-id";
import { Stat } from "#enums/stat";
import { StatusEffect } from "#enums/status-effect"; import { StatusEffect } from "#enums/status-effect";
import type { PokemonHeldItemModifierType } from "#modifiers/modifier-type";
import { PokemonMove } from "#moves/pokemon-move"; import { PokemonMove } from "#moves/pokemon-move";
import { queueEncounterMessage } from "#mystery-encounters/encounter-dialogue-utils"; import { queueEncounterMessage } from "#mystery-encounters/encounter-dialogue-utils";
import type { EnemyPartyConfig, EnemyPokemonConfig } from "#mystery-encounters/encounter-phase-utils"; import type { EnemyPartyConfig, EnemyPokemonConfig } from "#mystery-encounters/encounter-phase-utils";
import { import {
generateModifierType,
initBattleWithEnemyConfig, initBattleWithEnemyConfig,
leaveEncounterWithoutBattle, leaveEncounterWithoutBattle,
loadCustomMovesForEncounter, loadCustomMovesForEncounter,
@ -78,24 +74,12 @@ export const SlumberingSnorlaxEncounter: MysteryEncounter = MysteryEncounterBuil
status: [StatusEffect.SLEEP, 6], // Extra turns on timer for Snorlax's start of fight moves status: [StatusEffect.SLEEP, 6], // Extra turns on timer for Snorlax's start of fight moves
nature: Nature.DOCILE, nature: Nature.DOCILE,
moveSet: [MoveId.BODY_SLAM, MoveId.CRUNCH, MoveId.SLEEP_TALK, MoveId.REST], moveSet: [MoveId.BODY_SLAM, MoveId.CRUNCH, MoveId.SLEEP_TALK, MoveId.REST],
modifierConfigs: [ heldItemConfig: [
{ { entry: HeldItemId.SITRUS_BERRY, count: 1 },
modifier: generateModifierType(modifierTypes.BERRY, [BerryType.SITRUS]) as PokemonHeldItemModifierType, { entry: HeldItemId.ENIGMA_BERRY, count: 1 },
}, { entry: HeldItemId.HP_UP, count: 1 },
{ { entry: HeldItemId.SOOTHE_BELL, count: randSeedInt(2, 0) },
modifier: generateModifierType(modifierTypes.BERRY, [BerryType.ENIGMA]) as PokemonHeldItemModifierType, { entry: HeldItemId.LUCKY_EGG, count: randSeedInt(2, 0) },
},
{
modifier: generateModifierType(modifierTypes.BASE_STAT_BOOSTER, [Stat.HP]) as PokemonHeldItemModifierType,
},
{
modifier: generateModifierType(modifierTypes.SOOTHE_BELL) as PokemonHeldItemModifierType,
stackCount: randSeedInt(2, 0),
},
{
modifier: generateModifierType(modifierTypes.LUCKY_EGG) as PokemonHeldItemModifierType,
stackCount: randSeedInt(2, 0),
},
], ],
customPokemonData: new CustomPokemonData({ spriteScale: 1.25 }), customPokemonData: new CustomPokemonData({ spriteScale: 1.25 }),
aiType: AiType.SMART, // Required to ensure Snorlax uses Sleep Talk while it is asleep aiType: AiType.SMART, // Required to ensure Snorlax uses Sleep Talk while it is asleep
@ -131,7 +115,7 @@ export const SlumberingSnorlaxEncounter: MysteryEncounter = MysteryEncounterBuil
// Pick battle // Pick battle
const encounter = globalScene.currentBattle.mysteryEncounter!; const encounter = globalScene.currentBattle.mysteryEncounter!;
setEncounterRewards({ setEncounterRewards({
guaranteedModifierTypeFuncs: [modifierTypes.LEFTOVERS], guaranteedRewardSpecs: [HeldItemId.LEFTOVERS],
fillRemaining: true, fillRemaining: true,
}); });
encounter.startOfBattleEffects.push({ encounter.startOfBattleEffects.push({
@ -178,7 +162,7 @@ export const SlumberingSnorlaxEncounter: MysteryEncounter = MysteryEncounterBuil
// Steal the Snorlax's Leftovers // Steal the Snorlax's Leftovers
const instance = globalScene.currentBattle.mysteryEncounter!; const instance = globalScene.currentBattle.mysteryEncounter!;
setEncounterRewards({ setEncounterRewards({
guaranteedModifierTypeFuncs: [modifierTypes.LEFTOVERS], guaranteedRewardSpecs: [HeldItemId.LEFTOVERS],
fillRemaining: false, fillRemaining: false,
}); });
// Snorlax exp to Pokemon that did the stealing // Snorlax exp to Pokemon that did the stealing

View File

@ -1,9 +1,9 @@
import { CLASSIC_MODE_MYSTERY_ENCOUNTER_WAVES } from "#app/constants"; import { CLASSIC_MODE_MYSTERY_ENCOUNTER_WAVES } from "#app/constants";
import { globalScene } from "#app/global-scene"; import { globalScene } from "#app/global-scene";
import { getPokemonNameWithAffix } from "#app/messages"; import { getPokemonNameWithAffix } from "#app/messages";
import { modifierTypes } from "#data/data-lists";
import { BattlerTagType } from "#enums/battler-tag-type"; import { BattlerTagType } from "#enums/battler-tag-type";
import { BiomeId } from "#enums/biome-id"; import { BiomeId } from "#enums/biome-id";
import { HeldItemId } from "#enums/held-item-id";
import { MysteryEncounterOptionMode } from "#enums/mystery-encounter-option-mode"; import { MysteryEncounterOptionMode } from "#enums/mystery-encounter-option-mode";
import { MysteryEncounterTier } from "#enums/mystery-encounter-tier"; import { MysteryEncounterTier } from "#enums/mystery-encounter-tier";
import { MysteryEncounterType } from "#enums/mystery-encounter-type"; import { MysteryEncounterType } from "#enums/mystery-encounter-type";
@ -13,11 +13,9 @@ import { TrainerSlot } from "#enums/trainer-slot";
import { getBiomeKey } from "#field/arena"; import { getBiomeKey } from "#field/arena";
import type { Pokemon } from "#field/pokemon"; import type { Pokemon } from "#field/pokemon";
import { EnemyPokemon } from "#field/pokemon"; import { EnemyPokemon } from "#field/pokemon";
import { getPartyLuckValue } from "#modifiers/modifier-type";
import { queueEncounterMessage, showEncounterText } from "#mystery-encounters/encounter-dialogue-utils"; import { queueEncounterMessage, showEncounterText } from "#mystery-encounters/encounter-dialogue-utils";
import type { EnemyPartyConfig } from "#mystery-encounters/encounter-phase-utils"; import type { EnemyPartyConfig } from "#mystery-encounters/encounter-phase-utils";
import { import {
generateModifierTypeOption,
initBattleWithEnemyConfig, initBattleWithEnemyConfig,
setEncounterExp, setEncounterExp,
setEncounterRewards, setEncounterRewards,
@ -34,6 +32,7 @@ import { MysteryEncounterOptionBuilder } from "#mystery-encounters/mystery-encou
import { MoneyRequirement, WaveModulusRequirement } from "#mystery-encounters/mystery-encounter-requirements"; import { MoneyRequirement, WaveModulusRequirement } from "#mystery-encounters/mystery-encounter-requirements";
import { PokemonData } from "#system/pokemon-data"; import { PokemonData } from "#system/pokemon-data";
import { randSeedInt } from "#utils/common"; import { randSeedInt } from "#utils/common";
import { getPartyLuckValue } from "#utils/party";
/** the i18n namespace for this encounter */ /** the i18n namespace for this encounter */
const namespace = "mysteryEncounters/teleportingHijinks"; const namespace = "mysteryEncounters/teleportingHijinks";
@ -173,10 +172,8 @@ export const TeleportingHijinksEncounter: MysteryEncounter = MysteryEncounterBui
], ],
}; };
const magnet = generateModifierTypeOption(modifierTypes.ATTACK_TYPE_BOOSTER, [PokemonType.STEEL])!;
const metalCoat = generateModifierTypeOption(modifierTypes.ATTACK_TYPE_BOOSTER, [PokemonType.ELECTRIC])!;
setEncounterRewards({ setEncounterRewards({
guaranteedModifierTypeOptions: [magnet, metalCoat], guaranteedRewardSpecs: [HeldItemId.MAGNET, HeldItemId.METAL_COAT],
fillRemaining: true, fillRemaining: true,
}); });
await transitionMysteryEncounterIntroVisuals(true, true); await transitionMysteryEncounterIntroVisuals(true, true);

View File

@ -1,11 +1,11 @@
import { globalScene } from "#app/global-scene"; import { globalScene } from "#app/global-scene";
import { speciesStarterCosts } from "#balance/starters"; import { speciesStarterCosts } from "#balance/starters";
import { modifierTypes } from "#data/data-lists";
import type { IEggOptions } from "#data/egg"; import type { IEggOptions } from "#data/egg";
import { getPokeballTintColor } from "#data/pokeball"; import { getPokeballTintColor } from "#data/pokeball";
import { BiomeId } from "#enums/biome-id"; import { BiomeId } from "#enums/biome-id";
import { EggSourceType } from "#enums/egg-source-types"; import { EggSourceType } from "#enums/egg-source-types";
import { EggTier } from "#enums/egg-type"; import { EggTier } from "#enums/egg-type";
import { HeldItemId } from "#enums/held-item-id";
import { MoveId } from "#enums/move-id"; import { MoveId } from "#enums/move-id";
import { MysteryEncounterOptionMode } from "#enums/mystery-encounter-option-mode"; import { MysteryEncounterOptionMode } from "#enums/mystery-encounter-option-mode";
import { MysteryEncounterTier } from "#enums/mystery-encounter-tier"; import { MysteryEncounterTier } from "#enums/mystery-encounter-tier";
@ -302,7 +302,7 @@ export const TheExpertPokemonBreederEncounter: MysteryEncounter = MysteryEncount
const eggOptions = getEggOptions(pokemon1CommonEggs, pokemon1RareEggs); const eggOptions = getEggOptions(pokemon1CommonEggs, pokemon1RareEggs);
setEncounterRewards( setEncounterRewards(
{ {
guaranteedModifierTypeFuncs: [modifierTypes.SOOTHE_BELL], guaranteedRewardSpecs: [HeldItemId.SOOTHE_BELL],
fillRemaining: true, fillRemaining: true,
}, },
eggOptions, eggOptions,
@ -361,7 +361,7 @@ export const TheExpertPokemonBreederEncounter: MysteryEncounter = MysteryEncount
const eggOptions = getEggOptions(pokemon2CommonEggs, pokemon2RareEggs); const eggOptions = getEggOptions(pokemon2CommonEggs, pokemon2RareEggs);
setEncounterRewards( setEncounterRewards(
{ {
guaranteedModifierTypeFuncs: [modifierTypes.SOOTHE_BELL], guaranteedRewardSpecs: [HeldItemId.SOOTHE_BELL],
fillRemaining: true, fillRemaining: true,
}, },
eggOptions, eggOptions,
@ -420,7 +420,7 @@ export const TheExpertPokemonBreederEncounter: MysteryEncounter = MysteryEncount
const eggOptions = getEggOptions(pokemon3CommonEggs, pokemon3RareEggs); const eggOptions = getEggOptions(pokemon3CommonEggs, pokemon3RareEggs);
setEncounterRewards( setEncounterRewards(
{ {
guaranteedModifierTypeFuncs: [modifierTypes.SOOTHE_BELL], guaranteedRewardSpecs: [HeldItemId.SOOTHE_BELL],
fillRemaining: true, fillRemaining: true,
}, },
eggOptions, eggOptions,
@ -622,7 +622,6 @@ function removePokemonFromPartyAndStoreHeldItems(encounter: MysteryEncounter, ch
party[chosenIndex] = party[0]; party[chosenIndex] = party[0];
party[0] = chosenPokemon; party[0] = chosenPokemon;
encounter.misc.originalParty = globalScene.getPlayerParty().slice(1); encounter.misc.originalParty = globalScene.getPlayerParty().slice(1);
encounter.misc.originalPartyHeldItems = encounter.misc.originalParty.map(p => p.getHeldItems());
globalScene["party"] = [chosenPokemon]; globalScene["party"] = [chosenPokemon];
} }
@ -631,14 +630,7 @@ function restorePartyAndHeldItems() {
// Restore original party // Restore original party
globalScene.getPlayerParty().push(...encounter.misc.originalParty); globalScene.getPlayerParty().push(...encounter.misc.originalParty);
// Restore held items globalScene.updateItems(true);
const originalHeldItems = encounter.misc.originalPartyHeldItems;
for (const pokemonHeldItemsList of originalHeldItems) {
for (const heldItem of pokemonHeldItemsList) {
globalScene.addModifier(heldItem, true, false, false, true);
}
}
globalScene.updateModifiers(true);
} }
function onGameOver() { function onGameOver() {

View File

@ -1,10 +1,9 @@
import { CLASSIC_MODE_MYSTERY_ENCOUNTER_WAVES } from "#app/constants"; import { CLASSIC_MODE_MYSTERY_ENCOUNTER_WAVES } from "#app/constants";
import { globalScene } from "#app/global-scene"; import { globalScene } from "#app/global-scene";
import { modifierTypes } from "#data/data-lists";
import { CustomPokemonData } from "#data/pokemon-data"; import { CustomPokemonData } from "#data/pokemon-data";
import { BattlerIndex } from "#enums/battler-index"; import { BattlerIndex } from "#enums/battler-index";
import { BattlerTagType } from "#enums/battler-tag-type"; import { BattlerTagType } from "#enums/battler-tag-type";
import { BerryType } from "#enums/berry-type"; import { HeldItemId } from "#enums/held-item-id";
import { MoveId } from "#enums/move-id"; import { MoveId } from "#enums/move-id";
import { MoveUseMode } from "#enums/move-use-mode"; import { MoveUseMode } from "#enums/move-use-mode";
import { MysteryEncounterTier } from "#enums/mystery-encounter-tier"; import { MysteryEncounterTier } from "#enums/mystery-encounter-tier";
@ -13,19 +12,16 @@ import { Nature } from "#enums/nature";
import { SpeciesId } from "#enums/species-id"; import { SpeciesId } from "#enums/species-id";
import { Stat } from "#enums/stat"; import { Stat } from "#enums/stat";
import type { Pokemon } from "#field/pokemon"; import type { Pokemon } from "#field/pokemon";
import type { PokemonHeldItemModifierType } from "#modifiers/modifier-type";
import { PokemonMove } from "#moves/pokemon-move"; import { PokemonMove } from "#moves/pokemon-move";
import { queueEncounterMessage, showEncounterText } from "#mystery-encounters/encounter-dialogue-utils"; import { queueEncounterMessage, showEncounterText } from "#mystery-encounters/encounter-dialogue-utils";
import type { EnemyPartyConfig } from "#mystery-encounters/encounter-phase-utils"; import type { EnemyPartyConfig } from "#mystery-encounters/encounter-phase-utils";
import { import {
generateModifierType,
initBattleWithEnemyConfig, initBattleWithEnemyConfig,
leaveEncounterWithoutBattle, leaveEncounterWithoutBattle,
loadCustomMovesForEncounter, loadCustomMovesForEncounter,
setEncounterRewards, setEncounterRewards,
transitionMysteryEncounterIntroVisuals, transitionMysteryEncounterIntroVisuals,
} from "#mystery-encounters/encounter-phase-utils"; } from "#mystery-encounters/encounter-phase-utils";
import { modifyPlayerPokemonBST } from "#mystery-encounters/encounter-pokemon-utils";
import type { MysteryEncounter } from "#mystery-encounters/mystery-encounter"; import type { MysteryEncounter } from "#mystery-encounters/mystery-encounter";
import { MysteryEncounterBuilder } from "#mystery-encounters/mystery-encounter"; import { MysteryEncounterBuilder } from "#mystery-encounters/mystery-encounter";
import { getPokemonSpecies } from "#utils/pokemon-utils"; import { getPokemonSpecies } from "#utils/pokemon-utils";
@ -95,23 +91,12 @@ export const TheStrongStuffEncounter: MysteryEncounter = MysteryEncounterBuilder
customPokemonData: new CustomPokemonData({ spriteScale: 1.25 }), customPokemonData: new CustomPokemonData({ spriteScale: 1.25 }),
nature: Nature.HARDY, nature: Nature.HARDY,
moveSet: [MoveId.INFESTATION, MoveId.SALT_CURE, MoveId.GASTRO_ACID, MoveId.HEAL_ORDER], moveSet: [MoveId.INFESTATION, MoveId.SALT_CURE, MoveId.GASTRO_ACID, MoveId.HEAL_ORDER],
modifierConfigs: [ heldItemConfig: [
{ { entry: HeldItemId.SITRUS_BERRY, count: 1 },
modifier: generateModifierType(modifierTypes.BERRY, [BerryType.SITRUS]) as PokemonHeldItemModifierType, { entry: HeldItemId.ENIGMA_BERRY, count: 1 },
}, { entry: HeldItemId.APICOT_BERRY, count: 1 },
{ { entry: HeldItemId.GANLON_BERRY, count: 1 },
modifier: generateModifierType(modifierTypes.BERRY, [BerryType.ENIGMA]) as PokemonHeldItemModifierType, { entry: HeldItemId.LUM_BERRY, count: 2 },
},
{
modifier: generateModifierType(modifierTypes.BERRY, [BerryType.APICOT]) as PokemonHeldItemModifierType,
},
{
modifier: generateModifierType(modifierTypes.BERRY, [BerryType.GANLON]) as PokemonHeldItemModifierType,
},
{
modifier: generateModifierType(modifierTypes.BERRY, [BerryType.LUM]) as PokemonHeldItemModifierType,
stackCount: 2,
},
], ],
tags: [BattlerTagType.MYSTERY_ENCOUNTER_POST_SUMMON], tags: [BattlerTagType.MYSTERY_ENCOUNTER_POST_SUMMON],
mysteryEncounterBattleEffects: (pokemon: Pokemon) => { mysteryEncounterBattleEffects: (pokemon: Pokemon) => {
@ -171,11 +156,11 @@ export const TheStrongStuffEncounter: MysteryEncounter = MysteryEncounterBuilder
sortedParty.forEach((pokemon, index) => { sortedParty.forEach((pokemon, index) => {
if (index < 2) { if (index < 2) {
// -15 to the two highest BST mons // -15 to the two highest BST mons
modifyPlayerPokemonBST(pokemon, false); pokemon.heldItemManager.add(HeldItemId.SHUCKLE_JUICE_BAD);
encounter.setDialogueToken("highBstPokemon" + (index + 1), pokemon.getNameToRender()); encounter.setDialogueToken("highBstPokemon" + (index + 1), pokemon.getNameToRender());
} else { } else {
// +10 for the rest // +10 for the rest
modifyPlayerPokemonBST(pokemon, true); pokemon.heldItemManager.add(HeldItemId.SHUCKLE_JUICE_GOOD);
} }
}); });
@ -207,7 +192,7 @@ export const TheStrongStuffEncounter: MysteryEncounter = MysteryEncounterBuilder
// Pick battle // Pick battle
const encounter = globalScene.currentBattle.mysteryEncounter!; const encounter = globalScene.currentBattle.mysteryEncounter!;
setEncounterRewards({ setEncounterRewards({
guaranteedModifierTypeFuncs: [modifierTypes.SOUL_DEW], guaranteedRewardSpecs: [HeldItemId.SOUL_DEW],
fillRemaining: true, fillRemaining: true,
}); });
encounter.startOfBattleEffects.push( encounter.startOfBattleEffects.push(

View File

@ -1,27 +1,24 @@
import { applyAbAttrs } from "#abilities/apply-ab-attrs"; import { applyAbAttrs } from "#abilities/apply-ab-attrs";
import { CLASSIC_MODE_MYSTERY_ENCOUNTER_WAVES } from "#app/constants"; import { CLASSIC_MODE_MYSTERY_ENCOUNTER_WAVES } from "#app/constants";
import { globalScene } from "#app/global-scene"; import { globalScene } from "#app/global-scene";
import { modifierTypes } from "#data/data-lists";
import { SpeciesFormChangeAbilityTrigger } from "#data/form-change-triggers"; import { SpeciesFormChangeAbilityTrigger } from "#data/form-change-triggers";
import { AbilityId } from "#enums/ability-id"; import { AbilityId } from "#enums/ability-id";
import { BattlerTagType } from "#enums/battler-tag-type"; import { BattlerTagType } from "#enums/battler-tag-type";
import { BerryType } from "#enums/berry-type"; import { HeldItemId } from "#enums/held-item-id";
import { ModifierTier } from "#enums/modifier-tier";
import { MoveId } from "#enums/move-id"; import { MoveId } from "#enums/move-id";
import { MysteryEncounterMode } from "#enums/mystery-encounter-mode"; import { MysteryEncounterMode } from "#enums/mystery-encounter-mode";
import { MysteryEncounterTier } from "#enums/mystery-encounter-tier"; import { MysteryEncounterTier } from "#enums/mystery-encounter-tier";
import { MysteryEncounterType } from "#enums/mystery-encounter-type"; import { MysteryEncounterType } from "#enums/mystery-encounter-type";
import { Nature } from "#enums/nature"; import { Nature } from "#enums/nature";
import { PokemonType } from "#enums/pokemon-type"; import { RewardId } from "#enums/reward-id";
import { RarityTier } from "#enums/reward-tier";
import { SpeciesId } from "#enums/species-id"; import { SpeciesId } from "#enums/species-id";
import { Stat } from "#enums/stat";
import { TrainerType } from "#enums/trainer-type"; import { TrainerType } from "#enums/trainer-type";
import type { PokemonHeldItemModifierType } from "#modifiers/modifier-type"; import type { Reward } from "#items/reward";
import { generateRewardOptionFromId } from "#items/reward-utils";
import { showEncounterDialogue, showEncounterText } from "#mystery-encounters/encounter-dialogue-utils"; import { showEncounterDialogue, showEncounterText } from "#mystery-encounters/encounter-dialogue-utils";
import type { EnemyPartyConfig } from "#mystery-encounters/encounter-phase-utils"; import type { EnemyPartyConfig } from "#mystery-encounters/encounter-phase-utils";
import { import {
generateModifierType,
generateModifierTypeOption,
initBattleWithEnemyConfig, initBattleWithEnemyConfig,
leaveEncounterWithoutBattle, leaveEncounterWithoutBattle,
setEncounterRewards, setEncounterRewards,
@ -32,6 +29,8 @@ import { MysteryEncounterBuilder } from "#mystery-encounters/mystery-encounter";
import { getPokemonSpecies } from "#utils/pokemon-utils"; import { getPokemonSpecies } from "#utils/pokemon-utils";
import i18next from "i18next"; import i18next from "i18next";
// TODO: make all items unstealable
/** the i18n namespace for the encounter */ /** the i18n namespace for the encounter */
const namespace = "mysteryEncounters/theWinstrateChallenge"; const namespace = "mysteryEncounters/theWinstrateChallenge";
@ -142,7 +141,7 @@ export const TheWinstrateChallengeEncounter: MysteryEncounter = MysteryEncounter
// Refuse the challenge, they full heal the party and give the player a Rarer Candy // Refuse the challenge, they full heal the party and give the player a Rarer Candy
globalScene.phaseManager.unshiftNew("PartyHealPhase", true); globalScene.phaseManager.unshiftNew("PartyHealPhase", true);
setEncounterRewards({ setEncounterRewards({
guaranteedModifierTypeFuncs: [modifierTypes.RARER_CANDY], guaranteedRewardSpecs: [RewardId.RARER_CANDY],
fillRemaining: false, fillRemaining: false,
}); });
leaveEncounterWithoutBattle(); leaveEncounterWithoutBattle();
@ -158,17 +157,17 @@ async function spawnNextTrainerOrEndEncounter() {
await showEncounterDialogue(`${namespace}:victory`, `${namespace}:speaker`); await showEncounterDialogue(`${namespace}:victory`, `${namespace}:speaker`);
// Give 10x Voucher // Give 10x Voucher
const newModifier = modifierTypes.VOUCHER_PREMIUM().newModifier(); const reward = generateRewardOptionFromId(RewardId.VOUCHER_PREMIUM).type;
globalScene.addModifier(newModifier); globalScene.applyReward(reward as Reward, {});
globalScene.playSound("item_fanfare"); globalScene.playSound("item_fanfare");
await showEncounterText(i18next.t("battle:rewardGain", { modifierName: newModifier?.type.name })); await showEncounterText(i18next.t("battle:rewardGain", { modifierName: (reward as Reward).name }));
await showEncounterDialogue(`${namespace}:victory_2`, `${namespace}:speaker`); await showEncounterDialogue(`${namespace}:victory_2`, `${namespace}:speaker`);
globalScene.ui.clearText(); // Clears "Winstrate" title from screen as rewards get animated in globalScene.ui.clearText(); // Clears "Winstrate" title from screen as rewards get animated in
const machoBrace = generateModifierTypeOption(modifierTypes.MYSTERY_ENCOUNTER_MACHO_BRACE)!; const machoBrace = generateRewardOptionFromId(HeldItemId.MACHO_BRACE)!;
machoBrace.type.tier = ModifierTier.MASTER; machoBrace.type.tier = RarityTier.MASTER;
setEncounterRewards({ setEncounterRewards({
guaranteedModifierTypeOptions: [machoBrace], guaranteedRewardOptions: [machoBrace],
fillRemaining: false, fillRemaining: false,
}); });
encounter.doContinueEncounter = undefined; encounter.doContinueEncounter = undefined;
@ -258,16 +257,9 @@ function getVictorTrainerConfig(): EnemyPartyConfig {
abilityIndex: 0, // Guts abilityIndex: 0, // Guts
nature: Nature.ADAMANT, nature: Nature.ADAMANT,
moveSet: [MoveId.FACADE, MoveId.BRAVE_BIRD, MoveId.PROTECT, MoveId.QUICK_ATTACK], moveSet: [MoveId.FACADE, MoveId.BRAVE_BIRD, MoveId.PROTECT, MoveId.QUICK_ATTACK],
modifierConfigs: [ heldItemConfig: [
{ { entry: HeldItemId.FLAME_ORB, count: 1 },
modifier: generateModifierType(modifierTypes.FLAME_ORB) as PokemonHeldItemModifierType, { entry: HeldItemId.FOCUS_BAND, count: 2 },
isTransferable: false,
},
{
modifier: generateModifierType(modifierTypes.FOCUS_BAND) as PokemonHeldItemModifierType,
stackCount: 2,
isTransferable: false,
},
], ],
}, },
{ {
@ -276,16 +268,9 @@ function getVictorTrainerConfig(): EnemyPartyConfig {
abilityIndex: 1, // Guts abilityIndex: 1, // Guts
nature: Nature.ADAMANT, nature: Nature.ADAMANT,
moveSet: [MoveId.FACADE, MoveId.OBSTRUCT, MoveId.NIGHT_SLASH, MoveId.FIRE_PUNCH], moveSet: [MoveId.FACADE, MoveId.OBSTRUCT, MoveId.NIGHT_SLASH, MoveId.FIRE_PUNCH],
modifierConfigs: [ heldItemConfig: [
{ { entry: HeldItemId.FLAME_ORB, count: 1 },
modifier: generateModifierType(modifierTypes.FLAME_ORB) as PokemonHeldItemModifierType, { entry: HeldItemId.LEFTOVERS, count: 2 },
isTransferable: false,
},
{
modifier: generateModifierType(modifierTypes.LEFTOVERS) as PokemonHeldItemModifierType,
stackCount: 2,
isTransferable: false,
},
], ],
}, },
], ],
@ -302,16 +287,9 @@ function getVictoriaTrainerConfig(): EnemyPartyConfig {
abilityIndex: 0, // Natural Cure abilityIndex: 0, // Natural Cure
nature: Nature.CALM, nature: Nature.CALM,
moveSet: [MoveId.SYNTHESIS, MoveId.SLUDGE_BOMB, MoveId.GIGA_DRAIN, MoveId.SLEEP_POWDER], moveSet: [MoveId.SYNTHESIS, MoveId.SLUDGE_BOMB, MoveId.GIGA_DRAIN, MoveId.SLEEP_POWDER],
modifierConfigs: [ heldItemConfig: [
{ { entry: HeldItemId.SOUL_DEW, count: 1 },
modifier: generateModifierType(modifierTypes.SOUL_DEW) as PokemonHeldItemModifierType, { entry: HeldItemId.QUICK_CLAW, count: 2 },
isTransferable: false,
},
{
modifier: generateModifierType(modifierTypes.QUICK_CLAW) as PokemonHeldItemModifierType,
stackCount: 2,
isTransferable: false,
},
], ],
}, },
{ {
@ -320,21 +298,9 @@ function getVictoriaTrainerConfig(): EnemyPartyConfig {
formIndex: 1, formIndex: 1,
nature: Nature.TIMID, nature: Nature.TIMID,
moveSet: [MoveId.PSYSHOCK, MoveId.MOONBLAST, MoveId.SHADOW_BALL, MoveId.WILL_O_WISP], moveSet: [MoveId.PSYSHOCK, MoveId.MOONBLAST, MoveId.SHADOW_BALL, MoveId.WILL_O_WISP],
modifierConfigs: [ heldItemConfig: [
{ { entry: HeldItemId.TWISTED_SPOON, count: 1 },
modifier: generateModifierType(modifierTypes.ATTACK_TYPE_BOOSTER, [ { entry: HeldItemId.FAIRY_FEATHER, count: 1 },
PokemonType.PSYCHIC,
]) as PokemonHeldItemModifierType,
stackCount: 1,
isTransferable: false,
},
{
modifier: generateModifierType(modifierTypes.ATTACK_TYPE_BOOSTER, [
PokemonType.FAIRY,
]) as PokemonHeldItemModifierType,
stackCount: 1,
isTransferable: false,
},
], ],
}, },
], ],
@ -351,17 +317,9 @@ function getViviTrainerConfig(): EnemyPartyConfig {
abilityIndex: 3, // Lightning Rod abilityIndex: 3, // Lightning Rod
nature: Nature.ADAMANT, nature: Nature.ADAMANT,
moveSet: [MoveId.WATERFALL, MoveId.MEGAHORN, MoveId.KNOCK_OFF, MoveId.REST], moveSet: [MoveId.WATERFALL, MoveId.MEGAHORN, MoveId.KNOCK_OFF, MoveId.REST],
modifierConfigs: [ heldItemConfig: [
{ { entry: HeldItemId.LUM_BERRY, count: 2 },
modifier: generateModifierType(modifierTypes.BERRY, [BerryType.LUM]) as PokemonHeldItemModifierType, { entry: HeldItemId.HP_UP, count: 4 },
stackCount: 2,
isTransferable: false,
},
{
modifier: generateModifierType(modifierTypes.BASE_STAT_BOOSTER, [Stat.HP]) as PokemonHeldItemModifierType,
stackCount: 4,
isTransferable: false,
},
], ],
}, },
{ {
@ -370,16 +328,9 @@ function getViviTrainerConfig(): EnemyPartyConfig {
abilityIndex: 1, // Poison Heal abilityIndex: 1, // Poison Heal
nature: Nature.JOLLY, nature: Nature.JOLLY,
moveSet: [MoveId.SPORE, MoveId.SWORDS_DANCE, MoveId.SEED_BOMB, MoveId.DRAIN_PUNCH], moveSet: [MoveId.SPORE, MoveId.SWORDS_DANCE, MoveId.SEED_BOMB, MoveId.DRAIN_PUNCH],
modifierConfigs: [ heldItemConfig: [
{ { entry: HeldItemId.HP_UP, count: 4 },
modifier: generateModifierType(modifierTypes.BASE_STAT_BOOSTER, [Stat.HP]) as PokemonHeldItemModifierType, { entry: HeldItemId.TOXIC_ORB, count: 1 },
stackCount: 4,
isTransferable: false,
},
{
modifier: generateModifierType(modifierTypes.TOXIC_ORB) as PokemonHeldItemModifierType,
isTransferable: false,
},
], ],
}, },
{ {
@ -388,13 +339,7 @@ function getViviTrainerConfig(): EnemyPartyConfig {
formIndex: 1, formIndex: 1,
nature: Nature.CALM, nature: Nature.CALM,
moveSet: [MoveId.EARTH_POWER, MoveId.FIRE_BLAST, MoveId.YAWN, MoveId.PROTECT], moveSet: [MoveId.EARTH_POWER, MoveId.FIRE_BLAST, MoveId.YAWN, MoveId.PROTECT],
modifierConfigs: [ heldItemConfig: [{ entry: HeldItemId.QUICK_CLAW, count: 3 }],
{
modifier: generateModifierType(modifierTypes.QUICK_CLAW) as PokemonHeldItemModifierType,
stackCount: 3,
isTransferable: false,
},
],
}, },
], ],
}; };
@ -410,12 +355,7 @@ function getVickyTrainerConfig(): EnemyPartyConfig {
formIndex: 1, formIndex: 1,
nature: Nature.IMPISH, nature: Nature.IMPISH,
moveSet: [MoveId.AXE_KICK, MoveId.ICE_PUNCH, MoveId.ZEN_HEADBUTT, MoveId.BULLET_PUNCH], moveSet: [MoveId.AXE_KICK, MoveId.ICE_PUNCH, MoveId.ZEN_HEADBUTT, MoveId.BULLET_PUNCH],
modifierConfigs: [ heldItemConfig: [{ entry: HeldItemId.SHELL_BELL, count: 1 }],
{
modifier: generateModifierType(modifierTypes.SHELL_BELL) as PokemonHeldItemModifierType,
isTransferable: false,
},
],
}, },
], ],
}; };
@ -431,13 +371,7 @@ function getVitoTrainerConfig(): EnemyPartyConfig {
abilityIndex: 0, // Soundproof abilityIndex: 0, // Soundproof
nature: Nature.MODEST, nature: Nature.MODEST,
moveSet: [MoveId.THUNDERBOLT, MoveId.GIGA_DRAIN, MoveId.FOUL_PLAY, MoveId.THUNDER_WAVE], moveSet: [MoveId.THUNDERBOLT, MoveId.GIGA_DRAIN, MoveId.FOUL_PLAY, MoveId.THUNDER_WAVE],
modifierConfigs: [ heldItemConfig: [{ entry: HeldItemId.ZINC, count: 2 }],
{
modifier: generateModifierType(modifierTypes.BASE_STAT_BOOSTER, [Stat.SPD]) as PokemonHeldItemModifierType,
stackCount: 2,
isTransferable: false,
},
],
}, },
{ {
species: getPokemonSpecies(SpeciesId.SWALOT), species: getPokemonSpecies(SpeciesId.SWALOT),
@ -445,51 +379,18 @@ function getVitoTrainerConfig(): EnemyPartyConfig {
abilityIndex: 2, // Gluttony abilityIndex: 2, // Gluttony
nature: Nature.QUIET, nature: Nature.QUIET,
moveSet: [MoveId.SLUDGE_BOMB, MoveId.GIGA_DRAIN, MoveId.ICE_BEAM, MoveId.EARTHQUAKE], moveSet: [MoveId.SLUDGE_BOMB, MoveId.GIGA_DRAIN, MoveId.ICE_BEAM, MoveId.EARTHQUAKE],
modifierConfigs: [ heldItemConfig: [
{ { entry: HeldItemId.SITRUS_BERRY, count: 2 },
modifier: generateModifierType(modifierTypes.BERRY, [BerryType.SITRUS]) as PokemonHeldItemModifierType, { entry: HeldItemId.APICOT_BERRY, count: 2 },
stackCount: 2, { entry: HeldItemId.GANLON_BERRY, count: 2 },
}, { entry: HeldItemId.STARF_BERRY, count: 2 },
{ { entry: HeldItemId.SALAC_BERRY, count: 2 },
modifier: generateModifierType(modifierTypes.BERRY, [BerryType.APICOT]) as PokemonHeldItemModifierType, { entry: HeldItemId.LUM_BERRY, count: 2 },
stackCount: 2, { entry: HeldItemId.LANSAT_BERRY, count: 2 },
}, { entry: HeldItemId.LIECHI_BERRY, count: 2 },
{ { entry: HeldItemId.PETAYA_BERRY, count: 2 },
modifier: generateModifierType(modifierTypes.BERRY, [BerryType.GANLON]) as PokemonHeldItemModifierType, { entry: HeldItemId.ENIGMA_BERRY, count: 2 },
stackCount: 2, { entry: HeldItemId.LEPPA_BERRY, count: 2 },
},
{
modifier: generateModifierType(modifierTypes.BERRY, [BerryType.STARF]) as PokemonHeldItemModifierType,
stackCount: 2,
},
{
modifier: generateModifierType(modifierTypes.BERRY, [BerryType.SALAC]) as PokemonHeldItemModifierType,
stackCount: 2,
},
{
modifier: generateModifierType(modifierTypes.BERRY, [BerryType.LUM]) as PokemonHeldItemModifierType,
stackCount: 2,
},
{
modifier: generateModifierType(modifierTypes.BERRY, [BerryType.LANSAT]) as PokemonHeldItemModifierType,
stackCount: 2,
},
{
modifier: generateModifierType(modifierTypes.BERRY, [BerryType.LIECHI]) as PokemonHeldItemModifierType,
stackCount: 2,
},
{
modifier: generateModifierType(modifierTypes.BERRY, [BerryType.PETAYA]) as PokemonHeldItemModifierType,
stackCount: 2,
},
{
modifier: generateModifierType(modifierTypes.BERRY, [BerryType.ENIGMA]) as PokemonHeldItemModifierType,
stackCount: 2,
},
{
modifier: generateModifierType(modifierTypes.BERRY, [BerryType.LEPPA]) as PokemonHeldItemModifierType,
stackCount: 2,
},
], ],
}, },
{ {
@ -498,13 +399,7 @@ function getVitoTrainerConfig(): EnemyPartyConfig {
abilityIndex: 2, // Tangled Feet abilityIndex: 2, // Tangled Feet
nature: Nature.JOLLY, nature: Nature.JOLLY,
moveSet: [MoveId.DRILL_PECK, MoveId.QUICK_ATTACK, MoveId.THRASH, MoveId.KNOCK_OFF], moveSet: [MoveId.DRILL_PECK, MoveId.QUICK_ATTACK, MoveId.THRASH, MoveId.KNOCK_OFF],
modifierConfigs: [ heldItemConfig: [{ entry: HeldItemId.KINGS_ROCK, count: 2 }],
{
modifier: generateModifierType(modifierTypes.KINGS_ROCK) as PokemonHeldItemModifierType,
stackCount: 2,
isTransferable: false,
},
],
}, },
{ {
species: getPokemonSpecies(SpeciesId.ALAKAZAM), species: getPokemonSpecies(SpeciesId.ALAKAZAM),
@ -512,13 +407,7 @@ function getVitoTrainerConfig(): EnemyPartyConfig {
formIndex: 1, formIndex: 1,
nature: Nature.BOLD, nature: Nature.BOLD,
moveSet: [MoveId.PSYCHIC, MoveId.SHADOW_BALL, MoveId.FOCUS_BLAST, MoveId.THUNDERBOLT], moveSet: [MoveId.PSYCHIC, MoveId.SHADOW_BALL, MoveId.FOCUS_BLAST, MoveId.THUNDERBOLT],
modifierConfigs: [ heldItemConfig: [{ entry: HeldItemId.WIDE_LENS, count: 2 }],
{
modifier: generateModifierType(modifierTypes.WIDE_LENS) as PokemonHeldItemModifierType,
stackCount: 2,
isTransferable: false,
},
],
}, },
{ {
species: getPokemonSpecies(SpeciesId.DARMANITAN), species: getPokemonSpecies(SpeciesId.DARMANITAN),
@ -526,13 +415,7 @@ function getVitoTrainerConfig(): EnemyPartyConfig {
abilityIndex: 0, // Sheer Force abilityIndex: 0, // Sheer Force
nature: Nature.IMPISH, nature: Nature.IMPISH,
moveSet: [MoveId.EARTHQUAKE, MoveId.U_TURN, MoveId.FLARE_BLITZ, MoveId.ROCK_SLIDE], moveSet: [MoveId.EARTHQUAKE, MoveId.U_TURN, MoveId.FLARE_BLITZ, MoveId.ROCK_SLIDE],
modifierConfigs: [ heldItemConfig: [{ entry: HeldItemId.QUICK_CLAW, count: 2 }],
{
modifier: generateModifierType(modifierTypes.QUICK_CLAW) as PokemonHeldItemModifierType,
stackCount: 2,
isTransferable: false,
},
],
}, },
], ],
}; };

View File

@ -12,7 +12,6 @@ import { MysteryEncounterType } from "#enums/mystery-encounter-type";
import { Nature } from "#enums/nature"; import { Nature } from "#enums/nature";
import { getStatKey } from "#enums/stat"; import { getStatKey } from "#enums/stat";
import type { PlayerPokemon, Pokemon } from "#field/pokemon"; import type { PlayerPokemon, Pokemon } from "#field/pokemon";
import type { PokemonHeldItemModifier } from "#modifiers/modifier";
import { queueEncounterMessage, showEncounterText } from "#mystery-encounters/encounter-dialogue-utils"; import { queueEncounterMessage, showEncounterText } from "#mystery-encounters/encounter-dialogue-utils";
import type { EnemyPartyConfig } from "#mystery-encounters/encounter-phase-utils"; import type { EnemyPartyConfig } from "#mystery-encounters/encounter-phase-utils";
import { import {
@ -26,7 +25,6 @@ import type { MysteryEncounter } from "#mystery-encounters/mystery-encounter";
import { MysteryEncounterBuilder } from "#mystery-encounters/mystery-encounter"; import { MysteryEncounterBuilder } from "#mystery-encounters/mystery-encounter";
import { MysteryEncounterOptionBuilder } from "#mystery-encounters/mystery-encounter-option"; import { MysteryEncounterOptionBuilder } from "#mystery-encounters/mystery-encounter-option";
import { PokemonData } from "#system/pokemon-data"; import { PokemonData } from "#system/pokemon-data";
import type { HeldModifierConfig } from "#types/held-modifier-config";
import type { OptionSelectItem } from "#ui/abstract-option-select-ui-handler"; import type { OptionSelectItem } from "#ui/abstract-option-select-ui-handler";
import { isNullOrUndefined, randSeedShuffle } from "#utils/common"; import { isNullOrUndefined, randSeedShuffle } from "#utils/common";
import { getEnumValues } from "#utils/enums"; import { getEnumValues } from "#utils/enums";
@ -102,8 +100,7 @@ export const TrainingSessionEncounter: MysteryEncounter = MysteryEncounterBuilde
// Spawn light training session with chosen pokemon // Spawn light training session with chosen pokemon
// Every 50 waves, add +1 boss segment, capping at 5 // Every 50 waves, add +1 boss segment, capping at 5
const segments = Math.min(2 + Math.floor(globalScene.currentBattle.waveIndex / 50), 5); const segments = Math.min(2 + Math.floor(globalScene.currentBattle.waveIndex / 50), 5);
const modifiers = new ModifiersHolder(); const config = getEnemyConfig(playerPokemon, segments);
const config = getEnemyConfig(playerPokemon, segments, modifiers);
globalScene.removePokemonFromPlayerParty(playerPokemon, false); globalScene.removePokemonFromPlayerParty(playerPokemon, false);
const onBeforeRewardsPhase = () => { const onBeforeRewardsPhase = () => {
@ -152,13 +149,8 @@ export const TrainingSessionEncounter: MysteryEncounter = MysteryEncounterBuilde
globalScene.gameData.setPokemonCaught(playerPokemon, false); globalScene.gameData.setPokemonCaught(playerPokemon, false);
} }
// Add pokemon and mods back // Make held items show up again
globalScene.getPlayerParty().push(playerPokemon); globalScene.updateItems(true);
for (const mod of modifiers.value) {
mod.pokemonId = playerPokemon.id;
globalScene.addModifier(mod, true, false, false, true);
}
globalScene.updateModifiers(true);
queueEncounterMessage(`${namespace}:option.1.finished`); queueEncounterMessage(`${namespace}:option.1.finished`);
}; };
@ -217,8 +209,7 @@ export const TrainingSessionEncounter: MysteryEncounter = MysteryEncounterBuilde
// Spawn medium training session with chosen pokemon // Spawn medium training session with chosen pokemon
// Every 40 waves, add +1 boss segment, capping at 6 // Every 40 waves, add +1 boss segment, capping at 6
const segments = Math.min(2 + Math.floor(globalScene.currentBattle.waveIndex / 40), 6); const segments = Math.min(2 + Math.floor(globalScene.currentBattle.waveIndex / 40), 6);
const modifiers = new ModifiersHolder(); const config = getEnemyConfig(playerPokemon, segments);
const config = getEnemyConfig(playerPokemon, segments, modifiers);
globalScene.removePokemonFromPlayerParty(playerPokemon, false); globalScene.removePokemonFromPlayerParty(playerPokemon, false);
const onBeforeRewardsPhase = () => { const onBeforeRewardsPhase = () => {
@ -227,13 +218,8 @@ export const TrainingSessionEncounter: MysteryEncounter = MysteryEncounterBuilde
playerPokemon.setCustomNature(encounter.misc.chosenNature); playerPokemon.setCustomNature(encounter.misc.chosenNature);
globalScene.gameData.unlockSpeciesNature(playerPokemon.species, encounter.misc.chosenNature); globalScene.gameData.unlockSpeciesNature(playerPokemon.species, encounter.misc.chosenNature);
// Add pokemon and modifiers back // Make held items show up again
globalScene.getPlayerParty().push(playerPokemon); globalScene.updateItems(true);
for (const mod of modifiers.value) {
mod.pokemonId = playerPokemon.id;
globalScene.addModifier(mod, true, false, false, true);
}
globalScene.updateModifiers(true);
}; };
setEncounterRewards({ fillRemaining: true }, undefined, onBeforeRewardsPhase); setEncounterRewards({ fillRemaining: true }, undefined, onBeforeRewardsPhase);
@ -308,8 +294,7 @@ export const TrainingSessionEncounter: MysteryEncounter = MysteryEncounterBuilde
// Every 30 waves, add +1 boss segment, capping at 6 // Every 30 waves, add +1 boss segment, capping at 6
// Also starts with +1 to all stats // Also starts with +1 to all stats
const segments = Math.min(2 + Math.floor(globalScene.currentBattle.waveIndex / 30), 6); const segments = Math.min(2 + Math.floor(globalScene.currentBattle.waveIndex / 30), 6);
const modifiers = new ModifiersHolder(); const config = getEnemyConfig(playerPokemon, segments);
const config = getEnemyConfig(playerPokemon, segments, modifiers);
config.pokemonConfigs![0].tags = [BattlerTagType.MYSTERY_ENCOUNTER_POST_SUMMON]; config.pokemonConfigs![0].tags = [BattlerTagType.MYSTERY_ENCOUNTER_POST_SUMMON];
globalScene.removePokemonFromPlayerParty(playerPokemon, false); globalScene.removePokemonFromPlayerParty(playerPokemon, false);
@ -340,13 +325,8 @@ export const TrainingSessionEncounter: MysteryEncounter = MysteryEncounterBuilde
playerPokemon.calculateStats(); playerPokemon.calculateStats();
globalScene.gameData.setPokemonCaught(playerPokemon, false); globalScene.gameData.setPokemonCaught(playerPokemon, false);
// Add pokemon and mods back // Make held items show up again
globalScene.getPlayerParty().push(playerPokemon); globalScene.updateItems(true);
for (const mod of modifiers.value) {
mod.pokemonId = playerPokemon.id;
globalScene.addModifier(mod, true, false, false, true);
}
globalScene.updateModifiers(true);
}; };
setEncounterRewards({ fillRemaining: true }, undefined, onBeforeRewardsPhase); setEncounterRewards({ fillRemaining: true }, undefined, onBeforeRewardsPhase);
@ -373,18 +353,12 @@ export const TrainingSessionEncounter: MysteryEncounter = MysteryEncounterBuilde
) )
.build(); .build();
function getEnemyConfig(playerPokemon: PlayerPokemon, segments: number, modifiers: ModifiersHolder): EnemyPartyConfig { function getEnemyConfig(playerPokemon: PlayerPokemon, segments: number): EnemyPartyConfig {
playerPokemon.resetSummonData(); playerPokemon.resetSummonData();
// Passes modifiers by reference // Passes modifiers by reference
modifiers.value = playerPokemon.getHeldItems(); // TODO: fix various things, like make enemy items untransferable, make sure form change items can come back
const modifierConfigs = modifiers.value.map(mod => { const config = playerPokemon.heldItemManager.generateHeldItemConfiguration();
return {
modifier: mod.clone(),
isTransferable: false,
stackCount: mod.stackCount,
};
}) as HeldModifierConfig[];
const data = new PokemonData(playerPokemon); const data = new PokemonData(playerPokemon);
return { return {
@ -396,12 +370,8 @@ function getEnemyConfig(playerPokemon: PlayerPokemon, segments: number, modifier
formIndex: playerPokemon.formIndex, formIndex: playerPokemon.formIndex,
level: playerPokemon.level, level: playerPokemon.level,
dataSource: data, dataSource: data,
modifierConfigs: modifierConfigs, heldItemConfig: config,
}, },
], ],
}; };
} }
class ModifiersHolder {
public value: PokemonHeldItemModifier[] = [];
}

View File

@ -1,29 +1,28 @@
import { CLASSIC_MODE_MYSTERY_ENCOUNTER_WAVES } from "#app/constants"; import { CLASSIC_MODE_MYSTERY_ENCOUNTER_WAVES } from "#app/constants";
import { globalScene } from "#app/global-scene"; import { globalScene } from "#app/global-scene";
import { modifierTypes } from "#data/data-lists"; import { allHeldItems, allTrainerItems } from "#data/data-lists";
import { BattlerIndex } from "#enums/battler-index"; import { BattlerIndex } from "#enums/battler-index";
import { ModifierTier } from "#enums/modifier-tier"; import { HeldItemCategoryId, HeldItemId } from "#enums/held-item-id";
import { MoveId } from "#enums/move-id"; import { MoveId } from "#enums/move-id";
import { MoveUseMode } from "#enums/move-use-mode"; import { MoveUseMode } from "#enums/move-use-mode";
import { MysteryEncounterOptionMode } from "#enums/mystery-encounter-option-mode"; import { MysteryEncounterOptionMode } from "#enums/mystery-encounter-option-mode";
import { MysteryEncounterTier } from "#enums/mystery-encounter-tier"; import { MysteryEncounterTier } from "#enums/mystery-encounter-tier";
import { MysteryEncounterType } from "#enums/mystery-encounter-type"; import { MysteryEncounterType } from "#enums/mystery-encounter-type";
import { RarityTier } from "#enums/reward-tier";
import { SpeciesId } from "#enums/species-id"; import { SpeciesId } from "#enums/species-id";
import { HitHealModifier, PokemonHeldItemModifier, TurnHealModifier } from "#modifiers/modifier"; import { TrainerItemId } from "#enums/trainer-item-id";
import type { PokemonHeldItemModifierType } from "#modifiers/modifier-type"; import { assignItemToFirstFreePokemon } from "#items/item-utility";
import { PokemonMove } from "#moves/pokemon-move"; import { PokemonMove } from "#moves/pokemon-move";
import { showEncounterText } from "#mystery-encounters/encounter-dialogue-utils"; import { showEncounterText } from "#mystery-encounters/encounter-dialogue-utils";
import { import {
type EnemyPartyConfig, type EnemyPartyConfig,
type EnemyPokemonConfig, type EnemyPokemonConfig,
generateModifierType,
initBattleWithEnemyConfig, initBattleWithEnemyConfig,
leaveEncounterWithoutBattle, leaveEncounterWithoutBattle,
loadCustomMovesForEncounter, loadCustomMovesForEncounter,
setEncounterRewards, setEncounterRewards,
transitionMysteryEncounterIntroVisuals, transitionMysteryEncounterIntroVisuals,
} from "#mystery-encounters/encounter-phase-utils"; } from "#mystery-encounters/encounter-phase-utils";
import { applyModifierTypeToPlayerPokemon } from "#mystery-encounters/encounter-pokemon-utils";
import { type MysteryEncounter, MysteryEncounterBuilder } from "#mystery-encounters/mystery-encounter"; import { type MysteryEncounter, MysteryEncounterBuilder } from "#mystery-encounters/mystery-encounter";
import { MysteryEncounterOptionBuilder } from "#mystery-encounters/mystery-encounter-option"; import { MysteryEncounterOptionBuilder } from "#mystery-encounters/mystery-encounter-option";
import i18next from "#plugins/i18n"; import i18next from "#plugins/i18n";
@ -83,41 +82,13 @@ export const TrashToTreasureEncounter: MysteryEncounter = MysteryEncounterBuilde
formIndex: 1, // Gmax formIndex: 1, // Gmax
bossSegmentModifier: 1, // +1 Segment from normal bossSegmentModifier: 1, // +1 Segment from normal
moveSet: [MoveId.GUNK_SHOT, MoveId.STOMPING_TANTRUM, MoveId.HAMMER_ARM, MoveId.PAYBACK], moveSet: [MoveId.GUNK_SHOT, MoveId.STOMPING_TANTRUM, MoveId.HAMMER_ARM, MoveId.PAYBACK],
modifierConfigs: [ heldItemConfig: [
{ { entry: HeldItemCategoryId.BERRY, count: 4 },
modifier: generateModifierType(modifierTypes.BERRY) as PokemonHeldItemModifierType, { entry: HeldItemCategoryId.BASE_STAT_BOOST, count: 2 },
}, { entry: HeldItemId.TOXIC_ORB, count: randSeedInt(2, 0) },
{ { entry: HeldItemId.SOOTHE_BELL, count: randSeedInt(2, 1) },
modifier: generateModifierType(modifierTypes.BERRY) as PokemonHeldItemModifierType, { entry: HeldItemId.LUCKY_EGG, count: randSeedInt(3, 1) },
}, { entry: HeldItemId.GOLDEN_EGG, count: randSeedInt(2, 0) },
{
modifier: generateModifierType(modifierTypes.BERRY) as PokemonHeldItemModifierType,
},
{
modifier: generateModifierType(modifierTypes.BERRY) as PokemonHeldItemModifierType,
},
{
modifier: generateModifierType(modifierTypes.BASE_STAT_BOOSTER) as PokemonHeldItemModifierType,
},
{
modifier: generateModifierType(modifierTypes.BASE_STAT_BOOSTER) as PokemonHeldItemModifierType,
},
{
modifier: generateModifierType(modifierTypes.TOXIC_ORB) as PokemonHeldItemModifierType,
stackCount: randSeedInt(2, 0),
},
{
modifier: generateModifierType(modifierTypes.SOOTHE_BELL) as PokemonHeldItemModifierType,
stackCount: randSeedInt(2, 1),
},
{
modifier: generateModifierType(modifierTypes.LUCKY_EGG) as PokemonHeldItemModifierType,
stackCount: randSeedInt(3, 1),
},
{
modifier: generateModifierType(modifierTypes.GOLDEN_EGG) as PokemonHeldItemModifierType,
stackCount: randSeedInt(2, 0),
},
], ],
}; };
const config: EnemyPartyConfig = { const config: EnemyPartyConfig = {
@ -157,18 +128,14 @@ export const TrashToTreasureEncounter: MysteryEncounter = MysteryEncounterBuilde
await transitionMysteryEncounterIntroVisuals(); await transitionMysteryEncounterIntroVisuals();
await tryApplyDigRewardItems(); await tryApplyDigRewardItems();
const blackSludge = generateModifierType(modifierTypes.MYSTERY_ENCOUNTER_BLACK_SLUDGE, [ const blackSludge = globalScene.trainerItems.add(TrainerItemId.BLACK_SLUDGE);
SHOP_ITEM_COST_MULTIPLIER, if (blackSludge) {
]);
const modifier = blackSludge?.newModifier();
if (modifier) {
await globalScene.addModifier(modifier, false, false, false, true);
globalScene.playSound("battle_anims/PRSFX- Venom Drench", { globalScene.playSound("battle_anims/PRSFX- Venom Drench", {
volume: 2, volume: 2,
}); });
await showEncounterText( await showEncounterText(
i18next.t("battle:rewardGain", { i18next.t("battle:rewardGain", {
modifierName: modifier.type.name, modifierName: allTrainerItems[TrainerItemId.BLACK_SLUDGE].name,
}), }),
null, null,
undefined, undefined,
@ -200,8 +167,8 @@ export const TrashToTreasureEncounter: MysteryEncounter = MysteryEncounterBuilde
const encounter = globalScene.currentBattle.mysteryEncounter!; const encounter = globalScene.currentBattle.mysteryEncounter!;
setEncounterRewards({ setEncounterRewards({
guaranteedModifierTypeFuncs: [modifierTypes.LEFTOVERS], guaranteedRewardSpecs: [HeldItemId.LEFTOVERS],
guaranteedModifierTiers: [ModifierTier.ROGUE, ModifierTier.ULTRA, ModifierTier.GREAT], guaranteedRarityTiers: [RarityTier.ROGUE, RarityTier.ULTRA, RarityTier.GREAT],
fillRemaining: true, fillRemaining: true,
}); });
encounter.startOfBattleEffects.push( encounter.startOfBattleEffects.push(
@ -225,44 +192,18 @@ export const TrashToTreasureEncounter: MysteryEncounter = MysteryEncounterBuilde
.build(); .build();
async function tryApplyDigRewardItems() { async function tryApplyDigRewardItems() {
const shellBell = generateModifierType(modifierTypes.SHELL_BELL) as PokemonHeldItemModifierType;
const leftovers = generateModifierType(modifierTypes.LEFTOVERS) as PokemonHeldItemModifierType;
const party = globalScene.getPlayerParty(); const party = globalScene.getPlayerParty();
// Iterate over the party until an item was successfully given
// First leftovers // First leftovers
for (const pokemon of party) { assignItemToFirstFreePokemon(HeldItemId.LEFTOVERS, party);
const heldItems = globalScene.findModifiers(
m => m instanceof PokemonHeldItemModifier && m.pokemonId === pokemon.id,
true,
) as PokemonHeldItemModifier[];
const existingLeftovers = heldItems.find(m => m instanceof TurnHealModifier) as TurnHealModifier;
if (!existingLeftovers || existingLeftovers.getStackCount() < existingLeftovers.getMaxStackCount()) {
await applyModifierTypeToPlayerPokemon(pokemon, leftovers);
break;
}
}
// Second leftovers // Second leftovers
for (const pokemon of party) { assignItemToFirstFreePokemon(HeldItemId.LEFTOVERS, party);
const heldItems = globalScene.findModifiers(
m => m instanceof PokemonHeldItemModifier && m.pokemonId === pokemon.id,
true,
) as PokemonHeldItemModifier[];
const existingLeftovers = heldItems.find(m => m instanceof TurnHealModifier) as TurnHealModifier;
if (!existingLeftovers || existingLeftovers.getStackCount() < existingLeftovers.getMaxStackCount()) {
await applyModifierTypeToPlayerPokemon(pokemon, leftovers);
break;
}
}
globalScene.playSound("item_fanfare"); globalScene.playSound("item_fanfare");
await showEncounterText( await showEncounterText(
i18next.t("battle:rewardGainCount", { i18next.t("battle:rewardGainCount", {
modifierName: leftovers.name, modifierName: allHeldItems[HeldItemId.LEFTOVERS].name,
count: 2, count: 2,
}), }),
null, null,
@ -271,23 +212,12 @@ async function tryApplyDigRewardItems() {
); );
// Only Shell bell // Only Shell bell
for (const pokemon of party) { assignItemToFirstFreePokemon(HeldItemId.SHELL_BELL, party);
const heldItems = globalScene.findModifiers(
m => m instanceof PokemonHeldItemModifier && m.pokemonId === pokemon.id,
true,
) as PokemonHeldItemModifier[];
const existingShellBell = heldItems.find(m => m instanceof HitHealModifier) as HitHealModifier;
if (!existingShellBell || existingShellBell.getStackCount() < existingShellBell.getMaxStackCount()) {
await applyModifierTypeToPlayerPokemon(pokemon, shellBell);
break;
}
}
globalScene.playSound("item_fanfare"); globalScene.playSound("item_fanfare");
await showEncounterText( await showEncounterText(
i18next.t("battle:rewardGainCount", { i18next.t("battle:rewardGainCount", {
modifierName: shellBell.name, modifierName: allHeldItems[HeldItemId.SHELL_BELL].name,
count: 1, count: 1,
}), }),
null, null,

View File

@ -2,6 +2,7 @@ import { CLASSIC_MODE_MYSTERY_ENCOUNTER_WAVES } from "#app/constants";
import { globalScene } from "#app/global-scene"; import { globalScene } from "#app/global-scene";
import { BattlerIndex } from "#enums/battler-index"; import { BattlerIndex } from "#enums/battler-index";
import { BattlerTagType } from "#enums/battler-tag-type"; import { BattlerTagType } from "#enums/battler-tag-type";
import { HeldItemCategoryId, type HeldItemId } from "#enums/held-item-id";
import type { MoveId } from "#enums/move-id"; import type { MoveId } from "#enums/move-id";
import { MoveUseMode } from "#enums/move-use-mode"; import { MoveUseMode } from "#enums/move-use-mode";
import { MysteryEncounterOptionMode } from "#enums/mystery-encounter-option-mode"; import { MysteryEncounterOptionMode } from "#enums/mystery-encounter-option-mode";
@ -10,7 +11,8 @@ import { MysteryEncounterType } from "#enums/mystery-encounter-type";
import { PokeballType } from "#enums/pokeball"; import { PokeballType } from "#enums/pokeball";
import { Stat } from "#enums/stat"; import { Stat } from "#enums/stat";
import type { EnemyPokemon, Pokemon } from "#field/pokemon"; import type { EnemyPokemon, Pokemon } from "#field/pokemon";
import { BerryModifier } from "#modifiers/modifier"; import type { HeldItemSpecs } from "#items/held-item-data-types";
import { getPartyBerries } from "#items/item-utility";
import { PokemonMove } from "#moves/pokemon-move"; import { PokemonMove } from "#moves/pokemon-move";
import { queueEncounterMessage } from "#mystery-encounters/encounter-dialogue-utils"; import { queueEncounterMessage } from "#mystery-encounters/encounter-dialogue-utils";
import type { EnemyPartyConfig } from "#mystery-encounters/encounter-phase-utils"; import type { EnemyPartyConfig } from "#mystery-encounters/encounter-phase-utils";
@ -29,10 +31,10 @@ import {
import type { MysteryEncounter } from "#mystery-encounters/mystery-encounter"; import type { MysteryEncounter } from "#mystery-encounters/mystery-encounter";
import { MysteryEncounterBuilder } from "#mystery-encounters/mystery-encounter"; import { MysteryEncounterBuilder } from "#mystery-encounters/mystery-encounter";
import { MysteryEncounterOptionBuilder } from "#mystery-encounters/mystery-encounter-option"; import { MysteryEncounterOptionBuilder } from "#mystery-encounters/mystery-encounter-option";
import { MoveRequirement, PersistentModifierRequirement } from "#mystery-encounters/mystery-encounter-requirements"; import { HeldItemRequirement, MoveRequirement } from "#mystery-encounters/mystery-encounter-requirements";
import { CHARMING_MOVES } from "#mystery-encounters/requirement-groups"; import { CHARMING_MOVES } from "#mystery-encounters/requirement-groups";
import { PokemonData } from "#system/pokemon-data"; import { PokemonData } from "#system/pokemon-data";
import { isNullOrUndefined, randSeedInt } from "#utils/common"; import { isNullOrUndefined, pickWeightedIndex, randSeedInt } from "#utils/common";
/** the i18n namespace for the encounter */ /** the i18n namespace for the encounter */
const namespace = "mysteryEncounters/uncommonBreed"; const namespace = "mysteryEncounters/uncommonBreed";
@ -187,7 +189,7 @@ export const UncommonBreedEncounter: MysteryEncounter = MysteryEncounterBuilder.
) )
.withOption( .withOption(
MysteryEncounterOptionBuilder.newOptionWithMode(MysteryEncounterOptionMode.DISABLED_OR_SPECIAL) MysteryEncounterOptionBuilder.newOptionWithMode(MysteryEncounterOptionMode.DISABLED_OR_SPECIAL)
.withSceneRequirement(new PersistentModifierRequirement("BerryModifier", 4)) // Will set option2PrimaryName and option2PrimaryMove dialogue tokens automatically .withSceneRequirement(new HeldItemRequirement(HeldItemCategoryId.BERRY, 4)) // Will set option2PrimaryName and option2PrimaryMove dialogue tokens automatically
.withDialogue({ .withDialogue({
buttonLabel: `${namespace}:option.2.label`, buttonLabel: `${namespace}:option.2.label`,
buttonTooltip: `${namespace}:option.2.tooltip`, buttonTooltip: `${namespace}:option.2.tooltip`,
@ -202,20 +204,16 @@ export const UncommonBreedEncounter: MysteryEncounter = MysteryEncounterBuilder.
// Give it some food // Give it some food
// Remove 4 random berries from player's party // Remove 4 random berries from player's party
// Get all player berry items, remove from party, and store reference const berryMap = getPartyBerries();
const berryItems: BerryModifier[] = globalScene.findModifiers(
m => m instanceof BerryModifier,
) as BerryModifier[];
for (let i = 0; i < 4; i++) { for (let i = 0; i < 4; i++) {
const index = randSeedInt(berryItems.length); const berryWeights = berryMap.map(b => (b.item as HeldItemSpecs).stack);
const randBerry = berryItems[index]; const index = pickWeightedIndex(berryWeights) ?? 0;
randBerry.stackCount--; const randBerry = berryMap[index];
if (randBerry.stackCount === 0) { globalScene.getPokemonById(randBerry.pokemonId)?.heldItemManager.remove(randBerry.item.id as HeldItemId);
globalScene.removeModifier(randBerry); (randBerry.item as HeldItemSpecs).stack -= 1;
berryItems.splice(index, 1);
} }
} await globalScene.updateItems(true);
await globalScene.updateModifiers(true, true);
// Pokemon joins the team, with 2 egg moves // Pokemon joins the team, with 2 egg moves
const encounter = globalScene.currentBattle.mysteryEncounter!; const encounter = globalScene.currentBattle.mysteryEncounter!;

View File

@ -1,9 +1,9 @@
import { globalScene } from "#app/global-scene"; import { globalScene } from "#app/global-scene";
import { allSpecies, modifierTypes } from "#data/data-lists"; import { allSpecies } 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 { Challenges } from "#enums/challenges"; import { Challenges } from "#enums/challenges";
import { ModifierTier } from "#enums/modifier-tier"; import { HeldItemId } from "#enums/held-item-id";
import { MysteryEncounterOptionMode } from "#enums/mystery-encounter-option-mode"; import { MysteryEncounterOptionMode } from "#enums/mystery-encounter-option-mode";
import { MysteryEncounterTier } from "#enums/mystery-encounter-tier"; import { MysteryEncounterTier } from "#enums/mystery-encounter-tier";
import { MysteryEncounterType } from "#enums/mystery-encounter-type"; import { MysteryEncounterType } from "#enums/mystery-encounter-type";
@ -11,17 +11,17 @@ 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 { PokemonType } from "#enums/pokemon-type";
import { RewardId } from "#enums/reward-id";
import { RarityTier } from "#enums/reward-tier";
import { SpeciesId } from "#enums/species-id"; import { SpeciesId } from "#enums/species-id";
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 { HeldItemConfiguration } from "#items/held-item-data-types";
import { HiddenAbilityRateBoosterModifier, PokemonFormChangeItemModifier } from "#modifiers/modifier"; import { TrainerItemEffect } from "#items/trainer-item";
import type { PokemonHeldItemModifierType } from "#modifiers/modifier-type";
import { PokemonMove } from "#moves/pokemon-move"; import { PokemonMove } from "#moves/pokemon-move";
import { showEncounterText } from "#mystery-encounters/encounter-dialogue-utils"; import { showEncounterText } from "#mystery-encounters/encounter-dialogue-utils";
import type { EnemyPartyConfig, EnemyPokemonConfig } from "#mystery-encounters/encounter-phase-utils"; import type { EnemyPartyConfig, EnemyPokemonConfig } from "#mystery-encounters/encounter-phase-utils";
import { import {
generateModifierType,
initBattleWithEnemyConfig, initBattleWithEnemyConfig,
leaveEncounterWithoutBattle, leaveEncounterWithoutBattle,
setEncounterRewards, setEncounterRewards,
@ -38,7 +38,6 @@ import { achvs } from "#system/achv";
import { PokemonData } from "#system/pokemon-data"; import { PokemonData } from "#system/pokemon-data";
import { trainerConfigs } from "#trainers/trainer-config"; import { trainerConfigs } from "#trainers/trainer-config";
import { TrainerPartyTemplate } from "#trainers/trainer-party-template"; import { TrainerPartyTemplate } from "#trainers/trainer-party-template";
import type { HeldModifierConfig } from "#types/held-modifier-config";
import { isNullOrUndefined, NumberHolder, randSeedInt, randSeedShuffle } from "#utils/common"; import { isNullOrUndefined, NumberHolder, randSeedInt, randSeedShuffle } from "#utils/common";
import { getPokemonSpecies } from "#utils/pokemon-utils"; import { getPokemonSpecies } from "#utils/pokemon-utils";
@ -220,13 +219,13 @@ export const WeirdDreamEncounter: MysteryEncounter = MysteryEncounterBuilder.wit
await doNewTeamPostProcess(transformations); await doNewTeamPostProcess(transformations);
setEncounterRewards({ setEncounterRewards({
guaranteedModifierTypeFuncs: [ guaranteedRewardSpecs: [
modifierTypes.MEMORY_MUSHROOM, RewardId.MEMORY_MUSHROOM,
modifierTypes.ROGUE_BALL, RewardId.ROGUE_BALL,
modifierTypes.MINT, RewardId.MINT,
modifierTypes.MINT, RewardId.MINT,
modifierTypes.MINT, RewardId.MINT,
modifierTypes.MINT, RewardId.MINT,
], ],
fillRemaining: false, fillRemaining: false,
}); });
@ -245,7 +244,7 @@ export const WeirdDreamEncounter: MysteryEncounter = MysteryEncounterBuilder.wit
], ],
}, },
async () => { async () => {
// Battle your "future" team for some item rewards // Battle your "future" team for some item RewardId
const transformations: PokemonTransformation[] = const transformations: PokemonTransformation[] =
globalScene.currentBattle.mysteryEncounter!.misc.teamTransformations; globalScene.currentBattle.mysteryEncounter!.misc.teamTransformations;
@ -261,20 +260,14 @@ export const WeirdDreamEncounter: MysteryEncounter = MysteryEncounterBuilder.wit
dataSource.player = false; dataSource.player = false;
// Copy held items to new pokemon // Copy held items to new pokemon
const newPokemonHeldItemConfigs: HeldModifierConfig[] = []; // TODO: Make items untransferable
for (const item of transformation.heldItems) { const newPokemonHeldItemConfig = transformation.heldItems;
newPokemonHeldItemConfigs.push({
modifier: item.clone() as PokemonHeldItemModifier,
stackCount: item.getStackCount(),
isTransferable: false,
});
}
// Any pokemon that is below 570 BST gets +20 permanent BST to 3 stats // Any pokemon that is below 570 BST gets +20 permanent BST to 3 stats
if (shouldGetOldGateau(newPokemon)) { if (shouldGetOldGateau(newPokemon)) {
newPokemonHeldItemConfigs.push({ newPokemonHeldItemConfig.push({
modifier: generateModifierType(modifierTypes.MYSTERY_ENCOUNTER_OLD_GATEAU) as PokemonHeldItemModifierType, entry: HeldItemId.OLD_GATEAU,
stackCount: 1, count: 1,
isTransferable: false,
}); });
} }
@ -283,7 +276,7 @@ export const WeirdDreamEncounter: MysteryEncounter = MysteryEncounterBuilder.wit
isBoss: newPokemon.getSpeciesForm().getBaseStatTotal() > NON_LEGENDARY_BST_THRESHOLD, isBoss: newPokemon.getSpeciesForm().getBaseStatTotal() > NON_LEGENDARY_BST_THRESHOLD,
level: previousPokemon.level, level: previousPokemon.level,
dataSource: dataSource, dataSource: dataSource,
modifierConfigs: newPokemonHeldItemConfigs, heldItemConfig: newPokemonHeldItemConfig,
}; };
enemyPokemonConfigs.push(enemyConfig); enemyPokemonConfigs.push(enemyConfig);
@ -302,7 +295,7 @@ export const WeirdDreamEncounter: MysteryEncounter = MysteryEncounterBuilder.wit
}; };
const onBeforeRewards = () => { const onBeforeRewards = () => {
// Before battle rewards, unlock the passive on a pokemon in the player's team for the rest of the run (not permanently) // Before battle RewardId, unlock the passive on a pokemon in the player's team for the rest of the run (not permanently)
// One random pokemon will get its passive unlocked // One random pokemon will get its passive unlocked
const passiveDisabledPokemon = globalScene.getPlayerParty().filter(p => !p.passive); const passiveDisabledPokemon = globalScene.getPlayerParty().filter(p => !p.passive);
if (passiveDisabledPokemon?.length > 0) { if (passiveDisabledPokemon?.length > 0) {
@ -315,13 +308,13 @@ export const WeirdDreamEncounter: MysteryEncounter = MysteryEncounterBuilder.wit
setEncounterRewards( setEncounterRewards(
{ {
guaranteedModifierTiers: [ guaranteedRarityTiers: [
ModifierTier.ROGUE, RarityTier.ROGUE,
ModifierTier.ROGUE, RarityTier.ROGUE,
ModifierTier.ULTRA, RarityTier.ULTRA,
ModifierTier.ULTRA, RarityTier.ULTRA,
ModifierTier.GREAT, RarityTier.GREAT,
ModifierTier.GREAT, RarityTier.GREAT,
], ],
fillRemaining: false, fillRemaining: false,
}, },
@ -365,7 +358,7 @@ interface PokemonTransformation {
previousPokemon: PlayerPokemon; previousPokemon: PlayerPokemon;
newSpecies: PokemonSpecies; newSpecies: PokemonSpecies;
newPokemon: PlayerPokemon; newPokemon: PlayerPokemon;
heldItems: PokemonHeldItemModifier[]; heldItems: HeldItemConfiguration;
} }
function getTeamTransformations(): PokemonTransformation[] { function getTeamTransformations(): PokemonTransformation[] {
@ -390,9 +383,7 @@ function getTeamTransformations(): PokemonTransformation[] {
for (let i = 0; i < numPokemon; i++) { for (let i = 0; i < numPokemon; i++) {
const removed = removedPokemon[i]; const removed = removedPokemon[i];
const index = pokemonTransformations.findIndex(p => p.previousPokemon.id === removed.id); const index = pokemonTransformations.findIndex(p => p.previousPokemon.id === removed.id);
pokemonTransformations[index].heldItems = removed pokemonTransformations[index].heldItems = removed.heldItemManager.generateHeldItemConfiguration();
.getHeldItems()
.filter(m => !(m instanceof PokemonFormChangeItemModifier));
const bst = removed.getSpeciesForm().getBaseStatTotal(); const bst = removed.getSpeciesForm().getBaseStatTotal();
let newBstRange: [number, number]; let newBstRange: [number, number];
@ -448,17 +439,14 @@ async function doNewTeamPostProcess(transformations: PokemonTransformation[]) {
} }
// Copy old items to new pokemon // Copy old items to new pokemon
for (const item of transformation.heldItems) { const heldItemConfiguration = transformation.heldItems;
item.pokemonId = newPokemon.id;
globalScene.addModifier(item, false, false, false, true);
}
// Any pokemon that is below 570 BST gets +20 permanent BST to 3 stats // Any pokemon that is below 570 BST gets +20 permanent BST to 3 stats
if (shouldGetOldGateau(newPokemon)) { if (shouldGetOldGateau(newPokemon)) {
const modType = modifierTypes.MYSTERY_ENCOUNTER_OLD_GATEAU(); heldItemConfiguration.push({
const modifier = modType?.newModifier(newPokemon); entry: HeldItemId.OLD_GATEAU,
if (modifier) { count: 1,
globalScene.addModifier(modifier, false, false, false, true); });
}
} }
newPokemon.calculateStats(); newPokemon.calculateStats();
@ -500,7 +488,9 @@ async function postProcessTransformedPokemon(
const hiddenIndex = newPokemon.species.ability2 ? 2 : 1; const hiddenIndex = newPokemon.species.ability2 ? 2 : 1;
if (newPokemon.abilityIndex < hiddenIndex) { if (newPokemon.abilityIndex < hiddenIndex) {
const hiddenAbilityChance = new NumberHolder(256); const hiddenAbilityChance = new NumberHolder(256);
globalScene.applyModifiers(HiddenAbilityRateBoosterModifier, true, hiddenAbilityChance); globalScene.applyPlayerItems(TrainerItemEffect.HIDDEN_ABILITY_CHANCE_BOOSTER, {
numberHolder: hiddenAbilityChance,
});
const hasHiddenAbility = !randSeedInt(hiddenAbilityChance.value); const hasHiddenAbility = !randSeedInt(hiddenAbilityChance.value);

View File

@ -1,9 +1,10 @@
import { globalScene } from "#app/global-scene"; import { globalScene } from "#app/global-scene";
import { allAbilities } from "#data/data-lists"; import { allAbilities, allHeldItems } from "#data/data-lists";
import { SpeciesFormChangeItemTrigger } from "#data/form-change-triggers"; import { SpeciesFormChangeItemTrigger } from "#data/form-change-triggers";
import { pokemonFormChanges } from "#data/pokemon-forms"; import { pokemonFormChanges } from "#data/pokemon-forms";
import type { AbilityId } from "#enums/ability-id"; import type { AbilityId } from "#enums/ability-id";
import { FormChangeItem } from "#enums/form-change-item"; import { FormChangeItem } from "#enums/form-change-item";
import { getHeldItemCategory, type HeldItemCategoryId, type HeldItemId } from "#enums/held-item-id";
import { MoveId } from "#enums/move-id"; import { MoveId } from "#enums/move-id";
import type { MysteryEncounterType } from "#enums/mystery-encounter-type"; import type { MysteryEncounterType } from "#enums/mystery-encounter-type";
import { Nature } from "#enums/nature"; import { Nature } from "#enums/nature";
@ -13,8 +14,6 @@ import { StatusEffect } from "#enums/status-effect";
import { TimeOfDay } from "#enums/time-of-day"; import { TimeOfDay } from "#enums/time-of-day";
import { WeatherType } from "#enums/weather-type"; import { WeatherType } from "#enums/weather-type";
import type { PlayerPokemon } from "#field/pokemon"; import type { PlayerPokemon } from "#field/pokemon";
import { AttackTypeBoosterModifier } from "#modifiers/modifier";
import type { AttackTypeBoosterModifierType } from "#modifiers/modifier-type";
import { coerceArray, isNullOrUndefined } from "#utils/common"; import { coerceArray, isNullOrUndefined } from "#utils/common";
export interface EncounterRequirement { export interface EncounterRequirement {
@ -351,39 +350,6 @@ export class PartySizeRequirement extends EncounterSceneRequirement {
} }
} }
export class PersistentModifierRequirement extends EncounterSceneRequirement {
requiredHeldItemModifiers: string[];
minNumberOfItems: number;
constructor(heldItem: string | string[], minNumberOfItems = 1) {
super();
this.minNumberOfItems = minNumberOfItems;
this.requiredHeldItemModifiers = coerceArray(heldItem);
}
override meetsRequirement(): boolean {
const partyPokemon = globalScene.getPlayerParty();
if (isNullOrUndefined(partyPokemon) || this.requiredHeldItemModifiers?.length < 0) {
return false;
}
let modifierCount = 0;
for (const modifier of this.requiredHeldItemModifiers) {
const matchingMods = globalScene.findModifiers(m => m.constructor.name === modifier);
if (matchingMods?.length > 0) {
for (const matchingMod of matchingMods) {
modifierCount += matchingMod.stackCount;
}
}
}
return modifierCount >= this.minNumberOfItems;
}
override getDialogueToken(_pokemon?: PlayerPokemon): [string, string] {
return ["requiredItem", this.requiredHeldItemModifiers[0]];
}
}
export class MoneyRequirement extends EncounterSceneRequirement { export class MoneyRequirement extends EncounterSceneRequirement {
requiredMoney: number; // Static value requiredMoney: number; // Static value
scalingMultiplier: number; // Calculates required money based off wave index scalingMultiplier: number; // Calculates required money based off wave index
@ -832,73 +798,14 @@ export class CanFormChangeWithItemRequirement extends EncounterPokemonRequiremen
} }
} }
export class HeldItemRequirement extends EncounterPokemonRequirement { export class HoldingItemRequirement extends EncounterPokemonRequirement {
requiredHeldItemModifiers: string[]; requiredHeldItems: (HeldItemId | HeldItemCategoryId)[];
minNumberOfPokemon: number;
invertQuery: boolean;
requireTransferable: boolean;
constructor(heldItem: string | string[], minNumberOfPokemon = 1, invertQuery = false, requireTransferable = true) {
super();
this.minNumberOfPokemon = minNumberOfPokemon;
this.invertQuery = invertQuery;
this.requiredHeldItemModifiers = coerceArray(heldItem);
this.requireTransferable = requireTransferable;
}
override meetsRequirement(): boolean {
const partyPokemon = globalScene.getPlayerParty();
if (isNullOrUndefined(partyPokemon)) {
return false;
}
return this.queryParty(partyPokemon).length >= this.minNumberOfPokemon;
}
override queryParty(partyPokemon: PlayerPokemon[]): PlayerPokemon[] {
if (!this.invertQuery) {
return partyPokemon.filter(pokemon =>
this.requiredHeldItemModifiers.some(heldItem => {
return pokemon.getHeldItems().some(it => {
return it.constructor.name === heldItem && (!this.requireTransferable || it.isTransferable);
});
}),
);
}
// for an inverted query, we only want to get the pokemon that have any held items that are NOT in requiredHeldItemModifiers
// E.g. functions as a blacklist
return partyPokemon.filter(
pokemon =>
pokemon.getHeldItems().filter(it => {
return (
!this.requiredHeldItemModifiers.some(heldItem => it.constructor.name === heldItem) &&
(!this.requireTransferable || it.isTransferable)
);
}).length > 0,
);
}
override getDialogueToken(pokemon?: PlayerPokemon): [string, string] {
const requiredItems = pokemon?.getHeldItems().filter(it => {
return (
this.requiredHeldItemModifiers.some(heldItem => it.constructor.name === heldItem) &&
(!this.requireTransferable || it.isTransferable)
);
});
if (requiredItems && requiredItems.length > 0) {
return ["heldItem", requiredItems[0].type.name];
}
return ["heldItem", ""];
}
}
export class AttackTypeBoosterHeldItemTypeRequirement extends EncounterPokemonRequirement {
requiredHeldItemTypes: PokemonType[];
minNumberOfPokemon: number; minNumberOfPokemon: number;
invertQuery: boolean; invertQuery: boolean;
requireTransferable: boolean; requireTransferable: boolean;
constructor( constructor(
heldItemTypes: PokemonType | PokemonType[], heldItem: HeldItemId | HeldItemCategoryId | (HeldItemId | HeldItemCategoryId)[],
minNumberOfPokemon = 1, minNumberOfPokemon = 1,
invertQuery = false, invertQuery = false,
requireTransferable = true, requireTransferable = true,
@ -906,7 +813,7 @@ export class AttackTypeBoosterHeldItemTypeRequirement extends EncounterPokemonRe
super(); super();
this.minNumberOfPokemon = minNumberOfPokemon; this.minNumberOfPokemon = minNumberOfPokemon;
this.invertQuery = invertQuery; this.invertQuery = invertQuery;
this.requiredHeldItemTypes = coerceArray(heldItemTypes); this.requiredHeldItems = coerceArray(heldItem);
this.requireTransferable = requireTransferable; this.requireTransferable = requireTransferable;
} }
@ -921,45 +828,92 @@ export class AttackTypeBoosterHeldItemTypeRequirement extends EncounterPokemonRe
override queryParty(partyPokemon: PlayerPokemon[]): PlayerPokemon[] { override queryParty(partyPokemon: PlayerPokemon[]): PlayerPokemon[] {
if (!this.invertQuery) { if (!this.invertQuery) {
return partyPokemon.filter(pokemon => return partyPokemon.filter(pokemon =>
this.requiredHeldItemTypes.some(heldItemType => { this.requiredHeldItems.some(heldItem => {
return pokemon.getHeldItems().some(it => { return this.requireTransferable
return ( ? pokemon.heldItemManager.hasTransferableItem(heldItem)
it instanceof AttackTypeBoosterModifier && : pokemon.heldItemManager.hasItem(heldItem);
(it.type as AttackTypeBoosterModifierType).moveType === heldItemType &&
(!this.requireTransferable || it.isTransferable)
);
});
}), }),
); );
} }
// for an inverted query, we only want to get the pokemon that have any held items that are NOT in requiredHeldItemModifiers // for an inverted query, we only want to get the pokemon that have any held items that are NOT in requiredHeldItemModifiers
// E.g. functions as a blacklist // E.g. functions as a blacklist
return partyPokemon.filter( return partyPokemon.filter(pokemon =>
pokemon => pokemon.getHeldItems().some(item => {
pokemon.getHeldItems().filter(it => { return (
return !this.requiredHeldItemTypes.some( !this.requiredHeldItems.some(heldItem => item === heldItem || getHeldItemCategory(item) === heldItem) &&
heldItemType => (!this.requireTransferable || allHeldItems[item].isTransferable)
it instanceof AttackTypeBoosterModifier &&
(it.type as AttackTypeBoosterModifierType).moveType === heldItemType &&
(!this.requireTransferable || it.isTransferable),
); );
}).length > 0, }),
); );
} }
override getDialogueToken(pokemon?: PlayerPokemon): [string, string] { override getDialogueToken(pokemon?: PlayerPokemon): [string, string] {
const requiredItems = pokemon?.getHeldItems().filter(it => { const requiredItems = pokemon?.getHeldItems().filter(item => {
return ( return (
this.requiredHeldItemTypes.some( this.requiredHeldItems.some(heldItem => item === heldItem) &&
heldItemType => (!this.requireTransferable || allHeldItems[item].isTransferable)
it instanceof AttackTypeBoosterModifier &&
(it.type as AttackTypeBoosterModifierType).moveType === heldItemType,
) &&
(!this.requireTransferable || it.isTransferable)
); );
}); });
if (requiredItems && requiredItems.length > 0) { if (requiredItems && requiredItems.length > 0) {
return ["heldItem", requiredItems[0].type.name]; return ["heldItem", allHeldItems[requiredItems[0]].name];
}
return ["heldItem", ""];
}
}
export class HeldItemRequirement extends EncounterSceneRequirement {
requiredHeldItems: (HeldItemId | HeldItemCategoryId)[];
minNumberOfItems: number;
invertQuery: boolean;
requireTransferable: boolean;
constructor(
heldItem: HeldItemId | HeldItemCategoryId | (HeldItemId | HeldItemCategoryId)[],
minNumberOfItems = 1,
invertQuery = false,
requireTransferable = true,
) {
super();
this.minNumberOfItems = minNumberOfItems;
this.invertQuery = invertQuery;
this.requiredHeldItems = coerceArray(heldItem);
this.requireTransferable = requireTransferable;
}
override meetsRequirement(): boolean {
const partyPokemon = globalScene.getPlayerParty();
if (isNullOrUndefined(partyPokemon)) {
return false;
}
console.log("COUNTED:", this.queryPartyForItems(partyPokemon), this.minNumberOfItems);
return this.queryPartyForItems(partyPokemon) >= this.minNumberOfItems;
}
queryPartyForItems(partyPokemon: PlayerPokemon[]): number {
let count = 0;
for (const pokemon of partyPokemon) {
for (const item of pokemon.getHeldItems()) {
const itemInList = this.requiredHeldItems.some(
heldItem => item === heldItem || getHeldItemCategory(item) === heldItem,
);
const requiredItem = this.invertQuery ? !itemInList : itemInList;
if (requiredItem && (!this.requireTransferable || allHeldItems[item].isTransferable)) {
count += pokemon.heldItemManager.getStack(item);
}
}
}
return count;
}
override getDialogueToken(pokemon?: PlayerPokemon): [string, string] {
const requiredItems = pokemon?.getHeldItems().filter(item => {
return (
this.requiredHeldItems.some(heldItem => item === heldItem) &&
(!this.requireTransferable || allHeldItems[item].isTransferable)
);
});
if (requiredItems && requiredItems.length > 0) {
return ["heldItem", allHeldItems[requiredItems[0]].name];
} }
return ["heldItem", ""]; return ["heldItem", ""];
} }

View File

@ -174,11 +174,11 @@ export class MysteryEncounter implements IMysteryEncounter {
onVisualsStart?: () => boolean; onVisualsStart?: () => boolean;
/** Event triggered prior to {@linkcode CommandPhase}, during {@linkcode TurnInitPhase} */ /** Event triggered prior to {@linkcode CommandPhase}, during {@linkcode TurnInitPhase} */
onTurnStart?: () => boolean; onTurnStart?: () => boolean;
/** Event prior to any rewards logic in {@linkcode MysteryEncounterRewardsPhase} */ /** Event prior to any reward logic in {@linkcode MysteryEncounterRewardsPhase} */
onRewards?: () => Promise<void>; onRewards?: () => Promise<void>;
/** Will provide the player party EXP before rewards are displayed for that wave */ /** Will provide the player party EXP before rewards are displayed for that wave */
doEncounterExp?: () => boolean; doEncounterExp?: () => boolean;
/** Will provide the player a rewards shop for that wave */ /** Will provide the player a reward shop for that wave */
doEncounterRewards?: () => boolean; doEncounterRewards?: () => boolean;
/** Will execute callback during VictoryPhase of a continuousEncounter */ /** Will execute callback during VictoryPhase of a continuousEncounter */
doContinueEncounter?: () => Promise<void>; doContinueEncounter?: () => Promise<void>;
@ -241,7 +241,7 @@ export class MysteryEncounter implements IMysteryEncounter {
* Defaults to true so that the first shop does not override the specified rewards. * Defaults to true so that the first shop does not override the specified rewards.
* Will be set to false after a shop is shown (so can't reroll same rarity items for free) * Will be set to false after a shop is shown (so can't reroll same rarity items for free)
*/ */
lockEncounterRewardTiers: boolean; lockEncounterRarityTiers: boolean;
/** /**
* Will be set automatically, indicates special moves in startOfBattleEffects are complete (so will not repeat) * Will be set automatically, indicates special moves in startOfBattleEffects are complete (so will not repeat)
*/ */
@ -296,7 +296,7 @@ export class MysteryEncounter implements IMysteryEncounter {
// Reset any dirty flags or encounter data // Reset any dirty flags or encounter data
this.startOfBattleEffectsComplete = false; this.startOfBattleEffectsComplete = false;
this.lockEncounterRewardTiers = true; this.lockEncounterRarityTiers = true;
this.dialogueTokens = {}; this.dialogueTokens = {};
this.enemyPartyConfigs = []; this.enemyPartyConfigs = [];
this.startOfBattleEffects = []; this.startOfBattleEffects = [];
@ -562,7 +562,7 @@ export class MysteryEncounterBuilder implements Partial<IMysteryEncounter> {
continuousEncounter = false; continuousEncounter = false;
catchAllowed = false; catchAllowed = false;
fleeAllowed = true; fleeAllowed = true;
lockEncounterRewardTiers = false; lockEncounterRarityTiers = false;
startOfBattleEffectsComplete = false; startOfBattleEffectsComplete = false;
hasBattleAnimationsWithoutTargets = false; hasBattleAnimationsWithoutTargets = false;
skipEnemyBattleTurns = false; skipEnemyBattleTurns = false;
@ -928,34 +928,6 @@ export class MysteryEncounterBuilder implements Partial<IMysteryEncounter> {
}); });
} }
/**
* Can set custom encounter rewards via this callback function
* If rewards are always deterministic for an encounter, this is a good way to set them
*
* NOTE: If rewards are dependent on options selected, runtime data, etc.,
* It may be better to programmatically set doEncounterRewards elsewhere.
* There is a helper function in mystery-encounter utils, setEncounterRewards(), which can be called programmatically to set rewards
* @param doEncounterRewards Synchronous callback function to perform during rewards phase of the encounter
* @returns
*/
withRewards(doEncounterRewards: () => boolean): this & Required<Pick<IMysteryEncounter, "doEncounterRewards">> {
return Object.assign(this, { doEncounterRewards: doEncounterRewards });
}
/**
* Can set custom encounter exp via this callback function
* If exp always deterministic for an encounter, this is a good way to set them
*
* NOTE: If rewards are dependent on options selected, runtime data, etc.,
* It may be better to programmatically set doEncounterExp elsewhere.
* There is a helper function in mystery-encounter utils, setEncounterExp(), which can be called programmatically to set rewards
* @param doEncounterExp Synchronous callback function to perform during rewards phase of the encounter
* @returns
*/
withExp(doEncounterExp: () => boolean): this & Required<Pick<IMysteryEncounter, "doEncounterExp">> {
return Object.assign(this, { doEncounterExp: doEncounterExp });
}
/** /**
* Can be used to perform init logic before intro visuals are shown and before the MysteryEncounterPhase begins * Can be used to perform init logic before intro visuals are shown and before the MysteryEncounterPhase begins
* Useful for performing things like procedural generation of intro sprites, etc. * Useful for performing things like procedural generation of intro sprites, etc.

View File

@ -5,7 +5,6 @@ import { globalScene } from "#app/global-scene";
import { getPokemonNameWithAffix } from "#app/messages"; import { getPokemonNameWithAffix } from "#app/messages";
import { BiomePoolTier, biomeLinks } from "#balance/biomes"; import { BiomePoolTier, biomeLinks } from "#balance/biomes";
import { initMoveAnim, loadMoveAnimAssets } from "#data/battle-anims"; import { initMoveAnim, loadMoveAnimAssets } from "#data/battle-anims";
import { modifierTypes } from "#data/data-lists";
import type { IEggOptions } from "#data/egg"; import type { IEggOptions } from "#data/egg";
import { Egg } from "#data/egg"; import { Egg } from "#data/egg";
import type { Gender } from "#data/gender"; import type { Gender } from "#data/gender";
@ -14,11 +13,9 @@ import type { CustomPokemonData } from "#data/pokemon-data";
import type { PokemonSpecies } from "#data/pokemon-species"; import type { PokemonSpecies } from "#data/pokemon-species";
import { Status } from "#data/status-effect"; import { Status } from "#data/status-effect";
import type { AiType } from "#enums/ai-type"; import type { AiType } from "#enums/ai-type";
import { BattleType } from "#enums/battle-type";
import type { BattlerTagType } from "#enums/battler-tag-type"; import type { BattlerTagType } from "#enums/battler-tag-type";
import { BiomeId } from "#enums/biome-id"; import { BiomeId } from "#enums/biome-id";
import { FieldPosition } from "#enums/field-position"; import { FieldPosition } from "#enums/field-position";
import { ModifierPoolType } from "#enums/modifier-pool-type";
import type { MoveId } from "#enums/move-id"; import type { MoveId } from "#enums/move-id";
import { MysteryEncounterMode } from "#enums/mystery-encounter-mode"; import { MysteryEncounterMode } from "#enums/mystery-encounter-mode";
import type { Nature } from "#enums/nature"; import type { Nature } from "#enums/nature";
@ -31,13 +28,8 @@ import { UiMode } from "#enums/ui-mode";
import type { PlayerPokemon, Pokemon } from "#field/pokemon"; import type { PlayerPokemon, Pokemon } from "#field/pokemon";
import { EnemyPokemon } from "#field/pokemon"; import { EnemyPokemon } from "#field/pokemon";
import { Trainer } from "#field/trainer"; import { Trainer } from "#field/trainer";
import type { CustomModifierSettings, ModifierType } from "#modifiers/modifier-type"; import type { HeldItemConfiguration } from "#items/held-item-data-types";
import { import type { CustomRewardSettings } from "#items/reward-pool-utils";
getPartyLuckValue,
ModifierTypeGenerator,
ModifierTypeOption,
regenerateModifierPoolThresholds,
} from "#modifiers/modifier-type";
import { PokemonMove } from "#moves/pokemon-move"; import { PokemonMove } from "#moves/pokemon-move";
import { showEncounterText } from "#mystery-encounters/encounter-dialogue-utils"; import { showEncounterText } from "#mystery-encounters/encounter-dialogue-utils";
import type { MysteryEncounterOption } from "#mystery-encounters/mystery-encounter-option"; import type { MysteryEncounterOption } from "#mystery-encounters/mystery-encounter-option";
@ -45,11 +37,11 @@ import type { Variant } from "#sprites/variant";
import type { PokemonData } from "#system/pokemon-data"; import type { PokemonData } from "#system/pokemon-data";
import type { TrainerConfig } from "#trainers/trainer-config"; import type { TrainerConfig } from "#trainers/trainer-config";
import { trainerConfigs } from "#trainers/trainer-config"; import { trainerConfigs } from "#trainers/trainer-config";
import type { HeldModifierConfig } from "#types/held-modifier-config";
import type { OptionSelectConfig, OptionSelectItem } from "#ui/abstract-option-select-ui-handler"; import type { OptionSelectConfig, OptionSelectItem } from "#ui/abstract-option-select-ui-handler";
import type { PartyOption, PokemonSelectFilter } from "#ui/party-ui-handler"; import type { PartyOption, PokemonSelectFilter } from "#ui/party-ui-handler";
import { PartyUiMode } from "#ui/party-ui-handler"; import { PartyUiMode } from "#ui/party-ui-handler";
import { coerceArray, isNullOrUndefined, randomString, randSeedInt, randSeedItem } from "#utils/common"; import { coerceArray, isNullOrUndefined, randomString, randSeedInt, randSeedItem } from "#utils/common";
import { getPartyLuckValue } from "#utils/party";
import { getPokemonSpecies } from "#utils/pokemon-utils"; import { getPokemonSpecies } from "#utils/pokemon-utils";
import i18next from "i18next"; import i18next from "i18next";
@ -101,7 +93,7 @@ export interface EnemyPokemonConfig {
/** Can set just the status, or pass a timer on the status turns */ /** Can set just the status, or pass a timer on the status turns */
status?: StatusEffect | [StatusEffect, number]; status?: StatusEffect | [StatusEffect, number];
mysteryEncounterBattleEffects?: (pokemon: Pokemon) => void; mysteryEncounterBattleEffects?: (pokemon: Pokemon) => void;
modifierConfigs?: HeldModifierConfig[]; heldItemConfig?: HeldItemConfiguration;
tags?: BattlerTagType[]; tags?: BattlerTagType[];
dataSource?: PokemonData; dataSource?: PokemonData;
tera?: PokemonType; tera?: PokemonType;
@ -199,6 +191,7 @@ export async function initBattleWithEnemyConfig(partyConfig: EnemyPartyConfig):
battle.enemyLevels.forEach((level, e) => { battle.enemyLevels.forEach((level, e) => {
let enemySpecies: PokemonSpecies | undefined; let enemySpecies: PokemonSpecies | undefined;
let heldItemConfig: HeldItemConfiguration = [];
let dataSource: PokemonData | undefined; let dataSource: PokemonData | undefined;
let isBoss = false; let isBoss = false;
if (!loaded) { if (!loaded) {
@ -210,12 +203,14 @@ export async function initBattleWithEnemyConfig(partyConfig: EnemyPartyConfig):
dataSource = config.dataSource; dataSource = config.dataSource;
enemySpecies = config.species; enemySpecies = config.species;
isBoss = config.isBoss; isBoss = config.isBoss;
heldItemConfig = config.heldItemConfig ?? [];
battle.enemyParty[e] = globalScene.addEnemyPokemon( battle.enemyParty[e] = globalScene.addEnemyPokemon(
enemySpecies, enemySpecies,
level, level,
TrainerSlot.TRAINER, TrainerSlot.TRAINER,
isBoss, isBoss,
false, false,
heldItemConfig,
dataSource, dataSource,
); );
} else { } else {
@ -225,6 +220,7 @@ export async function initBattleWithEnemyConfig(partyConfig: EnemyPartyConfig):
if (partyConfig?.pokemonConfigs && e < partyConfig.pokemonConfigs.length) { if (partyConfig?.pokemonConfigs && e < partyConfig.pokemonConfigs.length) {
const config = partyConfig.pokemonConfigs[e]; const config = partyConfig.pokemonConfigs[e];
level = config.level ? config.level : level; level = config.level ? config.level : level;
heldItemConfig = config.heldItemConfig ?? [];
dataSource = config.dataSource; dataSource = config.dataSource;
enemySpecies = config.species; enemySpecies = config.species;
isBoss = config.isBoss; isBoss = config.isBoss;
@ -241,6 +237,7 @@ export async function initBattleWithEnemyConfig(partyConfig: EnemyPartyConfig):
TrainerSlot.NONE, TrainerSlot.NONE,
isBoss, isBoss,
false, false,
heldItemConfig,
dataSource, dataSource,
); );
} }
@ -427,16 +424,6 @@ export async function initBattleWithEnemyConfig(partyConfig: EnemyPartyConfig):
enemyPokemon_2.x += 300; enemyPokemon_2.x += 300;
} }
}); });
if (!loaded) {
regenerateModifierPoolThresholds(
globalScene.getEnemyField(),
battle.battleType === BattleType.TRAINER ? ModifierPoolType.TRAINER : ModifierPoolType.WILD,
);
const customModifierTypes = partyConfig?.pokemonConfigs
?.filter(config => config?.modifierConfigs)
.map(config => config.modifierConfigs!);
globalScene.generateEnemyModifiers(customModifierTypes);
}
} }
/** /**
@ -485,45 +472,6 @@ export function updatePlayerMoney(changeValue: number, playSound = true, showMes
} }
} }
/**
* Converts modifier bullshit to an actual item
* @param modifier
* @param pregenArgs Can specify BerryType for berries, TM for TMs, AttackBoostType for item, etc.
*/
export function generateModifierType(modifier: () => ModifierType, pregenArgs?: any[]): ModifierType | null {
const modifierId = Object.keys(modifierTypes).find(k => modifierTypes[k] === modifier);
if (!modifierId) {
return null;
}
let result: ModifierType = modifierTypes[modifierId]();
// Populates item id and tier (order matters)
result = result
.withIdFromFunc(modifierTypes[modifierId])
.withTierFromPool(ModifierPoolType.PLAYER, globalScene.getPlayerParty());
return result instanceof ModifierTypeGenerator
? result.generateType(globalScene.getPlayerParty(), pregenArgs)
: result;
}
/**
* Converts modifier bullshit to an actual item
* @param modifier
* @param pregenArgs - can specify BerryType for berries, TM for TMs, AttackBoostType for item, etc.
*/
export function generateModifierTypeOption(
modifier: () => ModifierType,
pregenArgs?: any[],
): ModifierTypeOption | null {
const result = generateModifierType(modifier, pregenArgs);
if (result) {
return new ModifierTypeOption(result, 0);
}
return result;
}
/** /**
* This function is intended for use inside onPreOptionPhase() of an encounter option * This function is intended for use inside onPreOptionPhase() of an encounter option
* @param onPokemonSelected - Any logic that needs to be performed when Pokemon is chosen * @param onPokemonSelected - Any logic that needs to be performed when Pokemon is chosen
@ -738,12 +686,12 @@ export function selectOptionThenPokemon(
/** /**
* Will initialize reward phases to follow the mystery encounter * Will initialize reward phases to follow the mystery encounter
* Can have shop displayed or skipped * Can have shop displayed or skipped
* @param customShopRewards - adds a shop phase with the specified rewards / reward tiers * @param customShopRewards - adds a shop phase with the specified reward tiers
* @param eggRewards * @param eggRewards
* @param preRewardsCallback - can execute an arbitrary callback before the new phases if necessary (useful for updating items/party/injecting new phases before {@linkcode MysteryEncounterRewardsPhase}) * @param preRewardsCallback - can execute an arbitrary callback before the new phases if necessary (useful for updating items/party/injecting new phases before {@linkcode MysteryEncounterRewardsPhase})
*/ */
export function setEncounterRewards( export function setEncounterRewards(
customShopRewards?: CustomModifierSettings, customShopRewards?: CustomRewardSettings,
eggRewards?: IEggOptions[], eggRewards?: IEggOptions[],
preRewardsCallback?: Function, preRewardsCallback?: Function,
) { ) {
@ -753,7 +701,7 @@ export function setEncounterRewards(
} }
if (customShopRewards) { if (customShopRewards) {
globalScene.phaseManager.unshiftNew("SelectModifierPhase", 0, undefined, customShopRewards); globalScene.phaseManager.unshiftNew("SelectRewardPhase", 0, undefined, customShopRewards);
} else { } else {
globalScene.phaseManager.tryRemovePhase(p => p.is("MysteryEncounterRewardsPhase")); globalScene.phaseManager.tryRemovePhase(p => p.is("MysteryEncounterRewardsPhase"));
} }

View File

@ -1,7 +1,6 @@
import { globalScene } from "#app/global-scene"; import { globalScene } from "#app/global-scene";
import { getPokemonNameWithAffix } from "#app/messages"; import { getPokemonNameWithAffix } from "#app/messages";
import { speciesStarterCosts } from "#balance/starters"; import { speciesStarterCosts } from "#balance/starters";
import { modifierTypes } from "#data/data-lists";
import { Gender } from "#data/gender"; import { Gender } from "#data/gender";
import { import {
doPokeballBounceAnim, doPokeballBounceAnim,
@ -14,6 +13,7 @@ import type { PokemonSpecies } from "#data/pokemon-species";
import { getStatusEffectCatchRateMultiplier } from "#data/status-effect"; import { getStatusEffectCatchRateMultiplier } from "#data/status-effect";
import type { AbilityId } from "#enums/ability-id"; import type { AbilityId } from "#enums/ability-id";
import { ChallengeType } from "#enums/challenge-type"; import { ChallengeType } from "#enums/challenge-type";
import type { HeldItemId } from "#enums/held-item-id";
import { PlayerGender } from "#enums/player-gender"; import { PlayerGender } from "#enums/player-gender";
import type { PokeballType } from "#enums/pokeball"; import type { PokeballType } from "#enums/pokeball";
import type { PokemonType } from "#enums/pokemon-type"; import type { PokemonType } from "#enums/pokemon-type";
@ -23,8 +23,6 @@ import { StatusEffect } from "#enums/status-effect";
import { UiMode } from "#enums/ui-mode"; import { UiMode } from "#enums/ui-mode";
import { addPokeballCaptureStars, addPokeballOpenParticles } from "#field/anims"; import { addPokeballCaptureStars, addPokeballOpenParticles } from "#field/anims";
import type { EnemyPokemon, PlayerPokemon, Pokemon } from "#field/pokemon"; import type { EnemyPokemon, PlayerPokemon, Pokemon } from "#field/pokemon";
import { PokemonHeldItemModifier } from "#modifiers/modifier";
import type { PokemonHeldItemModifierType } from "#modifiers/modifier-type";
import { import {
getEncounterText, getEncounterText,
queueEncounterMessage, queueEncounterMessage,
@ -374,60 +372,13 @@ export function applyHealToPokemon(pokemon: PlayerPokemon, heal: number) {
applyHpChangeToPokemon(pokemon, heal); applyHpChangeToPokemon(pokemon, heal);
} }
/** export function applyHeldItemWithFallback(pokemon: Pokemon, item: HeldItemId, fallbackItem?: HeldItemId) {
* Will modify all of a Pokemon's base stats by a flat value const added = pokemon.heldItemManager.add(item);
* Base stats can never go below 1 if (!added && fallbackItem) {
* @param pokemon pokemon.heldItemManager.add(fallbackItem);
* @param value
*/
export async function modifyPlayerPokemonBST(pokemon: PlayerPokemon, good: boolean) {
const modType = modifierTypes
.MYSTERY_ENCOUNTER_SHUCKLE_JUICE()
.generateType(globalScene.getPlayerParty(), [good ? 10 : -15])
?.withIdFromFunc(modifierTypes.MYSTERY_ENCOUNTER_SHUCKLE_JUICE);
const modifier = modType?.newModifier(pokemon);
if (modifier) {
globalScene.addModifier(modifier, false, false, false, true);
pokemon.calculateStats();
} }
} }
/**
* Will attempt to add a new modifier to a Pokemon.
* If the Pokemon already has max stacks of that item, it will instead apply 'fallbackModifierType', if specified.
* @param scene
* @param pokemon
* @param modType
* @param fallbackModifierType
*/
export async function applyModifierTypeToPlayerPokemon(
pokemon: PlayerPokemon,
modType: PokemonHeldItemModifierType,
fallbackModifierType?: PokemonHeldItemModifierType,
) {
// Check if the Pokemon has max stacks of that item already
const modifier = modType.newModifier(pokemon);
const existing = globalScene.findModifier(
(m): m is PokemonHeldItemModifier =>
m instanceof PokemonHeldItemModifier &&
m.type.id === modType.id &&
m.pokemonId === pokemon.id &&
m.matchType(modifier),
) as PokemonHeldItemModifier | undefined;
// At max stacks
if (existing && existing.getStackCount() >= existing.getMaxStackCount()) {
if (!fallbackModifierType) {
return;
}
// Apply fallback
return applyModifierTypeToPlayerPokemon(pokemon, fallbackModifierType);
}
globalScene.addModifier(modifier, false, false, false, true);
}
/** /**
* Alternative to using AttemptCapturePhase * Alternative to using AttemptCapturePhase
* Assumes player sprite is visible on the screen (this is intended for non-combat uses) * Assumes player sprite is visible on the screen (this is intended for non-combat uses)
@ -693,19 +644,16 @@ export async function catchPokemon(
}; };
const addToParty = (slotIndex?: number) => { const addToParty = (slotIndex?: number) => {
const newPokemon = pokemon.addToParty(pokeballType, slotIndex); const newPokemon = pokemon.addToParty(pokeballType, slotIndex);
const modifiers = globalScene.findModifiers(m => m instanceof PokemonHeldItemModifier, false);
if (globalScene.getPlayerParty().filter(p => p.isShiny()).length === 6) { if (globalScene.getPlayerParty().filter(p => p.isShiny()).length === 6) {
globalScene.validateAchv(achvs.SHINY_PARTY); globalScene.validateAchv(achvs.SHINY_PARTY);
} }
Promise.all(modifiers.map(m => globalScene.addModifier(m, true))).then(() => { globalScene.updateItems(true);
globalScene.updateModifiers(true);
removePokemon(); removePokemon();
if (newPokemon) { if (newPokemon) {
newPokemon.loadAssets().then(end); newPokemon.loadAssets().then(end);
} else { } else {
end(); end();
} }
});
}; };
Promise.all([pokemon.hideInfo(), globalScene.gameData.setPokemonCaught(pokemon)]).then(() => { Promise.all([pokemon.hideInfo(), globalScene.gameData.setPokemonCaught(pokemon)]).then(() => {
const addStatus = new BooleanHolder(true); const addStatus = new BooleanHolder(true);
@ -737,6 +685,7 @@ export async function catchPokemon(
pokemon.variant, pokemon.variant,
pokemon.ivs, pokemon.ivs,
pokemon.nature, pokemon.nature,
pokemon.heldItemManager.generateHeldItemConfiguration(),
pokemon, pokemon,
); );
globalScene.ui.setMode( globalScene.ui.setMode(

View File

@ -1,5 +1,6 @@
import { globalScene } from "#app/global-scene"; import { globalScene } from "#app/global-scene";
import { PokeballType } from "#enums/pokeball"; import { PokeballType } from "#enums/pokeball";
import { TrainerItemEffect } from "#items/trainer-item";
import { NumberHolder } from "#utils/common"; import { NumberHolder } from "#utils/common";
import i18next from "i18next"; import i18next from "i18next";
@ -93,7 +94,9 @@ export function getCriticalCaptureChance(modifiedCatchRate: number): number {
} }
const dexCount = globalScene.gameData.getSpeciesCount(d => !!d.caughtAttr); const dexCount = globalScene.gameData.getSpeciesCount(d => !!d.caughtAttr);
const catchingCharmMultiplier = new NumberHolder(1); const catchingCharmMultiplier = new NumberHolder(1);
globalScene.findModifier(m => m.is("CriticalCatchChanceBoosterModifier"))?.apply(catchingCharmMultiplier); globalScene.applyPlayerItems(TrainerItemEffect.CRITICAL_CATCH_CHANCE_BOOSTER, {
numberHolder: catchingCharmMultiplier,
});
const dexMultiplier = const dexMultiplier =
globalScene.gameMode.isDaily || dexCount > 800 globalScene.gameMode.isDaily || dexCount > 800
? 2.5 ? 2.5

View File

@ -10,8 +10,8 @@ import { StatusEffect } from "#enums/status-effect";
import type { TimeOfDay } from "#enums/time-of-day"; import type { TimeOfDay } from "#enums/time-of-day";
import { WeatherType } from "#enums/weather-type"; import { WeatherType } from "#enums/weather-type";
import type { Pokemon } from "#field/pokemon"; import type { Pokemon } from "#field/pokemon";
import type { PokemonFormChangeItemModifier } from "#modifiers/modifier";
import { type Constructor, coerceArray } from "#utils/common"; import { type Constructor, coerceArray } from "#utils/common";
import { toCamelCase } from "#utils/strings";
import i18next from "i18next"; import i18next from "i18next";
export abstract class SpeciesFormChangeTrigger { export abstract class SpeciesFormChangeTrigger {
@ -77,16 +77,12 @@ export class SpeciesFormChangeItemTrigger extends SpeciesFormChangeTrigger {
} }
canChange(pokemon: Pokemon): boolean { canChange(pokemon: Pokemon): boolean {
return !!globalScene.findModifier(r => { const matchItem = pokemon.heldItemManager.formChangeItems[this.item];
// Assume that if m has the `formChangeItem` property, then it is a PokemonFormChangeItemModifier if (!matchItem) {
const m = r as PokemonFormChangeItemModifier; return false;
return ( }
"formChangeItem" in m && console.log("CAN CHANGE FORMS:", matchItem.active === this.active);
m.pokemonId === pokemon.id && return matchItem.active === this.active;
m.formChangeItem === this.item &&
m.active === this.active
);
});
} }
} }
@ -143,11 +139,7 @@ export class SpeciesFormChangeMoveLearnedTrigger extends SpeciesFormChangeTrigge
super(); super();
this.move = move; this.move = move;
this.known = known; this.known = known;
const moveKey = MoveId[this.move] const moveKey = toCamelCase(MoveId[this.move]);
.split("_")
.filter(f => f)
.map((f, i) => (i ? `${f[0]}${f.slice(1).toLowerCase()}` : f.toLowerCase()))
.join("") as unknown as string;
this.description = known this.description = known
? i18next.t("pokemonEvolutions:Forms.moveLearned", { ? i18next.t("pokemonEvolutions:Forms.moveLearned", {
move: i18next.t(`move:${moveKey}.name`), move: i18next.t(`move:${moveKey}.name`),

View File

@ -4,8 +4,8 @@ import { globalScene } from "#app/global-scene";
import { randSeedInt } from "#app/utils/common"; import { randSeedInt } from "#app/utils/common";
import { BattleType } from "#enums/battle-type"; import { BattleType } from "#enums/battle-type";
import { ClassicFixedBossWaves } from "#enums/fixed-boss-waves"; import { ClassicFixedBossWaves } from "#enums/fixed-boss-waves";
import { ModifierTier } from "#enums/modifier-tier";
import { PlayerGender } from "#enums/player-gender"; import { PlayerGender } from "#enums/player-gender";
import { RarityTier } from "#enums/reward-tier";
import { TrainerType } from "#enums/trainer-type"; import { TrainerType } from "#enums/trainer-type";
import { TrainerVariant } from "#enums/trainer-variant"; import { TrainerVariant } from "#enums/trainer-variant";
@ -45,8 +45,8 @@ export const classicFixedBattles: FixedBattleConfigs = {
globalScene.gameData.gender === PlayerGender.MALE ? TrainerVariant.FEMALE : TrainerVariant.DEFAULT, globalScene.gameData.gender === PlayerGender.MALE ? TrainerVariant.FEMALE : TrainerVariant.DEFAULT,
), ),
) )
.setCustomModifierRewards({ .setCustomRewards({
guaranteedModifierTiers: [ModifierTier.ULTRA, ModifierTier.GREAT, ModifierTier.GREAT], guaranteedRarityTiers: [RarityTier.ULTRA, RarityTier.GREAT, RarityTier.GREAT],
allowLuckUpgrades: false, allowLuckUpgrades: false,
}), }),
[ClassicFixedBossWaves.EVIL_GRUNT_1]: new FixedBattleConfig() [ClassicFixedBossWaves.EVIL_GRUNT_1]: new FixedBattleConfig()
@ -77,8 +77,8 @@ export const classicFixedBattles: FixedBattleConfigs = {
globalScene.gameData.gender === PlayerGender.MALE ? TrainerVariant.FEMALE : TrainerVariant.DEFAULT, globalScene.gameData.gender === PlayerGender.MALE ? TrainerVariant.FEMALE : TrainerVariant.DEFAULT,
), ),
) )
.setCustomModifierRewards({ .setCustomRewards({
guaranteedModifierTiers: [ModifierTier.ULTRA, ModifierTier.ULTRA, ModifierTier.GREAT, ModifierTier.GREAT], guaranteedRarityTiers: [RarityTier.ULTRA, RarityTier.ULTRA, RarityTier.GREAT, RarityTier.GREAT],
allowLuckUpgrades: false, allowLuckUpgrades: false,
}), }),
[ClassicFixedBossWaves.EVIL_GRUNT_2]: new FixedBattleConfig() [ClassicFixedBossWaves.EVIL_GRUNT_2]: new FixedBattleConfig()
@ -150,8 +150,8 @@ export const classicFixedBattles: FixedBattleConfigs = {
globalScene.gameData.gender === PlayerGender.MALE ? TrainerVariant.FEMALE : TrainerVariant.DEFAULT, globalScene.gameData.gender === PlayerGender.MALE ? TrainerVariant.FEMALE : TrainerVariant.DEFAULT,
), ),
) )
.setCustomModifierRewards({ .setCustomRewards({
guaranteedModifierTiers: [ModifierTier.ULTRA, ModifierTier.ULTRA, ModifierTier.ULTRA, ModifierTier.ULTRA], guaranteedRarityTiers: [RarityTier.ULTRA, RarityTier.ULTRA, RarityTier.ULTRA, RarityTier.ULTRA],
allowLuckUpgrades: false, allowLuckUpgrades: false,
}), }),
[ClassicFixedBossWaves.EVIL_GRUNT_4]: new FixedBattleConfig() [ClassicFixedBossWaves.EVIL_GRUNT_4]: new FixedBattleConfig()
@ -212,14 +212,8 @@ export const classicFixedBattles: FixedBattleConfigs = {
TrainerType.PENNY, TrainerType.PENNY,
]), ]),
) )
.setCustomModifierRewards({ .setCustomRewards({
guaranteedModifierTiers: [ guaranteedRarityTiers: [RarityTier.ROGUE, RarityTier.ROGUE, RarityTier.ULTRA, RarityTier.ULTRA, RarityTier.ULTRA],
ModifierTier.ROGUE,
ModifierTier.ROGUE,
ModifierTier.ULTRA,
ModifierTier.ULTRA,
ModifierTier.ULTRA,
],
allowLuckUpgrades: false, allowLuckUpgrades: false,
}), }),
[ClassicFixedBossWaves.RIVAL_5]: new FixedBattleConfig() [ClassicFixedBossWaves.RIVAL_5]: new FixedBattleConfig()
@ -231,14 +225,8 @@ export const classicFixedBattles: FixedBattleConfigs = {
globalScene.gameData.gender === PlayerGender.MALE ? TrainerVariant.FEMALE : TrainerVariant.DEFAULT, globalScene.gameData.gender === PlayerGender.MALE ? TrainerVariant.FEMALE : TrainerVariant.DEFAULT,
), ),
) )
.setCustomModifierRewards({ .setCustomRewards({
guaranteedModifierTiers: [ guaranteedRarityTiers: [RarityTier.ROGUE, RarityTier.ROGUE, RarityTier.ROGUE, RarityTier.ULTRA, RarityTier.ULTRA],
ModifierTier.ROGUE,
ModifierTier.ROGUE,
ModifierTier.ROGUE,
ModifierTier.ULTRA,
ModifierTier.ULTRA,
],
allowLuckUpgrades: false, allowLuckUpgrades: false,
}), }),
[ClassicFixedBossWaves.EVIL_BOSS_2]: new FixedBattleConfig() [ClassicFixedBossWaves.EVIL_BOSS_2]: new FixedBattleConfig()
@ -258,14 +246,14 @@ export const classicFixedBattles: FixedBattleConfigs = {
TrainerType.PENNY_2, TrainerType.PENNY_2,
]), ]),
) )
.setCustomModifierRewards({ .setCustomRewards({
guaranteedModifierTiers: [ guaranteedRarityTiers: [
ModifierTier.ROGUE, RarityTier.ROGUE,
ModifierTier.ROGUE, RarityTier.ROGUE,
ModifierTier.ULTRA, RarityTier.ULTRA,
ModifierTier.ULTRA, RarityTier.ULTRA,
ModifierTier.ULTRA, RarityTier.ULTRA,
ModifierTier.ULTRA, RarityTier.ULTRA,
], ],
allowLuckUpgrades: false, allowLuckUpgrades: false,
}), }),
@ -362,14 +350,14 @@ export const classicFixedBattles: FixedBattleConfigs = {
globalScene.gameData.gender === PlayerGender.MALE ? TrainerVariant.FEMALE : TrainerVariant.DEFAULT, globalScene.gameData.gender === PlayerGender.MALE ? TrainerVariant.FEMALE : TrainerVariant.DEFAULT,
), ),
) )
.setCustomModifierRewards({ .setCustomRewards({
guaranteedModifierTiers: [ guaranteedRarityTiers: [
ModifierTier.ROGUE, RarityTier.ROGUE,
ModifierTier.ROGUE, RarityTier.ROGUE,
ModifierTier.ULTRA, RarityTier.ULTRA,
ModifierTier.ULTRA, RarityTier.ULTRA,
ModifierTier.GREAT, RarityTier.GREAT,
ModifierTier.GREAT, RarityTier.GREAT,
], ],
allowLuckUpgrades: false, allowLuckUpgrades: false,
}), }),

View File

@ -3,7 +3,6 @@ import { globalScene } from "#app/global-scene";
import { pokemonEvolutions, pokemonPrevolutions } from "#balance/pokemon-evolutions"; import { pokemonEvolutions, pokemonPrevolutions } from "#balance/pokemon-evolutions";
import { signatureSpecies } from "#balance/signature-species"; import { signatureSpecies } from "#balance/signature-species";
import { tmSpecies } from "#balance/tms"; import { tmSpecies } from "#balance/tms";
import { modifierTypes } from "#data/data-lists";
import { doubleBattleDialogue } from "#data/double-battle-dialogue"; import { doubleBattleDialogue } from "#data/double-battle-dialogue";
import { Gender } from "#data/gender"; import { Gender } from "#data/gender";
import type { PokemonSpecies, PokemonSpeciesFilter } from "#data/pokemon-species"; import type { PokemonSpecies, PokemonSpeciesFilter } from "#data/pokemon-species";
@ -14,6 +13,7 @@ import { PokeballType } from "#enums/pokeball";
import { PokemonType } from "#enums/pokemon-type"; import { PokemonType } from "#enums/pokemon-type";
import { SpeciesId } from "#enums/species-id"; import { SpeciesId } from "#enums/species-id";
import { TeraAIMode } from "#enums/tera-ai-mode"; import { TeraAIMode } from "#enums/tera-ai-mode";
import { TrainerItemId } from "#enums/trainer-item-id";
import { TrainerPoolTier } from "#enums/trainer-pool-tier"; import { TrainerPoolTier } from "#enums/trainer-pool-tier";
import { TrainerSlot } from "#enums/trainer-slot"; import { TrainerSlot } from "#enums/trainer-slot";
import { TrainerType } from "#enums/trainer-type"; import { TrainerType } from "#enums/trainer-type";
@ -31,10 +31,10 @@ import {
TrainerPartyTemplate, TrainerPartyTemplate,
trainerPartyTemplates, trainerPartyTemplates,
} from "#trainers/trainer-party-template"; } from "#trainers/trainer-party-template";
import type { ModifierTypeFunc } from "#types/modifier-types"; import type { SilentReward } from "#types/rewards";
import type { import type {
GenAIFunc, GenAIFunc,
GenModifiersFunc, GenTrainerItemsFunc,
PartyMemberFunc, PartyMemberFunc,
PartyMemberFuncs, PartyMemberFuncs,
PartyTemplateFunc, PartyTemplateFunc,
@ -107,9 +107,9 @@ export class TrainerConfig {
public femaleEncounterBgm: string; public femaleEncounterBgm: string;
public doubleEncounterBgm: string; public doubleEncounterBgm: string;
public victoryBgm: string; public victoryBgm: string;
public genModifiersFunc: GenModifiersFunc; public genTrainerItemsFunc: GenTrainerItemsFunc;
public genAIFuncs: GenAIFunc[] = []; public genAIFuncs: GenAIFunc[] = [];
public modifierRewardFuncs: ModifierTypeFunc[] = []; public silentRewards: SilentReward[] = [];
public partyTemplates: TrainerPartyTemplate[]; public partyTemplates: TrainerPartyTemplate[];
public partyTemplateFunc: PartyTemplateFunc; public partyTemplateFunc: PartyTemplateFunc;
public partyMemberFuncs: PartyMemberFuncs = {}; public partyMemberFuncs: PartyMemberFuncs = {};
@ -458,8 +458,8 @@ export class TrainerConfig {
return this; return this;
} }
setGenModifiersFunc(genModifiersFunc: GenModifiersFunc): TrainerConfig { setGenTrainerItemsFunc(genTrainerItemsFunc: GenTrainerItemsFunc): TrainerConfig {
this.genModifiersFunc = genModifiersFunc; this.genTrainerItemsFunc = genTrainerItemsFunc;
return this; return this;
} }
@ -469,7 +469,7 @@ export class TrainerConfig {
* @param slot Optional, a specified slot that should be terastallized. Wraps to match party size (-1 will get the last slot and so on). * @param slot Optional, a specified slot that should be terastallized. Wraps to match party size (-1 will get the last slot and so on).
* @returns this * @returns this
*/ */
setRandomTeraModifiers(count: () => number, slot?: number): TrainerConfig { setRandomTeraType(count: () => number, slot?: number): TrainerConfig {
this.genAIFuncs.push((party: EnemyPokemon[]) => { this.genAIFuncs.push((party: EnemyPokemon[]) => {
const shedinjaCanTera = !this.hasSpecialtyType() || this.specialtyType === PokemonType.BUG; // Better to check one time than 6 const shedinjaCanTera = !this.hasSpecialtyType() || this.specialtyType === PokemonType.BUG; // Better to check one time than 6
const partyMemberIndexes = new Array(party.length) const partyMemberIndexes = new Array(party.length)
@ -500,24 +500,8 @@ export class TrainerConfig {
return this; return this;
} }
// function getRandomTeraModifiers(party: EnemyPokemon[], count: integer, types?: Type[]): PersistentModifier[] { setSilentReward(...silentRewards: SilentReward[]): TrainerConfig {
// const ret: PersistentModifier[] = []; this.silentRewards = silentRewards;
// const partyMemberIndexes = new Array(party.length).fill(null).map((_, i) => i);
// for (let t = 0; t < Math.min(count, party.length); t++) {
// const randomIndex = Utils.randSeedItem(partyMemberIndexes);
// partyMemberIndexes.splice(partyMemberIndexes.indexOf(randomIndex), 1);
// ret.push(modifierTypes.TERA_SHARD().generateType([], [ Utils.randSeedItem(types ? types : party[randomIndex].getTypes()) ])!.withIdFromFunc(modifierTypes.TERA_SHARD).newModifier(party[randomIndex]) as PersistentModifier); // TODO: is the bang correct?
// }
// return ret;
// }
setModifierRewardFuncs(...modifierTypeFuncs: (() => ModifierTypeFunc)[]): TrainerConfig {
this.modifierRewardFuncs = modifierTypeFuncs.map(func => () => {
const modifierTypeFunc = func();
const modifierType = modifierTypeFunc();
modifierType.withIdFromFunc(modifierTypeFunc);
return modifierType;
});
return this; return this;
} }
@ -682,7 +666,7 @@ export class TrainerConfig {
this.setHasVoucher(true); this.setHasVoucher(true);
this.setBattleBgm("battle_unova_gym"); this.setBattleBgm("battle_unova_gym");
this.setVictoryBgm("victory_gym"); this.setVictoryBgm("victory_gym");
this.setRandomTeraModifiers( this.setRandomTeraType(
() => (ignoreMinTeraWave || globalScene.currentBattle.waveIndex >= GYM_LEADER_TERA_WAVE ? 1 : 0), () => (ignoreMinTeraWave || globalScene.currentBattle.waveIndex >= GYM_LEADER_TERA_WAVE ? 1 : 0),
teraSlot, teraSlot,
); );
@ -743,7 +727,7 @@ export class TrainerConfig {
this.setHasVoucher(true); this.setHasVoucher(true);
this.setBattleBgm("battle_unova_elite"); this.setBattleBgm("battle_unova_elite");
this.setVictoryBgm("victory_gym"); this.setVictoryBgm("victory_gym");
this.setRandomTeraModifiers(() => 1, teraSlot); this.setRandomTeraType(() => 1, teraSlot);
return this; return this;
} }
@ -920,11 +904,11 @@ export class TrainerConfig {
clone = this.battleBgm ? clone.setBattleBgm(this.battleBgm) : clone; clone = this.battleBgm ? clone.setBattleBgm(this.battleBgm) : clone;
clone = this.encounterBgm ? clone.setEncounterBgm(this.encounterBgm) : clone; clone = this.encounterBgm ? clone.setEncounterBgm(this.encounterBgm) : clone;
clone = this.victoryBgm ? clone.setVictoryBgm(this.victoryBgm) : clone; clone = this.victoryBgm ? clone.setVictoryBgm(this.victoryBgm) : clone;
clone = this.genModifiersFunc ? clone.setGenModifiersFunc(this.genModifiersFunc) : clone; clone = this.genTrainerItemsFunc ? clone.setGenTrainerItemsFunc(this.genTrainerItemsFunc) : clone;
if (this.modifierRewardFuncs) { if (this.silentRewards) {
// Clones array instead of passing ref // Clones array instead of passing ref
clone.modifierRewardFuncs = this.modifierRewardFuncs.slice(0); clone.silentRewards = this.silentRewards.slice(0);
} }
if (this.partyTemplates) { if (this.partyTemplates) {
@ -992,6 +976,7 @@ export function getRandomPartyMemberFunc(
undefined, undefined,
false, false,
undefined, undefined,
undefined,
postProcess, postProcess,
); );
}; };
@ -1016,7 +1001,16 @@ function getSpeciesFilterRandomPartyMemberFunc(
.getTrainerSpeciesForLevel(level, true, strength, waveIndex), .getTrainerSpeciesForLevel(level, true, strength, waveIndex),
); );
return globalScene.addEnemyPokemon(species, level, trainerSlot, undefined, false, undefined, postProcess); return globalScene.addEnemyPokemon(
species,
level,
trainerSlot,
undefined,
false,
undefined,
undefined,
postProcess,
);
}; };
} }
@ -1865,27 +1859,43 @@ export const trainerConfigs: TrainerConfigs = {
.setPartyMemberFunc( .setPartyMemberFunc(
0, 0,
getRandomPartyMemberFunc([ getRandomPartyMemberFunc([
SpeciesId.METAPOD,
SpeciesId.LEDYBA,
SpeciesId.CLEFFA,
SpeciesId.WOOPER,
SpeciesId.TEDDIURSA,
SpeciesId.REMORAID,
SpeciesId.HOUNDOUR,
SpeciesId.SILCOON,
SpeciesId.PLUSLE, SpeciesId.PLUSLE,
SpeciesId.VOLBEAT, SpeciesId.VOLBEAT,
SpeciesId.PACHIRISU, SpeciesId.SPINDA,
SpeciesId.SILCOON, SpeciesId.BONSLY,
SpeciesId.METAPOD,
SpeciesId.IGGLYBUFF,
SpeciesId.PETILIL, SpeciesId.PETILIL,
SpeciesId.EEVEE, SpeciesId.SPRITZEE,
SpeciesId.MILCERY,
SpeciesId.PICHU,
]), ]),
) )
.setPartyMemberFunc( .setPartyMemberFunc(
1, 1,
getRandomPartyMemberFunc( getRandomPartyMemberFunc(
[ [
SpeciesId.KAKUNA,
SpeciesId.SPINARAK,
SpeciesId.IGGLYBUFF,
SpeciesId.PALDEA_WOOPER,
SpeciesId.PHANPY,
SpeciesId.MANTYKE,
SpeciesId.ELECTRIKE,
SpeciesId.CASCOON,
SpeciesId.MINUN, SpeciesId.MINUN,
SpeciesId.ILLUMISE, SpeciesId.ILLUMISE,
SpeciesId.EMOLGA, SpeciesId.SPINDA,
SpeciesId.CASCOON, SpeciesId.MIME_JR,
SpeciesId.KAKUNA,
SpeciesId.CLEFFA,
SpeciesId.COTTONEE, SpeciesId.COTTONEE,
SpeciesId.SWIRLIX,
SpeciesId.FIDOUGH,
SpeciesId.EEVEE, SpeciesId.EEVEE,
], ],
TrainerSlot.TRAINER_PARTNER, TrainerSlot.TRAINER_PARTNER,
@ -4512,10 +4522,7 @@ export const trainerConfigs: TrainerConfigs = {
.setBattleBgm("battle_rival") .setBattleBgm("battle_rival")
.setMixedBattleBgm("battle_rival") .setMixedBattleBgm("battle_rival")
.setPartyTemplates(trainerPartyTemplates.RIVAL) .setPartyTemplates(trainerPartyTemplates.RIVAL)
.setModifierRewardFuncs( .setSilentReward(TrainerItemId.SUPER_EXP_CHARM, TrainerItemId.EXP_SHARE)
() => modifierTypes.SUPER_EXP_CHARM,
() => modifierTypes.EXP_SHARE,
)
.setPartyMemberFunc( .setPartyMemberFunc(
0, 0,
getRandomPartyMemberFunc( getRandomPartyMemberFunc(
@ -4582,7 +4589,7 @@ export const trainerConfigs: TrainerConfigs = {
.setBattleBgm("battle_rival") .setBattleBgm("battle_rival")
.setMixedBattleBgm("battle_rival") .setMixedBattleBgm("battle_rival")
.setPartyTemplates(trainerPartyTemplates.RIVAL_2) .setPartyTemplates(trainerPartyTemplates.RIVAL_2)
.setModifierRewardFuncs(() => modifierTypes.EXP_SHARE) .setSilentReward(TrainerItemId.EXP_SHARE)
.setPartyMemberFunc( .setPartyMemberFunc(
0, 0,
getRandomPartyMemberFunc( getRandomPartyMemberFunc(
@ -4735,7 +4742,7 @@ export const trainerConfigs: TrainerConfigs = {
.setBattleBgm("battle_rival_2") .setBattleBgm("battle_rival_2")
.setMixedBattleBgm("battle_rival_2") .setMixedBattleBgm("battle_rival_2")
.setPartyTemplates(trainerPartyTemplates.RIVAL_4) .setPartyTemplates(trainerPartyTemplates.RIVAL_4)
.setModifierRewardFuncs(() => modifierTypes.TERA_ORB) .setSilentReward(TrainerItemId.TERA_ORB)
.setPartyMemberFunc( .setPartyMemberFunc(
0, 0,
getRandomPartyMemberFunc( getRandomPartyMemberFunc(

View File

@ -0,0 +1,37 @@
import type { ObjectValues } from "#types/type-helpers";
/**
* Enum representing the various "classes" of item effects that can be applied.
*/
export const HeldItemEffect = {
ATTACK_TYPE_BOOST: 1,
TURN_END_HEAL: 2,
HIT_HEAL: 3,
RESET_NEGATIVE_STAT_STAGE: 4,
EXP_BOOSTER: 5,
// Should we actually distinguish different berry effects?
BERRY: 6,
BASE_STAT_BOOSTER: 7,
INSTANT_REVIVE: 8,
STAT_BOOST: 9,
CRIT_BOOST: 10,
TURN_END_STATUS: 11,
SURVIVE_CHANCE: 12,
BYPASS_SPEED_CHANCE: 13,
FLINCH_CHANCE: 14,
FIELD_EFFECT: 15,
FRIENDSHIP_BOOSTER: 16,
NATURE_WEIGHT_BOOSTER: 17,
ACCURACY_BOOSTER: 18,
MULTI_HIT: 19,
DAMAGE_MONEY_REWARD: 20,
BATON: 21,
TURN_END_ITEM_STEAL: 22,
CONTACT_ITEM_STEAL_CHANCE: 23,
EVO_TRACKER: 40,
BASE_STAT_TOTAL: 50,
BASE_STAT_FLAT: 51,
INCREMENTING_STAT: 52,
} as const;
export type HeldItemEffect = ObjectValues<typeof HeldItemEffect>;

151
src/enums/held-item-id.ts Normal file
View File

@ -0,0 +1,151 @@
import type { ObjectValues } from "#types/type-helpers";
// TODO: make category the lower 2 bytes
export const HeldItemId = {
NONE: 0x0000,
// Berries
SITRUS_BERRY: 0x0101,
LUM_BERRY: 0x0102,
ENIGMA_BERRY: 0x0103,
LIECHI_BERRY: 0x0104,
GANLON_BERRY: 0x0105,
PETAYA_BERRY: 0x0106,
APICOT_BERRY: 0x0107,
SALAC_BERRY: 0x0108,
LANSAT_BERRY: 0x0109,
STARF_BERRY: 0x010A,
LEPPA_BERRY: 0x010B,
// Other items that are consumed
REVIVER_SEED: 0x0201,
WHITE_HERB: 0x0202,
// Type Boosters
SILK_SCARF: 0x0301,
BLACK_BELT: 0x0302,
SHARP_BEAK: 0x0303,
POISON_BARB: 0x0304,
SOFT_SAND: 0x0305,
HARD_STONE: 0x0306,
SILVER_POWDER: 0x0307,
SPELL_TAG: 0x0308,
METAL_COAT: 0x0309,
CHARCOAL: 0x030A,
MYSTIC_WATER: 0x030B,
MIRACLE_SEED: 0x030C,
MAGNET: 0x030D,
TWISTED_SPOON: 0x030E,
NEVER_MELT_ICE: 0x030F,
DRAGON_FANG: 0x0310,
BLACK_GLASSES: 0x0311,
FAIRY_FEATHER: 0x0312,
// Species Stat Boosters
LIGHT_BALL: 0x0401,
THICK_CLUB: 0x0402,
METAL_POWDER: 0x0403,
QUICK_POWDER: 0x0404,
DEEP_SEA_SCALE: 0x0405,
DEEP_SEA_TOOTH: 0x0406,
// Crit Boosters
SCOPE_LENS: 0x0501,
LEEK: 0x0502,
// Items increasing gains
LUCKY_EGG: 0x0601,
GOLDEN_EGG: 0x0602,
SOOTHE_BELL: 0x0603,
// Unique items
FOCUS_BAND: 0x0701,
QUICK_CLAW: 0x0702,
KINGS_ROCK: 0x0703,
LEFTOVERS: 0x0704,
SHELL_BELL: 0x0705,
MYSTICAL_ROCK: 0x0706,
WIDE_LENS: 0x0707,
MULTI_LENS: 0x0708,
GOLDEN_PUNCH: 0x0709,
GRIP_CLAW: 0x070A,
TOXIC_ORB: 0x070B,
FLAME_ORB: 0x070C,
SOUL_DEW: 0x070D,
BATON: 0x070E,
MINI_BLACK_HOLE: 0x070F,
EVIOLITE: 0x0710,
// Vitamins
HP_UP: 0x0801,
PROTEIN: 0x0802,
IRON: 0x0803,
CALCIUM: 0x0804,
ZINC: 0x0805,
CARBOS: 0x0806,
// Other stat boosting items
SHUCKLE_JUICE_GOOD: 0x0901,
SHUCKLE_JUICE_BAD: 0x0902,
OLD_GATEAU: 0x0903,
MACHO_BRACE: 0x0904,
// Evo trackers
GIMMIGHOUL_EVO_TRACKER: 0x0A01,
} as const;
export type HeldItemId = ObjectValues<typeof HeldItemId>;
type HeldItemNameMap = {
[k in HeldItemName as (typeof HeldItemId)[k]]: k
}
type HeldItemName = keyof typeof HeldItemId;
/** `const object` mapping all held item IDs to their respective names. */
// TODO: This stores names as UPPER_SNAKE_CASE, but the locales are in PascalCase...
export const HeldItemNames = Object.freeze(Object.entries(HeldItemId).reduce(
// Use a type-safe reducer to force number keys and values
(acc, [key, value]) => {
acc[value] = key;
return acc;
},
{}
)) as HeldItemNameMap;
export const HeldItemCategoryId = {
NONE: 0x0000,
BERRY: 0x0100,
CONSUMABLE: 0x0200,
TYPE_ATTACK_BOOSTER: 0x0300,
SPECIES_STAT_BOOSTER: 0x0400,
CRIT_BOOSTER: 0x0500,
GAIN_INCREASE: 0x0600,
UNIQUE: 0x0700,
VITAMIN: 0x0800,
BASE_STAT_BOOST: 0x0900,
EVO_TRACKER: 0x0A00,
} as const;
export type HeldItemCategoryId = ObjectValues<typeof HeldItemCategoryId>;
const ITEM_CATEGORY_MASK = 0xFF00
export function getHeldItemCategory(itemId: HeldItemId): HeldItemCategoryId {
return (itemId & ITEM_CATEGORY_MASK) as HeldItemCategoryId;
}
export function isCategoryId(id: number): id is HeldItemCategoryId {
return (Object.values(HeldItemCategoryId) as number[]).includes(id);
}
export function isItemInCategory(itemId: HeldItemId, category: HeldItemCategoryId): boolean {
return getHeldItemCategory(itemId) === category;
}
export function isItemInRequested(
itemId: HeldItemId,
requestedItems: (HeldItemCategoryId | HeldItemId)[]
): boolean {
return requestedItems.some(entry => itemId === entry || (itemId & ITEM_CATEGORY_MASK) === entry);
}

View File

@ -1,7 +0,0 @@
export enum ModifierPoolType {
PLAYER,
WILD,
TRAINER,
ENEMY_BUFF,
DAILY_STARTER
}

98
src/enums/reward-id.ts Normal file
View File

@ -0,0 +1,98 @@
import type { ObjectValues } from "#types/type-helpers";
export const RewardId = {
NONE: 0x0000,
POKEBALL: 0x2001,
GREAT_BALL: 0x2002,
ULTRA_BALL: 0x2003,
ROGUE_BALL: 0x2004,
MASTER_BALL: 0x2005,
VOUCHER: 0x2101,
VOUCHER_PLUS: 0x2102,
VOUCHER_PREMIUM: 0x2103,
NUGGET: 0x2201,
BIG_NUGGET: 0x2202,
RELIC_GOLD: 0x2203,
RARE_CANDY: 0x2301,
RARER_CANDY: 0x2302,
EVOLUTION_ITEM: 0x2401,
RARE_EVOLUTION_ITEM: 0x2402,
POTION: 0x2501,
SUPER_POTION: 0x2502,
HYPER_POTION: 0x2503,
MAX_POTION: 0x2504,
FULL_HEAL: 0x2505,
FULL_RESTORE: 0x2506,
REVIVE: 0x2601,
MAX_REVIVE: 0x2602,
SACRED_ASH: 0x2603,
ETHER: 0x2701,
MAX_ETHER: 0x2702,
ELIXIR: 0x2801,
MAX_ELIXIR: 0x2802,
PP_UP: 0x2901,
PP_MAX: 0x2902,
TM_COMMON: 0x2A01,
TM_GREAT: 0x2A02,
TM_ULTRA: 0x2A03,
MINT: 0x2B01,
TERA_SHARD: 0x2B02,
MEMORY_MUSHROOM: 0x2B03,
DNA_SPLICERS: 0x2B04,
SPECIES_STAT_BOOSTER: 0x2C01,
RARE_SPECIES_STAT_BOOSTER: 0x2C02,
BASE_STAT_BOOSTER: 0x2C03,
ATTACK_TYPE_BOOSTER: 0x2C04,
BERRY: 0x2C05,
TEMP_STAT_STAGE_BOOSTER: 0x2D01,
DIRE_HIT: 0x2D02,
LURE: 0x2D03,
SUPER_LURE: 0x2D04,
MAX_LURE: 0x2D05,
FORM_CHANGE_ITEM: 0x2E01,
RARE_FORM_CHANGE_ITEM: 0x2E02,
} as const;
export type RewardId = ObjectValues<typeof RewardId>;
export const RewardCategoryId = {
NONE: 0x0000,
POKEBALL: 0x0100,
VOUCHER: 0x0200,
MONEY: 0x0300,
CANDY: 0x0400,
EVOLUTION_ITEM: 0x0500,
HEALING: 0x0600,
REVIVE: 0x0700,
ETHER: 0x0800,
ELIXIR: 0x0900,
PP_UP: 0x0A00,
TM: 0x0B00,
OTHER: 0x0C00,
HELD_ITEM: 0x0D00,
TRAINER_ITEM: 0x0E00,
FORM_CHANGE_ITEM: 0x0F00,
} as const;
export type RewardCategoryId = ObjectValues<typeof RewardCategoryId>;
const ITEM_CATEGORY_MASK = 0xFF00
export function getRewardCategory(itemId: RewardId): RewardCategoryId {
return (itemId & ITEM_CATEGORY_MASK) as RewardCategoryId;
}

View File

@ -0,0 +1,13 @@
export enum RewardPoolType {
PLAYER,
}
export enum HeldItemPoolType {
WILD,
TRAINER,
DAILY_STARTER,
}
export enum TrainerItemPoolType {
ENEMY_BUFF,
}

View File

@ -1,4 +1,4 @@
export enum ModifierTier { export enum RarityTier {
COMMON, COMMON,
GREAT, GREAT,
ULTRA, ULTRA,

View File

@ -0,0 +1,68 @@
export const TrainerItemId = {
NONE: 0x0000,
MAP: 0x1001,
IV_SCANNER: 0x1002,
LOCK_CAPSULE: 0x1003,
MEGA_BRACELET: 0x1004,
DYNAMAX_BAND: 0x1005,
TERA_ORB: 0x1006,
GOLDEN_POKEBALL: 0x1007,
OVAL_CHARM: 0x1008,
EXP_SHARE: 0x1009,
EXP_BALANCE: 0x100A,
CANDY_JAR: 0x100B,
BERRY_POUCH: 0x100C,
HEALING_CHARM: 0x100D,
EXP_CHARM: 0x100E,
SUPER_EXP_CHARM: 0x100F,
GOLDEN_EXP_CHARM: 0x1010,
AMULET_COIN: 0x1011,
ABILITY_CHARM: 0x1012,
SHINY_CHARM: 0x1013,
CATCHING_CHARM: 0x1014,
BLACK_SLUDGE: 0x1015,
GOLDEN_BUG_NET: 0x1016,
LURE: 0x1101,
SUPER_LURE: 0x1102,
MAX_LURE: 0x1103,
X_ATTACK: 0x1201,
X_DEFENSE: 0x1202,
X_SP_ATK: 0x1203,
X_SP_DEF: 0x1204,
X_SPEED: 0x1205,
X_ACCURACY: 0x1206,
DIRE_HIT: 0x1207,
ENEMY_DAMAGE_BOOSTER: 0x1301,
ENEMY_DAMAGE_REDUCTION: 0x1302,
ENEMY_HEAL: 0x1303,
ENEMY_ATTACK_POISON_CHANCE: 0x1304,
ENEMY_ATTACK_PARALYZE_CHANCE: 0x1305,
ENEMY_ATTACK_BURN_CHANCE: 0x1306,
ENEMY_STATUS_EFFECT_HEAL_CHANCE: 0x1307,
ENEMY_ENDURE_CHANCE: 0x1308,
ENEMY_FUSED_CHANCE: 0x1309,
} as const;
export type TrainerItemId = (typeof TrainerItemId)[keyof typeof TrainerItemId];
type TrainerItemName = keyof typeof TrainerItemId;
type TrainerItemValue = typeof TrainerItemId[TrainerItemName];
// Use a type-safe reducer to force number keys and values
export const TrainerItemNames: Record<TrainerItemValue, TrainerItemName> = Object.entries(TrainerItemId).reduce(
(acc, [key, value]) => {
acc[value as TrainerItemValue] = key as TrainerItemName;
return acc;
},
{} as Record<TrainerItemValue, TrainerItemName>
);

View File

@ -5,7 +5,7 @@ export enum UiMode {
FIGHT, FIGHT,
BALL, BALL,
TARGET_SELECT, TARGET_SELECT,
MODIFIER_SELECT, REWARD_SELECT,
SAVE_SLOT, SAVE_SLOT,
PARTY, PARTY,
SUMMARY, SUMMARY,
@ -38,6 +38,7 @@ export enum UiMode {
UNAVAILABLE, UNAVAILABLE,
CHALLENGE_SELECT, CHALLENGE_SELECT,
RENAME_POKEMON, RENAME_POKEMON,
RENAME_RUN,
RUN_HISTORY, RUN_HISTORY,
RUN_INFO, RUN_INFO,
TEST_DIALOGUE, TEST_DIALOGUE,

View File

@ -1,4 +1,5 @@
import type { BerryModifier } from "#modifiers/modifier"; import type { BerryType } from "#enums/berry-type";
import type { Pokemon } from "#field/pokemon";
import type { Move } from "#moves/move"; import type { Move } from "#moves/move";
/** Alias for all {@linkcode BattleScene} events */ /** Alias for all {@linkcode BattleScene} events */
@ -81,12 +82,13 @@ export class MoveUsedEvent extends Event {
* @extends Event * @extends Event
*/ */
export class BerryUsedEvent extends Event { export class BerryUsedEvent extends Event {
/** The {@linkcode BerryModifier} being used */ /** The {@linkcode BerryType} being used */
public berryModifier: BerryModifier; public pokemon: Pokemon;
constructor(berry: BerryModifier) { public berryType: BerryType;
constructor(pokemon: Pokemon, berryType: BerryType) {
super(BattleSceneEventType.BERRY_USED); super(BattleSceneEventType.BERRY_USED);
this.pokemon = pokemon;
this.berryModifier = berry; this.berryType = berryType;
} }
} }

View File

@ -24,6 +24,7 @@ import { ArenaTagSide } from "#enums/arena-tag-side";
import type { ArenaTagType } from "#enums/arena-tag-type"; import type { ArenaTagType } from "#enums/arena-tag-type";
import type { BattlerIndex } from "#enums/battler-index"; import type { BattlerIndex } from "#enums/battler-index";
import { BiomeId } from "#enums/biome-id"; import { BiomeId } from "#enums/biome-id";
import { HeldItemEffect } from "#enums/held-item-effect";
import { CommonAnim } from "#enums/move-anims-common"; import { CommonAnim } from "#enums/move-anims-common";
import type { MoveId } from "#enums/move-id"; import type { MoveId } from "#enums/move-id";
import type { PokemonType } from "#enums/pokemon-type"; import type { PokemonType } from "#enums/pokemon-type";
@ -33,7 +34,7 @@ import { TrainerType } from "#enums/trainer-type";
import { WeatherType } from "#enums/weather-type"; import { WeatherType } from "#enums/weather-type";
import { TagAddedEvent, TagRemovedEvent, TerrainChangedEvent, WeatherChangedEvent } from "#events/arena"; import { TagAddedEvent, TagRemovedEvent, TerrainChangedEvent, WeatherChangedEvent } from "#events/arena";
import type { Pokemon } from "#field/pokemon"; import type { Pokemon } from "#field/pokemon";
import { FieldEffectModifier } from "#modifiers/modifier"; import { applyHeldItems } from "#items/all-held-items";
import type { Move } from "#moves/move"; import type { Move } from "#moves/move";
import type { AbstractConstructor } from "#types/type-helpers"; import type { AbstractConstructor } from "#types/type-helpers";
import { type Constructor, isNullOrUndefined, NumberHolder, randSeedInt } from "#utils/common"; import { type Constructor, isNullOrUndefined, NumberHolder, randSeedInt } from "#utils/common";
@ -54,7 +55,7 @@ export class Arena {
public bgm: string; public bgm: string;
public ignoreAbilities: boolean; public ignoreAbilities: boolean;
public ignoringEffectSource: BattlerIndex | null; public ignoringEffectSource: BattlerIndex | null;
public playerTerasUsed: number; public playerTerasUsed = 0;
/** /**
* Saves the number of times a party pokemon faints during a arena encounter. * Saves the number of times a party pokemon faints during a arena encounter.
* {@linkcode globalScene.currentBattle.enemyFaints} is the corresponding faint counter for the enemy (this resets every wave). * {@linkcode globalScene.currentBattle.enemyFaints} is the corresponding faint counter for the enemy (this resets every wave).
@ -68,12 +69,11 @@ export class Arena {
public readonly eventTarget: EventTarget = new EventTarget(); public readonly eventTarget: EventTarget = new EventTarget();
constructor(biome: BiomeId, bgm: string, playerFaints = 0) { constructor(biome: BiomeId, playerFaints = 0) {
this.biomeType = biome; this.biomeType = biome;
this.bgm = bgm; this.bgm = BiomeId[biome].toLowerCase();
this.trainerPool = biomeTrainerPools[biome]; this.trainerPool = biomeTrainerPools[biome];
this.updatePoolsForTimeOfDay(); this.updatePoolsForTimeOfDay();
this.playerTerasUsed = 0;
this.playerFaints = playerFaints; this.playerFaints = playerFaints;
} }
@ -341,7 +341,7 @@ export class Arena {
if (!isNullOrUndefined(user)) { if (!isNullOrUndefined(user)) {
weatherDuration.value = 5; weatherDuration.value = 5;
globalScene.applyModifier(FieldEffectModifier, user.isPlayer(), user, weatherDuration); applyHeldItems(HeldItemEffect.FIELD_EFFECT, { pokemon: user, fieldDuration: weatherDuration });
} }
this.weather = weather ? new Weather(weather, weatherDuration.value) : null; this.weather = weather ? new Weather(weather, weatherDuration.value) : null;
@ -428,7 +428,7 @@ export class Arena {
if (!isNullOrUndefined(user)) { if (!isNullOrUndefined(user)) {
terrainDuration.value = 5; terrainDuration.value = 5;
globalScene.applyModifier(FieldEffectModifier, user.isPlayer(), user, terrainDuration); applyHeldItems(HeldItemEffect.FIELD_EFFECT, { pokemon: user, fieldDuration: terrainDuration });
} }
this.terrain = terrain ? new Terrain(terrain, terrainDuration.value) : null; this.terrain = terrain ? new Terrain(terrain, terrainDuration.value) : null;
@ -895,7 +895,7 @@ export class Arena {
case BiomeId.CAVE: case BiomeId.CAVE:
return 14.24; return 14.24;
case BiomeId.DESERT: case BiomeId.DESERT:
return 1.143; return 9.02;
case BiomeId.ICE_CAVE: case BiomeId.ICE_CAVE:
return 0.0; return 0.0;
case BiomeId.MEADOW: case BiomeId.MEADOW:
@ -923,7 +923,7 @@ export class Arena {
case BiomeId.JUNGLE: case BiomeId.JUNGLE:
return 0.0; return 0.0;
case BiomeId.FAIRY_CAVE: case BiomeId.FAIRY_CAVE:
return 4.542; return 0.0;
case BiomeId.TEMPLE: case BiomeId.TEMPLE:
return 2.547; return 2.547;
case BiomeId.ISLAND: case BiomeId.ISLAND:

View File

@ -336,7 +336,7 @@ export class MysteryEncounterIntroVisuals extends Phaser.GameObjects.Container {
tryPlaySprite( tryPlaySprite(
sprite: Phaser.GameObjects.Sprite, sprite: Phaser.GameObjects.Sprite,
tintSprite: Phaser.GameObjects.Sprite, tintSprite: Phaser.GameObjects.Sprite,
animConfig: Phaser.Types.Animations.PlayAnimationConfig, animConfig: PlayAnimationConfig,
): boolean { ): boolean {
// Show an error in the console if there isn't a texture loaded // Show an error in the console if there isn't a texture loaded
if (sprite.texture.key === "__MISSING") { if (sprite.texture.key === "__MISSING") {

View File

@ -1,7 +1,7 @@
import type { Ability, PreAttackModifyDamageAbAttrParams } from "#abilities/ability"; import type { Ability, PreAttackModifyDamageAbAttrParams } from "#abilities/ability";
import { applyAbAttrs, applyOnGainAbAttrs, applyOnLoseAbAttrs } from "#abilities/apply-ab-attrs"; import { applyAbAttrs, applyOnGainAbAttrs, applyOnLoseAbAttrs } from "#abilities/apply-ab-attrs";
import type { AnySound, BattleScene } from "#app/battle-scene"; import type { AnySound, BattleScene } from "#app/battle-scene";
import { PLAYER_PARTY_MAX_SIZE } from "#app/constants"; import { PLAYER_PARTY_MAX_SIZE, RARE_CANDY_FRIENDSHIP_CAP } from "#app/constants";
import { timedEventManager } from "#app/global-event-manager"; import { timedEventManager } from "#app/global-event-manager";
import { globalScene } from "#app/global-scene"; import { globalScene } from "#app/global-scene";
import { getPokemonNameWithAffix } from "#app/messages"; import { getPokemonNameWithAffix } from "#app/messages";
@ -39,6 +39,7 @@ import {
TrappedTag, TrappedTag,
TypeImmuneTag, TypeImmuneTag,
} from "#data/battler-tags"; } from "#data/battler-tags";
import { getDailyEventSeedBoss } from "#data/daily-run";
import { allAbilities, allMoves } from "#data/data-lists"; import { allAbilities, allMoves } from "#data/data-lists";
import { getLevelTotalExp } from "#data/exp"; import { getLevelTotalExp } from "#data/exp";
import { import {
@ -78,9 +79,10 @@ import { ChallengeType } from "#enums/challenge-type";
import { Challenges } from "#enums/challenges"; import { Challenges } from "#enums/challenges";
import { DexAttr } from "#enums/dex-attr"; import { DexAttr } from "#enums/dex-attr";
import { FieldPosition } from "#enums/field-position"; import { FieldPosition } from "#enums/field-position";
import { HeldItemEffect } from "#enums/held-item-effect";
import { HeldItemId } from "#enums/held-item-id";
import { HitResult } from "#enums/hit-result"; import { HitResult } from "#enums/hit-result";
import { LearnMoveSituation } from "#enums/learn-move-situation"; import { LearnMoveSituation } from "#enums/learn-move-situation";
import { ModifierTier } from "#enums/modifier-tier";
import { MoveCategory } from "#enums/move-category"; import { MoveCategory } from "#enums/move-category";
import { MoveFlags } from "#enums/move-flags"; import { MoveFlags } from "#enums/move-flags";
import { MoveId } from "#enums/move-id"; import { MoveId } from "#enums/move-id";
@ -90,6 +92,7 @@ import { Nature } from "#enums/nature";
import { PokeballType } from "#enums/pokeball"; import { PokeballType } from "#enums/pokeball";
import { PokemonAnimType } from "#enums/pokemon-anim-type"; import { PokemonAnimType } from "#enums/pokemon-anim-type";
import { PokemonType } from "#enums/pokemon-type"; import { PokemonType } from "#enums/pokemon-type";
import { RarityTier } from "#enums/reward-tier";
import { SpeciesFormKey } from "#enums/species-form-key"; import { SpeciesFormKey } from "#enums/species-form-key";
import { SpeciesId } from "#enums/species-id"; import { SpeciesId } from "#enums/species-id";
import { import {
@ -107,27 +110,11 @@ import type { TrainerSlot } from "#enums/trainer-slot";
import { UiMode } from "#enums/ui-mode"; import { UiMode } from "#enums/ui-mode";
import { WeatherType } from "#enums/weather-type"; import { WeatherType } from "#enums/weather-type";
import { doShinySparkleAnim } from "#field/anims"; import { doShinySparkleAnim } from "#field/anims";
import { import { applyHeldItems } from "#items/all-held-items";
BaseStatModifier, import type { HeldItemConfiguration } from "#items/held-item-data-types";
CritBoosterModifier, import { HeldItemManager } from "#items/held-item-manager";
EnemyDamageBoosterModifier, import { assignItemsFromConfiguration } from "#items/held-item-pool";
EnemyDamageReducerModifier, import { TrainerItemEffect } from "#items/trainer-item";
EnemyFusionChanceModifier,
EvoTrackerModifier,
HiddenAbilityRateBoosterModifier,
PokemonBaseStatFlatModifier,
PokemonBaseStatTotalModifier,
PokemonFriendshipBoosterModifier,
PokemonHeldItemModifier,
PokemonIncrementingStatModifier,
PokemonMultiHitModifier,
PokemonNatureWeightModifier,
ShinyRateBoosterModifier,
StatBoosterModifier,
SurviveDamageModifier,
TempCritBoosterModifier,
TempStatStageBoosterModifier,
} from "#modifiers/modifier";
import { applyMoveAttrs } from "#moves/apply-attrs"; import { applyMoveAttrs } from "#moves/apply-attrs";
import type { Move } from "#moves/move"; import type { Move } from "#moves/move";
import { getMoveTargets } from "#moves/move-utils"; import { getMoveTargets } from "#moves/move-utils";
@ -138,6 +125,8 @@ import { populateVariantColors, variantColorCache, variantData } from "#sprites/
import { achvs } from "#system/achv"; import { achvs } from "#system/achv";
import type { StarterDataEntry, StarterMoveset } from "#system/game-data"; import type { StarterDataEntry, StarterMoveset } from "#system/game-data";
import type { PokemonData } from "#system/pokemon-data"; import type { PokemonData } from "#system/pokemon-data";
import { RibbonData } from "#system/ribbons/ribbon-data";
import { awardRibbonsToSpeciesLine } from "#system/ribbons/ribbon-methods";
import type { AbAttrMap, AbAttrString, TypeMultiplierAbAttrParams } from "#types/ability-types"; import type { AbAttrMap, AbAttrString, TypeMultiplierAbAttrParams } from "#types/ability-types";
import type { DamageCalculationResult, DamageResult } from "#types/damage-result"; import type { DamageCalculationResult, DamageResult } from "#types/damage-result";
import type { IllusionData } from "#types/illusion-data"; import type { IllusionData } from "#types/illusion-data";
@ -289,6 +278,8 @@ export abstract class Pokemon extends Phaser.GameObjects.Container {
private shinySparkle: Phaser.GameObjects.Sprite; private shinySparkle: Phaser.GameObjects.Sprite;
public readonly heldItemManager: HeldItemManager = new HeldItemManager();
// TODO: Rework this eventually // TODO: Rework this eventually
constructor( constructor(
x: number, x: number,
@ -302,6 +293,7 @@ export abstract class Pokemon extends Phaser.GameObjects.Container {
variant?: Variant, variant?: Variant,
ivs?: number[], ivs?: number[],
nature?: Nature, nature?: Nature,
heldItemConfig?: HeldItemConfiguration,
dataSource?: Pokemon | PokemonData, dataSource?: Pokemon | PokemonData,
) { ) {
super(globalScene, x, y); super(globalScene, x, y);
@ -331,6 +323,11 @@ export abstract class Pokemon extends Phaser.GameObjects.Container {
this.exp = dataSource?.exp || getLevelTotalExp(this.level, species.growthRate); this.exp = dataSource?.exp || getLevelTotalExp(this.level, species.growthRate);
this.levelExp = dataSource?.levelExp || 0; this.levelExp = dataSource?.levelExp || 0;
this.heldItemManager = new HeldItemManager();
if (heldItemConfig) {
assignItemsFromConfiguration(heldItemConfig, this);
}
if (dataSource) { if (dataSource) {
this.id = dataSource.id; this.id = dataSource.id;
this.hp = dataSource.hp; this.hp = dataSource.hp;
@ -408,7 +405,7 @@ export abstract class Pokemon extends Phaser.GameObjects.Container {
if (level > 1) { if (level > 1) {
const fused = new BooleanHolder(globalScene.gameMode.isSplicedOnly); const fused = new BooleanHolder(globalScene.gameMode.isSplicedOnly);
if (!fused.value && this.isEnemy() && !this.hasTrainer()) { if (!fused.value && this.isEnemy() && !this.hasTrainer()) {
globalScene.applyModifier(EnemyFusionChanceModifier, false, fused); globalScene.applyPlayerItems(TrainerItemEffect.ENEMY_FUSED_CHANCE, { booleanHolder: fused });
} }
if (fused.value) { if (fused.value) {
@ -596,7 +593,9 @@ export abstract class Pokemon extends Phaser.GameObjects.Container {
// Roll for hidden ability chance, applying any ability charms for enemy mons // Roll for hidden ability chance, applying any ability charms for enemy mons
const hiddenAbilityChance = new NumberHolder(BASE_HIDDEN_ABILITY_CHANCE); const hiddenAbilityChance = new NumberHolder(BASE_HIDDEN_ABILITY_CHANCE);
if (!this.hasTrainer()) { if (!this.hasTrainer()) {
globalScene.applyModifiers(HiddenAbilityRateBoosterModifier, true, hiddenAbilityChance); globalScene.applyPlayerItems(TrainerItemEffect.HIDDEN_ABILITY_CHANCE_BOOSTER, {
numberHolder: hiddenAbilityChance,
});
} }
// If the roll succeeded and we have one, use HA; otherwise pick a random ability // If the roll succeeded and we have one, use HA; otherwise pick a random ability
@ -1152,14 +1151,8 @@ export abstract class Pokemon extends Phaser.GameObjects.Container {
this.setScale(this.getSpriteScale()); this.setScale(this.getSpriteScale());
} }
getHeldItems(): PokemonHeldItemModifier[] { getHeldItems(): HeldItemId[] {
if (!globalScene) { return this.heldItemManager.getHeldItems();
return [];
}
return globalScene.findModifiers(
m => m instanceof PokemonHeldItemModifier && m.pokemonId === this.id,
this.isPlayer(),
) as PokemonHeldItemModifier[];
} }
updateScale(): void { updateScale(): void {
@ -1383,8 +1376,8 @@ export abstract class Pokemon extends Phaser.GameObjects.Container {
getCritStage(source: Pokemon, move: Move): number { getCritStage(source: Pokemon, move: Move): number {
const critStage = new NumberHolder(0); const critStage = new NumberHolder(0);
applyMoveAttrs("HighCritAttr", source, this, move, critStage); applyMoveAttrs("HighCritAttr", source, this, move, critStage);
globalScene.applyModifiers(CritBoosterModifier, source.isPlayer(), source, critStage); applyHeldItems(HeldItemEffect.CRIT_BOOST, { pokemon: source, critStage: critStage });
globalScene.applyModifiers(TempCritBoosterModifier, source.isPlayer(), critStage); globalScene.applyPlayerItems(TrainerItemEffect.TEMP_CRIT_BOOSTER, { numberHolder: critStage });
applyAbAttrs("BonusCritAbAttr", { pokemon: source, critStage }); applyAbAttrs("BonusCritAbAttr", { pokemon: source, critStage });
const critBoostTag = source.getTag(CritBoostTag); const critBoostTag = source.getTag(CritBoostTag);
if (critBoostTag) { if (critBoostTag) {
@ -1439,7 +1432,7 @@ export abstract class Pokemon extends Phaser.GameObjects.Container {
): number { ): number {
const statVal = new NumberHolder(this.getStat(stat, false)); const statVal = new NumberHolder(this.getStat(stat, false));
if (!ignoreHeldItems) { if (!ignoreHeldItems) {
globalScene.applyModifiers(StatBoosterModifier, this.isPlayer(), this, stat, statVal); applyHeldItems(HeldItemEffect.STAT_BOOST, { pokemon: this, stat: stat, statValue: statVal });
} }
// The Ruin abilities here are never ignored, but they reveal themselves on summon anyway // The Ruin abilities here are never ignored, but they reveal themselves on summon anyway
@ -1549,7 +1542,7 @@ export abstract class Pokemon extends Phaser.GameObjects.Container {
const statHolder = new NumberHolder(Math.floor((2 * baseStats[s] + this.ivs[s]) * this.level * 0.01)); const statHolder = new NumberHolder(Math.floor((2 * baseStats[s] + this.ivs[s]) * this.level * 0.01));
if (s === Stat.HP) { if (s === Stat.HP) {
statHolder.value = statHolder.value + this.level + 10; statHolder.value = statHolder.value + this.level + 10;
globalScene.applyModifier(PokemonIncrementingStatModifier, this.isPlayer(), this, s, statHolder); applyHeldItems(HeldItemEffect.INCREMENTING_STAT, { pokemon: this, stat: s, statHolder: statHolder });
if (this.hasAbility(AbilityId.WONDER_GUARD, false, true)) { if (this.hasAbility(AbilityId.WONDER_GUARD, false, true)) {
statHolder.value = 1; statHolder.value = 1;
} }
@ -1564,14 +1557,14 @@ export abstract class Pokemon extends Phaser.GameObjects.Container {
} else { } else {
statHolder.value += 5; statHolder.value += 5;
const natureStatMultiplier = new NumberHolder(getNatureStatMultiplier(this.getNature(), s)); const natureStatMultiplier = new NumberHolder(getNatureStatMultiplier(this.getNature(), s));
globalScene.applyModifier(PokemonNatureWeightModifier, this.isPlayer(), this, natureStatMultiplier); applyHeldItems(HeldItemEffect.NATURE_WEIGHT_BOOSTER, { pokemon: this, multiplier: natureStatMultiplier });
if (natureStatMultiplier.value !== 1) { if (natureStatMultiplier.value !== 1) {
statHolder.value = Math.max( statHolder.value = Math.max(
Math[natureStatMultiplier.value > 1 ? "ceil" : "floor"](statHolder.value * natureStatMultiplier.value), Math[natureStatMultiplier.value > 1 ? "ceil" : "floor"](statHolder.value * natureStatMultiplier.value),
1, 1,
); );
} }
globalScene.applyModifier(PokemonIncrementingStatModifier, this.isPlayer(), this, s, statHolder); applyHeldItems(HeldItemEffect.INCREMENTING_STAT, { pokemon: this, stat: s, statHolder: statHolder });
} }
statHolder.value = Phaser.Math.Clamp(statHolder.value, 1, Number.MAX_SAFE_INTEGER); statHolder.value = Phaser.Math.Clamp(statHolder.value, 1, Number.MAX_SAFE_INTEGER);
@ -1584,9 +1577,9 @@ export abstract class Pokemon extends Phaser.GameObjects.Container {
const baseStats = this.getSpeciesForm(true).baseStats.slice(0); const baseStats = this.getSpeciesForm(true).baseStats.slice(0);
applyChallenges(ChallengeType.FLIP_STAT, this, baseStats); applyChallenges(ChallengeType.FLIP_STAT, this, baseStats);
// Shuckle Juice // Shuckle Juice
globalScene.applyModifiers(PokemonBaseStatTotalModifier, this.isPlayer(), this, baseStats); applyHeldItems(HeldItemEffect.BASE_STAT_TOTAL, { pokemon: this, baseStats: baseStats });
// Old Gateau // Old Gateau
globalScene.applyModifiers(PokemonBaseStatFlatModifier, this.isPlayer(), this, baseStats); applyHeldItems(HeldItemEffect.BASE_STAT_FLAT, { pokemon: this, baseStats: baseStats });
if (this.isFusion()) { if (this.isFusion()) {
const fusionBaseStats = this.getFusionSpeciesForm(true).baseStats; const fusionBaseStats = this.getFusionSpeciesForm(true).baseStats;
applyChallenges(ChallengeType.FLIP_STAT, this, fusionBaseStats); applyChallenges(ChallengeType.FLIP_STAT, this, fusionBaseStats);
@ -1600,7 +1593,7 @@ export abstract class Pokemon extends Phaser.GameObjects.Container {
} }
} }
// Vitamins // Vitamins
globalScene.applyModifiers(BaseStatModifier, this.isPlayer(), this, baseStats); applyHeldItems(HeldItemEffect.BASE_STAT_BOOSTER, { pokemon: this, baseStats: baseStats });
return baseStats; return baseStats;
} }
@ -1824,7 +1817,7 @@ export abstract class Pokemon extends Phaser.GameObjects.Container {
// Overrides moveset based on arrays specified in overrides.ts // Overrides moveset based on arrays specified in overrides.ts
let overrideArray: MoveId | Array<MoveId> = this.isPlayer() let overrideArray: MoveId | Array<MoveId> = this.isPlayer()
? Overrides.MOVESET_OVERRIDE ? Overrides.MOVESET_OVERRIDE
: Overrides.OPP_MOVESET_OVERRIDE; : Overrides.ENEMY_MOVESET_OVERRIDE;
overrideArray = coerceArray(overrideArray); overrideArray = coerceArray(overrideArray);
if (overrideArray.length > 0) { if (overrideArray.length > 0) {
if (!this.isPlayer()) { if (!this.isPlayer()) {
@ -2029,8 +2022,8 @@ export abstract class Pokemon extends Phaser.GameObjects.Container {
if (Overrides.ABILITY_OVERRIDE && this.isPlayer()) { if (Overrides.ABILITY_OVERRIDE && this.isPlayer()) {
return allAbilities[Overrides.ABILITY_OVERRIDE]; return allAbilities[Overrides.ABILITY_OVERRIDE];
} }
if (Overrides.OPP_ABILITY_OVERRIDE && this.isEnemy()) { if (Overrides.ENEMY_ABILITY_OVERRIDE && this.isEnemy()) {
return allAbilities[Overrides.OPP_ABILITY_OVERRIDE]; return allAbilities[Overrides.ENEMY_ABILITY_OVERRIDE];
} }
if (this.isFusion()) { if (this.isFusion()) {
if (!isNullOrUndefined(this.fusionCustomPokemonData?.ability) && this.fusionCustomPokemonData.ability !== -1) { if (!isNullOrUndefined(this.fusionCustomPokemonData?.ability) && this.fusionCustomPokemonData.ability !== -1) {
@ -2059,8 +2052,8 @@ export abstract class Pokemon extends Phaser.GameObjects.Container {
if (Overrides.PASSIVE_ABILITY_OVERRIDE && this.isPlayer()) { if (Overrides.PASSIVE_ABILITY_OVERRIDE && this.isPlayer()) {
return allAbilities[Overrides.PASSIVE_ABILITY_OVERRIDE]; return allAbilities[Overrides.PASSIVE_ABILITY_OVERRIDE];
} }
if (Overrides.OPP_PASSIVE_ABILITY_OVERRIDE && this.isEnemy()) { if (Overrides.ENEMY_PASSIVE_ABILITY_OVERRIDE && this.isEnemy()) {
return allAbilities[Overrides.OPP_PASSIVE_ABILITY_OVERRIDE]; return allAbilities[Overrides.ENEMY_PASSIVE_ABILITY_OVERRIDE];
} }
if (!isNullOrUndefined(this.customPokemonData.passive) && this.customPokemonData.passive !== -1) { if (!isNullOrUndefined(this.customPokemonData.passive) && this.customPokemonData.passive !== -1) {
return allAbilities[this.customPokemonData.passive]; return allAbilities[this.customPokemonData.passive];
@ -2127,14 +2120,14 @@ export abstract class Pokemon extends Phaser.GameObjects.Container {
// returns override if valid for current case // returns override if valid for current case
if ( if (
(Overrides.HAS_PASSIVE_ABILITY_OVERRIDE === false && this.isPlayer()) || (Overrides.HAS_PASSIVE_ABILITY_OVERRIDE === false && this.isPlayer()) ||
(Overrides.OPP_HAS_PASSIVE_ABILITY_OVERRIDE === false && this.isEnemy()) (Overrides.ENEMY_HAS_PASSIVE_ABILITY_OVERRIDE === false && this.isEnemy())
) { ) {
return false; return false;
} }
if ( if (
((Overrides.PASSIVE_ABILITY_OVERRIDE !== AbilityId.NONE || Overrides.HAS_PASSIVE_ABILITY_OVERRIDE) && ((Overrides.PASSIVE_ABILITY_OVERRIDE !== AbilityId.NONE || Overrides.HAS_PASSIVE_ABILITY_OVERRIDE) &&
this.isPlayer()) || this.isPlayer()) ||
((Overrides.OPP_PASSIVE_ABILITY_OVERRIDE !== AbilityId.NONE || Overrides.OPP_HAS_PASSIVE_ABILITY_OVERRIDE) && ((Overrides.ENEMY_PASSIVE_ABILITY_OVERRIDE !== AbilityId.NONE || Overrides.ENEMY_HAS_PASSIVE_ABILITY_OVERRIDE) &&
this.isEnemy()) this.isEnemy())
) { ) {
return true; return true;
@ -2231,8 +2224,8 @@ export abstract class Pokemon extends Phaser.GameObjects.Container {
} }
/** /**
* Gets the weight of the Pokemon with subtractive modifiers (Autotomize) happening first * Gets the weight of the Pokemon with subtractive abilities (Autotomize) happening first
* and then multiplicative modifiers happening after (Heavy Metal and Light Metal) * and then multiplicative abilities happening after (Heavy Metal and Light Metal)
* @returns the kg of the Pokemon (minimum of 0.1) * @returns the kg of the Pokemon (minimum of 0.1)
*/ */
public getWeight(): number { public getWeight(): number {
@ -2857,7 +2850,7 @@ export abstract class Pokemon extends Phaser.GameObjects.Container {
} }
} }
if (!this.hasTrainer()) { if (!this.hasTrainer()) {
globalScene.applyModifiers(ShinyRateBoosterModifier, true, shinyThreshold); globalScene.applyPlayerItems(TrainerItemEffect.SHINY_RATE_BOOSTER, { numberHolder: shinyThreshold });
} }
} else { } else {
shinyThreshold.value = thresholdOverride; shinyThreshold.value = thresholdOverride;
@ -2879,17 +2872,17 @@ export abstract class Pokemon extends Phaser.GameObjects.Container {
* *
* The base shiny odds are {@linkcode BASE_SHINY_CHANCE} / `65536` * The base shiny odds are {@linkcode BASE_SHINY_CHANCE} / `65536`
* @param thresholdOverride number that is divided by `2^16` (`65536`) to get the shiny chance, overrides {@linkcode shinyThreshold} if set (bypassing shiny rate modifiers such as Shiny Charm) * @param thresholdOverride number that is divided by `2^16` (`65536`) to get the shiny chance, overrides {@linkcode shinyThreshold} if set (bypassing shiny rate modifiers such as Shiny Charm)
* @param applyModifiersToOverride If {@linkcode thresholdOverride} is set and this is true, will apply Shiny Charm and event modifiers to {@linkcode thresholdOverride} * @param applyItemsToOverride If {@linkcode thresholdOverride} is set and this is true, will apply Shiny Charm and event modifiers to {@linkcode thresholdOverride}
* @returns `true` if the Pokemon has been set as a shiny, `false` otherwise * @returns `true` if the Pokemon has been set as a shiny, `false` otherwise
*/ */
public trySetShinySeed(thresholdOverride?: number, applyModifiersToOverride?: boolean): boolean { public trySetShinySeed(thresholdOverride?: number, applyItemsToOverride?: boolean): boolean {
if (!this.shiny) { if (!this.shiny) {
const shinyThreshold = new NumberHolder(thresholdOverride ?? BASE_SHINY_CHANCE); const shinyThreshold = new NumberHolder(thresholdOverride ?? BASE_SHINY_CHANCE);
if (applyModifiersToOverride) { if (applyItemsToOverride) {
if (timedEventManager.isEventActive()) { if (timedEventManager.isEventActive()) {
shinyThreshold.value *= timedEventManager.getShinyMultiplier(); shinyThreshold.value *= timedEventManager.getShinyMultiplier();
} }
globalScene.applyModifiers(ShinyRateBoosterModifier, true, shinyThreshold); globalScene.applyPlayerItems(TrainerItemEffect.SHINY_RATE_BOOSTER, { numberHolder: shinyThreshold });
} }
this.shiny = randSeedInt(65536) < shinyThreshold.value; this.shiny = randSeedInt(65536) < shinyThreshold.value;
@ -2951,17 +2944,17 @@ export abstract class Pokemon extends Phaser.GameObjects.Container {
* *
* The base hidden ability odds are {@linkcode BASE_HIDDEN_ABILITY_CHANCE} / `65536` * The base hidden ability odds are {@linkcode BASE_HIDDEN_ABILITY_CHANCE} / `65536`
* @param thresholdOverride number that is divided by `2^16` (`65536`) to get the HA chance, overrides {@linkcode haThreshold} if set (bypassing HA rate modifiers such as Ability Charm) * @param thresholdOverride number that is divided by `2^16` (`65536`) to get the HA chance, overrides {@linkcode haThreshold} if set (bypassing HA rate modifiers such as Ability Charm)
* @param applyModifiersToOverride If {@linkcode thresholdOverride} is set and this is true, will apply Ability Charm to {@linkcode thresholdOverride} * @param applyItemsToOverride If {@linkcode thresholdOverride} is set and this is true, will apply Ability Charm to {@linkcode thresholdOverride}
* @returns `true` if the Pokemon has been set to have its hidden ability, `false` otherwise * @returns `true` if the Pokemon has been set to have its hidden ability, `false` otherwise
*/ */
public tryRerollHiddenAbilitySeed(thresholdOverride?: number, applyModifiersToOverride?: boolean): boolean { public tryRerollHiddenAbilitySeed(thresholdOverride?: number, applyItemsToOverride?: boolean): boolean {
if (!this.species.abilityHidden) { if (!this.species.abilityHidden) {
return false; return false;
} }
const haThreshold = new NumberHolder(thresholdOverride ?? BASE_HIDDEN_ABILITY_CHANCE); const haThreshold = new NumberHolder(thresholdOverride ?? BASE_HIDDEN_ABILITY_CHANCE);
if (applyModifiersToOverride) { if (applyItemsToOverride) {
if (!this.hasTrainer()) { if (!this.hasTrainer()) {
globalScene.applyModifiers(HiddenAbilityRateBoosterModifier, true, haThreshold); globalScene.applyPlayerItems(TrainerItemEffect.HIDDEN_ABILITY_CHANCE_BOOSTER, { numberHolder: haThreshold });
} }
} }
@ -2975,7 +2968,9 @@ export abstract class Pokemon extends Phaser.GameObjects.Container {
public generateFusionSpecies(forStarter?: boolean): void { public generateFusionSpecies(forStarter?: boolean): void {
const hiddenAbilityChance = new NumberHolder(BASE_HIDDEN_ABILITY_CHANCE); const hiddenAbilityChance = new NumberHolder(BASE_HIDDEN_ABILITY_CHANCE);
if (!this.hasTrainer()) { if (!this.hasTrainer()) {
globalScene.applyModifiers(HiddenAbilityRateBoosterModifier, true, hiddenAbilityChance); globalScene.applyPlayerItems(TrainerItemEffect.HIDDEN_ABILITY_CHANCE_BOOSTER, {
numberHolder: hiddenAbilityChance,
});
} }
const hasHiddenAbility = !randSeedInt(hiddenAbilityChance.value); const hasHiddenAbility = !randSeedInt(hiddenAbilityChance.value);
@ -3000,8 +2995,8 @@ export abstract class Pokemon extends Phaser.GameObjects.Container {
if (forStarter && this.isPlayer() && Overrides.STARTER_FUSION_SPECIES_OVERRIDE) { if (forStarter && this.isPlayer() && Overrides.STARTER_FUSION_SPECIES_OVERRIDE) {
fusionOverride = getPokemonSpecies(Overrides.STARTER_FUSION_SPECIES_OVERRIDE); fusionOverride = getPokemonSpecies(Overrides.STARTER_FUSION_SPECIES_OVERRIDE);
} else if (this.isEnemy() && Overrides.OPP_FUSION_SPECIES_OVERRIDE) { } else if (this.isEnemy() && Overrides.ENEMY_FUSION_SPECIES_OVERRIDE) {
fusionOverride = getPokemonSpecies(Overrides.OPP_FUSION_SPECIES_OVERRIDE); fusionOverride = getPokemonSpecies(Overrides.ENEMY_FUSION_SPECIES_OVERRIDE);
} }
this.fusionSpecies = this.fusionSpecies =
@ -3103,11 +3098,11 @@ export abstract class Pokemon extends Phaser.GameObjects.Container {
} }
} }
if (compatible && !movePool.some(m => m[0] === moveId) && !allMoves[moveId].name.endsWith(" (N)")) { if (compatible && !movePool.some(m => m[0] === moveId) && !allMoves[moveId].name.endsWith(" (N)")) {
if (tmPoolTiers[moveId] === ModifierTier.COMMON && this.level >= 15) { if (tmPoolTiers[moveId] === RarityTier.COMMON && this.level >= 15) {
movePool.push([moveId, 4]); movePool.push([moveId, 4]);
} else if (tmPoolTiers[moveId] === ModifierTier.GREAT && this.level >= 30) { } else if (tmPoolTiers[moveId] === RarityTier.GREAT && this.level >= 30) {
movePool.push([moveId, 8]); movePool.push([moveId, 8]);
} else if (tmPoolTiers[moveId] === ModifierTier.ULTRA && this.level >= 50) { } else if (tmPoolTiers[moveId] === RarityTier.ULTRA && this.level >= 50) {
movePool.push([moveId, 14]); movePool.push([moveId, 14]);
} }
} }
@ -3485,7 +3480,9 @@ export abstract class Pokemon extends Phaser.GameObjects.Container {
if (!ignoreStatStage.value) { if (!ignoreStatStage.value) {
const statStageMultiplier = new NumberHolder(Math.max(2, 2 + statStage.value) / Math.max(2, 2 - statStage.value)); const statStageMultiplier = new NumberHolder(Math.max(2, 2 + statStage.value) / Math.max(2, 2 - statStage.value));
if (!ignoreHeldItems) { if (!ignoreHeldItems) {
globalScene.applyModifiers(TempStatStageBoosterModifier, this.isPlayer(), stat, statStageMultiplier); globalScene.applyPlayerItems(TrainerItemEffect.TEMP_STAT_STAGE_BOOSTER, {
numberHolder: statStageMultiplier,
});
} }
return Math.min(statStageMultiplier.value, 4); return Math.min(statStageMultiplier.value, 4);
} }
@ -3519,7 +3516,7 @@ export abstract class Pokemon extends Phaser.GameObjects.Container {
applyAbAttrs("IgnoreOpponentStatStagesAbAttr", { pokemon: this, stat: Stat.EVA, ignored: ignoreEvaStatStage }); applyAbAttrs("IgnoreOpponentStatStagesAbAttr", { pokemon: this, stat: Stat.EVA, ignored: ignoreEvaStatStage });
applyMoveAttrs("IgnoreOpponentStatStagesAttr", this, target, sourceMove, ignoreEvaStatStage); applyMoveAttrs("IgnoreOpponentStatStagesAttr", this, target, sourceMove, ignoreEvaStatStage);
globalScene.applyModifiers(TempStatStageBoosterModifier, this.isPlayer(), Stat.ACC, userAccStage); globalScene.applyPlayerItems(TrainerItemEffect.TEMP_ACCURACY_BOOSTER, { numberHolder: userAccStage });
userAccStage.value = ignoreAccStatStage.value ? 0 : Math.min(userAccStage.value, 6); userAccStage.value = ignoreAccStatStage.value ? 0 : Math.min(userAccStage.value, 6);
targetEvaStage.value = ignoreEvaStatStage.value ? 0 : targetEvaStage.value; targetEvaStage.value = ignoreEvaStatStage.value ? 0 : targetEvaStage.value;
@ -3772,14 +3769,11 @@ export abstract class Pokemon extends Phaser.GameObjects.Container {
applyMoveAttrs("FixedDamageAttr", source, this, move, fixedDamage); applyMoveAttrs("FixedDamageAttr", source, this, move, fixedDamage);
if (fixedDamage.value) { if (fixedDamage.value) {
const multiLensMultiplier = new NumberHolder(1); const multiLensMultiplier = new NumberHolder(1);
globalScene.applyModifiers( applyHeldItems(HeldItemEffect.MULTI_HIT, {
PokemonMultiHitModifier, pokemon: source,
source.isPlayer(), moveId: move.id,
source, damageMultiplier: multiLensMultiplier,
move.id, });
null,
multiLensMultiplier,
);
fixedDamage.value = toDmgValue(fixedDamage.value * multiLensMultiplier.value); fixedDamage.value = toDmgValue(fixedDamage.value * multiLensMultiplier.value);
return { return {
@ -3823,14 +3817,11 @@ export abstract class Pokemon extends Phaser.GameObjects.Container {
/** Multiplier for moves enhanced by Multi-Lens and/or Parental Bond */ /** Multiplier for moves enhanced by Multi-Lens and/or Parental Bond */
const multiStrikeEnhancementMultiplier = new NumberHolder(1); const multiStrikeEnhancementMultiplier = new NumberHolder(1);
globalScene.applyModifiers( applyHeldItems(HeldItemEffect.MULTI_HIT, {
PokemonMultiHitModifier, pokemon: source,
source.isPlayer(), moveId: move.id,
source, damageMultiplier: multiStrikeEnhancementMultiplier,
move.id, });
null,
multiStrikeEnhancementMultiplier,
);
if (!ignoreSourceAbility) { if (!ignoreSourceAbility) {
applyAbAttrs("AddSecondStrikeAbAttr", { applyAbAttrs("AddSecondStrikeAbAttr", {
@ -3949,10 +3940,10 @@ export abstract class Pokemon extends Phaser.GameObjects.Container {
/** Apply the enemy's Damage and Resistance tokens */ /** Apply the enemy's Damage and Resistance tokens */
if (!source.isPlayer()) { if (!source.isPlayer()) {
globalScene.applyModifiers(EnemyDamageBoosterModifier, false, damage); globalScene.applyPlayerItems(TrainerItemEffect.ENEMY_DAMAGE_BOOSTER, { numberHolder: damage });
} }
if (!this.isPlayer()) { if (!this.isPlayer()) {
globalScene.applyModifiers(EnemyDamageReducerModifier, false, damage); globalScene.applyPlayerItems(TrainerItemEffect.ENEMY_DAMAGE_REDUCER, { numberHolder: damage });
} }
const abAttrParams: PreAttackModifyDamageAbAttrParams = { const abAttrParams: PreAttackModifyDamageAbAttrParams = {
@ -3962,7 +3953,7 @@ export abstract class Pokemon extends Phaser.GameObjects.Container {
simulated, simulated,
damage, damage,
}; };
/** Apply this Pokemon's post-calc defensive modifiers (e.g. Fur Coat) */ /** Apply this Pokemon's post-calc defensive attributes (e.g. Fur Coat) */
if (!ignoreAbility) { if (!ignoreAbility) {
applyAbAttrs("ReceivedMoveDamageMultiplierAbAttr", abAttrParams); applyAbAttrs("ReceivedMoveDamageMultiplierAbAttr", abAttrParams);
@ -4064,7 +4055,7 @@ export abstract class Pokemon extends Phaser.GameObjects.Container {
surviveDamage.value = this.lapseTag(BattlerTagType.ENDURE_TOKEN); surviveDamage.value = this.lapseTag(BattlerTagType.ENDURE_TOKEN);
} }
if (!surviveDamage.value) { if (!surviveDamage.value) {
globalScene.applyModifiers(SurviveDamageModifier, this.isPlayer(), this, surviveDamage); applyHeldItems(HeldItemEffect.SURVIVE_CHANCE, { pokemon: this, surviveDamage: surviveDamage });
} }
if (surviveDamage.value) { if (surviveDamage.value) {
damage = this.hp - 1; damage = this.hp - 1;
@ -4513,7 +4504,7 @@ export abstract class Pokemon extends Phaser.GameObjects.Container {
this.setScale(this.getSpriteScale()); this.setScale(this.getSpriteScale());
this.loadAssets().then(() => { this.loadAssets().then(() => {
this.calculateStats(); this.calculateStats();
globalScene.updateModifiers(this.isPlayer(), true); globalScene.updateItems(this.isPlayer());
Promise.all([this.updateInfo(), globalScene.updateFieldScale()]).then(() => resolve()); Promise.all([this.updateInfo(), globalScene.updateFieldScale()]).then(() => resolve());
}); });
}); });
@ -5631,16 +5622,14 @@ export abstract class Pokemon extends Phaser.GameObjects.Container {
* Should be `false` for all item loss occurring outside of battle (MEs, etc.). * Should be `false` for all item loss occurring outside of battle (MEs, etc.).
* @returns Whether the item was removed successfully. * @returns Whether the item was removed successfully.
*/ */
public loseHeldItem(heldItem: PokemonHeldItemModifier, forBattle = true): boolean { public loseHeldItem(heldItemId: HeldItemId, forBattle = true): boolean {
// TODO: What does a -1 pokemon id mean? // TODO: What does a -1 pokemon id mean?
if (heldItem.pokemonId !== -1 && heldItem.pokemonId !== this.id) { if (!this.heldItemManager.hasItem(heldItemId)) {
return false; return false;
} }
heldItem.stackCount--; this.heldItemManager.remove(heldItemId);
if (heldItem.stackCount <= 0) {
globalScene.removeModifier(heldItem, this.isEnemy());
}
if (forBattle) { if (forBattle) {
applyAbAttrs("PostItemLostAbAttr", { pokemon: this }); applyAbAttrs("PostItemLostAbAttr", { pokemon: this });
} }
@ -5662,13 +5651,6 @@ export abstract class Pokemon extends Phaser.GameObjects.Container {
} }
this.turnData.berriesEaten.push(berryType); this.turnData.berriesEaten.push(berryType);
} }
getPersistentTreasureCount(): number {
return (
this.getHeldItems().filter(m => m.is("DamageMoneyRewardModifier")).length +
globalScene.findModifiers(m => m.is("MoneyMultiplierModifier") || m.is("ExtraModifierModifier")).length
);
}
} }
export class PlayerPokemon extends Pokemon { export class PlayerPokemon extends Pokemon {
@ -5685,9 +5667,24 @@ export class PlayerPokemon extends Pokemon {
variant?: Variant, variant?: Variant,
ivs?: number[], ivs?: number[],
nature?: Nature, nature?: Nature,
heldItemConfig?: HeldItemConfiguration,
dataSource?: Pokemon | PokemonData, dataSource?: Pokemon | PokemonData,
) { ) {
super(106, 148, species, level, abilityIndex, formIndex, gender, shiny, variant, ivs, nature, dataSource); super(
106,
148,
species,
level,
abilityIndex,
formIndex,
gender,
shiny,
variant,
ivs,
nature,
heldItemConfig,
dataSource,
);
if (Overrides.STATUS_OVERRIDE) { if (Overrides.STATUS_OVERRIDE) {
this.status = new Status(Overrides.STATUS_OVERRIDE, 0, 4); this.status = new Status(Overrides.STATUS_OVERRIDE, 0, 4);
@ -5821,45 +5818,59 @@ export class PlayerPokemon extends Pokemon {
); );
}); });
} }
/**
* Add friendship to this Pokemon
*
* @remarks
* This adds friendship to the pokemon's friendship stat (used for evolution, return, etc.) and candy progress.
* For fusions, candy progress for each species in the fusion is halved.
*
* @param friendship - The amount of friendship to add. Negative values will reduce friendship, though not below 0.
* @param capped - If true, don't allow the friendship gain to exceed 200. Used to cap friendship gains from rare candies.
*/
addFriendship(friendship: number, capped = false): void {
// Short-circuit friendship loss, which doesn't impact candy friendship
if (friendship <= 0) {
this.friendship = Math.max(this.friendship + friendship, 0);
return;
}
addFriendship(friendship: number): void {
if (friendship > 0) {
const starterSpeciesId = this.species.getRootSpeciesId(); const starterSpeciesId = this.species.getRootSpeciesId();
const fusionStarterSpeciesId = this.isFusion() && this.fusionSpecies ? this.fusionSpecies.getRootSpeciesId() : 0; const fusionStarterSpeciesId = this.isFusion() && this.fusionSpecies ? this.fusionSpecies.getRootSpeciesId() : 0;
const starterData = [ const starterGameData = globalScene.gameData.starterData;
globalScene.gameData.starterData[starterSpeciesId], const starterData: [StarterDataEntry, SpeciesId][] = [[starterGameData[starterSpeciesId], starterSpeciesId]];
fusionStarterSpeciesId ? globalScene.gameData.starterData[fusionStarterSpeciesId] : null, if (fusionStarterSpeciesId) {
].filter(d => !!d); starterData.push([starterGameData[fusionStarterSpeciesId], fusionStarterSpeciesId]);
}
const amount = new NumberHolder(friendship); const amount = new NumberHolder(friendship);
globalScene.applyModifier(PokemonFriendshipBoosterModifier, true, this, amount); applyHeldItems(HeldItemEffect.FRIENDSHIP_BOOSTER, { pokemon: this, friendship: amount });
const candyFriendshipMultiplier = globalScene.gameMode.isClassic friendship = amount.value;
const newFriendship = this.friendship + friendship;
// If capped is true, only adjust friendship if the new friendship is less than or equal to 200.
if (!capped || newFriendship <= RARE_CANDY_FRIENDSHIP_CAP) {
this.friendship = Math.min(newFriendship, 255);
if (newFriendship >= 255) {
globalScene.validateAchv(achvs.MAX_FRIENDSHIP);
awardRibbonsToSpeciesLine(this.species.speciesId, RibbonData.FRIENDSHIP);
}
}
let candyFriendshipMultiplier = globalScene.gameMode.isClassic
? timedEventManager.getClassicFriendshipMultiplier() ? timedEventManager.getClassicFriendshipMultiplier()
: 1; : 1;
const fusionReduction = fusionStarterSpeciesId if (fusionStarterSpeciesId) {
? timedEventManager.areFusionsBoosted() candyFriendshipMultiplier /= timedEventManager.areFusionsBoosted() ? 1.5 : 2;
? 1.5 // Divide candy gain for fusions by 1.5 during events
: 2 // 2 for fusions outside events
: 1; // 1 for non-fused mons
const starterAmount = new NumberHolder(Math.floor((amount.value * candyFriendshipMultiplier) / fusionReduction));
// Add friendship to this PlayerPokemon
this.friendship = Math.min(this.friendship + amount.value, 255);
if (this.friendship === 255) {
globalScene.validateAchv(achvs.MAX_FRIENDSHIP);
} }
const candyFriendshipAmount = Math.floor(friendship * candyFriendshipMultiplier);
// Add to candy progress for this mon's starter species and its fused species (if it has one) // Add to candy progress for this mon's starter species and its fused species (if it has one)
starterData.forEach((sd: StarterDataEntry, i: number) => { starterData.forEach(([sd, id]: [StarterDataEntry, SpeciesId]) => {
const speciesId = !i ? starterSpeciesId : (fusionStarterSpeciesId as SpeciesId); sd.friendship = (sd.friendship || 0) + candyFriendshipAmount;
sd.friendship = (sd.friendship || 0) + starterAmount.value; if (sd.friendship >= getStarterValueFriendshipCap(speciesStarterCosts[id])) {
if (sd.friendship >= getStarterValueFriendshipCap(speciesStarterCosts[speciesId])) { globalScene.gameData.addStarterCandy(getPokemonSpecies(id), 1);
globalScene.gameData.addStarterCandy(getPokemonSpecies(speciesId), 1);
sd.friendship = 0; sd.friendship = 0;
} }
}); });
} else {
// Lose friendship upon fainting
this.friendship = Math.max(this.friendship + friendship, 0);
}
} }
getPossibleEvolution(evolution: SpeciesFormEvolution | null): Promise<Pokemon> { getPossibleEvolution(evolution: SpeciesFormEvolution | null): Promise<Pokemon> {
@ -5891,6 +5902,7 @@ export class PlayerPokemon extends Pokemon {
this.variant, this.variant,
this.ivs, this.ivs,
this.nature, this.nature,
this.heldItemManager.generateHeldItemConfiguration(),
this, this,
); );
this.fusionSpecies = originalFusionSpecies; this.fusionSpecies = originalFusionSpecies;
@ -5913,6 +5925,7 @@ export class PlayerPokemon extends Pokemon {
this.variant, this.variant,
this.ivs, this.ivs,
this.nature, this.nature,
this.heldItemManager.generateHeldItemConfiguration(),
this, this,
); );
} }
@ -5985,9 +5998,9 @@ export class PlayerPokemon extends Pokemon {
}); });
}; };
if (preEvolution.speciesId === SpeciesId.GIMMIGHOUL) { if (preEvolution.speciesId === SpeciesId.GIMMIGHOUL) {
const evotracker = this.getHeldItems().filter(m => m instanceof EvoTrackerModifier)[0] ?? null; const evotracker = this.heldItemManager.hasItem(HeldItemId.GIMMIGHOUL_EVO_TRACKER);
if (evotracker) { if (evotracker) {
globalScene.removeModifier(evotracker); this.heldItemManager.remove(HeldItemId.GIMMIGHOUL_EVO_TRACKER, 0, true);
} }
} }
if (!globalScene.gameMode.isDaily || this.metBiome > -1) { if (!globalScene.gameMode.isDaily || this.metBiome > -1) {
@ -6040,16 +6053,12 @@ export class PlayerPokemon extends Pokemon {
globalScene.getPlayerParty().push(newPokemon); globalScene.getPlayerParty().push(newPokemon);
newPokemon.evolve(!isFusion ? newEvolution : new FusionSpeciesFormEvolution(this.id, newEvolution), evoSpecies); newPokemon.evolve(!isFusion ? newEvolution : new FusionSpeciesFormEvolution(this.id, newEvolution), evoSpecies);
const modifiers = globalScene.findModifiers( //TODO: This currently does not consider any values associated with the items e.g. disabled
m => m instanceof PokemonHeldItemModifier && m.pokemonId === this.id, const heldItems = this.getHeldItems();
true, heldItems.forEach(item => {
) as PokemonHeldItemModifier[]; newPokemon.heldItemManager.add(item, this.heldItemManager.getStack(item));
modifiers.forEach(m => {
const clonedModifier = m.clone() as PokemonHeldItemModifier;
clonedModifier.pokemonId = newPokemon.id;
globalScene.addModifier(clonedModifier, true);
}); });
globalScene.updateModifiers(true); globalScene.updateItems(true);
} }
} }
} }
@ -6070,6 +6079,7 @@ export class PlayerPokemon extends Pokemon {
this.variant, this.variant,
this.ivs, this.ivs,
this.nature, this.nature,
this.heldItemManager.generateHeldItemConfiguration(),
this, this,
); );
ret.loadAssets().then(() => resolve(ret)); ret.loadAssets().then(() => resolve(ret));
@ -6094,7 +6104,7 @@ export class PlayerPokemon extends Pokemon {
const updateAndResolve = () => { const updateAndResolve = () => {
this.loadAssets().then(() => { this.loadAssets().then(() => {
this.calculateStats(); this.calculateStats();
globalScene.updateModifiers(true, true); globalScene.updateItems(true);
this.updateInfo(true).then(() => resolve()); this.updateInfo(true).then(() => resolve());
}); });
}; };
@ -6160,15 +6170,11 @@ export class PlayerPokemon extends Pokemon {
} }
// combine the two mons' held items // combine the two mons' held items
const fusedPartyMemberHeldModifiers = globalScene.findModifiers( const fusedPartyMemberHeldItems = pokemon.getHeldItems();
m => m instanceof PokemonHeldItemModifier && m.pokemonId === pokemon.id, for (const item of fusedPartyMemberHeldItems) {
true, globalScene.tryTransferHeldItem(item, pokemon, this, false, pokemon.heldItemManager.getStack(item), true, false);
) as PokemonHeldItemModifier[];
for (const modifier of fusedPartyMemberHeldModifiers) {
globalScene.tryTransferHeldItemModifier(modifier, this, false, modifier.getStackCount(), true, true, false);
} }
globalScene.updateModifiers(true, true); globalScene.updateItems(true);
globalScene.removePartyMemberModifiers(fusedPartyMemberIndex);
globalScene.getPlayerParty().splice(fusedPartyMemberIndex, 1)[0]; globalScene.getPlayerParty().splice(fusedPartyMemberIndex, 1)[0];
const newPartyMemberIndex = globalScene.getPlayerParty().indexOf(this); const newPartyMemberIndex = globalScene.getPlayerParty().indexOf(this);
pokemon pokemon
@ -6216,6 +6222,7 @@ export class EnemyPokemon extends Pokemon {
trainerSlot: TrainerSlot, trainerSlot: TrainerSlot,
boss: boolean, boss: boolean,
shinyLock = false, shinyLock = false,
heldItemConfig?: HeldItemConfiguration,
dataSource?: PokemonData, dataSource?: PokemonData,
) { ) {
super( super(
@ -6230,6 +6237,7 @@ export class EnemyPokemon extends Pokemon {
!shinyLock && dataSource ? dataSource.variant : undefined, !shinyLock && dataSource ? dataSource.variant : undefined,
undefined, undefined,
dataSource ? dataSource.nature : undefined, dataSource ? dataSource.nature : undefined,
heldItemConfig,
dataSource, dataSource,
); );
@ -6240,41 +6248,46 @@ export class EnemyPokemon extends Pokemon {
this.setBoss(boss, dataSource?.bossSegments); this.setBoss(boss, dataSource?.bossSegments);
} }
if (Overrides.OPP_STATUS_OVERRIDE) { if (Overrides.ENEMY_STATUS_OVERRIDE) {
this.status = new Status(Overrides.OPP_STATUS_OVERRIDE, 0, 4); this.status = new Status(Overrides.ENEMY_STATUS_OVERRIDE, 0, 4);
} }
if (Overrides.OPP_GENDER_OVERRIDE !== null) { if (Overrides.ENEMY_GENDER_OVERRIDE !== null) {
this.gender = Overrides.OPP_GENDER_OVERRIDE; this.gender = Overrides.ENEMY_GENDER_OVERRIDE;
} }
const speciesId = this.species.speciesId; const speciesId = this.species.speciesId;
if ( if (
speciesId in Overrides.OPP_FORM_OVERRIDES && speciesId in Overrides.ENEMY_FORM_OVERRIDES &&
!isNullOrUndefined(Overrides.OPP_FORM_OVERRIDES[speciesId]) && !isNullOrUndefined(Overrides.ENEMY_FORM_OVERRIDES[speciesId]) &&
this.species.forms[Overrides.OPP_FORM_OVERRIDES[speciesId]] this.species.forms[Overrides.ENEMY_FORM_OVERRIDES[speciesId]]
) { ) {
this.formIndex = Overrides.OPP_FORM_OVERRIDES[speciesId]; this.formIndex = Overrides.ENEMY_FORM_OVERRIDES[speciesId];
} else if (globalScene.gameMode.isDaily && globalScene.gameMode.isWaveFinal(globalScene.currentBattle.waveIndex)) {
const eventBoss = getDailyEventSeedBoss(globalScene.seed);
if (!isNullOrUndefined(eventBoss)) {
this.formIndex = eventBoss.formIndex;
}
} }
if (!dataSource) { if (!dataSource) {
this.generateAndPopulateMoveset(); this.generateAndPopulateMoveset();
if (shinyLock || Overrides.OPP_SHINY_OVERRIDE === false) { if (shinyLock || Overrides.ENEMY_SHINY_OVERRIDE === false) {
this.shiny = false; this.shiny = false;
} else { } else {
this.trySetShiny(); this.trySetShiny();
} }
if (!this.shiny && Overrides.OPP_SHINY_OVERRIDE) { if (!this.shiny && Overrides.ENEMY_SHINY_OVERRIDE) {
this.shiny = true; this.shiny = true;
this.initShinySparkle(); this.initShinySparkle();
} }
if (this.shiny) { if (this.shiny) {
this.variant = this.generateShinyVariant(); this.variant = this.generateShinyVariant();
if (Overrides.OPP_VARIANT_OVERRIDE !== null) { if (Overrides.ENEMY_VARIANT_OVERRIDE !== null) {
this.variant = Overrides.OPP_VARIANT_OVERRIDE; this.variant = Overrides.ENEMY_VARIANT_OVERRIDE;
} }
} }
@ -6867,6 +6880,7 @@ export class EnemyPokemon extends Pokemon {
this.variant, this.variant,
this.ivs, this.ivs,
this.nature, this.nature,
this.heldItemManager.generateHeldItemConfiguration(),
this, this,
); );

View File

@ -12,7 +12,7 @@ import { TrainerSlot } from "#enums/trainer-slot";
import { TrainerType } from "#enums/trainer-type"; import { TrainerType } from "#enums/trainer-type";
import { TrainerVariant } from "#enums/trainer-variant"; import { TrainerVariant } from "#enums/trainer-variant";
import type { EnemyPokemon } from "#field/pokemon"; import type { EnemyPokemon } from "#field/pokemon";
import type { PersistentModifier } from "#modifiers/modifier"; import type { TrainerItemConfiguration } from "#items/trainer-item-data-types";
import { getIsInitialized, initI18n } from "#plugins/i18n"; import { getIsInitialized, initI18n } from "#plugins/i18n";
import type { TrainerConfig } from "#trainers/trainer-config"; import type { TrainerConfig } from "#trainers/trainer-config";
import { trainerConfigs } from "#trainers/trainer-config"; import { trainerConfigs } from "#trainers/trainer-config";
@ -634,7 +634,7 @@ export class Trainer extends Phaser.GameObjects.Container {
return maxScorePartyMemberIndexes[0]; return maxScorePartyMemberIndexes[0];
} }
getPartyMemberModifierChanceMultiplier(index: number): number { getPartyMemberItemChanceMultiplier(index: number): number {
switch (this.getPartyTemplate().getStrength(index)) { switch (this.getPartyTemplate().getStrength(index)) {
case PartyMemberStrength.WEAKER: case PartyMemberStrength.WEAKER:
return 0.75; return 0.75;
@ -647,14 +647,14 @@ export class Trainer extends Phaser.GameObjects.Container {
case PartyMemberStrength.STRONGER: case PartyMemberStrength.STRONGER:
return 0.375; return 0.375;
default: default:
console.warn("getPartyMemberModifierChanceMultiplier not defined. Using default 0"); console.warn("getPartyMemberItemChanceMultiplier not defined. Using default 0");
return 0; return 0;
} }
} }
genModifiers(party: EnemyPokemon[]): PersistentModifier[] { genTrainerItems(party: EnemyPokemon[]): TrainerItemConfiguration {
if (this.config.genModifiersFunc) { if (this.config.genTrainerItemsFunc) {
return this.config.genModifiersFunc(party); return this.config.genTrainerItemsFunc(party);
} }
return []; return [];
} }

View File

@ -3,7 +3,7 @@ import { CHALLENGE_MODE_MYSTERY_ENCOUNTER_WAVES, CLASSIC_MODE_MYSTERY_ENCOUNTER_
import { globalScene } from "#app/global-scene"; import { globalScene } from "#app/global-scene";
import Overrides from "#app/overrides"; import Overrides from "#app/overrides";
import { allChallenges, type Challenge, copyChallenge } from "#data/challenge"; import { allChallenges, type Challenge, copyChallenge } from "#data/challenge";
import { getDailyStartingBiome } from "#data/daily-run"; import { getDailyEventSeedBoss, getDailyStartingBiome } from "#data/daily-run";
import { allSpecies } from "#data/data-lists"; import { allSpecies } from "#data/data-lists";
import type { PokemonSpecies } from "#data/pokemon-species"; import type { PokemonSpecies } from "#data/pokemon-species";
import { BiomeId } from "#enums/biome-id"; import { BiomeId } from "#enums/biome-id";
@ -15,6 +15,7 @@ import type { Arena } from "#field/arena";
import { classicFixedBattles, type FixedBattleConfigs } from "#trainers/fixed-battle-configs"; import { classicFixedBattles, type FixedBattleConfigs } from "#trainers/fixed-battle-configs";
import { applyChallenges } from "#utils/challenge-utils"; import { applyChallenges } from "#utils/challenge-utils";
import { BooleanHolder, isNullOrUndefined, randSeedInt, randSeedItem } from "#utils/common"; import { BooleanHolder, isNullOrUndefined, randSeedInt, randSeedItem } from "#utils/common";
import { getPokemonSpecies } from "#utils/pokemon-utils";
import i18next from "i18next"; import i18next from "i18next";
interface GameModeConfig { interface GameModeConfig {
@ -211,6 +212,12 @@ export class GameMode implements GameModeConfig {
getOverrideSpecies(waveIndex: number): PokemonSpecies | null { getOverrideSpecies(waveIndex: number): PokemonSpecies | null {
if (this.isDaily && this.isWaveFinal(waveIndex)) { if (this.isDaily && this.isWaveFinal(waveIndex)) {
const eventBoss = getDailyEventSeedBoss(globalScene.seed);
if (!isNullOrUndefined(eventBoss)) {
// Cannot set form index here, it will be overriden when adding it as enemy pokemon.
return getPokemonSpecies(eventBoss.speciesId);
}
const allFinalBossSpecies = allSpecies.filter( const allFinalBossSpecies = allSpecies.filter(
s => s =>
(s.subLegendary || s.legendary || s.mythical) && (s.subLegendary || s.legendary || s.mythical) &&
@ -333,7 +340,7 @@ export class GameMode implements GameModeConfig {
} }
} }
getEnemyModifierChance(isBoss: boolean): number { getEnemyItemChance(isBoss: boolean): number {
switch (this.modeId) { switch (this.modeId) {
case GameModes.CLASSIC: case GameModes.CLASSIC:
case GameModes.CHALLENGE: case GameModes.CHALLENGE:

View File

@ -6,8 +6,11 @@ import { initSpecies } from "#balance/pokemon-species";
import { initChallenges } from "#data/challenge"; import { initChallenges } from "#data/challenge";
import { initTrainerTypeDialogue } from "#data/dialogue"; import { initTrainerTypeDialogue } from "#data/dialogue";
import { initPokemonForms } from "#data/pokemon-forms"; import { initPokemonForms } from "#data/pokemon-forms";
import { initModifierPools } from "#modifiers/init-modifier-pools"; import { initHeldItems } from "#items/all-held-items";
import { initModifierTypes } from "#modifiers/modifier-type"; import { initTrainerItems } from "#items/all-trainer-items";
import { initHeldItemPools } from "#items/init-held-item-pools";
import { initRewardPools } from "#items/init-reward-pools";
import { initTrainerItemPools } from "#items/init-trainer-item-pools";
import { initMoves } from "#moves/move"; import { initMoves } from "#moves/move";
import { initMysteryEncounters } from "#mystery-encounters/mystery-encounters"; import { initMysteryEncounters } from "#mystery-encounters/mystery-encounters";
import { initAchievements } from "#system/achv"; import { initAchievements } from "#system/achv";
@ -16,10 +19,9 @@ import { initStatsKeys } from "#ui/game-stats-ui-handler";
/** Initialize the game. */ /** Initialize the game. */
export function initializeGame() { export function initializeGame() {
initModifierTypes(); initItems();
initModifierPools();
initAchievements();
initVouchers(); initVouchers();
initAchievements();
initStatsKeys(); initStatsKeys();
initPokemonPrevolutions(); initPokemonPrevolutions();
initPokemonStarters(); initPokemonStarters();
@ -33,3 +35,14 @@ export function initializeGame() {
initChallenges(); initChallenges();
initMysteryEncounters(); initMysteryEncounters();
} }
/**
* Sub-method to initialize all the item-related code.
*/
function initItems() {
initHeldItems();
initHeldItemPools();
initTrainerItems();
initTrainerItemPools();
initRewardPools();
}

195
src/items/all-held-items.ts Normal file
View File

@ -0,0 +1,195 @@
import { allHeldItems } from "#data/data-lists";
import { BerryType } from "#enums/berry-type";
import { HeldItemEffect } from "#enums/held-item-effect";
import { HeldItemId } from "#enums/held-item-id";
import type { PokemonType } from "#enums/pokemon-type";
import { SpeciesId } from "#enums/species-id";
import { type PermanentStat, Stat } from "#enums/stat";
import { StatusEffect } from "#enums/status-effect";
import { AccuracyBoosterHeldItem, type AccuracyBoostParams } from "#items/accuracy-booster";
import {
AttackTypeBoosterHeldItem,
type AttackTypeBoostParams,
attackTypeToHeldItem,
} from "#items/attack-type-booster";
import { BaseStatBoosterHeldItem, type BaseStatBoosterParams, permanentStatToHeldItem } from "#items/base-stat-booster";
import { BaseStatFlatHeldItem, type BaseStatFlatParams } from "#items/base-stat-flat";
import { BaseStatTotalHeldItem, type BaseStatTotalParams } from "#items/base-stat-total";
import { BatonHeldItem, type BatonParams } from "#items/baton";
import { BerryHeldItem, type BerryParams, berryTypeToHeldItem } from "#items/berry";
import { BypassSpeedChanceHeldItem, type BypassSpeedChanceParams } from "#items/bypass-speed-chance";
import { CritBoostHeldItem, type CritBoostParams, SpeciesCritBoostHeldItem } from "#items/crit-booster";
import { DamageMoneyRewardHeldItem, type DamageMoneyRewardParams } from "#items/damage-money-reward";
import { type EvoTrackerParams, GimmighoulEvoTrackerHeldItem } from "#items/evo-tracker";
import { ExpBoosterHeldItem, type ExpBoostParams } from "#items/exp-booster";
import { FieldEffectHeldItem, type FieldEffectParams } from "#items/field-effect";
import { FlinchChanceHeldItem, type FlinchChanceParams } from "#items/flinch-chance";
import { FriendshipBoosterHeldItem, type FriendshipBoostParams } from "#items/friendship-booster";
import { HitHealHeldItem, type HitHealParams } from "#items/hit-heal";
import { IncrementingStatHeldItem, type IncrementingStatParams } from "#items/incrementing-stat";
import { InstantReviveHeldItem, type InstantReviveParams } from "#items/instant-revive";
import { ContactItemStealChanceHeldItem, type ItemStealParams, TurnEndItemStealHeldItem } from "#items/item-steal";
import { MultiHitHeldItem, type MultiHitParams } from "#items/multi-hit";
import { NatureWeightBoosterHeldItem, type NatureWeightBoostParams } from "#items/nature-weight-booster";
import { ResetNegativeStatStageHeldItem, type ResetNegativeStatStageParams } from "#items/reset-negative-stat-stage";
import { EvolutionStatBoostHeldItem, SpeciesStatBoostHeldItem, type StatBoostParams } from "#items/stat-booster";
import { SurviveChanceHeldItem, type SurviveChanceParams } from "#items/survive-chance";
import { TurnEndHealHeldItem, type TurnEndHealParams } from "#items/turn-end-heal";
import { TurnEndStatusHeldItem, type TurnEndStatusParams } from "#items/turn-end-status";
import { getEnumValues } from "#utils/enums";
export function initHeldItems() {
for (const berry of getEnumValues(BerryType)) {
const maxStackCount = [BerryType.LUM, BerryType.LEPPA, BerryType.SITRUS, BerryType.ENIGMA].includes(berry) ? 2 : 3;
const berryId = berryTypeToHeldItem[berry];
allHeldItems[berryId] = new BerryHeldItem(berry, maxStackCount);
}
allHeldItems[HeldItemId.REVIVER_SEED] = new InstantReviveHeldItem(HeldItemId.REVIVER_SEED, 1);
allHeldItems[HeldItemId.WHITE_HERB] = new ResetNegativeStatStageHeldItem(HeldItemId.WHITE_HERB, 2);
// SILK_SCARF, BLACK_BELT, etc...
for (const [typeKey, heldItemType] of Object.entries(attackTypeToHeldItem)) {
// TODO: https://github.com/pagefaultgames/pokerogue/pull/5656#discussion_r2114957526
const pokemonType = Number(typeKey) as PokemonType;
allHeldItems[heldItemType] = new AttackTypeBoosterHeldItem(heldItemType, 99, pokemonType, 0.2);
}
// Items that boost specific stats
allHeldItems[HeldItemId.EVIOLITE] = new EvolutionStatBoostHeldItem(
HeldItemId.EVIOLITE,
1,
[Stat.DEF, Stat.SPDEF],
1.5,
);
allHeldItems[HeldItemId.LIGHT_BALL] = new SpeciesStatBoostHeldItem(
HeldItemId.LIGHT_BALL,
1,
[Stat.ATK, Stat.SPATK],
2,
[SpeciesId.PIKACHU],
);
allHeldItems[HeldItemId.THICK_CLUB] = new SpeciesStatBoostHeldItem(HeldItemId.LIGHT_BALL, 1, [Stat.ATK], 2, [
SpeciesId.CUBONE,
SpeciesId.MAROWAK,
SpeciesId.ALOLA_MAROWAK,
]);
allHeldItems[HeldItemId.METAL_POWDER] = new SpeciesStatBoostHeldItem(HeldItemId.LIGHT_BALL, 1, [Stat.DEF], 2, [
SpeciesId.DITTO,
]);
allHeldItems[HeldItemId.QUICK_POWDER] = new SpeciesStatBoostHeldItem(HeldItemId.LIGHT_BALL, 1, [Stat.SPD], 2, [
SpeciesId.DITTO,
]);
allHeldItems[HeldItemId.DEEP_SEA_SCALE] = new SpeciesStatBoostHeldItem(HeldItemId.LIGHT_BALL, 1, [Stat.SPDEF], 2, [
SpeciesId.CLAMPERL,
]);
allHeldItems[HeldItemId.DEEP_SEA_TOOTH] = new SpeciesStatBoostHeldItem(HeldItemId.LIGHT_BALL, 1, [Stat.SPATK], 2, [
SpeciesId.CLAMPERL,
]);
// Items that boost the crit rate
allHeldItems[HeldItemId.SCOPE_LENS] = new CritBoostHeldItem(HeldItemId.SCOPE_LENS, 1, 1);
allHeldItems[HeldItemId.LEEK] = new SpeciesCritBoostHeldItem(HeldItemId.LEEK, 1, 2, [
SpeciesId.FARFETCHD,
SpeciesId.GALAR_FARFETCHD,
SpeciesId.SIRFETCHD,
]);
allHeldItems[HeldItemId.LUCKY_EGG] = new ExpBoosterHeldItem(HeldItemId.LUCKY_EGG, 99, 40);
allHeldItems[HeldItemId.GOLDEN_EGG] = new ExpBoosterHeldItem(HeldItemId.GOLDEN_EGG, 99, 100);
allHeldItems[HeldItemId.SOOTHE_BELL] = new FriendshipBoosterHeldItem(HeldItemId.SOOTHE_BELL, 3);
allHeldItems[HeldItemId.LEFTOVERS] = new TurnEndHealHeldItem(HeldItemId.LEFTOVERS, 4);
allHeldItems[HeldItemId.SHELL_BELL] = new HitHealHeldItem(HeldItemId.SHELL_BELL, 4);
allHeldItems[HeldItemId.FOCUS_BAND] = new SurviveChanceHeldItem(HeldItemId.FOCUS_BAND, 5);
allHeldItems[HeldItemId.QUICK_CLAW] = new BypassSpeedChanceHeldItem(HeldItemId.QUICK_CLAW, 3);
allHeldItems[HeldItemId.KINGS_ROCK] = new FlinchChanceHeldItem(HeldItemId.KINGS_ROCK, 3, 10);
allHeldItems[HeldItemId.MYSTICAL_ROCK] = new FieldEffectHeldItem(HeldItemId.MYSTICAL_ROCK, 2);
allHeldItems[HeldItemId.SOUL_DEW] = new NatureWeightBoosterHeldItem(HeldItemId.SOUL_DEW, 10);
allHeldItems[HeldItemId.WIDE_LENS] = new AccuracyBoosterHeldItem(HeldItemId.WIDE_LENS, 3, 5);
allHeldItems[HeldItemId.MULTI_LENS] = new MultiHitHeldItem(HeldItemId.MULTI_LENS, 2);
allHeldItems[HeldItemId.GOLDEN_PUNCH] = new DamageMoneyRewardHeldItem(HeldItemId.GOLDEN_PUNCH, 5);
allHeldItems[HeldItemId.BATON] = new BatonHeldItem(HeldItemId.BATON, 1);
allHeldItems[HeldItemId.GRIP_CLAW] = new ContactItemStealChanceHeldItem(HeldItemId.GRIP_CLAW, 5, 10);
allHeldItems[HeldItemId.MINI_BLACK_HOLE] = new TurnEndItemStealHeldItem(HeldItemId.MINI_BLACK_HOLE, 1)
.unstealable()
.untransferable();
allHeldItems[HeldItemId.FLAME_ORB] = new TurnEndStatusHeldItem(HeldItemId.FLAME_ORB, 1, StatusEffect.BURN);
allHeldItems[HeldItemId.TOXIC_ORB] = new TurnEndStatusHeldItem(HeldItemId.TOXIC_ORB, 1, StatusEffect.TOXIC);
// vitamins
for (const [statKey, heldItemType] of Object.entries(permanentStatToHeldItem)) {
const stat = Number(statKey) as PermanentStat;
allHeldItems[heldItemType] = new BaseStatBoosterHeldItem(heldItemType, 30, stat)
.unstealable()
.untransferable()
.unsuppressable();
}
allHeldItems[HeldItemId.SHUCKLE_JUICE_GOOD] = new BaseStatTotalHeldItem(HeldItemId.SHUCKLE_JUICE_GOOD, 1, 10)
.unstealable()
.untransferable()
.unsuppressable();
allHeldItems[HeldItemId.SHUCKLE_JUICE_BAD] = new BaseStatTotalHeldItem(HeldItemId.SHUCKLE_JUICE_BAD, 1, -15)
.unstealable()
.untransferable()
.unsuppressable();
allHeldItems[HeldItemId.OLD_GATEAU] = new BaseStatFlatHeldItem(HeldItemId.OLD_GATEAU, 1)
.unstealable()
.untransferable()
.unsuppressable();
allHeldItems[HeldItemId.MACHO_BRACE] = new IncrementingStatHeldItem(HeldItemId.MACHO_BRACE, 50)
.unstealable()
.untransferable()
.unsuppressable();
allHeldItems[HeldItemId.GIMMIGHOUL_EVO_TRACKER] = new GimmighoulEvoTrackerHeldItem(
HeldItemId.GIMMIGHOUL_EVO_TRACKER,
999,
SpeciesId.GIMMIGHOUL,
10,
);
}
type ApplyHeldItemsParams = {
[HeldItemEffect.ATTACK_TYPE_BOOST]: AttackTypeBoostParams;
[HeldItemEffect.TURN_END_HEAL]: TurnEndHealParams;
[HeldItemEffect.HIT_HEAL]: HitHealParams;
[HeldItemEffect.RESET_NEGATIVE_STAT_STAGE]: ResetNegativeStatStageParams;
[HeldItemEffect.EXP_BOOSTER]: ExpBoostParams;
[HeldItemEffect.BERRY]: BerryParams;
[HeldItemEffect.BASE_STAT_BOOSTER]: BaseStatBoosterParams;
[HeldItemEffect.INSTANT_REVIVE]: InstantReviveParams;
[HeldItemEffect.STAT_BOOST]: StatBoostParams;
[HeldItemEffect.CRIT_BOOST]: CritBoostParams;
[HeldItemEffect.TURN_END_STATUS]: TurnEndStatusParams;
[HeldItemEffect.SURVIVE_CHANCE]: SurviveChanceParams;
[HeldItemEffect.BYPASS_SPEED_CHANCE]: BypassSpeedChanceParams;
[HeldItemEffect.FLINCH_CHANCE]: FlinchChanceParams;
[HeldItemEffect.FIELD_EFFECT]: FieldEffectParams;
[HeldItemEffect.FRIENDSHIP_BOOSTER]: FriendshipBoostParams;
[HeldItemEffect.NATURE_WEIGHT_BOOSTER]: NatureWeightBoostParams;
[HeldItemEffect.ACCURACY_BOOSTER]: AccuracyBoostParams;
[HeldItemEffect.MULTI_HIT]: MultiHitParams;
[HeldItemEffect.DAMAGE_MONEY_REWARD]: DamageMoneyRewardParams;
[HeldItemEffect.BATON]: BatonParams;
[HeldItemEffect.CONTACT_ITEM_STEAL_CHANCE]: ItemStealParams;
[HeldItemEffect.TURN_END_ITEM_STEAL]: ItemStealParams;
[HeldItemEffect.BASE_STAT_TOTAL]: BaseStatTotalParams;
[HeldItemEffect.BASE_STAT_FLAT]: BaseStatFlatParams;
[HeldItemEffect.INCREMENTING_STAT]: IncrementingStatParams;
[HeldItemEffect.EVO_TRACKER]: EvoTrackerParams;
};
export function applyHeldItems<T extends HeldItemEffect>(effect: T, params: ApplyHeldItemsParams[T]) {
const pokemon = params.pokemon;
if (pokemon) {
// TODO: Make this use `getHeldItems` and make `heldItems` array private
for (const item of Object.keys(pokemon.heldItemManager.heldItems)) {
if (allHeldItems[item].effects.includes(effect)) {
allHeldItems[item].apply(params);
}
}
}
}

191
src/items/all-rewards.ts Normal file
View File

@ -0,0 +1,191 @@
import { PokeballType } from "#enums/pokeball";
import { RewardId } from "#enums/reward-id";
import { RarityTier } from "#enums/reward-tier";
import { TrainerItemId } from "#enums/trainer-item-id";
import { VoucherType } from "#system/voucher";
import {
AddMoneyReward,
AddPokeballReward,
AddVoucherReward,
AllPokemonFullReviveReward,
AllPokemonLevelIncrementReward,
AttackTypeBoosterRewardGenerator,
BaseStatBoosterRewardGenerator,
BerryRewardGenerator,
EvolutionItemRewardGenerator,
FormChangeItemRewardGenerator,
FusePokemonReward,
LapsingTrainerItemReward,
MintRewardGenerator,
NoneReward,
PokemonAllMovePpRestoreReward,
PokemonHpRestoreReward,
PokemonLevelIncrementReward,
PokemonPpRestoreReward,
PokemonPpUpReward,
PokemonReviveReward,
PokemonStatusHealReward,
RememberMoveReward,
type Reward,
type RewardGenerator,
SpeciesStatBoosterRewardGenerator,
TempStatStageBoosterRewardGenerator,
TeraTypeRewardGenerator,
TmRewardGenerator,
} from "./reward";
// TODO: Move to `reward-utils.ts` and un-exportt
export const allRewards = {
[RewardId.NONE]: new NoneReward(),
// Pokeball rewards
[RewardId.POKEBALL]: new AddPokeballReward("pb", PokeballType.POKEBALL, 5, RewardId.POKEBALL),
[RewardId.GREAT_BALL]: new AddPokeballReward("gb", PokeballType.GREAT_BALL, 5, RewardId.GREAT_BALL),
[RewardId.ULTRA_BALL]: new AddPokeballReward("ub", PokeballType.ULTRA_BALL, 5, RewardId.ULTRA_BALL),
[RewardId.ROGUE_BALL]: new AddPokeballReward("rb", PokeballType.ROGUE_BALL, 5, RewardId.ROGUE_BALL),
[RewardId.MASTER_BALL]: new AddPokeballReward("mb", PokeballType.MASTER_BALL, 1, RewardId.MASTER_BALL),
// Voucher rewards
[RewardId.VOUCHER]: new AddVoucherReward(VoucherType.REGULAR, 1, RewardId.VOUCHER),
[RewardId.VOUCHER_PLUS]: new AddVoucherReward(VoucherType.PLUS, 1, RewardId.VOUCHER_PLUS),
[RewardId.VOUCHER_PREMIUM]: new AddVoucherReward(VoucherType.PREMIUM, 1, RewardId.VOUCHER_PREMIUM),
// Money rewards
[RewardId.NUGGET]: new AddMoneyReward(
"modifierType:ModifierType.NUGGET",
"nugget",
1,
"modifierType:ModifierType.MoneyRewardModifierType.extra.small",
RewardId.NUGGET,
),
[RewardId.BIG_NUGGET]: new AddMoneyReward(
"modifierType:ModifierType.BIG_NUGGET",
"big_nugget",
2.5,
"modifierType:ModifierType.MoneyRewardModifierType.extra.moderate",
RewardId.BIG_NUGGET,
),
[RewardId.RELIC_GOLD]: new AddMoneyReward(
"modifierType:ModifierType.RELIC_GOLD",
"relic_gold",
10,
"modifierType:ModifierType.MoneyRewardModifierType.extra.large",
RewardId.RELIC_GOLD,
),
// Party-wide consumables
[RewardId.RARER_CANDY]: new AllPokemonLevelIncrementReward("modifierType:ModifierType.RARER_CANDY", "rarer_candy"),
[RewardId.SACRED_ASH]: new AllPokemonFullReviveReward("modifierType:ModifierType.SACRED_ASH", "sacred_ash"),
// Pokemon consumables
[RewardId.RARE_CANDY]: new PokemonLevelIncrementReward("modifierType:ModifierType.RARE_CANDY", "rare_candy"),
[RewardId.EVOLUTION_ITEM]: new EvolutionItemRewardGenerator(false),
[RewardId.RARE_EVOLUTION_ITEM]: new EvolutionItemRewardGenerator(true),
[RewardId.POTION]: new PokemonHpRestoreReward("modifierType:ModifierType.POTION", "potion", RewardId.POTION, 20, 10),
[RewardId.SUPER_POTION]: new PokemonHpRestoreReward(
"modifierType:ModifierType.SUPER_POTION",
"super_potion",
RewardId.SUPER_POTION,
50,
25,
),
[RewardId.HYPER_POTION]: new PokemonHpRestoreReward(
"modifierType:ModifierType.HYPER_POTION",
"hyper_potion",
RewardId.HYPER_POTION,
200,
50,
),
[RewardId.MAX_POTION]: new PokemonHpRestoreReward(
"modifierType:ModifierType.MAX_POTION",
"max_potion",
RewardId.MAX_POTION,
0,
100,
),
[RewardId.FULL_RESTORE]: new PokemonHpRestoreReward(
"modifierType:ModifierType.FULL_RESTORE",
"full_restore",
RewardId.FULL_RESTORE,
0,
100,
true,
),
[RewardId.REVIVE]: new PokemonReviveReward("modifierType:ModifierType.REVIVE", "revive", RewardId.REVIVE, 50),
[RewardId.MAX_REVIVE]: new PokemonReviveReward(
"modifierType:ModifierType.MAX_REVIVE",
"max_revive",
RewardId.MAX_REVIVE,
100,
),
[RewardId.FULL_HEAL]: new PokemonStatusHealReward("modifierType:ModifierType.FULL_HEAL", "full_heal"),
[RewardId.ETHER]: new PokemonPpRestoreReward("modifierType:ModifierType.ETHER", "ether", RewardId.ETHER, 10),
[RewardId.MAX_ETHER]: new PokemonPpRestoreReward(
"modifierType:ModifierType.MAX_ETHER",
"max_ether",
RewardId.MAX_ETHER,
-1,
),
[RewardId.ELIXIR]: new PokemonAllMovePpRestoreReward(
"modifierType:ModifierType.ELIXIR",
"elixir",
RewardId.ELIXIR,
10,
),
[RewardId.MAX_ELIXIR]: new PokemonAllMovePpRestoreReward(
"modifierType:ModifierType.MAX_ELIXIR",
"max_elixir",
RewardId.MAX_ELIXIR,
-1,
),
[RewardId.PP_UP]: new PokemonPpUpReward("modifierType:ModifierType.PP_UP", "pp_up", RewardId.PP_UP, 1),
[RewardId.PP_MAX]: new PokemonPpUpReward("modifierType:ModifierType.PP_MAX", "pp_max", RewardId.PP_MAX, 3),
[RewardId.MINT]: new MintRewardGenerator(),
[RewardId.TERA_SHARD]: new TeraTypeRewardGenerator(),
[RewardId.TM_COMMON]: new TmRewardGenerator(RarityTier.COMMON),
[RewardId.TM_GREAT]: new TmRewardGenerator(RarityTier.GREAT),
[RewardId.TM_ULTRA]: new TmRewardGenerator(RarityTier.ULTRA),
[RewardId.MEMORY_MUSHROOM]: new RememberMoveReward("modifierType:ModifierType.MEMORY_MUSHROOM", "big_mushroom"),
[RewardId.DNA_SPLICERS]: new FusePokemonReward("modifierType:ModifierType.DNA_SPLICERS", "dna_splicers"),
// Form change items
[RewardId.FORM_CHANGE_ITEM]: new FormChangeItemRewardGenerator(false),
[RewardId.RARE_FORM_CHANGE_ITEM]: new FormChangeItemRewardGenerator(true),
// Held items
[RewardId.SPECIES_STAT_BOOSTER]: new SpeciesStatBoosterRewardGenerator(false),
[RewardId.RARE_SPECIES_STAT_BOOSTER]: new SpeciesStatBoosterRewardGenerator(true),
[RewardId.BASE_STAT_BOOSTER]: new BaseStatBoosterRewardGenerator(),
[RewardId.ATTACK_TYPE_BOOSTER]: new AttackTypeBoosterRewardGenerator(),
[RewardId.BERRY]: new BerryRewardGenerator(),
// Trainer items
[RewardId.LURE]: new LapsingTrainerItemReward(TrainerItemId.LURE),
[RewardId.SUPER_LURE]: new LapsingTrainerItemReward(TrainerItemId.SUPER_LURE),
[RewardId.MAX_LURE]: new LapsingTrainerItemReward(TrainerItemId.MAX_LURE),
[RewardId.TEMP_STAT_STAGE_BOOSTER]: new TempStatStageBoosterRewardGenerator(),
[RewardId.DIRE_HIT]: new LapsingTrainerItemReward(TrainerItemId.DIRE_HIT),
} as const satisfies {
[k in RewardId]: Reward | RewardGenerator;
};
export type allRewardsType = typeof allRewards;

View File

@ -0,0 +1,114 @@
import { allTrainerItems } from "#data/data-lists";
import { Stat, type TempBattleStat } from "#enums/stat";
import { StatusEffect } from "#enums/status-effect";
import { TrainerItemId } from "#enums/trainer-item-id";
import {
CriticalCatchChanceBoosterTrainerItem,
DoubleBattleChanceBoosterTrainerItem,
EnemyAttackStatusEffectChanceTrainerItem,
EnemyDamageBoosterTrainerItem,
EnemyDamageReducerTrainerItem,
EnemyEndureChanceTrainerItem,
EnemyFusionChanceTrainerItem,
EnemyStatusEffectHealChanceTrainerItem,
EnemyTurnHealTrainerItem,
ExpBoosterTrainerItem,
ExtraRewardTrainerItem,
HealingBoosterTrainerItem,
HealShopCostTrainerItem,
HiddenAbilityChanceBoosterTrainerItem,
LevelIncrementBoosterTrainerItem,
MoneyMultiplierTrainerItem,
PreserveBerryTrainerItem,
ShinyRateBoosterTrainerItem,
TempAccuracyBoosterTrainerItem,
TempCritBoosterTrainerItem,
TempStatStageBoosterTrainerItem,
TrainerItem,
tempStatToTrainerItem,
} from "#items/trainer-item";
export function initTrainerItems() {
allTrainerItems[TrainerItemId.MAP] = new TrainerItem(TrainerItemId.MAP, 1);
allTrainerItems[TrainerItemId.IV_SCANNER] = new TrainerItem(TrainerItemId.IV_SCANNER, 1);
allTrainerItems[TrainerItemId.LOCK_CAPSULE] = new TrainerItem(TrainerItemId.LOCK_CAPSULE, 1);
allTrainerItems[TrainerItemId.MEGA_BRACELET] = new TrainerItem(TrainerItemId.MEGA_BRACELET, 1);
allTrainerItems[TrainerItemId.DYNAMAX_BAND] = new TrainerItem(TrainerItemId.DYNAMAX_BAND, 1);
allTrainerItems[TrainerItemId.TERA_ORB] = new TrainerItem(TrainerItemId.TERA_ORB, 1);
allTrainerItems[TrainerItemId.OVAL_CHARM] = new TrainerItem(TrainerItemId.OVAL_CHARM, 5);
allTrainerItems[TrainerItemId.EXP_SHARE] = new TrainerItem(TrainerItemId.EXP_SHARE, 5);
allTrainerItems[TrainerItemId.EXP_BALANCE] = new TrainerItem(TrainerItemId.EXP_BALANCE, 4);
allTrainerItems[TrainerItemId.CANDY_JAR] = new LevelIncrementBoosterTrainerItem(TrainerItemId.CANDY_JAR, 99);
allTrainerItems[TrainerItemId.BERRY_POUCH] = new PreserveBerryTrainerItem(TrainerItemId.BERRY_POUCH, 3);
allTrainerItems[TrainerItemId.HEALING_CHARM] = new HealingBoosterTrainerItem(TrainerItemId.HEALING_CHARM, 0.1, 5);
allTrainerItems[TrainerItemId.EXP_CHARM] = new ExpBoosterTrainerItem(TrainerItemId.EXP_CHARM, 25, 99);
allTrainerItems[TrainerItemId.SUPER_EXP_CHARM] = new ExpBoosterTrainerItem(TrainerItemId.SUPER_EXP_CHARM, 60, 30);
allTrainerItems[TrainerItemId.GOLDEN_EXP_CHARM] = new ExpBoosterTrainerItem(TrainerItemId.GOLDEN_EXP_CHARM, 100, 10);
allTrainerItems[TrainerItemId.AMULET_COIN] = new MoneyMultiplierTrainerItem(TrainerItemId.AMULET_COIN, 5);
allTrainerItems[TrainerItemId.ABILITY_CHARM] = new HiddenAbilityChanceBoosterTrainerItem(
TrainerItemId.ABILITY_CHARM,
4,
);
allTrainerItems[TrainerItemId.GOLDEN_POKEBALL] = new ExtraRewardTrainerItem(TrainerItemId.GOLDEN_POKEBALL, 3);
allTrainerItems[TrainerItemId.SHINY_CHARM] = new ShinyRateBoosterTrainerItem(TrainerItemId.SHINY_CHARM, 4);
allTrainerItems[TrainerItemId.CATCHING_CHARM] = new CriticalCatchChanceBoosterTrainerItem(
TrainerItemId.CATCHING_CHARM,
3,
);
allTrainerItems[TrainerItemId.BLACK_SLUDGE] = new HealShopCostTrainerItem(TrainerItemId.BLACK_SLUDGE, 2.5, 1);
allTrainerItems[TrainerItemId.GOLDEN_BUG_NET] = new TrainerItem(TrainerItemId.GOLDEN_BUG_NET, 1);
allTrainerItems[TrainerItemId.LURE] = new DoubleBattleChanceBoosterTrainerItem(TrainerItemId.LURE, 10);
allTrainerItems[TrainerItemId.SUPER_LURE] = new DoubleBattleChanceBoosterTrainerItem(TrainerItemId.SUPER_LURE, 15);
allTrainerItems[TrainerItemId.MAX_LURE] = new DoubleBattleChanceBoosterTrainerItem(TrainerItemId.MAX_LURE, 30);
for (const [statKey, trainerItemType] of Object.entries(tempStatToTrainerItem)) {
const stat = Number(statKey) as TempBattleStat;
if (stat === Stat.ACC) {
allTrainerItems[trainerItemType] = new TempAccuracyBoosterTrainerItem(trainerItemType, 5);
} else {
allTrainerItems[trainerItemType] = new TempStatStageBoosterTrainerItem(trainerItemType, stat, 5);
}
}
allTrainerItems[TrainerItemId.DIRE_HIT] = new TempCritBoosterTrainerItem(TrainerItemId.DIRE_HIT, 5);
allTrainerItems[TrainerItemId.ENEMY_DAMAGE_BOOSTER] = new EnemyDamageBoosterTrainerItem(
TrainerItemId.ENEMY_DAMAGE_BOOSTER,
);
allTrainerItems[TrainerItemId.ENEMY_DAMAGE_REDUCTION] = new EnemyDamageReducerTrainerItem(
TrainerItemId.ENEMY_DAMAGE_REDUCTION,
);
allTrainerItems[TrainerItemId.ENEMY_HEAL] = new EnemyTurnHealTrainerItem(TrainerItemId.ENEMY_HEAL, 10);
allTrainerItems[TrainerItemId.ENEMY_ATTACK_POISON_CHANCE] = new EnemyAttackStatusEffectChanceTrainerItem(
TrainerItemId.ENEMY_ATTACK_POISON_CHANCE,
StatusEffect.POISON,
10,
);
allTrainerItems[TrainerItemId.ENEMY_ATTACK_PARALYZE_CHANCE] = new EnemyAttackStatusEffectChanceTrainerItem(
TrainerItemId.ENEMY_ATTACK_PARALYZE_CHANCE,
StatusEffect.PARALYSIS,
10,
);
allTrainerItems[TrainerItemId.ENEMY_ATTACK_BURN_CHANCE] = new EnemyAttackStatusEffectChanceTrainerItem(
TrainerItemId.ENEMY_ATTACK_BURN_CHANCE,
StatusEffect.BURN,
10,
);
allTrainerItems[TrainerItemId.ENEMY_STATUS_EFFECT_HEAL_CHANCE] = new EnemyStatusEffectHealChanceTrainerItem(
TrainerItemId.ENEMY_STATUS_EFFECT_HEAL_CHANCE,
10,
);
allTrainerItems[TrainerItemId.ENEMY_ENDURE_CHANCE] = new EnemyEndureChanceTrainerItem(
TrainerItemId.ENEMY_ENDURE_CHANCE,
10,
);
allTrainerItems[TrainerItemId.ENEMY_FUSED_CHANCE] = new EnemyFusionChanceTrainerItem(
TrainerItemId.ENEMY_FUSED_CHANCE,
10,
);
}

View File

@ -0,0 +1,47 @@
import { allTrainerItems } from "#data/data-lists";
import {
type BooleanHolderParams,
type NumberHolderParams,
type PokemonParams,
type PreserveBerryParams,
TrainerItemEffect,
} from "#items/trainer-item";
import type { TrainerItemManager } from "#items/trainer-item-manager";
export type ApplyTrainerItemsParams = {
[TrainerItemEffect.LEVEL_INCREMENT_BOOSTER]: NumberHolderParams;
[TrainerItemEffect.PRESERVE_BERRY]: PreserveBerryParams;
[TrainerItemEffect.HEALING_BOOSTER]: NumberHolderParams;
[TrainerItemEffect.EXP_BOOSTER]: NumberHolderParams;
[TrainerItemEffect.MONEY_MULTIPLIER]: NumberHolderParams;
[TrainerItemEffect.HIDDEN_ABILITY_CHANCE_BOOSTER]: NumberHolderParams;
[TrainerItemEffect.SHINY_RATE_BOOSTER]: NumberHolderParams;
[TrainerItemEffect.CRITICAL_CATCH_CHANCE_BOOSTER]: NumberHolderParams;
[TrainerItemEffect.EXTRA_REWARD]: NumberHolderParams;
[TrainerItemEffect.HEAL_SHOP_COST]: NumberHolderParams;
[TrainerItemEffect.DOUBLE_BATTLE_CHANCE_BOOSTER]: NumberHolderParams;
[TrainerItemEffect.TEMP_STAT_STAGE_BOOSTER]: NumberHolderParams;
[TrainerItemEffect.TEMP_ACCURACY_BOOSTER]: NumberHolderParams;
[TrainerItemEffect.TEMP_CRIT_BOOSTER]: NumberHolderParams;
[TrainerItemEffect.ENEMY_DAMAGE_BOOSTER]: NumberHolderParams;
[TrainerItemEffect.ENEMY_DAMAGE_REDUCER]: NumberHolderParams;
[TrainerItemEffect.ENEMY_HEAL]: PokemonParams;
[TrainerItemEffect.ENEMY_ATTACK_STATUS_CHANCE]: PokemonParams;
[TrainerItemEffect.ENEMY_STATUS_HEAL_CHANCE]: PokemonParams;
[TrainerItemEffect.ENEMY_ENDURE_CHANCE]: PokemonParams;
[TrainerItemEffect.ENEMY_FUSED_CHANCE]: BooleanHolderParams;
};
export function applyTrainerItems<T extends TrainerItemEffect>(
effect: T,
manager: TrainerItemManager,
params: ApplyTrainerItemsParams[T],
) {
if (manager) {
for (const item of Object.keys(manager.trainerItems)) {
if (allTrainerItems[item].effects.includes(effect)) {
allTrainerItems[item].apply(manager, params);
}
}
}
}

View File

@ -0,0 +1,93 @@
// TODO: move all types to `src/@types/` and all functions to a utility place
import type { FormChangeItem } from "#enums/form-change-item";
import type { HeldItemCategoryId, HeldItemId } from "#enums/held-item-id";
import type { RarityTier } from "#enums/reward-tier";
import type { Pokemon } from "#field/pokemon";
export type HeldItemData = {
stack: number;
/**
* Whether this item is currently disabled.
* @defaultValue `false`
*/
disabled?: boolean;
/**
* The item's current cooldown.
* @defaultValue `0`
*/
cooldown?: number;
};
export type HeldItemDataMap = {
[key in HeldItemId]?: HeldItemData;
};
export type HeldItemSpecs = HeldItemData & {
id: HeldItemId;
};
export function isHeldItemSpecs(entry: any): entry is HeldItemSpecs {
return typeof entry.id === "number" && "stack" in entry;
}
// Types used for form change items
export interface FormChangeItemData {
active: boolean;
}
export type FormChangeItemPropertyMap = {
[key in FormChangeItem]?: FormChangeItemData;
};
export type FormChangeItemSpecs = FormChangeItemData & {
id: FormChangeItem;
};
export function isFormChangeItemSpecs(entry: any): entry is FormChangeItemSpecs {
return typeof entry.id === "number" && "active" in entry;
}
export type HeldItemWeights = {
[key in HeldItemId]?: number;
};
export type HeldItemWeightFunc = (party: Pokemon[]) => number;
export type HeldItemCategoryEntry = HeldItemData & {
id: HeldItemCategoryId;
customWeights?: HeldItemWeights;
};
export function isHeldItemCategoryEntry(entry: any): entry is HeldItemCategoryEntry {
return entry?.id && isHeldItemCategoryEntry(entry.id) && "customWeights" in entry;
}
type HeldItemPoolEntry = {
entry: HeldItemId | HeldItemCategoryId | HeldItemCategoryEntry | HeldItemSpecs;
weight: number | HeldItemWeightFunc;
};
export type HeldItemPool = HeldItemPoolEntry[];
export function isHeldItemPool(value: any): value is HeldItemPool {
return Array.isArray(value) && value.every(entry => "entry" in entry && "weight" in entry);
}
export type HeldItemTieredPool = {
[key in RarityTier]?: HeldItemPool;
};
type HeldItemConfigurationEntry = {
entry: HeldItemId | HeldItemCategoryId | HeldItemCategoryEntry | HeldItemSpecs | HeldItemPool | FormChangeItemSpecs;
count?: number | (() => number);
};
export type HeldItemConfiguration = HeldItemConfigurationEntry[];
export type PokemonItemMap = {
item: HeldItemSpecs | FormChangeItemSpecs;
pokemonId: number;
};
export type HeldItemSaveData = (HeldItemSpecs | FormChangeItemSpecs)[];

View File

@ -0,0 +1,47 @@
import { getHeldItemCategory, HeldItemCategoryId, HeldItemId } from "#enums/held-item-id";
import { RarityTier } from "#enums/reward-tier";
export const heldItemRarities = {
[HeldItemCategoryId.BERRY]: RarityTier.COMMON,
[HeldItemCategoryId.BASE_STAT_BOOST]: RarityTier.GREAT,
[HeldItemId.WHITE_HERB]: RarityTier.GREAT,
[HeldItemId.METAL_POWDER]: RarityTier.GREAT,
[HeldItemId.QUICK_POWDER]: RarityTier.GREAT,
[HeldItemId.DEEP_SEA_SCALE]: RarityTier.GREAT,
[HeldItemId.DEEP_SEA_TOOTH]: RarityTier.GREAT,
[HeldItemId.SOOTHE_BELL]: RarityTier.GREAT,
[HeldItemCategoryId.TYPE_ATTACK_BOOSTER]: RarityTier.ULTRA,
[HeldItemId.REVIVER_SEED]: RarityTier.ULTRA,
[HeldItemId.LIGHT_BALL]: RarityTier.ULTRA,
[HeldItemId.EVIOLITE]: RarityTier.ULTRA,
[HeldItemId.QUICK_CLAW]: RarityTier.ULTRA,
[HeldItemId.MYSTICAL_ROCK]: RarityTier.ULTRA,
[HeldItemId.WIDE_LENS]: RarityTier.ULTRA,
[HeldItemId.GOLDEN_PUNCH]: RarityTier.ULTRA,
[HeldItemId.TOXIC_ORB]: RarityTier.ULTRA,
[HeldItemId.FLAME_ORB]: RarityTier.ULTRA,
[HeldItemId.LUCKY_EGG]: RarityTier.ULTRA,
[HeldItemId.FOCUS_BAND]: RarityTier.ROGUE,
[HeldItemId.KINGS_ROCK]: RarityTier.ROGUE,
[HeldItemId.LEFTOVERS]: RarityTier.ROGUE,
[HeldItemId.SHELL_BELL]: RarityTier.ROGUE,
[HeldItemId.GRIP_CLAW]: RarityTier.ROGUE,
[HeldItemId.SOUL_DEW]: RarityTier.ROGUE,
[HeldItemId.BATON]: RarityTier.ROGUE,
[HeldItemId.GOLDEN_EGG]: RarityTier.ULTRA,
[HeldItemId.MINI_BLACK_HOLE]: RarityTier.MASTER,
[HeldItemId.MULTI_LENS]: RarityTier.MASTER,
};
export function getHeldItemTier(item: HeldItemId): RarityTier {
let tier = heldItemRarities[item];
if (!tier) {
const category = getHeldItemCategory(item);
tier = heldItemRarities[category];
}
return tier ?? RarityTier.LUXURY;
}

View File

@ -0,0 +1,252 @@
import { allHeldItems } from "#data/data-lists";
import type { FormChangeItem } from "#enums/form-change-item";
import {
type HeldItemCategoryId,
type HeldItemId,
isCategoryId,
isItemInCategory,
isItemInRequested,
} from "#enums/held-item-id";
import {
type FormChangeItemData,
type FormChangeItemPropertyMap,
type FormChangeItemSpecs,
type HeldItemConfiguration,
type HeldItemDataMap,
type HeldItemSaveData,
type HeldItemSpecs,
isHeldItemSpecs,
} from "#items/held-item-data-types";
import { getTypedKeys } from "#utils/common";
export class HeldItemManager {
// TODO: There should be a way of making these private...
public heldItems: HeldItemDataMap;
public formChangeItems: FormChangeItemPropertyMap;
constructor() {
this.heldItems = {};
this.formChangeItems = {};
}
getItemSpecs(id: HeldItemId): HeldItemSpecs | undefined {
const item = this.heldItems[id];
if (item) {
const itemSpecs: HeldItemSpecs = {
...item,
id,
};
return itemSpecs;
}
return undefined;
}
generateHeldItemConfiguration(restrictedIds?: HeldItemId[]): HeldItemConfiguration {
const config: HeldItemConfiguration = [];
for (const [id, item] of this.getHeldItemEntries()) {
// TODO: `in` breaks with arrays
if (item && (!restrictedIds || id in restrictedIds)) {
const specs: HeldItemSpecs = { ...item, id };
config.push({ entry: specs, count: 1 });
}
}
for (const [id, item] of this.getFormChangeItemEntries()) {
if (item) {
const specs: FormChangeItemSpecs = { ...item, id };
config.push({ entry: specs, count: 1 });
}
}
return config;
}
generateSaveData(): HeldItemSaveData {
const saveData: HeldItemSaveData = [];
for (const [id, item] of this.getHeldItemEntries()) {
if (item) {
const specs: HeldItemSpecs = { ...item, id };
saveData.push(specs);
}
}
for (const [id, item] of this.getFormChangeItemEntries()) {
if (item) {
const specs: FormChangeItemSpecs = { ...item, id };
saveData.push(specs);
}
}
return saveData;
}
getHeldItems(): HeldItemId[] {
return getTypedKeys(this.heldItems);
}
private getHeldItemEntries(): [HeldItemId, HeldItemSpecs][] {
return Object.entries(this.heldItems) as unknown as [HeldItemId, HeldItemSpecs][];
}
getTransferableHeldItems(): HeldItemId[] {
return this.getHeldItems().filter(k => allHeldItems[k].isTransferable);
}
getStealableHeldItems(): HeldItemId[] {
return this.getHeldItems().filter(k => allHeldItems[k].isStealable);
}
getSuppressableHeldItems(): HeldItemId[] {
return this.getHeldItems().filter(k => allHeldItems[k].isSuppressable);
}
hasItem(itemType: HeldItemId | HeldItemCategoryId): boolean {
if (isCategoryId(itemType)) {
return this.getHeldItems().some(id => isItemInCategory(id, itemType as HeldItemCategoryId));
}
return itemType in this.heldItems;
}
hasTransferableItem(itemType: HeldItemId | HeldItemCategoryId): boolean {
if (isCategoryId(itemType)) {
return this.getHeldItems().some(
id => isItemInCategory(id, itemType as HeldItemCategoryId) && allHeldItems[id].isTransferable,
);
}
return itemType in this.heldItems && allHeldItems[itemType].isTransferable;
}
// TODO: Consider renaming?
getStack(itemType: HeldItemId): number {
const item = this.heldItems[itemType];
return item?.stack ?? 0;
}
// Use for tests if necessary to go over stack limit
// TODO: Do we need this? We can just use overrides
setStack(itemType: HeldItemId, stack: number): void {
const item = this.heldItems[itemType];
if (item) {
item.stack = stack;
}
}
isMaxStack(itemType: HeldItemId): boolean {
const item = this.heldItems[itemType];
return item ? item.stack >= allHeldItems[itemType].getMaxStackCount() : false;
}
overrideItems(newItems: HeldItemDataMap) {
this.heldItems = newItems;
// The following is to allow randomly generated item configs to have stack 0
for (const [item, properties] of this.getHeldItemEntries()) {
if (!properties || properties.stack <= 0) {
delete this.heldItems[item];
}
}
}
add(itemType: HeldItemId | HeldItemSpecs, addStack = 1): boolean {
if (isHeldItemSpecs(itemType)) {
return this.addItemWithSpecs(itemType);
}
const maxStack = allHeldItems[itemType].getMaxStackCount();
const item = this.heldItems[itemType];
if (item) {
// TODO: We may want an error message of some kind instead
if (item.stack < maxStack) {
item.stack = Math.min(item.stack + addStack, maxStack);
return true;
}
} else {
this.heldItems[itemType] = { stack: Math.min(addStack, maxStack) };
return true;
}
return false;
}
addItemWithSpecs(itemSpecs: HeldItemSpecs): boolean {
const id = itemSpecs.id;
const maxStack = allHeldItems[id].getMaxStackCount();
const item = this.heldItems[id];
const tempStack = item?.stack ?? 0;
this.heldItems[id] = itemSpecs;
this.heldItems[id].stack = Math.min(itemSpecs.stack + tempStack, maxStack);
return true;
}
remove(itemType: HeldItemId, removeStack = 1, all = false) {
const item = this.heldItems[itemType];
if (item) {
item.stack -= removeStack;
if (all || item.stack <= 0) {
// TODO: Delete is bad for performance
delete this.heldItems[itemType];
}
}
}
filterRequestedItems(requestedItems: (HeldItemCategoryId | HeldItemId)[], transferableOnly = true, exclude = false) {
const currentItems = transferableOnly ? this.getTransferableHeldItems() : this.getHeldItems();
return currentItems.filter(it => !exclude && isItemInRequested(it, requestedItems));
}
getHeldItemCount(): number {
let total = 0;
for (const properties of Object.values(this.heldItems)) {
total += properties?.stack ?? 0;
}
return total;
}
addFormChangeItem(id: FormChangeItem) {
if (!(id in this.formChangeItems)) {
this.formChangeItems[id] = { active: false };
}
}
addFormChangeItemWithSpecs(item: FormChangeItemSpecs) {
if (!(item.id in this.formChangeItems)) {
this.formChangeItems[item.id] = { active: item.active };
}
}
hasFormChangeItem(id: FormChangeItem): boolean {
return id in this.formChangeItems;
}
hasActiveFormChangeItem(id: FormChangeItem): boolean {
const item = this.formChangeItems[id];
if (item) {
return item.active;
}
return false;
}
getFormChangeItems(): FormChangeItem[] {
return getTypedKeys(this.formChangeItems);
}
private getFormChangeItemEntries(): [FormChangeItem, FormChangeItemData | undefined][] {
return Object.entries(this.formChangeItems) as unknown as [FormChangeItem, FormChangeItemData | undefined][];
}
getActiveFormChangeItems(): FormChangeItem[] {
return this.getFormChangeItems().filter(m => this.formChangeItems[m]?.active);
}
toggleActive(id: FormChangeItem) {
const item = this.formChangeItems[id];
if (item) {
item.active = !item.active;
}
}
clearItems() {
this.heldItems = {};
this.formChangeItems = {};
}
}

324
src/items/held-item-pool.ts Normal file
View File

@ -0,0 +1,324 @@
import { allHeldItems } from "#data/data-lists";
import { BerryType } from "#enums/berry-type";
import { HeldItemCategoryId, HeldItemId, HeldItemNames, isCategoryId } from "#enums/held-item-id";
import type { PokemonType } from "#enums/pokemon-type";
import { HeldItemPoolType } from "#enums/reward-pool-type";
import { RarityTier } from "#enums/reward-tier";
import { PERMANENT_STATS } from "#enums/stat";
import type { EnemyPokemon, PlayerPokemon, Pokemon } from "#field/pokemon";
import { attackTypeToHeldItem } from "#items/attack-type-booster";
import { permanentStatToHeldItem } from "#items/base-stat-booster";
import { berryTypeToHeldItem } from "#items/berry";
import {
type HeldItemConfiguration,
type HeldItemPool,
type HeldItemSaveData,
type HeldItemSpecs,
type HeldItemTieredPool,
type HeldItemWeights,
isFormChangeItemSpecs,
isHeldItemCategoryEntry,
isHeldItemPool,
isHeldItemSpecs,
} from "#items/held-item-data-types";
import { coerceArray, isNullOrUndefined, pickWeightedIndex, randSeedInt } from "#utils/common";
import { getEnumValues } from "#utils/enums";
export const wildHeldItemPool: HeldItemTieredPool = {};
export const trainerHeldItemPool: HeldItemTieredPool = {};
export const dailyStarterHeldItemPool: HeldItemTieredPool = {};
export function assignDailyRunStarterHeldItems(party: PlayerPokemon[]) {
for (const p of party) {
for (let m = 0; m < 3; m++) {
const tierValue = randSeedInt(64);
const tier = getDailyRarityTier(tierValue);
const item = getNewHeldItemFromPool(
getHeldItemPool(HeldItemPoolType.DAILY_STARTER)[tier] as HeldItemPool,
p,
party,
);
p.heldItemManager.add(item);
}
}
}
function getDailyRarityTier(tierValue: number): RarityTier {
if (tierValue > 25) {
return RarityTier.COMMON;
}
if (tierValue > 12) {
return RarityTier.GREAT;
}
if (tierValue > 4) {
return RarityTier.ULTRA;
}
if (tierValue > 0) {
return RarityTier.ROGUE;
}
return RarityTier.MASTER;
}
function getHeldItemPool(poolType: HeldItemPoolType): HeldItemTieredPool {
let pool: HeldItemTieredPool;
switch (poolType) {
case HeldItemPoolType.WILD:
pool = wildHeldItemPool;
break;
case HeldItemPoolType.TRAINER:
pool = trainerHeldItemPool;
break;
case HeldItemPoolType.DAILY_STARTER:
pool = dailyStarterHeldItemPool;
break;
}
return pool;
}
// TODO: Add proper documentation to this function (once it fully works...)
export function assignEnemyHeldItemsForWave(
waveIndex: number,
count: number,
enemy: EnemyPokemon,
poolType: HeldItemPoolType.WILD | HeldItemPoolType.TRAINER,
upgradeChance = 0,
): void {
for (let i = 1; i <= count; i++) {
const item = getNewHeldItemFromTieredPool(
getHeldItemPool(poolType),
enemy,
upgradeChance && !randSeedInt(upgradeChance) ? 1 : 0,
);
if (item) {
enemy.heldItemManager.add(item);
}
}
if (!(waveIndex % 1000)) {
enemy.heldItemManager.add(HeldItemId.MINI_BLACK_HOLE);
}
}
function getRandomTier(): RarityTier {
const tierValue = randSeedInt(1024);
if (tierValue > 255) {
return RarityTier.COMMON;
}
if (tierValue > 60) {
return RarityTier.GREAT;
}
if (tierValue > 12) {
return RarityTier.ULTRA;
}
if (tierValue) {
return RarityTier.ROGUE;
}
return RarityTier.MASTER;
}
function determineItemPoolTier(pool: HeldItemTieredPool, upgradeCount?: number): RarityTier {
let tier = getRandomTier();
if (!upgradeCount) {
upgradeCount = 0;
}
tier += upgradeCount;
while (tier && !pool[tier]?.length) {
tier--;
if (upgradeCount) {
upgradeCount--;
}
}
return tier;
}
function getNewHeldItemFromTieredPool(
pool: HeldItemTieredPool,
pokemon: Pokemon,
upgradeCount: number,
): HeldItemId | HeldItemSpecs {
const tier = determineItemPoolTier(pool, upgradeCount);
const tierPool = pool[tier];
return getNewHeldItemFromPool(tierPool!, pokemon);
}
export function getNewVitaminHeldItem(customWeights: HeldItemWeights = {}, target?: Pokemon): HeldItemId {
const items = PERMANENT_STATS.map(s => permanentStatToHeldItem[s]);
const weights = items.map(t => (target?.heldItemManager.isMaxStack(t) ? 0 : (customWeights[t] ?? 1)));
const pickedIndex = pickWeightedIndex(weights);
return !isNullOrUndefined(pickedIndex) ? items[pickedIndex] : 0;
}
export function getNewBerryHeldItem(customWeights: HeldItemWeights = {}, target?: Pokemon): HeldItemId {
const berryTypes = getEnumValues(BerryType);
const items = berryTypes.map(b => berryTypeToHeldItem[b]);
const weights = items.map(t =>
target?.heldItemManager.isMaxStack(t)
? 0
: (customWeights[t] ??
(t === HeldItemId.SITRUS_BERRY || t === HeldItemId.LUM_BERRY || t === HeldItemId.LEPPA_BERRY))
? 2
: 1,
);
const pickedIndex = pickWeightedIndex(weights);
return !isNullOrUndefined(pickedIndex) ? items[pickedIndex] : 0;
}
export function getNewAttackTypeBoosterHeldItem(
pokemon: Pokemon | Pokemon[],
customWeights: HeldItemWeights = {},
target?: Pokemon,
): HeldItemId | null {
const party = coerceArray(pokemon);
// TODO: make this consider moves or abilities that change types
const attackMoveTypes = party.flatMap(p =>
p
.getMoveset()
.filter(m => m.getMove().is("AttackMove"))
.map(m => p.getMoveType(m.getMove(), true)),
);
if (!attackMoveTypes.length) {
return null;
}
const attackMoveTypeWeights = attackMoveTypes.reduce((map, type) => {
const current = map.get(type) ?? 0;
if (current < 3) {
map.set(type, current + 1);
}
return map;
}, new Map<PokemonType, number>());
const types = Array.from(attackMoveTypeWeights.keys());
const weights = types.map(type =>
target?.heldItemManager.isMaxStack(attackTypeToHeldItem[type])
? 0
: (customWeights[attackTypeToHeldItem[type]] ?? attackMoveTypeWeights.get(type)!),
);
const pickedIndex = pickWeightedIndex(weights);
return !isNullOrUndefined(pickedIndex) ? attackTypeToHeldItem[types[pickedIndex]] : 0;
}
export function getNewHeldItemFromCategory(
id: HeldItemCategoryId,
pokemon: Pokemon | Pokemon[],
customWeights: HeldItemWeights = {},
target?: Pokemon,
): HeldItemId | null {
if (id === HeldItemCategoryId.BERRY) {
return getNewBerryHeldItem(customWeights, target);
}
if (id === HeldItemCategoryId.VITAMIN) {
return getNewVitaminHeldItem(customWeights, target);
}
if (id === HeldItemCategoryId.TYPE_ATTACK_BOOSTER) {
return getNewAttackTypeBoosterHeldItem(pokemon, customWeights, target);
}
return null;
}
function getPoolWeights(pool: HeldItemPool, pokemon: Pokemon): number[] {
return pool.map(p => {
let weight = typeof p.weight === "function" ? p.weight(coerceArray(pokemon)) : p.weight;
if (typeof p.entry === "number" && !isCategoryId(p.entry)) {
const itemId = p.entry as HeldItemId;
console.log("ITEM ID: ", itemId, HeldItemNames[itemId]);
console.log(allHeldItems[itemId]);
if (pokemon.heldItemManager.getStack(itemId) >= allHeldItems[itemId].getMaxStackCount()) {
weight = 0;
}
}
return weight;
});
}
function getNewHeldItemFromPool(pool: HeldItemPool, pokemon: Pokemon, party?: Pokemon[]): HeldItemId | HeldItemSpecs {
const weights = getPoolWeights(pool, pokemon);
const pickedIndex = pickWeightedIndex(weights);
if (isNullOrUndefined(pickedIndex)) {
return 0;
}
const entry = pool[pickedIndex].entry;
if (typeof entry === "number") {
if (isCategoryId(entry)) {
return getNewHeldItemFromCategory(entry, party ?? pokemon, {}, pokemon) as HeldItemId;
}
return entry as HeldItemId;
}
if (isHeldItemCategoryEntry(entry)) {
return getNewHeldItemFromCategory(entry.id, party ?? pokemon, entry?.customWeights, pokemon) as HeldItemId;
}
return entry as HeldItemSpecs;
}
function assignItemsFromCategory(id: HeldItemCategoryId, pokemon: Pokemon, count: number) {
for (let i = 1; i <= count; i++) {
const newItem = getNewHeldItemFromCategory(id, pokemon, {}, pokemon);
if (newItem) {
pokemon.heldItemManager.add(newItem);
}
}
}
export function assignItemsFromConfiguration(config: HeldItemConfiguration, pokemon: Pokemon) {
config.forEach(item => {
const { entry, count } = item;
const actualCount = typeof count === "function" ? count() : (count ?? 1);
if (typeof entry === "number") {
if (isCategoryId(entry)) {
assignItemsFromCategory(entry, pokemon, actualCount);
}
pokemon.heldItemManager.add(entry as HeldItemId, actualCount);
}
if (isHeldItemSpecs(entry)) {
pokemon.heldItemManager.add(entry);
}
if (isFormChangeItemSpecs(entry)) {
pokemon.heldItemManager.addFormChangeItemWithSpecs(entry);
}
if (isHeldItemCategoryEntry(entry)) {
assignItemsFromCategory(entry.id, pokemon, actualCount);
}
if (isHeldItemPool(entry)) {
for (let i = 1; i <= actualCount; i++) {
const newItem = getNewHeldItemFromPool(entry, pokemon);
if (newItem) {
pokemon.heldItemManager.add(newItem);
}
}
}
});
}
// TODO: Handle form change items
export function saveDataToConfig(saveData: HeldItemSaveData): HeldItemConfiguration {
const config: HeldItemConfiguration = [];
for (const specs of saveData) {
config.push({ entry: specs, count: 1 });
}
return config;
}

139
src/items/held-item.ts Normal file
View File

@ -0,0 +1,139 @@
import { applyAbAttrs } from "#abilities/apply-ab-attrs";
import { globalScene } from "#app/global-scene";
import { type HeldItemId, HeldItemNames } from "#enums/held-item-id";
import type { Pokemon } from "#field/pokemon";
import i18next from "i18next";
export class HeldItem {
// public pokemonId: number;
// TODO: Should this be readonly?
public type: HeldItemId;
public readonly maxStackCount: number;
public isTransferable = true;
public isStealable = true;
public isSuppressable = true;
//TODO: If this is actually never changed by any subclass, perhaps it should not be here
public soundName = "se/restore";
constructor(type: HeldItemId, maxStackCount = 1) {
this.type = type;
this.maxStackCount = maxStackCount;
this.isTransferable = true;
this.isStealable = true;
this.isSuppressable = true;
}
get name(): string {
return i18next.t(`modifierType:ModifierType.${HeldItemNames[this.type]}.name`);
}
get description(): string {
return i18next.t(`modifierType:ModifierType.${HeldItemNames[this.type]}.description`);
}
get iconName(): string {
return `${HeldItemNames[this.type]?.toLowerCase()}`;
}
// TODO: Aren't these fine as just properties to set in the subclass definition?
untransferable(): HeldItem {
this.isTransferable = false;
return this;
}
unstealable(): HeldItem {
this.isStealable = false;
return this;
}
unsuppressable(): HeldItem {
this.isSuppressable = false;
return this;
}
// TODO: https://github.com/pagefaultgames/pokerogue/pull/5656#discussion_r2114950716
getMaxStackCount(): number {
return this.maxStackCount;
}
createSummaryIcon(pokemon?: Pokemon, overrideStackCount?: number): Phaser.GameObjects.Container {
const stackCount = overrideStackCount ?? (pokemon ? this.getStackCount(pokemon) : 0);
const container = globalScene.add.container(0, 0);
const item = globalScene.add.sprite(0, 12, "items").setFrame(this.iconName).setOrigin(0, 0.5);
container.add(item);
const stackText = this.getIconStackText(stackCount);
if (stackText) {
container.add(stackText);
}
container.setScale(0.5);
return container;
}
createPokemonIcon(pokemon: Pokemon): Phaser.GameObjects.Container {
const container = globalScene.add.container(0, 0);
const pokemonIcon = globalScene.addPokemonIcon(pokemon, -2, 10, 0, 0.5, undefined, true);
container.add(pokemonIcon);
container.setName(pokemon.id.toString());
const item = globalScene.add
.sprite(16, 16, "items")
.setScale(0.5)
.setOrigin(0, 0.5)
.setTexture("items", this.iconName);
container.add(item);
const stackText = this.getIconStackText(this.getStackCount(pokemon));
if (stackText) {
container.add(stackText);
}
return container;
}
getIconStackText(stackCount: number): Phaser.GameObjects.BitmapText | null {
if (this.getMaxStackCount() === 1) {
return null;
}
const text = globalScene.add.bitmapText(10, 15, "item-count", stackCount.toString(), 11);
text.letterSpacing = -0.5;
if (stackCount >= this.getMaxStackCount()) {
// TODO: https://github.com/pagefaultgames/pokerogue/pull/5656#discussion_r2114955458
text.setTint(0xf89890);
}
text.setOrigin(0);
return text;
}
getStackCount(pokemon: Pokemon): number {
const stackCount = pokemon.heldItemManager.getStack(this.type);
return stackCount;
}
getScoreMultiplier(): number {
return 1;
}
}
export class ConsumableHeldItem extends HeldItem {
// Sometimes berries are not eaten, some stuff may not proc unburden...
consume(pokemon: Pokemon, isPlayer: boolean, remove = true, unburden = true): void {
if (remove) {
pokemon.heldItemManager.remove(this.type, 1);
// TODO: Turn this into updateItemBar or something
globalScene.updateItems(isPlayer);
}
if (unburden) {
applyAbAttrs("PostItemLostAbAttr", { pokemon: pokemon });
}
}
}

View File

@ -0,0 +1,46 @@
import { HeldItemEffect } from "#enums/held-item-effect";
import type { HeldItemId } from "#enums/held-item-id";
import type { Pokemon } from "#field/pokemon";
import { HeldItem } from "#items/held-item";
import type { NumberHolder } from "#utils/common";
export interface AccuracyBoostParams {
/** The pokemon with the item */
pokemon: Pokemon;
/** Holds the move's accuracy, which may be modified after item application */
moveAccuracy: NumberHolder;
}
/**
* @sealed
*/
export class AccuracyBoosterHeldItem extends HeldItem {
public effects: HeldItemEffect[] = [HeldItemEffect.ACCURACY_BOOSTER];
private accuracyAmount: number;
constructor(type: HeldItemId, maxStackCount: number, accuracy: number) {
super(type, maxStackCount);
this.accuracyAmount = accuracy;
}
/**
* Checks if {@linkcode PokemonMoveAccuracyBoosterHeldItem} should be applied
* @param pokemon - The {@linkcode Pokemon} to apply the move accuracy boost to
* @param moveAccuracy - {@linkcode NumberHolder} holding the move accuracy boost
* @returns `true` if {@linkcode PokemonMoveAccuracyBoosterHeldItem} should be applied
*/
// override shouldApply(pokemon?: Pokemon, moveAccuracy?: NumberHolder): boolean {
// return super.shouldApply(pokemon, moveAccuracy) && !!moveAccuracy;
// }
/**
* Applies {@linkcode PokemonMoveAccuracyBoosterHeldItem}
*/
apply({ pokemon, moveAccuracy }: AccuracyBoostParams): true {
const stackCount = pokemon.heldItemManager.getStack(this.type);
moveAccuracy.value += this.accuracyAmount * stackCount;
return true;
}
}

View File

@ -0,0 +1,76 @@
import { HeldItemEffect } from "#enums/held-item-effect";
import { HeldItemId, HeldItemNames } from "#enums/held-item-id";
import { PokemonType } from "#enums/pokemon-type";
import type { Pokemon } from "#field/pokemon";
import { HeldItem } from "#items/held-item";
import type { NumberHolder } from "#utils/common";
import i18next from "i18next";
export interface AttackTypeBoostParams {
/** The pokemon with the item */
pokemon: Pokemon;
/** The resolved type of the move */
moveType: PokemonType;
/** Holder for the damage value */
// TODO: https://github.com/pagefaultgames/pokerogue/pull/5656#discussion_r2119660807
movePower: NumberHolder;
}
interface AttackTypeToHeldItemMap {
[key: number]: HeldItemId;
}
export const attackTypeToHeldItem: AttackTypeToHeldItemMap = {
[PokemonType.NORMAL]: HeldItemId.SILK_SCARF,
[PokemonType.FIGHTING]: HeldItemId.BLACK_BELT,
[PokemonType.FLYING]: HeldItemId.SHARP_BEAK,
[PokemonType.POISON]: HeldItemId.POISON_BARB,
[PokemonType.GROUND]: HeldItemId.SOFT_SAND,
[PokemonType.ROCK]: HeldItemId.HARD_STONE,
[PokemonType.BUG]: HeldItemId.SILVER_POWDER,
[PokemonType.GHOST]: HeldItemId.SPELL_TAG,
[PokemonType.STEEL]: HeldItemId.METAL_COAT,
[PokemonType.FIRE]: HeldItemId.CHARCOAL,
[PokemonType.WATER]: HeldItemId.MYSTIC_WATER,
[PokemonType.GRASS]: HeldItemId.MIRACLE_SEED,
[PokemonType.ELECTRIC]: HeldItemId.MAGNET,
[PokemonType.PSYCHIC]: HeldItemId.TWISTED_SPOON,
[PokemonType.ICE]: HeldItemId.NEVER_MELT_ICE,
[PokemonType.DRAGON]: HeldItemId.DRAGON_FANG,
[PokemonType.DARK]: HeldItemId.BLACK_GLASSES,
[PokemonType.FAIRY]: HeldItemId.FAIRY_FEATHER,
};
export class AttackTypeBoosterHeldItem extends HeldItem {
public effects: HeldItemEffect[] = [HeldItemEffect.TURN_END_HEAL];
public moveType: PokemonType;
public powerBoost: number;
// This constructor may need a revision
constructor(type: HeldItemId, maxStackCount: number, moveType: PokemonType, powerBoost: number) {
super(type, maxStackCount);
this.moveType = moveType;
this.powerBoost = powerBoost;
}
get name(): string {
return i18next.t(`modifierType:AttackTypeBoosterItem.${HeldItemNames[this.type]?.toLowerCase()}`);
}
get description(): string {
return i18next.t("modifierType:ModifierType.AttackTypeBoosterModifierType.description", {
moveType: i18next.t(`pokemonInfo:Type.${PokemonType[this.moveType]}`),
});
}
get iconName(): string {
return `${HeldItemNames[this.type]?.toLowerCase()}`;
}
apply({ pokemon, moveType, movePower }: AttackTypeBoostParams): void {
const stackCount = pokemon.heldItemManager.getStack(this.type);
if (moveType === this.moveType && movePower.value >= 1) {
movePower.value = Math.floor(movePower.value * (1 + stackCount * this.powerBoost));
}
}
}

View File

@ -0,0 +1,78 @@
import { HeldItemEffect } from "#enums/held-item-effect";
import { HeldItemId } from "#enums/held-item-id";
import { getStatKey, type PermanentStat, Stat } from "#enums/stat";
import type { Pokemon } from "#field/pokemon";
import { HeldItem } from "#items/held-item";
import i18next from "i18next";
export interface BaseStatBoosterParams {
/** The pokemon with the item */
pokemon: Pokemon;
/** The base stats of the {@linkcode pokemon} */
baseStats: number[];
}
type PermanentStatToHeldItemMap = {
[key in PermanentStat]: HeldItemId;
};
export const permanentStatToHeldItem: PermanentStatToHeldItemMap = {
[Stat.HP]: HeldItemId.HP_UP,
[Stat.ATK]: HeldItemId.PROTEIN,
[Stat.DEF]: HeldItemId.IRON,
[Stat.SPATK]: HeldItemId.CALCIUM,
[Stat.SPDEF]: HeldItemId.ZINC,
[Stat.SPD]: HeldItemId.CARBOS,
};
export const statBoostItems: Record<PermanentStat, string> = {
[Stat.HP]: "hp_up",
[Stat.ATK]: "protein",
[Stat.DEF]: "iron",
[Stat.SPATK]: "calcium",
[Stat.SPDEF]: "zinc",
[Stat.SPD]: "carbos",
};
export class BaseStatBoosterHeldItem extends HeldItem {
public effects: HeldItemEffect[] = [HeldItemEffect.BASE_STAT_BOOSTER];
public stat: PermanentStat;
constructor(type: HeldItemId, maxStackCount: number, stat: PermanentStat) {
super(type, maxStackCount);
this.stat = stat;
}
get name(): string {
return i18next.t(`modifierType:BaseStatBoosterItem.${statBoostItems[this.stat]}`);
}
get description(): string {
return i18next.t("modifierType:ModifierType.BaseStatBoosterModifierType.description", {
stat: i18next.t(getStatKey(this.stat)),
});
}
get iconName(): string {
return statBoostItems[this.stat];
}
/**
* Checks if {@linkcode BaseStatModifier} should be applied to the specified {@linkcode Pokemon}.
* @param _pokemon - The {@linkcode Pokemon} to be modified
* @param baseStats - The base stats of the {@linkcode Pokemon}
* @returns `true` if the {@linkcode Pokemon} should be modified
*/
// override shouldApply(_pokemon?: Pokemon, baseStats?: number[]): boolean {
// return super.shouldApply(_pokemon, baseStats) && Array.isArray(baseStats);
// }
/**
* Applies the {@linkcode BaseStatModifier} to the specified {@linkcode Pokemon}.
*/
apply({ pokemon, baseStats }: BaseStatBoosterParams): boolean {
const stackCount = pokemon.heldItemManager.getStack(this.type);
baseStats[this.stat] = Math.floor(baseStats[this.stat] * (1 + stackCount * 0.1));
return true;
}
}

View File

@ -0,0 +1,64 @@
import { HeldItemEffect } from "#enums/held-item-effect";
import { Stat } from "#enums/stat";
import type { Pokemon } from "#field/pokemon";
import { HeldItem } from "#items/held-item";
import i18next from "i18next";
export interface BaseStatFlatParams {
/** The pokemon with the item */
pokemon: Pokemon;
/** The amount of exp to gain */
baseStats: number[];
}
/**
* Currently used by Old Gateau item
*/
export class BaseStatFlatHeldItem extends HeldItem {
public effects: HeldItemEffect[] = [HeldItemEffect.BASE_STAT_FLAT];
public isTransferable = false;
get description(): string {
return i18next.t("modifierType:ModifierType.PokemonBaseStatFlatModifierType.description");
}
/**
* Checks if the {@linkcode PokemonBaseStatFlatModifier} should be applied to the {@linkcode Pokemon}.
* @param pokemon The {@linkcode Pokemon} that holds the item
* @param baseStats The base stats of the {@linkcode Pokemon}
* @returns `true` if the {@linkcode PokemonBaseStatFlatModifier} should be applied
*/
// override shouldApply(pokemon?: Pokemon, baseStats?: number[]): boolean {
// return super.shouldApply(pokemon, baseStats) && Array.isArray(baseStats);
// }
/**
* Applies the {@linkcode PokemonBaseStatFlatModifier}
*/
apply({ pokemon, baseStats }: BaseStatFlatParams): true {
const stats = this.getStats(pokemon);
const statModifier = 20;
// Modifies the passed in baseStats[] array by a flat value, only if the stat is specified in this.stats
baseStats.forEach((v, i) => {
if (stats.includes(i)) {
const newVal = Math.floor(v + statModifier);
baseStats[i] = Math.min(Math.max(newVal, 1), 999999);
}
});
return true;
}
/**
* Get the lowest of HP/Spd, lowest of Atk/SpAtk, and lowest of Def/SpDef
* @returns Array of 3 {@linkcode Stat}s to boost
*/
getStats(pokemon: Pokemon): [HpOrSpeed: Stat, AtkOrSpAtk: Stat, DefOrSpDef: Stat] {
const baseStats = pokemon.getSpeciesForm().baseStats.slice(0);
return [
baseStats[Stat.HP] < baseStats[Stat.SPD] ? Stat.HP : Stat.SPD,
baseStats[Stat.ATK] < baseStats[Stat.SPATK] ? Stat.ATK : Stat.SPATK,
baseStats[Stat.DEF] < baseStats[Stat.SPDEF] ? Stat.DEF : Stat.SPDEF,
];
}
}

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