mirror of
https://github.com/pagefaultgames/pokerogue.git
synced 2025-01-25 02:11:05 +00:00
95242e463c
* feat: Add Google and Discord login functionality feat: Add link to Discord in menu UI feat: Add Discord and Google login functionality Add container around discord and google icons refactor: Update environment variable names for Discord and Google client IDs feat: Add "Or use" translation for login options in multiple languages feat: Update menu UI translations for multiple languages Code review fixes refactor: Update Discord and Google client IDs in environment variables * refactor: Add missing properties to initLoggedInUser function * Update src/locales/de/menu-ui-handler.ts Co-authored-by: Jannik Tappert <38758606+CodeTappert@users.noreply.github.com> * make i18n debugging an optional env setting this also reduces output noise in tests * set development default also to "0" * fix inaccurate docs for TypeImmunityAbAttr (#2957) * chore: Add beta branch to GitHub Actions tests workflow * chore: Add beta branch to GitHub Actions linter and gh-pages workflow * Update src/locales/ko/menu-ui-handler.ts Co-authored-by: lnuvy <lnuvy.code@gmail.com> * Update src/locales/ko/menu.ts Co-authored-by: lnuvy <lnuvy.code@gmail.com> * Localization(pt): translated bgm-name.ts (#2955) * Localization(pt): translated bgm-name.ts * fix * update VITE_I18N_DEBUG in .env files * chore: Update environment variables for beta and production environments * chore: Add beta branch to GitHub Actions deploy workflow * Hardcoded Pokemon should have proper names (#2941) * Refactor challenges and add fresh start (#2963) * [Balance] Update many TM learnsets (#2879) * Update TMs for more Indigo Disk changes * Fix typo, more Stored Power * Refactor challenges and add fresh start (#2964) * Refactor challenges and add fresh start * Add achievement for fresh start challenge * [Bug] Fix off-by-one error in damage calc (#2970) * Fix random damage roll to be 85-100% instead of 85-99% * Update battle.test.ts to reflect the fix * [Bug] Grounded on Terrain fixes (#2969) * [Help] [Move/Bug] Patches Psychic Terrain applicability edge cases Was cancelling moves even if targeted mons weren't on the terrain. * [Bug `]Pokemon.isGrounded` does not exist Replaced with `Pokemon.isGrounded()`, which does. * [Bug] Psychic Terrain priority move cancel ignoring ungrounded * [Bug] Semi-invulnerable should not be grounded * Update game-stats-ui-handler.ts (italian) (#2965) * [Bugfix] Fix a bug during bgm-bar initialization (#2822) * Prevent sizing error * Make reboot not necessary for show BGM * Makes the BGM Bar active by default + It had originally been decided that this would not be active by default because it was to be displayed outside the pause menu, but since its behavior has changed between this decision and its integration, the default deactivation is no longer necessary * [Mirror][Localization] Translate game victory sentences #2254 (#2906) * Translate forgotten sentences on phases * Translate to de forgotten sentences on phases * Translate to es forgotten sentences on phases * Translate to fr forgotten sentences on phases * Translate to it forgotten sentences on phases * Translate to ko forgotten sentences on phases * Translate to pt br forgotten sentences on phases * Translate to zh cn forgotten sentences on phases * Translate to zh tw forgotten sentences on phases * remove duplicate message * remove duplicate message * Update src/locales/pt_BR/battle.ts Co-authored-by: José Ricardo Fleury Oliveira <josefleury@discente.ufg.br> * Update src/locales/de/battle.ts Co-authored-by: Jannik Tappert <38758606+CodeTappert@users.noreply.github.com> * Update src/locales/de/battle.ts Co-authored-by: Jannik Tappert <38758606+CodeTappert@users.noreply.github.com> * Update battle.ts [Localization(it)] * Update src/locales/zh_CN/battle.ts Co-authored-by: Yonmaru40 <47717431+40chyan@users.noreply.github.com> --------- Co-authored-by: Alexis <alexis.faizeau@animedigitalnetwork.fr> Co-authored-by: 송지원 <jiwsong@gmarket.com> Co-authored-by: José Ricardo Fleury Oliveira <josefleury@discente.ufg.br> Co-authored-by: Jannik Tappert <38758606+CodeTappert@users.noreply.github.com> Co-authored-by: Niccolò <123510358+NicusPulcis@users.noreply.github.com> Co-authored-by: Yonmaru40 <47717431+40chyan@users.noreply.github.com> * [Mirror][Localization] Translate summary #2336 (#2907) * Translate pokemon summary * Translate pokemon summary to fr * Translate pokemon summary to de * Translate pokemon summary to es * Translate pokemon summary to it * Translate pokemon summary to ko * Translate pokemon summary to pt br * Translate pokemon summary to zh cn * Translate pokemon summary to zh tw * Fix import * Update partially for en and ko * Update interface name for en * Merge trainerLabel&Text and calculate typeLabel width * Update src/locales/es/pokemon-summary.ts Co-authored-by: GoldTra <162721984+GoldTra@users.noreply.github.com> * Apply translations key to all languages with interface TranslationEtries * Update ko/translationKey of status * Update ko/translationKey of pokemonInfo * Update de/translationKeys of memostring and metFragment * Update de/translationKeys of memostring and metFragment 2 * Update src/locales/ko/pokemon-summary.ts Co-authored-by: Enoch <enoch.jwsong@gmail.com> * Update src/locales/ko/pokemon-summary.ts Co-authored-by: returntoice <dieandbecome@gmail.com> * Update src/locales/pt_BR/pokemon-summary.ts Co-authored-by: José Ricardo Fleury Oliveira <josefleury@discente.ufg.br> * Update src/locales/ko/pokemon-summary.ts * Update src/locales/pt_BR/pokemon-summary.ts Co-authored-by: José Ricardo Fleury Oliveira <josefleury@discente.ufg.br> * Update pokemon-summary.ts [Localization(it)] * remove unused code * Update src/locales/zh_CN/pokemon-summary.ts Co-authored-by: Yonmaru40 <47717431+40chyan@users.noreply.github.com> * Update src/locales/zh_CN/pokemon-summary.ts Co-authored-by: Yonmaru40 <47717431+40chyan@users.noreply.github.com> * Update src/locales/zh_CN/pokemon-summary.ts Co-authored-by: Yonmaru40 <47717431+40chyan@users.noreply.github.com> * Update src/locales/es/pokemon-summary.ts Co-authored-by: Arxxer <javiptn7@gmail.com> * Update src/locales/es/pokemon-summary.ts Co-authored-by: Arxxer <javiptn7@gmail.com> --------- Co-authored-by: Alexis <alexis.faizeau@animedigitalnetwork.fr> Co-authored-by: Mr.WaterT <water.t.works@gmail.com> Co-authored-by: Alexis Faizeau <faizeau.alexis@gmail.com> Co-authored-by: GoldTra <162721984+GoldTra@users.noreply.github.com> Co-authored-by: returntoice <dieandbecome@gmail.com> Co-authored-by: José Ricardo Fleury Oliveira <josefleury@discente.ufg.br> Co-authored-by: 송지원 <jiwsong@gmarket.com> Co-authored-by: Niccolò <123510358+NicusPulcis@users.noreply.github.com> Co-authored-by: Yonmaru40 <47717431+40chyan@users.noreply.github.com> Co-authored-by: Arxxer <javiptn7@gmail.com> * [Localization] Localization arena flyout (Active Battle Effects) (#2932) * localizing Active Battle Effects (working) * Localize Active Battle Effects * Change return value * Modify arena terrain desc * Update src/locales/zh_CN/arena-flyout.ts Co-authored-by: Yonmaru40 <47717431+40chyan@users.noreply.github.com> * Update src/locales/de/arena-flyout.ts Co-authored-by: Jannik Tappert <38758606+CodeTappert@users.noreply.github.com> * Update src/locales/fr/arena-flyout.ts Co-authored-by: Lugiad' <adrien.grivel@hotmail.fr> * Update arena-flyout.ts (IT) * Update src/locales/pt_BR/arena-flyout.ts Co-authored-by: José Ricardo Fleury Oliveira <josefleury@discente.ufg.br> * Update src/locales/es/arena-flyout.ts Co-authored-by: Arxxer <javiptn7@gmail.com> * Change util function name --------- Co-authored-by: 송지원 <jiwsong@gmarket.com> Co-authored-by: Yonmaru40 <47717431+40chyan@users.noreply.github.com> Co-authored-by: Jannik Tappert <38758606+CodeTappert@users.noreply.github.com> Co-authored-by: Lugiad' <adrien.grivel@hotmail.fr> Co-authored-by: Niccolò <123510358+NicusPulcis@users.noreply.github.com> Co-authored-by: José Ricardo Fleury Oliveira <josefleury@discente.ufg.br> Co-authored-by: Arxxer <javiptn7@gmail.com> * This should prevent gym leaders appear as doubles (marnie & piers) when they arent fixed battles (#2904) * The stat messages can now be plural (#2600) * The stat messages are now plural * "And" can now be localized * Revert Override * Update src/locales/fr/battle.ts Co-authored-by: Lugiad' <adrien.grivel@hotmail.fr> * Update src/locales/fr/battle.ts Co-authored-by: Lugiad' <adrien.grivel@hotmail.fr> * Update src/locales/fr/battle.ts Co-authored-by: Lugiad' <adrien.grivel@hotmail.fr> * Update src/locales/zh_CN/battle.ts Co-authored-by: Yonmaru40 <47717431+40chyan@users.noreply.github.com> * Update src/locales/ko/battle.ts Co-authored-by: Enoch <enoch.jwsong@gmail.com> * Update src/locales/ko/battle.ts Co-authored-by: Enoch <enoch.jwsong@gmail.com> * Update src/locales/es/battle.ts Co-authored-by: GoldTra <162721984+GoldTra@users.noreply.github.com> * Update src/locales/es/battle.ts Co-authored-by: GoldTra <162721984+GoldTra@users.noreply.github.com> * Update src/locales/de/battle.ts * Update src/locales/pt_BR/battle.ts Co-authored-by: José Ricardo Fleury Oliveira <josefleury@discente.ufg.br> * Update src/locales/pt_BR/battle.ts Co-authored-by: José Ricardo Fleury Oliveira <josefleury@discente.ufg.br> * Update src/data/battle-stat.ts * Update src/locales/zh_CN/battle.ts Co-authored-by: Yonmaru40 <47717431+40chyan@users.noreply.github.com> * Changed the way the multiple is handeled to use the i18n way * Missed one file * Apply suggestions from code review * Apply suggestions from code review * Changed the tests so they work now with the i18n hting * Fixed some other tests (chinese still makes problems... * Fix tests for chinese * Tests * Update src/test/battle-stat.spec.ts * Update src/locales/zh_CN/battle.ts Co-authored-by: Yonmaru40 <47717431+40chyan@users.noreply.github.com> * Apply suggestions from code review --------- Co-authored-by: Lugiad' <adrien.grivel@hotmail.fr> Co-authored-by: Yonmaru40 <47717431+40chyan@users.noreply.github.com> Co-authored-by: Enoch <enoch.jwsong@gmail.com> Co-authored-by: GoldTra <162721984+GoldTra@users.noreply.github.com> Co-authored-by: José Ricardo Fleury Oliveira <josefleury@discente.ufg.br> * [Enhancement][QoL] Add option to adjust shop overlay opacity (#2622) * add option to adjust shop overlay opacity * add localization * fix bug * Update src/locales/fr/settings.ts Co-authored-by: Lugiad' <adrien.grivel@hotmail.fr> * Update src/locales/zh_CN/settings.ts Co-authored-by: Yonmaru40 <47717431+40chyan@users.noreply.github.com> * Update src/locales/fr/settings.ts Co-authored-by: Lugiad' <adrien.grivel@hotmail.fr> * update default value * update setting values * re-add value 10 * Update src/locales/pt_BR/settings.ts Co-authored-by: José Ricardo Fleury Oliveira <josefleury@discente.ufg.br> * Update src/locales/ko/settings.ts Co-authored-by: Enoch <enoch.jwsong@gmail.com> * Update src/locales/es/settings.ts Co-authored-by: InnocentGameDev <asdargmng@gmail.com> * Update settings.ts --------- Co-authored-by: Lugiad' <adrien.grivel@hotmail.fr> Co-authored-by: Yonmaru40 <47717431+40chyan@users.noreply.github.com> Co-authored-by: José Ricardo Fleury Oliveira <josefleury@discente.ufg.br> Co-authored-by: Enoch <enoch.jwsong@gmail.com> Co-authored-by: InnocentGameDev <asdargmng@gmail.com> Co-authored-by: Niccolò <123510358+NicusPulcis@users.noreply.github.com> * [Bug] Fix battler tags lapsing at incorrect times (#2944) * Fix battler tags lapsing at incorrect times * Document FlinchedTag * Update French pokemon-summary.ts (#2976) * [Test] Prevent tests from running if overrides are different from the default values (#2110) * mock default overrides in test setup * change beforeEach to beforeALl * move some more enums into the enums directory * replace modules that import i18n into overrides with modules that don't * add pre tests and update vitest configs, scripts * replace tabs with spaces * fix vitest server port overlap warning * add missing overrides and clean up workspace config * change test name * include spec files in main test suite * [QoL] Highlight targets of multitarget moves instead of immediate execution (#2863) * show targets for move targeting multiple pokemon * dont allow selecting target if multiple * fix targeting * cleanup * more cleanup * only highlight targets is move is not status * fix tests failing * fix tests * change "immediately" to "auto" * nevermind just remove auto * remove status move condition * [Refactor] rewrite applyAbAttrsInternal to use an iterator. (#1832) * initial rewrite of applyAbAttrsInternal * clean up applyAbAttrsInternal * remove the await because it wraps non Promises in a promise * add TODO comment about promises * fix broken costar test, hopefully * Update typescript and typedoc (#2988) * update typescript and typedoc to latest versions * forgot to add the package-lock * add fixes for breaking type gen * update workflow (#2989) * [Qol/Balance] Dynamax cannon tweak (#2540) * Dynamax Cannon fix * Update src/locales/fr/move.ts Added fr translation. Co-authored-by: Lugiad' <adrien.grivel@hotmail.fr> * Update src/locales/fr/move.ts Adding an important missing precision in French description Co-authored-by: Lugiad' <adrien.grivel@hotmail.fr> * Update src/locales/de/move.ts German locale commit. Co-authored-by: Jannik Tappert <38758606+CodeTappert@users.noreply.github.com> * Update src/locales/zh_CN/move.ts Chinese locale commit, checked by a native speaker Co-authored-by: Lugiad' <adrien.grivel@hotmail.fr> * Update src/locales/ko/move.ts Korean locale added Co-authored-by: returntoice <dieandbecome@gmail.com> * fixed trailing space in Chinese locale * added es locale * Different Dynamax Cannon fix. This one is the one * Dynamax Cannon fix localisations * Update src/locales/fr/move.ts Added fr locale Co-authored-by: Lugiad' <adrien.grivel@hotmail.fr> * Update src/locales/de/move.ts German translation ^^ Co-authored-by: Jannik Tappert <38758606+CodeTappert@users.noreply.github.com> * Update src/locales/pt_BR/move.ts pt-BR translation Co-authored-by: José Ricardo Fleury Oliveira <josefleury@discente.ufg.br> * Update src/locales/de/move.ts Eslint fix, good catch @Enoch Co-authored-by: Enoch <enoch.jwsong@gmail.com> * Update src/locales/ko/move.ts Korean locale ^^ Co-authored-by: sodam <66295123+sodaMelon@users.noreply.github.com> * Update src/locales/zh_CN/move.ts zh_CN locale Co-authored-by: RimKnight <rimknight852@gmail.com> * Update src/locales/zh_TW/move.ts zh_TW locale Co-authored-by: RimKnight <rimknight852@gmail.com> * Update move.ts linting mistake due to how my comment was written * Update move.ts linting mistake due to how my comment was written * Update move.ts [Localization(it)] * WIP test * WIP test part 2 * [Test] Add Unit Tests for Dynamax Cannon * removed some unnecessary cases in the test to reduce testing overhead * Update src/locales/ko/move.ts Updated kr locale Co-authored-by: Enoch <enoch.jwsong@gmail.com> * [Test] Adjust Unit Tests for Dynamax Cannon --------- Co-authored-by: Lugiad' <adrien.grivel@hotmail.fr> Co-authored-by: Jannik Tappert <38758606+CodeTappert@users.noreply.github.com> Co-authored-by: returntoice <dieandbecome@gmail.com> Co-authored-by: José Ricardo Fleury Oliveira <josefleury@discente.ufg.br> Co-authored-by: Enoch <enoch.jwsong@gmail.com> Co-authored-by: sodam <66295123+sodaMelon@users.noreply.github.com> Co-authored-by: RimKnight <rimknight852@gmail.com> Co-authored-by: Niccolò <123510358+NicusPulcis@users.noreply.github.com> Co-authored-by: xsn34kzx <xsn34kzx@gmail.com> * [Test] Fix/Extend Unit Test for Hard Press (#2992) * [Test] Update tests to enable no-crits override (#2971) * Update tests to enable no-crits override * Rename variable maxHP to initialHP * [Localization]Localized move-trigger text (#2913) * [Localization]Localized move-trigger text * [Localization]Localized zh-cn move-trigger text * [Localization]fix typo * [Localization]fix typo * Update src/locales/pt_BR/move-trigger.ts Co-authored-by: José Ricardo Fleury Oliveira <josefleury@discente.ufg.br> * [Localization]add pokemonName to move-trigger.ts * [Localization]add pokemonName to move-trigger.ts * Update zh_TW move-trigger.ts * Update zh_CN move-trigger.ts * Update move.ts * Update src/locales/ko/move-trigger.ts Co-authored-by: Enoch <enoch.jwsong@gmail.com> * Update src/locales/ko/move-trigger.ts Co-authored-by: Enoch <enoch.jwsong@gmail.com> * Update move-trigger.ts * Update src/locales/de/move-trigger.ts Co-authored-by: Jannik Tappert <38758606+CodeTappert@users.noreply.github.com> * [Localization]add affix to target pokemon names * Update src/locales/fr/move-trigger.ts Co-authored-by: Lugiad' <adrien.grivel@hotmail.fr> * localized type --------- Co-authored-by: José Ricardo Fleury Oliveira <josefleury@discente.ufg.br> Co-authored-by: Enoch <enoch.jwsong@gmail.com> Co-authored-by: Jannik Tappert <38758606+CodeTappert@users.noreply.github.com> Co-authored-by: Lugiad' <adrien.grivel@hotmail.fr> * [Bug] Fix description of rare candy to have proper amount of level (#2903) * [Bug] Fix description of rare candy to have proper amount of level * Update en locales * Add locales * Add locales * Update src/locales/de/modifier-type.ts Co-authored-by: Jannik Tappert <38758606+CodeTappert@users.noreply.github.com> * Update src/locales/de/modifier-type.ts Co-authored-by: Jannik Tappert <38758606+CodeTappert@users.noreply.github.com> * Update src/locales/fr/modifier-type.ts Co-authored-by: Lugiad' <adrien.grivel@hotmail.fr> * Update src/locales/fr/modifier-type.ts Co-authored-by: Lugiad' <adrien.grivel@hotmail.fr> * Update src/locales/ko/modifier-type.ts Co-authored-by: Enoch <enoch.jwsong@gmail.com> * Update src/locales/ko/modifier-type.ts Co-authored-by: Enoch <enoch.jwsong@gmail.com> * Update src/locales/zh_CN/modifier-type.ts Co-authored-by: Yonmaru40 <47717431+40chyan@users.noreply.github.com> * Update src/locales/zh_CN/modifier-type.ts Co-authored-by: Yonmaru40 <47717431+40chyan@users.noreply.github.com> * Update src/locales/pt_BR/modifier-type.ts Co-authored-by: José Ricardo Fleury Oliveira <josefleury@discente.ufg.br> * Update src/locales/pt_BR/modifier-type.ts Co-authored-by: José Ricardo Fleury Oliveira <josefleury@discente.ufg.br> * Update modifier-type.ts (IT) * Update src/locales/es/modifier-type.ts Co-authored-by: Arxxer <javiptn7@gmail.com> * Update src/locales/es/modifier-type.ts Co-authored-by: Arxxer <javiptn7@gmail.com> --------- Co-authored-by: Jannik Tappert <38758606+CodeTappert@users.noreply.github.com> Co-authored-by: Lugiad' <adrien.grivel@hotmail.fr> Co-authored-by: Enoch <enoch.jwsong@gmail.com> Co-authored-by: Yonmaru40 <47717431+40chyan@users.noreply.github.com> Co-authored-by: José Ricardo Fleury Oliveira <josefleury@discente.ufg.br> Co-authored-by: Niccolò <123510358+NicusPulcis@users.noreply.github.com> Co-authored-by: Arxxer <javiptn7@gmail.com> * move status effect enum into separate file (#2998) * add .env.test (#2997) block i18n debug output by default * [QoL] Create default overrides class and export that with custom overrides (#2999) * Create default overrides class and export that with custom overrides * add comment to mock import and replace typecast with type narrowing * change modifier override type to pick keys from modifierTypes * [Feature][FUN] add breedersInSpace splash message (#2631) * add spaceBreeder splash message * fix typo (whops) * Add pt_BR translation Co-authored-by: José Ricardo Fleury Oliveira <josefleury@discente.ufg.br> * replace text & key with breedersInSpace * add french tanslation Co-authored-by: Lugiad' <adrien.grivel@hotmail.fr> * remove obsolete array split in splash-messages.ts * Update src/locales/pt_BR/splash-messages.ts Co-authored-by: José Ricardo Fleury Oliveira <josefleury@discente.ufg.br> * Update src/locales/es/splash-messages.ts Co-authored-by: Asdar <asdargmng@gmail.com> * Update splash-messages.ts (Italian) * Update src/locales/zh_CN/splash-messages.ts Co-authored-by: Yonmaru40 <47717431+40chyan@users.noreply.github.com> * Update src/locales/ko/splash-messages.ts Co-authored-by: sodam <66295123+sodaMelon@users.noreply.github.com> --------- Co-authored-by: José Ricardo Fleury Oliveira <josefleury@discente.ufg.br> Co-authored-by: Lugiad' <adrien.grivel@hotmail.fr> Co-authored-by: Asdar <asdargmng@gmail.com> Co-authored-by: Niccolò <123510358+NicusPulcis@users.noreply.github.com> Co-authored-by: Yonmaru40 <47717431+40chyan@users.noreply.github.com> Co-authored-by: sodam <66295123+sodaMelon@users.noreply.github.com> * [Enhancement] Pokemon generation number tooltip (#2642) * Create tooltip to show Pokemon generation * Add option to toggle generation tooltip * Use roman numeral for generation tooltip * Revert "Add option to toggle generation tooltip" This reverts commit 414b2366fc55c5642f8858d6f4854aa62c4caf60. * Update src/locales/de/battle-info.ts Co-authored-by: Jannik Tappert <38758606+CodeTappert@users.noreply.github.com> * Update src/locales/fr/battle-info.ts Co-authored-by: Lugiad' <adrien.grivel@hotmail.fr> * Update src/locales/pt_BR/battle-info.ts Co-authored-by: José Ricardo Fleury Oliveira <josefleury@discente.ufg.br> * Update src/locales/zh_TW/battle-info.ts Co-authored-by: RimKnight <rimknight852@gmail.com> * Update src/locales/zh_CN/battle-info.ts Co-authored-by: RimKnight <rimknight852@gmail.com> * Update src/locales/ko/battle-info.ts Co-authored-by: Enoch <enoch.jwsong@gmail.com> * Update battle-info.ts (IT) --------- Co-authored-by: Jannik Tappert <38758606+CodeTappert@users.noreply.github.com> Co-authored-by: Lugiad' <adrien.grivel@hotmail.fr> Co-authored-by: José Ricardo Fleury Oliveira <josefleury@discente.ufg.br> Co-authored-by: RimKnight <rimknight852@gmail.com> Co-authored-by: Enoch <enoch.jwsong@gmail.com> Co-authored-by: Niccolò <123510358+NicusPulcis@users.noreply.github.com> * [Beta] Updating the manage data->unlock all option to work with all abilities, natures, forms and passives (#2967) * Updated code to allow user to unlock all pokemon from the manage data menu option * Added code to Utils to allow it to check for a beta env, and hid the unlock all code behind that. This should stop it from being accessed in prod envs * Updated another section to be locked behind beta check, and also updated the everything.prsv to have everything unlocked going forward * Fixed some code reviews * [Bug] vite port (for development) (#3003) * make vite-port configurable and make it default 8000 * add retries for `does not trigger by non damage moves` test * feat: Update isBeta check in utils.ts to use import.meta.env.MODE The current implementation of the isBeta check in utils.ts is using import.meta.env.DEV, which gives the same value for both beta and dev environments. This commit updates the check to use import.meta.env.MODE === "beta" to accurately determine if the environment is beta. This ensures that the unlock all code is only accessible in the beta environment and not in production environments. * refactor: Update trainerId and secretId in game data This commit updates the `trainerId` and `secretId` properties in the `GameData` class. The values are replaced with the corresponding values from the `this.trainerId` and `this.secretId` variables. This change ensures that the `trainerId` and `secretId` are correctly updated in the game data. * Adds bg glow behind starters with unlocked passives (#2497) * [Localization] Localize missed things in battlers-tag.ts (#3004) * Localize missed things in battlers-tag * Change orders of configs * Add missed phase * Update src/locales/zh_CN/battle.ts Co-authored-by: RimKnight <rimknight852@gmail.com> * Update src/locales/zh_CN/battle.ts Co-authored-by: RimKnight <rimknight852@gmail.com> * Update src/locales/zh_TW/battle.ts Co-authored-by: RimKnight <rimknight852@gmail.com> * Update src/locales/zh_TW/battle.ts Co-authored-by: RimKnight <rimknight852@gmail.com> * Update src/locales/zh_TW/battler-tags.ts Co-authored-by: RimKnight <rimknight852@gmail.com> * Update src/locales/zh_CN/battler-tags.ts Co-authored-by: RimKnight <rimknight852@gmail.com> --------- Co-authored-by: RimKnight <rimknight852@gmail.com> * [Feature] Fully implement Octolock (#2985) * implement octolock * Add tests * Add localization of arena tag (TODO : config add) * [Bug] Fix import of overrides in a test (#3009) * [BUG] Fixes Sketch copying the first move used by the opponent instead of the last (#2759) * Changes getMoveHistory to getLastXMoves to fix sketch copying first move used instead of last * Optimizes move search and early return * Reverts check for virtual moves * [Bug] Making FormChangeItems Untransferrable (#2695) * Made FormChangeItems untransferrable. Replaced getTransferrable() * Made isTransferrable readonly. Removed unnecessary 'm as PokemonHeldItemModifier'. * [Bug] Fix Clear terrains upon Trainer Battle (#2027) * Clear terrains upon Trainer Battle * Adjusted comment * Fix item reward overrides going out of bounds (#3012) * [Localization] Localize Berry Heal message (#2996) * Localize berry heal message (HP/PP) * Change location of translation * conflict resolve * Update src/locales/de/battle.ts Co-authored-by: Jannik Tappert <38758606+CodeTappert@users.noreply.github.com> Update src/locales/de/battle.ts Co-authored-by: Jannik Tappert <38758606+CodeTappert@users.noreply.github.com> Update src/locales/de/battle.ts Co-authored-by: Jannik Tappert <38758606+CodeTappert@users.noreply.github.com> * Update src/locales/zh_CN/battle.ts Co-authored-by: mercurius-00 <80205689+mercurius-00@users.noreply.github.com> * Update src/locales/zh_TW/battle.ts Co-authored-by: mercurius-00 <80205689+mercurius-00@users.noreply.github.com> * Update src/locales/fr/battle.ts Co-authored-by: Lugiad' <adrien.grivel@hotmail.fr> * Update src/locales/pt_BR/battle.ts Co-authored-by: José Ricardo Fleury Oliveira <josefleury@discente.ufg.br> * Update src/locales/pt_BR/battle.ts Co-authored-by: José Ricardo Fleury Oliveira <josefleury@discente.ufg.br> * Update battle.ts (Berries ITALIAN) --------- Co-authored-by: 송지원 <jiwsong@gmarket.com> Co-authored-by: Jannik Tappert <38758606+CodeTappert@users.noreply.github.com> Co-authored-by: mercurius-00 <80205689+mercurius-00@users.noreply.github.com> Co-authored-by: Lugiad' <adrien.grivel@hotmail.fr> Co-authored-by: José Ricardo Fleury Oliveira <josefleury@discente.ufg.br> Co-authored-by: Niccolò <123510358+NicusPulcis@users.noreply.github.com> * Adds a check to the bgm-bar display to prevent it from being displayed in case of an empty value (#3007) * Update src/locales/fr/arena-tag.ts Co-authored-by: Lugiad' <adrien.grivel@hotmail.fr> * Update src/locales/de/arena-tag.ts Co-authored-by: Jannik Tappert <38758606+CodeTappert@users.noreply.github.com> * [Sprite] Improves Corviknight Palette usage (#3020) * [Sprite] Compress Corviknight exp * [Sprite] Compress Corviknight exp * [Sprite] Compress Corviknight exp * [Sprite] Compress Corviknight exp * [Sprite] Compress Corviknight exp * [Beta][Localization] Update French battler-tags.ts and battle.ts (#3015) * Update French battler-tags.ts * Update battle.ts * [Bug] Fix Lock-On and Mind Reader not working on the first turn (#3001) * [Bug] Struggle Recoil should ignore Magic Guard (#3017) * Drafted testable conditions for Magic Guard * Weather Test * Update src/test/abilities/magic_guard.test.ts InnerThunder Co-authored-by: innerthunder <168692175+innerthunder@users.noreply.github.com> * Implemented checks for poison/toxic/burn * Added tests for recoil moves and volatile status * Updated Rock Head, Magic Guard, and Reckless interactions with Struggle * Removed stray file * Fixed Typedoc errors * Implemented innerthunder's feedback --------- Co-authored-by: Frutescens <info@laptop> Co-authored-by: innerthunder <168692175+innerthunder@users.noreply.github.com> * [Enhancement] Decouple move power calculation from Pokemon.apply(), Fixes Power Spot & Battery not boosting ally's move (#2984) * refactor power calc, fix battery & power spot * fix hard press unit test * fix hard press * refactor tests * use sypOn hp instead * rename method * cleanup tests * improve tests * use slow vs fast pokemon * fix steely spirit test * fix steely spirit for real this time * remove unnecessary test * address pr feedback * add removed code * [QoL] Offset the status indicator to keep pokeball in view (#2966) * Offset the status indicator to keep pokeball in view * Only use offsetX when the Pokemon is the enemy * Adjust position to accomodate boss health bar * [Bug] Fix to epic shiny Yungoos icon (#2991) (#3016) Fixed a floating pixel in epic shiny Yungoos' icon. Caused by Mimikyu's epic shiny sprite being placed too high on the sprite sheet. Moved Mimikyu's sprite down and updated json to reflect the change. * [Localization(ko)] Change line-break position of ghost type curse add message (#3022) * Update src/locales/de/arena-tag.ts Co-authored-by: Jannik Tappert <38758606+CodeTappert@users.noreply.github.com> * Revert "[Localization]Localized move-trigger text (#2913)" (#3026) This reverts commit 39bdfea0c8b33a759db760809d6488d825b510e2. * [Localization(ko)] Fixed typo in Bertha's dialogue (#3025) * [Bug] Prevent fixed-damage and OHKO moves from being modified by damage-reducing abilities (#2703) * ReceivedMoveDamageMultiplierAbAttr patch: WIP refactored damage calculation, reordered ReceivedMoveDamageMultiplierAbAttr to avoid issues with fixed damage and OHKO moves, stubbed unit tests for dragon rage (fixed damage) and fissure (OHKO) * ReceivedMoveDamageMultiplierAbAttr patch: commented concerns regarding EnemyDamageBooster/ReducerModifier for others' reference in WIP branch * ReceivedMoveDamageMultiplierAbAttr patch: reordered ReceivedMoveDamageMultiplierAbAttr and EnemyDamageBooster/ReducerModifier to not trigger for fixed damage and OHKO moves, completed relevant tests for dragon rage and fissure * ReceivedMoveDamageMultiplierAbAttr patch: removed newline * ReceivedMoveDamageMultiplierAbAttr patch: in the unit test, extracted hard-coded Dragon Rage damage to a variable * ReceivedMoveDamageMultiplierAbAttr patch: naming consistency * ReceivedMoveDamageMultiplierAbAttr patch: replaced awaiting DamagePhase with TurnEndPhase as the former assumes damage will be done * ReceivedMoveDamageMultiplierAbAttr patch: removed redundant overrides in Fissure tests * ReceivedMoveDamageMultiplierAbAttr patch: tests: refactored crit removal, removed berries, fixed bug associated with Porygon sometimes getting Trace and copying the opponent's ability, which would override the manual ability override * Fixed unit tests * Added a comment and cleaned up an existing one * Update src/locales/zh_CN/arena-tag.ts Co-authored-by: Yonmaru40 <47717431+40chyan@users.noreply.github.com> * [Localization] Brought german localization up to date (#3010) * Fixed errors in the german localization * Fresh Start Challenge * Update src/locales/de/move-trigger.ts Co-authored-by: flx-sta <50131232+flx-sta@users.noreply.github.com> * Apply suggestions from code review --------- Co-authored-by: flx-sta <50131232+flx-sta@users.noreply.github.com> * [Bug] Remove redundant damage number popups (#3024) * Remove magic number from Belly Drum's attr * Remove redundant damage number popup * Fix merge issue and remove another duplicate damage number instance * [Move] Implements Conversion 2 (#2943) * Creates function to get type resistances, implements conversion 2 * Removes unimplemented tag, adds condition for move history to exist * Cleans up type selection, creates i18n entries for typeChanged * Uses typeChanged i18n in Conversion move * More detailed docs, early return with stellar/unknown type * Adds note that it wont track type-changing moves properly * Rephrases doc description, adds partial since it can't track type-changing moves * Updates localization, removes typeChanged entry to use move-trigger entry * Missed locale de entry in last commit * Adds comment for reason of .partial() * Fixes localization error due to revert, removes improper merge conflict from prior commit * [BUG] Fixes bug with Metronome freezing the game (#2819) * Tests MovePhase with new PokemonMove instead of moveset search * Accounts for metronome call on charging moves * Update comment in ChargeAttr to be clearer * Add missing passive background graphic for legacy UI Relating to #2497 , this adds an identical graphic to the legacy UI files, as it will show up as a missing texture otherwise. * [Localization] Localize ability messages in ability.ts (trigger, apply...) (#2972) * localize ability messages * Update src/locales/de/ability-trigger.ts Co-authored-by: Jannik Tappert <38758606+CodeTappert@users.noreply.github.com> * Modify parameter name, fix eslint * Korean Translation, modify some wrong param * Add missed message * Update src/locales/de/ability-trigger.ts Co-authored-by: Jannik Tappert <38758606+CodeTappert@users.noreply.github.com> * Update ability-trigger.ts (Partial) (Italian) * Localize type name * Localize type name > Libero, Protean * param bug fix * Update src/data/ability.ts * Update zh-cn * Update ability-trigger.ts (Partial part 2, still not completed) (Italian) * Update src/locales/fr/ability-trigger.ts Co-authored-by: Lugiad' <adrien.grivel@hotmail.fr> * Update src/locales/pt_BR/ability-trigger.ts Co-authored-by: José Ricardo Fleury Oliveira <josefleury@discente.ufg.br> * Update src/locales/fr/ability-trigger.ts Co-authored-by: Lugiad' <adrien.grivel@hotmail.fr> --------- Co-authored-by: 송지원 <jiwsong@gmarket.com> Co-authored-by: Jannik Tappert <38758606+CodeTappert@users.noreply.github.com> Co-authored-by: Niccolò <123510358+NicusPulcis@users.noreply.github.com> Co-authored-by: Lugiad' <adrien.grivel@hotmail.fr> Co-authored-by: José Ricardo Fleury Oliveira <josefleury@discente.ufg.br> * Update egg.ts (italian) (#3038) * [Sprite] Revert Larvesta, fixing stray pixels (#3042) * [Sprite] Remove normal Larvesta stray pixels Taken from original commit * [Sprite] Revert shiny Larvesta stray pixels Taken from original commit * [Sprite] Match variant Larvesta colours * [Bug] Fix level 100 moves being skipped if leveled past 100 (#3040) * [Bug][Fix][Surf][Muddy Water][Sludge Wave][Animation] Added image to move animations. (#3044) * Update muddy-water.json * Update sludge-wave.json * Update surf.json * [Sprite] Fix Cofagrigus sprite (#3045) * Fix Cofagrigus sprite * Base sprite fix * Created json variant file... manually * Fix FR typo (#3051) * [Beta][Localization] Update Portuguese battler-tags.ts and battle.ts (#3050) * [Music/BGM] Fix bgm file & loop for battle_rival (#3053) * stop loading pride-update banner (#3057) * [Item] Add Scope Lens and Leek (#2666) * [Item] Add Scope Lens and Leek * Add Entry to pt_BR * Localize for pt_BR Co-authored-by: José Ricardo Fleury Oliveira <josefleury@discente.ufg.br> * Fix & Clean Unit Tests --------- Co-authored-by: José Ricardo Fleury Oliveira <josefleury@discente.ufg.br> * [Music/BGM] New music added for the "Slum" biome, composed by Andr06 (#3000) * Replacement of the BGM in the slum biome (by Andr06) * Modification of BGM credits in README.md * Replacement of the BGM name by the title chosen by its composer in all languages * Update BGM loop point for biome 'SLUM' * [Bug] implemented multi target damage penalty (#2329) * fix: damage multiplier by the number of targets * fix: used actual number value rather than the number holder * test: added unit test for counting targets * multi-target: fixed names of the unit tests. * test: simple-test changes * test: changed multi-target test code * test: testing damage decreasement * test: multi-target test fix * resolved conflicts in test --------- Co-authored-by: Tempoanon <163687446+Tempo-anon@users.noreply.github.com> * [BUG] Uturn faint switch bug fix (#2980) * reordering adding switch phases for u-turn * reverting some temp changes * generalizing function * adding PR suggestions * nit indenting * [Sprite] Remove extra Oinkologne sprites (#3067) * Delete public/images/pokemon/female/916.png * Delete public/images/pokemon/exp/female/916.json * Delete public/images/pokemon/exp/female/916.png * Delete public/images/pokemon/female/916.json * [Sprite] Mega Mewtwo sprite fixes (#3065) * [Sprite] Add static epic Mega Mewtwo Y Colours taken from exp, as consistent/exp rare backs share palettes. * [Sprite] Fix transparent pixels on Mega Mewtwo X * [Sprite] Fix transparent pixels on Mega Mewtwo X * [Sprite] Add static epic Mega Mewtwo Y Now exists, and is a paletteswap * [Music/BGM] New music added for the "Sea" biome, composed by Andr06 (#3063) * Replacement of the BGM in the sea biome (by Andr06) * Modification of BGM credits in README.md * Replacement of the BGM name by the title chosen by its composer in all languages * Update BGM loop point for biome 'SEA' * [Balance] Add 12 new TMs and remove 3 (#2733) * Added Counter TM * Counter, Aqua Tail, Gastro Acid, Pluck, Secret Power, Aurora Veil, Incinerate * Placed TMs in proper order, added to pool * Add Secret Power to the pool * Add TM for Synthesis * Covet, Heal Bell, PUPunch, Recycle, Volt Tackle, Worry Seed * Remove species specific TMs * Remove Secret Power TM until implementation * [Refactor/Test] Update Dynamax Cannon Unit Tests (#3074) * [Refactor/Test] Update Dynamax Cannon Unit Tests * Adjust Test Names * fix: update SameSite attribute in setCookie function to None * fix: update SameSite attribute in setCookie function to Strict * feat: Add prompt=none to Discord OAuth authorization URL * fix: Remove unnecessary cookie setting in LoginPhase and MenuUiHandler * feat: Improve cookie handling in getCookie function * feat: Delete duplicate cookies with the same name in getCookie function * [Localization] Add minor Korean translations to ability-trigger file (trace) (#3093) * modify trace trigger description (refered from official translation) modify trace trigger description (refered from official translation) * modify trace trigger description (refered from official translation) modify trace trigger description (refered from official translation) * Fix broken `multi_target` tests and remove RNG-based failures (#3095) * Prevent RNG from breaking the Quick Draw tests (#3096) * Make EXP boosting items stack additively and not multiplicatively (#3094) Fixes #2040 * [Bug] Fixes Encore bug with multi-target moves missing (#3060) * Pushes move history even when multi target move misses * Move pushMoveHistory out of Move Pending condition * Add remaining relearn moves (#2894) * [Enhancement] Decouple move accuracy and accuracy multiplier calculation from phases.ts (#2899) * refactor accuracy calc * update doc * move accuracy multiplier calculation outside phases * update wonder skin unit test * rename method * add docs * add unit tests * address feedback * rename method * fix imports * improve tests * add test for ohko move accuracy * [Sprite] Fix a number of sprite issues including the Zubat line, Goldeen, Golett, Dudunsparce, H-Sneasel, Garchomp, Sylveon, Marshadow (#3069) * Batfix, Goldeen, Chomp, Golett, genies * Fix Dudunsparce using Reborn shiny backsprite for whatever reason * Fix female Hisuian Sneasel missing variants * Marshadow epic front and Zenith, Sylveon back * Fix Zubat line back sprite inconsistencies * Fix Noivern shiny back * [Move] Finish implementation of Glaive Rush (#2720) * Finish implementation of Glaive Rush * Fix test RNG * Add code/test for Multi-Lens interaction * Fix off-by-one error in test caused by rounding issues * Update for code changes * Fix BattlerTag name * [Bug] fix not changing moveset after add to starter #1932 (#2396) * [Bug] Enemies can properly use stuff cheeks (#3090) * [Feature] Added pokemon nicknames/renaming (#2570) * Added basic temp renaming * Made nickname persistant after reloading * Localization and cancel button * Fixed instant rename on active pokemon * Small bugfix to prevent console errors * Changed logic to use the new nickname field. Replaced most .name with getNameToRender() for display. * Changed evolution message. Removed log messagesc * Added localization keys for other languages * Removed empty lines * French translation Co-authored-by: Lugiad' <adrien.grivel@hotmail.fr> * Chinese translation Co-authored-by: Yonmaru40 <47717431+40chyan@users.noreply.github.com> * Portuguese (Brazil) translation Co-authored-by: José Ricardo Fleury Oliveira <josefleury@discente.ufg.br> * Korean translation Co-authored-by: Enoch <enoch.jwsong@gmail.com> * Update menu.ts * Update menu.ts [Localization(it)] * Changed most .getNameToRender() instance to getPokemonNameWithAffix() * Changed wild encounter messages back to just use the name without affix. * Added localization for the party ui rename selection * Escaping nickname characters to support all characters * Better Error handling * Update src/field/pokemon.ts Co-authored-by: flx-sta <50131232+flx-sta@users.noreply.github.com> --------- Co-authored-by: Lugiad' <adrien.grivel@hotmail.fr> Co-authored-by: Yonmaru40 <47717431+40chyan@users.noreply.github.com> Co-authored-by: José Ricardo Fleury Oliveira <josefleury@discente.ufg.br> Co-authored-by: Enoch <enoch.jwsong@gmail.com> Co-authored-by: Niccolò <123510358+NicusPulcis@users.noreply.github.com> Co-authored-by: flx-sta <50131232+flx-sta@users.noreply.github.com> * [Sprite] Show Partner Eevee variants (#3097) * [Sprite] Show Partner Eevee variants Taken from non-partner Eevee * [Sprite] Show Partner Eevee variants Taken from non-partner Eevee * [Sprite] Show Partner Eevee variants * Allow Necrozma forms to play their special music (#3054) * [BUG] fixing multi-hit and move messages on faint (#2981) * fixing order of messages, scences, to render messages before fainting * updated fix for effectiveness text rendering order for multi hit moves * fixing messages not appearing for multi-hit moves on faint * updated multi-hit condition) * fixing PR conflicts * adding comments and FaintPhase setPhaseQueueSplice bug, fixing overrides merge conflict * writing better comments * removing space diff in overrides * adding fainting check for self damage moves * emergency fixing broken last commit * additional comments for multi-hit problem * updating comments, jsdoc style * fixing linter, destiny bond errors * splitting up varaible comments to be in JSDoc format * fixing tests and merge mistakes * adding rendering of multihit moves that only hit once * fixing comment formatting_tabs and spaces --------- Co-authored-by: Benjamin Odom <bennybroseph@gmail.com> * [Bug] Sheer Force/Serene Grace Flyout Bugfix (#2496) * Disable Show Ability for Serene Grace and Sheer Force when opponent calculates targetBenefitScore * Add comment and definition to argument * [Visual] Achievement icons (#2617) * Text Changes to MonoGen Challenges * Item Texture Files * MonoGen Ribbons added * Resolving merge conflict * Updated texture files? I think * Changed image names and added new localizations for syntax * Removed unused ribbons * [Feature] replace bug-report template with form (#2772) * replace bug-report template with form * update bug_report.yml * add palceholders for data & session file * Update item atlas * Update item atlas again This time, after merging main into the PR. * [Sprite] Show Partner Eevee variants (#3097) * [Sprite] Show Partner Eevee variants Taken from non-partner Eevee * [Sprite] Show Partner Eevee variants Taken from non-partner Eevee * [Sprite] Show Partner Eevee variants * Merged with beta's new items * Fixed typedoc issues * Removing localizations * Text Changes to MonoGen Challenges * Item Texture Files * MonoGen Ribbons added * Resolving merge conflict * Updated texture files? I think * Changed image names and added new localizations for syntax * Removed unused ribbons * Update item atlas * [Feature] replace bug-report template with form (#2772) * replace bug-report template with form * update bug_report.yml * add palceholders for data & session file * Update item atlas again This time, after merging main into the PR. * Merged with beta's new items * Fixed typedoc issues * Removing localizations * Allow Necrozma forms to play their special music (#3054) * Allow Necrozma forms to play their special music (#3054) * MonoGen Ribbons added * Text Changes to MonoGen Challenges * Item Texture Files * MonoGen Ribbons added * Updated texture files? I think * Changed image names and added new localizations for syntax * Fixed typedoc issues * Revert "Text Changes to MonoGen Challenges" This reverts commit 3bf79acc6a797b0e28e68e113e644b30096ea9ce. * Text Changes to MonoGen Challenges * Item Texture Files * MonoGen Ribbons added * Resolving merge conflict * Updated texture files? I think * Changed image names and added new localizations for syntax * Removed unused ribbons * Update item atlas * [Feature] replace bug-report template with form (#2772) * replace bug-report template with form * update bug_report.yml * add palceholders for data & session file * Update item atlas again This time, after merging main into the PR. * Merged with beta's new items * Fixed typedoc issues * Removing localizations * Text Changes to MonoGen Challenges * Item Texture Files * MonoGen Ribbons added * Resolving merge conflict * Updated texture files? I think * Changed image names and added new localizations for syntax * Removed unused ribbons * Update item atlas * Update item atlas again This time, after merging main into the PR. * Merged with beta's new items * Fixed typedoc issues * Removing localizations * MonoGen Ribbons added * Text Changes to MonoGen Challenges * Item Texture Files * MonoGen Ribbons added * Updated texture files? I think * Changed image names and added new localizations for syntax * Fixed typedoc issues * Revert "Text Changes to MonoGen Challenges" This reverts commit 3bf79acc6a797b0e28e68e113e644b30096ea9ce. * Please fix this. * Revert "[Feature] replace bug-report template with form (#2772)" This reverts commit aa69b107778123b2173bee6130e5d172d201b637. --------- Co-authored-by: Frutescens <info@laptop> Co-authored-by: flx-sta <50131232+flx-sta@users.noreply.github.com> Co-authored-by: Madmadness65 <blaze.the.fireman@gmail.com> Co-authored-by: chaosgrimmon <31082757+chaosgrimmon@users.noreply.github.com> Co-authored-by: Madmadness65 <59298170+Madmadness65@users.noreply.github.com> * Re-add changes accidentally deleted by #2617 * Fix some broken images in item atlas & achievement Fixed the broken sprites in the item atlas, and fixed the Baton Pass achievement to use the Baton item now (it was broken by the Leek being renamed). Also resized the Baton item image to 32*32, like all the other items. * fix: Remove unnecessary cookie setting in removeCookie function * fix: Update removeCookie function to use Max-Age=-1 instead of Expires header The removeCookie function was updated to use the Max-Age=-1 attribute instead of setting the Expires header to a past date. This change ensures that the cookie is immediately expired when removed, preventing any potential login loops. Additionally, a legacy cookie without a domain was added to handle older cookies. This commit resolves the unnecessary cookie setting in the removeCookie function. * [QoL] Starter UI selection update to allow removing specific pokemon from party (#1983) * Initial commits with logic to remove starters if they're in your party. Still need to make it work so that the starter selection cursor disappears when a starter is unselected * Updated code to be able to remove pokemon, including the side icons and cursor locations * Fixed popstarter to work with any index * Updating code to allow navigation of starter icons * Updating code to allow navigation of party starter icons * Updaing navigation of party icons * Updated logic to fix incorrect icon in top left of pokemon options when navigating the starter icons * Updated logic to include the ability to navigate and interact with the starter icons * Forgot to push the actual starter-select-ui-handler. Might be a bit hard to test things out without that :) * Removed some unnecessary comments * Fixed small bug with not being able to move from the far right to the gen selection when the starter icons were empty * Updated code to not be using a method to generate the party menu and made it more like it used to be. This should help with merge conflicts in the future * I committed the merged version but forgot to make the starter-select-ui-handler staged after making the changes * Accidentally broke challenges that had a specific typing requirement with last commit. This should fix it * Changed how navigation worked based on popular demand * Fixed code review comments * Accidentally left in a whole block of commented code. Intentionally removing it now * Started adding logic for mono type challenge runs to not break the game if the user tries to start a run with an invalid party * Updated the text to say the party is invalid * Updated logic to make invalid pokemon greyed out when no valid pokemon are in your party * Added comments on some code * Updated locales to include the key for trying to start with invalid parties during a challenge * Fixed some code from a bad merge where a challenge related param that was previously a number now needed to be a boolean and wasn't * Removed comment as per review * [Bug] Ability changing on evolution fix (#2995) * Prevent Pokemon with their second ability from evolving into their HA * Add check for fusions too * Localization(pt): Updated move.ts (#3078) * Localization(pt): Updated move.ts and fixed King's Shield * more fixes * [Bug] Enemy pokemon's Harvest creates berry icons on the player's side when triggered (#3077) * [Bug] Fix hustle not applying attack boost (#3101) * [Sprite] Mega Latis using opposite's eye colour (#3102) * [Sprite] Mega Latis using opposite eyes Taken from counterpart Lati * [Sprite] Update Mega Latios eyes * [Sprite] Update Mega Latias eyes * [Sprite] Mega Latias duplicated palette value Colour taken from non-exp Mega Latias * [Bug] Fix Thousand Arrows not hitting targets under the effects of Magnet Rise (#3100) * Fix Thousand Arrows not hitting through Magnet Rise * Add integration test for Thousand Arrows vs. Magnet Rise * ESLint * Remove unnecessary checks in integration tests * [Move] Aeroblast is a wind move (#3109) * Hardcoded Pokemon should have proper names * Aeroblast is a wind move * [Refactor] Replaces the single and double battles overrides with a single override (#3104) `SINGLE_BATTLE_OVERRIDE` and `DOUBLE_BATTLE_OVERRIDE` are now `BATTLE_TYPE` Also updates all uses of the old overrides with the new one * fix `Overrides` capitalization (#3111) * [Balance] More TM fixes (#3081) * More TM fixes * Leaf Storm * Removed sexy matcha * [Refactor] Code readability update (#3085) * Clean up/clarify `src/field/pokemon.ts` a bit Code provided by DerTapp on Discord * Update `PokemonSpeciesForm.getAbilityCount()` * Update `PokemonSpeciesForm.getAbility()` * Add explicit `Abilities.NONE` checks * Add tests * Add jsdoc and implement test suggestions * [Move] Implement Stockpile, Spit Up, Swallow (#2960) * feat: Implement Stockpile, Spit Up, Swallow * chore: Minor, likely unnecessary null checks * feat: Localization * Undo non-English localizations (unsure if they went through proper channels) * ko localization from @EnochG1 Co-authored-by: Enoch <enoch.jwsong@gmail.com> * linting fix * add tests, tiny (non-functional) tweaks * Remove unnecessary cast * Update src/data/move.ts (oops) * remove some unnecessary comments, rename something for clarity --------- Co-authored-by: Enoch <enoch.jwsong@gmail.com> * [Bug] Remove ability swap code in `PlayerPokemon.evolve()` (#3113) * [Test] Add test for rollout move (#3114) * add test for rollout move Co-authored-by: Zach Day <zachdayz@gmail.com> * fix tests still using `SINGLE_BATTLE_OVERRIDE` --------- Co-authored-by: Zach Day <zachdayz@gmail.com> * [Bug][Beta] Fix Hyper Beam, etc. permanently recharging (#3115) * Fix Hyper Beam, etc. permanently recharging * Fix override in hyper beam test * [QoL] New Starter Select UI with Filter (#2916) * update images for new UI * add updated starter UI with filter code * update starter-select test code * update win filter condition to pass test * remove unnecessary console log * update test code to match current filter UI * merge update * apply bugfix & chrry-pick small issues fix which are handled beta branch * resolve conflicts * fix lint errors * Fixed a bug where the target location for escaping using the left and right buttons on the starter button did not account for scrolling * update filter bar label color change when activated * fix lint error * fix lint * fix octolock.text.ts. it looks override import error. idk why it is happend in this PR. but it looks ok now * add passive dropdown in unlocks filter * fix lint * fix double button sound bug. refactoring genSpecies -> allSpecies, starterContainers -> starterContainer which are remove unnecessary generation axis * optimize updateStarterValueLabel function which is bottleneck of UI update latency * apply translation of gen filter label. fix lint * add # candies sort option * merge beta * resolve confilcts * fix offset of starter and start cursor * make compatible with starter UI * add missing feature * add images for legacy UI. adjust the position and size of the starterContainerWindow * [Localization] Implement Form localization (#3030) * Implement Pokemon forms localization * Update French pokemon-form.ts * Update French pokemon-form.ts * Update pokemon-form.ts * Add battle forms korean translation from returntoice Co-authored-by: returntoice <dieandbecome@gmail.com> * Add cosplay forms korean translation from returntoice Co-authored-by: returntoice <dieandbecome@gmail.com> * Add pichu form korean translation from returntoice Co-authored-by: returntoice <dieandbecome@gmail.com> * Add castform forms korean translation by returntoice Co-authored-by: returntoice <dieandbecome@gmail.com> * Add remaining forms korean translation by returntoice Co-authored-by: returntoice <dieandbecome@gmail.com> * French typo corrections pokemon-form.ts * Update Korean pokemon-form.ts * Modify froakiBattleBond like rockruff of OwnTempo case, it is froakie, not greninja. * Modify zygardePc Power construct is more important information Switch its position to 50% or 10% to avoid overlapping with the sprite * Modify mispelling * Added german forms * Changed Gigadynamax and Unendynamax so it fits at all * Add partner pikachu and eevee form localization * Add mimikyu forms localization * Partner Pikachu, Partner Evoli und Mimikyu Formen hinzugefügt * Update pokemon-form.ts * Update partners * Fix conflicts * Fix useless ? by flx-sta Co-authored-by: flx-sta <50131232+flx-sta@users.noreply.github.com> * Fix switch use by flx-sta * Fix conflicts * Please work !!!!!!! * Update src/locales/fr/pokemon-form.ts Co-authored-by: Lugiad' <adrien.grivel@hotmail.fr> * Update src/locales/fr/pokemon-form.ts Co-authored-by: Lugiad' <adrien.grivel@hotmail.fr> * Add pt_br primal localization Co-authored-by: José Ricardo Fleury Oliveira <josefleury@discente.ufg.br> * Add pt_br pikachu localization Co-authored-by: José Ricardo Fleury Oliveira <josefleury@discente.ufg.br> * Add pt_br castform localization Co-authored-by: José Ricardo Fleury Oliveira <josefleury@discente.ufg.br> * Add pt_br 3g localization Co-authored-by: José Ricardo Fleury Oliveira <josefleury@discente.ufg.br> * Add zh_cn localization Co-authored-by: Yonmaru40 <47717431+40chyan@users.noreply.github.com> * Add pt_br other localization Co-authored-by: José Ricardo Fleury Oliveira <josefleury@discente.ufg.br> * Add es castform localization Co-authored-by: InnocentGameDev <asdargmng@gmail.com> * Add es burmy localization Co-authored-by: InnocentGameDev <asdargmng@gmail.com> * Add es shellos localization Co-authored-by: InnocentGameDev <asdargmng@gmail.com> * Add es rotom localization Co-authored-by: InnocentGameDev <asdargmng@gmail.com> * Add es basculin localization Co-authored-by: InnocentGameDev <asdargmng@gmail.com> * Add es deerling localization Co-authored-by: InnocentGameDev <asdargmng@gmail.com> * Add es froakie localization Co-authored-by: InnocentGameDev <asdargmng@gmail.com> * Add es scatterbug localization Co-authored-by: InnocentGameDev <asdargmng@gmail.com> * Add es furfrou localization Co-authored-by: InnocentGameDev <asdargmng@gmail.com> * Add es xerneas localization Co-authored-by: InnocentGameDev <asdargmng@gmail.com> * Add es zygarde localization Co-authored-by: InnocentGameDev <asdargmng@gmail.com> * Add es pumpkaboo localization Co-authored-by: InnocentGameDev <asdargmng@gmail.com> * Add es flabebe localization Co-authored-by: InnocentGameDev <asdargmng@gmail.com> * Add es oricorio localization Co-authored-by: InnocentGameDev <asdargmng@gmail.com> * Add es minior localization Co-authored-by: InnocentGameDev <asdargmng@gmail.com> * Add es magearna localization Co-authored-by: InnocentGameDev <asdargmng@gmail.com> * Add es marshadow localization Co-authored-by: InnocentGameDev <asdargmng@gmail.com> * Add es sinistea ocalization Co-authored-by: InnocentGameDev <asdargmng@gmail.com> * Add es eiscue localization Co-authored-by: InnocentGameDev <asdargmng@gmail.com> * Add es indeedee localization Co-authored-by: InnocentGameDev <asdargmng@gmail.com> * Add es rockruff localization Co-authored-by: InnocentGameDev <asdargmng@gmail.com> * Add es mimikyu localization Co-authored-by: InnocentGameDev <asdargmng@gmail.com> * Add es zarude localization Co-authored-by: InnocentGameDev <asdargmng@gmail.com> * Add es squawkabilly localization Co-authored-by: InnocentGameDev <asdargmng@gmail.com> * Add es tatsugiri localization Co-authored-by: InnocentGameDev <asdargmng@gmail.com> * Add es gimmighoul localization Co-authored-by: InnocentGameDev <asdargmng@gmail.com> * Add es poltchageit localization Co-authored-by: InnocentGameDev <asdargmng@gmail.com> * Add es paldeaTauros localization Co-authored-by: InnocentGameDev <asdargmng@gmail.com> * Add es primal localization Co-authored-by: InnocentGameDev <asdargmng@gmail.com> * Add es pikachu localization Co-authored-by: InnocentGameDev <asdargmng@gmail.com> * Add es partner localization Co-authored-by: InnocentGameDev <asdargmng@gmail.com> * Fix typedocs error * Fix typedocs error * cn form prefix added Co-authored-by: Yonmaru40 <47717431+40chyan@users.noreply.github.com> * Fix german translate error * Fix typo Zh_CN Co-authored-by: mercurius-00 <80205689+mercurius-00@users.noreply.github.com> * Update pokemon-form.ts [Localization(it)] * Update src/locales/es/pokemon-form.ts Co-authored-by: InnocentGameDev <asdargmng@gmail.com> * Fix megas forms bug and add forgotten forms * Fix wrong ko config * Add fr localization for new forms Co-authored-by: Lugiad' <adrien.grivel@hotmail.fr> * Add de localization for new forms Co-authored-by: Jannik Tappert <38758606+CodeTappert@users.noreply.github.com> * Remove forgotten debug line Co-authored-by: Jannik Tappert <38758606+CodeTappert@users.noreply.github.com> * Optimize battle forms * Update pokemon-form.ts (additional forms, italian localization) * The same typo is in the zh_TW placeholder text too. Co-authored-by: Madmadness65 <59298170+Madmadness65@users.noreply.github.com> * Fix forgotten megaY Co-authored-by: Jannik Tappert <38758606+CodeTappert@users.noreply.github.com> * Add capitalizeString to utils * Fix typedoc error * Update src/data/pokemon-species.ts Co-authored-by: Jannik Tappert <38758606+CodeTappert@users.noreply.github.com> * Add ko localization for new forms Co-authored-by: MrWaterT <87186129+MrWaterT@users.noreply.github.com> --------- Co-authored-by: Lugiad <adrien.grivel@hotmail.fr> Co-authored-by: returntoice <dieandbecome@gmail.com> Co-authored-by: Mr.WaterT <water.t.works@gmail.com> Co-authored-by: MrWaterT <87186129+MrWaterT@users.noreply.github.com> Co-authored-by: Jannik Tappert <38758606+CodeTappert@users.noreply.github.com> Co-authored-by: flx-sta <50131232+flx-sta@users.noreply.github.com> Co-authored-by: Benjamin Odom <bennybroseph@gmail.com> Co-authored-by: José Ricardo Fleury Oliveira <josefleury@discente.ufg.br> Co-authored-by: Yonmaru40 <47717431+40chyan@users.noreply.github.com> Co-authored-by: InnocentGameDev <asdargmng@gmail.com> Co-authored-by: EnochG1 <enoch.jwsong@gmail.com> Co-authored-by: mercurius-00 <80205689+mercurius-00@users.noreply.github.com> Co-authored-by: Niccolò <123510358+NicusPulcis@users.noreply.github.com> Co-authored-by: Madmadness65 <59298170+Madmadness65@users.noreply.github.com> * [Move] Fully implement Jungle Healing/Lunar Blessing status heal (#2785) * [Bug] Fix tags not resetting on switch (#3119) Fixes #2982 * [Move] Water Shuriken guarantees 3 hits with battle bond (#2687) * [Bug] Make on-summon abilities trigger after the switch check (#3118) * Make on-summon abilities trigger after the switch check * Add test * [Ability] Cloud Nine now displays a message on activation Maintains parity with Air Lock. Both of these probably need to be localized. Should fix issue 491 though * [QoL] Summary Option for Caught Pokemon (#2921) * Option to view Summary before adding new Pokemon to party * Fixed issues described by HopsWas * Adjusted makeRoomForConfirmUi to improve window spacing * Fixed ESLint issue + addressed OrangeRed review * Fixed Github pages issue * Removed duplicate unshiftPhase * Fixed phase order * Don't start from beginning of catch function * Option to view Summary before adding new Pokemon to party * Fixed issues described by HopsWas * Adjusted makeRoomForConfirmUi to improve window spacing * Fixed Github pages issue * Fixed phase order * Quick fix * This should fix the summaryOption feature without bugging confirm-ui-handler in other cases * Revert "Merge remote-tracking branch 'origin/summaryOption1' into summaryOption1" This reverts commit ea7d0ce59e3f5631a8ef3d76646069a3945ed036, reversing changes made to 4c565958dafe6904925015ed7100e4940f041213. * Added a better conditional that reflects its source and purpose --------- Co-authored-by: Frutescens <info@laptop> Co-authored-by: AJ Fontaine <fontbane@gmail.com> * [Bug] Fix HP rounding issues (#2968) * [Bug] Fixes bug with frenzy moves keeping the confusion counter despite disruption (#3041) * Adds frenzyMissFunc trigger on NO_EFFECT hit result * Refactors FrenzyAttr to properly lapse tags each turn and remove tags on disrupt * Makes comment on CONFUSED tag clearer * Changes all integer usages to number in battler-tags * Update getBattlerTag function to use number instead of integer * [Tests] Updating Leftovers test (#3123) * Bug Fix * Update src/system/game-stats.ts Co-authored-by: flx-sta <50131232+flx-sta@users.noreply.github.com> * Fixing a test * Fixed import * Revert "Bug Fix" This reverts commit 834844978ef2583b00555969a99b3071c47a19d9. --------- Co-authored-by: Frutescens <info@laptop> Co-authored-by: flx-sta <50131232+flx-sta@users.noreply.github.com> * [Localization] Correctly Localized German names of the BGM for Evil Teams (#3124) * [Bug] Game Stats Sub-Legendary NaN bug for new game files (#3122) * Bug Fix * Update src/system/game-stats.ts Co-authored-by: flx-sta <50131232+flx-sta@users.noreply.github.com> * Fixing a test * Revert "Fixing a test" This reverts commit 1bdbe2da5a7e29dce17911eb7c1084de44328573. * Test Re-Run --------- Co-authored-by: Frutescens <info@laptop> Co-authored-by: flx-sta <50131232+flx-sta@users.noreply.github.com> * [Bug] Adjust how counter attacks target to account for uturn/voltswitch and double battles (#2462) * Adjust how counter attacks target to account for uturn/voltswitch * Creates move flag for metal burst/comeuppance to redirect in some cases * Remove debug printing * Bit shifts the redirect counter flag * Removes extraneous class from prior testing * Remove vitest timestamp file that was accidentally added * [Misc] Change fresh start achievement icon to reviver seed Mystic ticket icons in the achievements just confuse players * [Balance] Fix N-Solarizer etc appearing without secondary mon registered in dex (#2713) * Fix reins et al being available without unlocking second mon * Small fix * Parentheses for safety * Add documentation to new function * Bug Fix (#3129) Co-authored-by: Frutescens <info@laptop> * [Bug] Fixed OHKO moves being affected by accuracy and evasion battle stats (#3117) * Fixed OHKO moves being affected by accuracy and evasion battle stats * Added related tests for Fissure, unskipped related test for Hustle * Tweaked fissure accuracy and evasion tests to use spyOn() for getAccuracyMultiplier() as per feedback * Fixed accuracy test for Fissure * [Fix][Sprite] Politoed Back Sprites (#3130) * [Fix][Sprite] Politoed Back Sprites Retrieved first version from history. Rearranged frames to make jump a little less floaty. Set same anim length for all variations. Json is identical for all variations. Spritesheet frame position is identical for all variations. Trimmed the frame of extra space Reduced file size of spritesheets and JSON due to less unique frames needed. * [Fix][Sprite] Politoed Back Sprites - Reformatted JSON to texturepacker standard Reformatted json. * [Bug] Hotfix for Starter select UI with Filter (#3121) * fix remove pokemon menu * fix dropdown all is not changing bug * fix bug when go down with no starter on start button * fix starter corsor bug on deletion. out of screen cursor bug * fix challenge log * fix lint error * [Bug] fix and condition of shiny and passive (#3136) * change param name (because beta was changed) * add config and fix message main key * change message key in pokemon.ts * modify test message (also same as og game) * Fixed form names not working in starter select (#3139) * [Bug] Prevent evolution causing a swap from the second ability to the HA (#3138) * Prevent evolution causing a swap from the second ability to the HA * Add tests * Update `starter-select-ui-handler.ts` * [Feature] [Item] Add White Herb item (#2719) * More work * More work * Should be finished * Fixed an error in checking target * Moved white herb effect to StatChangePhase, 50% chance of consumption * Added graphics for White Herb * Balance and documentation * Add localization entry for White Herb apply msg * Add new keys to other localization files * German translations Co-authored-by: Jannik Tappert <38758606+CodeTappert@users.noreply.github.com> * Update item sprite atlas * Redo item atlas * Remove whitespace in move.ts * Moved decrement outside conditional * Fix item atlas --------- Co-authored-by: Jannik Tappert <38758606+CodeTappert@users.noreply.github.com> * [Localization] Translated missing portuguese entries and some fixes (#3142) * [Bug] Caught Pokemon Summary-Modifier Display Fix (#3145) * Fixed modifiers not displaying on summary screen * Addressed Typedoc issues * Documentation added --------- Co-authored-by: Frutescens <info@laptop> * add missing text key * [BUG] Fixes bug that prevents pokemon with froms from hatching as rare/epic shiny variant and preventing illegal variants from legendary gacha (#2940) * Changed PokemonSpecies hasVariants function to also include for pokemon with differend forms * Added check to prevent illegal shiny variants from happening if the egg rolls the gacha legendary and has no variants * Simplified variant check. Fixed spelling on unit test * Bugfix for legacy eggs * Removed formIndex variable * Changed unit test * Added new line to unit test function Co-authored-by: flx-sta <50131232+flx-sta@users.noreply.github.com> --------- Co-authored-by: flx-sta <50131232+flx-sta@users.noreply.github.com> * [Unittest] remove held item rng (white herb) from intimidate.test.ts (#3151) * Emergency delete unlock all functionality - should remove the unlock all functionality but keeps the isBeta utils function for future stuff (#3153) * Update `getAbility()` and `getAbilityCount()` for the ability changes (#3157) * Update src/data/arena-tag.ts Co-authored-by: sirzento <sirzento@gmx.de> * Update src/locales/zh_CN/arena-tag.ts Co-authored-by: Yonmaru40 <47717431+40chyan@users.noreply.github.com> * change message key of stickyWebActivateTrap (due to change of original) * Update src/locales/fr/arena-tag.ts Co-authored-by: Lugiad' <adrien.grivel@hotmail.fr> --------- Co-authored-by: Frederico Santos <frederico.f.santos@tecnico.ulisboa.pt> Co-authored-by: Jannik Tappert <38758606+CodeTappert@users.noreply.github.com> Co-authored-by: Felix Staud <felix.staud@headwire.com> Co-authored-by: Adrian T <68144167+torranx@users.noreply.github.com> Co-authored-by: lnuvy <lnuvy.code@gmail.com> Co-authored-by: José Ricardo Fleury Oliveira <josefleury@discente.ufg.br> Co-authored-by: Tempoanon <163687446+Tempo-anon@users.noreply.github.com> Co-authored-by: Xavion3 <xavion333@gmail.com> Co-authored-by: AJ Fontaine <36677462+Fontbane@users.noreply.github.com> Co-authored-by: NightKev <34855794+DayKev@users.noreply.github.com> Co-authored-by: chaosgrimmon <31082757+chaosgrimmon@users.noreply.github.com> Co-authored-by: Niccolò <123510358+NicusPulcis@users.noreply.github.com> Co-authored-by: Dakurei <maxime.palanchini@gmail.com> Co-authored-by: Alexis <alexis.faizeau@animedigitalnetwork.fr> Co-authored-by: 송지원 <jiwsong@gmarket.com> Co-authored-by: Yonmaru40 <47717431+40chyan@users.noreply.github.com> Co-authored-by: Mr.WaterT <water.t.works@gmail.com> Co-authored-by: Alexis Faizeau <faizeau.alexis@gmail.com> Co-authored-by: GoldTra <162721984+GoldTra@users.noreply.github.com> Co-authored-by: returntoice <dieandbecome@gmail.com> Co-authored-by: Arxxer <javiptn7@gmail.com> Co-authored-by: Lugiad' <adrien.grivel@hotmail.fr> Co-authored-by: InnocentGameDev <asdargmng@gmail.com> Co-authored-by: innerthunder <168692175+innerthunder@users.noreply.github.com> Co-authored-by: Dmitriy K <kagno.dmitriy@gmail.com> Co-authored-by: sodam <66295123+sodaMelon@users.noreply.github.com> Co-authored-by: RimKnight <rimknight852@gmail.com> Co-authored-by: xsn34kzx <xsn34kzx@gmail.com> Co-authored-by: Amani H <109637146+xsn34kzx@users.noreply.github.com> Co-authored-by: mercurius-00 <80205689+mercurius-00@users.noreply.github.com> Co-authored-by: hayuna <marek.kowalonek@gmail.com> Co-authored-by: flx-sta <50131232+flx-sta@users.noreply.github.com> Co-authored-by: EmberCM <emberdevteam@gmail.com> Co-authored-by: Opaque02 <66582645+Opaque02@users.noreply.github.com> Co-authored-by: mcmontag <54485715+mcmontag@users.noreply.github.com> Co-authored-by: schmidtc1 <62030095+schmidtc1@users.noreply.github.com> Co-authored-by: Arxalc <63990624+Arxalc@users.noreply.github.com> Co-authored-by: Mumble <kimjoanne@protonmail.com> Co-authored-by: Frutescens <info@laptop> Co-authored-by: EmberCM <kooly213@hotmail.com> Co-authored-by: Zoruu <113668528+ArielStevens@users.noreply.github.com> Co-authored-by: Corrade <49605314+Corrade@users.noreply.github.com> Co-authored-by: Madmadness65 <blaze.the.fireman@gmail.com> Co-authored-by: cam <lrlrliwoo@gmail.com> Co-authored-by: 송영진 <36808515+bucket1582@users.noreply.github.com> Co-authored-by: DustinLin <39450497+DustinLin@users.noreply.github.com> Co-authored-by: 서명인 (Myungin, SEO) <65226760+smee6@users.noreply.github.com> Co-authored-by: gjeodnd12165 <61226524+gjeodnd12165@users.noreply.github.com> Co-authored-by: sirzento <sirzento@gmx.de> Co-authored-by: Madmadness65 <59298170+Madmadness65@users.noreply.github.com> Co-authored-by: Benjamin Odom <bennybroseph@gmail.com> Co-authored-by: Daniel Gaston <danielgaston6@gmail.com> Co-authored-by: Zach Day <zachdayz@gmail.com> Co-authored-by: Leo Kim <47556641+KimJeongSun@users.noreply.github.com> Co-authored-by: Kiriox <66013753+Kiriox94@users.noreply.github.com> Co-authored-by: MrWaterT <87186129+MrWaterT@users.noreply.github.com> Co-authored-by: AJ Fontaine <fontbane@gmail.com>
4163 lines
162 KiB
TypeScript
4163 lines
162 KiB
TypeScript
import Phaser from "phaser";
|
|
import BattleScene, { AnySound } from "../battle-scene";
|
|
import { Variant, VariantSet, variantColorCache } from "#app/data/variant";
|
|
import { variantData } from "#app/data/variant";
|
|
import BattleInfo, { PlayerBattleInfo, EnemyBattleInfo } from "../ui/battle-info";
|
|
import Move, { HighCritAttr, HitsTagAttr, applyMoveAttrs, FixedDamageAttr, VariableAtkAttr, allMoves, MoveCategory, TypelessAttr, CritOnlyAttr, getMoveTargets, OneHitKOAttr, VariableMoveTypeAttr, StatusMoveTypeImmunityAttr, VariableDefAttr, AttackMove, ModifiedDamageAttr, VariableMoveTypeMultiplierAttr, IgnoreOpponentStatChangesAttr, SacrificialAttr, VariableMoveCategoryAttr, CounterDamageAttr, StatChangeAttr, RechargeAttr, ChargeAttr, IgnoreWeatherTypeDebuffAttr, BypassBurnDamageReductionAttr, SacrificialAttrOnHit, MoveFlags, NeutralDamageAgainstFlyingTypeMultiplierAttr, OneHitKOAccuracyAttr } from "../data/move";
|
|
import { default as PokemonSpecies, PokemonSpeciesForm, SpeciesFormKey, getFusedSpeciesName, getPokemonSpecies, getPokemonSpeciesForm, getStarterValueFriendshipCap, speciesStarters, starterPassiveAbilities } from "../data/pokemon-species";
|
|
import { Constructor } from "#app/utils";
|
|
import * as Utils from "../utils";
|
|
import { Type, TypeDamageMultiplier, getTypeDamageMultiplier, getTypeRgb } from "../data/type";
|
|
import { getLevelTotalExp } from "../data/exp";
|
|
import { Stat } from "../data/pokemon-stat";
|
|
import { DamageMoneyRewardModifier, EnemyDamageBoosterModifier, EnemyDamageReducerModifier, EnemyEndureChanceModifier, EnemyFusionChanceModifier, HiddenAbilityRateBoosterModifier, PokemonBaseStatModifier, PokemonFriendshipBoosterModifier, PokemonHeldItemModifier, PokemonNatureWeightModifier, ShinyRateBoosterModifier, SurviveDamageModifier, TempBattleStatBoosterModifier, StatBoosterModifier, CritBoosterModifier, TerastallizeModifier } from "../modifier/modifier";
|
|
import { PokeballType } from "../data/pokeball";
|
|
import { Gender } from "../data/gender";
|
|
import { initMoveAnim, loadMoveAnimAssets } from "../data/battle-anims";
|
|
import { Status, StatusEffect, getRandomStatus } from "../data/status-effect";
|
|
import { pokemonEvolutions, pokemonPrevolutions, SpeciesFormEvolution, SpeciesEvolutionCondition, FusionSpeciesFormEvolution } from "../data/pokemon-evolutions";
|
|
import { reverseCompatibleTms, tmSpecies, tmPoolTiers } from "../data/tms";
|
|
import { DamagePhase, FaintPhase, LearnMovePhase, MoveEffectPhase, ObtainStatusEffectPhase, StatChangePhase, SwitchSummonPhase, ToggleDoublePositionPhase, MoveEndPhase } from "../phases";
|
|
import { BattleStat } from "../data/battle-stat";
|
|
import { BattlerTag, BattlerTagLapseType, EncoreTag, GroundedTag, HighestStatBoostTag, TypeImmuneTag, getBattlerTag, SemiInvulnerableTag, TypeBoostTag } from "../data/battler-tags";
|
|
import { WeatherType } from "../data/weather";
|
|
import { TempBattleStat } from "../data/temp-battle-stat";
|
|
import { ArenaTagSide, WeakenMoveScreenTag } from "../data/arena-tag";
|
|
import { Ability, AbAttr, BattleStatMultiplierAbAttr, BlockCritAbAttr, BonusCritAbAttr, BypassBurnDamageReductionAbAttr, FieldPriorityMoveImmunityAbAttr, IgnoreOpponentStatChangesAbAttr, MoveImmunityAbAttr, PreApplyBattlerTagAbAttr, PreDefendFullHpEndureAbAttr, ReceivedMoveDamageMultiplierAbAttr, ReduceStatusEffectDurationAbAttr, StabBoostAbAttr, StatusEffectImmunityAbAttr, TypeImmunityAbAttr, WeightMultiplierAbAttr, allAbilities, applyAbAttrs, applyBattleStatMultiplierAbAttrs, applyPreApplyBattlerTagAbAttrs, applyPreAttackAbAttrs, applyPreDefendAbAttrs, applyPreSetStatusAbAttrs, UnsuppressableAbilityAbAttr, SuppressFieldAbilitiesAbAttr, NoFusionAbilityAbAttr, MultCritAbAttr, IgnoreTypeImmunityAbAttr, DamageBoostAbAttr, IgnoreTypeStatusEffectImmunityAbAttr, ConditionalCritAbAttr, applyFieldBattleStatMultiplierAbAttrs, FieldMultiplyBattleStatAbAttr, AddSecondStrikeAbAttr, IgnoreOpponentEvasionAbAttr } from "../data/ability";
|
|
import PokemonData from "../system/pokemon-data";
|
|
import { BattlerIndex } from "../battle";
|
|
import { Mode } from "../ui/ui";
|
|
import PartyUiHandler, { PartyOption, PartyUiMode } from "../ui/party-ui-handler";
|
|
import SoundFade from "phaser3-rex-plugins/plugins/soundfade";
|
|
import { LevelMoves } from "../data/pokemon-level-moves";
|
|
import { DamageAchv, achvs } from "../system/achv";
|
|
import { DexAttr, StarterDataEntry, StarterMoveset } from "../system/game-data";
|
|
import { QuantizerCelebi, argbFromRgba, rgbaFromArgb } from "@material/material-color-utilities";
|
|
import { Nature, getNatureStatMultiplier } from "../data/nature";
|
|
import { SpeciesFormChange, SpeciesFormChangeActiveTrigger, SpeciesFormChangeMoveLearnedTrigger, SpeciesFormChangePostMoveTrigger, SpeciesFormChangeStatusEffectTrigger } from "../data/pokemon-forms";
|
|
import { TerrainType } from "../data/terrain";
|
|
import { TrainerSlot } from "../data/trainer-config";
|
|
import Overrides from "#app/overrides";
|
|
import i18next from "i18next";
|
|
import { speciesEggMoves } from "../data/egg-moves";
|
|
import { ModifierTier } from "../modifier/modifier-tier";
|
|
import { applyChallenges, ChallengeType } from "#app/data/challenge.js";
|
|
import { Abilities } from "#enums/abilities";
|
|
import { ArenaTagType } from "#enums/arena-tag-type";
|
|
import { BattleSpec } from "#enums/battle-spec";
|
|
import { BattlerTagType } from "#enums/battler-tag-type";
|
|
import { BerryType } from "#enums/berry-type";
|
|
import { Biome } from "#enums/biome";
|
|
import { Moves } from "#enums/moves";
|
|
import { Species } from "#enums/species";
|
|
import { getPokemonNameWithAffix } from "#app/messages.js";
|
|
|
|
export enum FieldPosition {
|
|
CENTER,
|
|
LEFT,
|
|
RIGHT
|
|
}
|
|
|
|
export default abstract class Pokemon extends Phaser.GameObjects.Container {
|
|
public id: integer;
|
|
public name: string;
|
|
public nickname: string;
|
|
public species: PokemonSpecies;
|
|
public formIndex: integer;
|
|
public abilityIndex: integer;
|
|
public passive: boolean;
|
|
public shiny: boolean;
|
|
public variant: Variant;
|
|
public pokeball: PokeballType;
|
|
protected battleInfo: BattleInfo;
|
|
public level: integer;
|
|
public exp: integer;
|
|
public levelExp: integer;
|
|
public gender: Gender;
|
|
public hp: integer;
|
|
public stats: integer[];
|
|
public ivs: integer[];
|
|
public nature: Nature;
|
|
public natureOverride: Nature | -1;
|
|
public moveset: PokemonMove[];
|
|
public status: Status;
|
|
public friendship: integer;
|
|
public metLevel: integer;
|
|
public metBiome: Biome | -1;
|
|
public luck: integer;
|
|
public pauseEvolutions: boolean;
|
|
public pokerus: boolean;
|
|
|
|
public fusionSpecies: PokemonSpecies;
|
|
public fusionFormIndex: integer;
|
|
public fusionAbilityIndex: integer;
|
|
public fusionShiny: boolean;
|
|
public fusionVariant: Variant;
|
|
public fusionGender: Gender;
|
|
public fusionLuck: integer;
|
|
|
|
private summonDataPrimer: PokemonSummonData;
|
|
|
|
public summonData: PokemonSummonData;
|
|
public battleData: PokemonBattleData;
|
|
public battleSummonData: PokemonBattleSummonData;
|
|
public turnData: PokemonTurnData;
|
|
|
|
public fieldPosition: FieldPosition;
|
|
|
|
public maskEnabled: boolean;
|
|
public maskSprite: Phaser.GameObjects.Sprite;
|
|
|
|
private shinySparkle: Phaser.GameObjects.Sprite;
|
|
|
|
constructor(scene: BattleScene, x: number, y: number, species: PokemonSpecies, level: integer, abilityIndex?: integer, formIndex?: integer, gender?: Gender, shiny?: boolean, variant?: Variant, ivs?: integer[], nature?: Nature, dataSource?: Pokemon | PokemonData) {
|
|
super(scene, x, y);
|
|
|
|
if (!species.isObtainable() && this.isPlayer()) {
|
|
throw `Cannot create a player Pokemon for species '${species.getName(formIndex)}'`;
|
|
}
|
|
|
|
const hiddenAbilityChance = new Utils.IntegerHolder(256);
|
|
if (!this.hasTrainer()) {
|
|
this.scene.applyModifiers(HiddenAbilityRateBoosterModifier, true, hiddenAbilityChance);
|
|
}
|
|
|
|
const hasHiddenAbility = !Utils.randSeedInt(hiddenAbilityChance.value);
|
|
const randAbilityIndex = Utils.randSeedInt(2);
|
|
|
|
this.species = species;
|
|
this.pokeball = dataSource?.pokeball || PokeballType.POKEBALL;
|
|
this.level = level;
|
|
// Determine the ability index
|
|
if (abilityIndex !== undefined) {
|
|
this.abilityIndex = abilityIndex; // Use the provided ability index if it is defined
|
|
} else {
|
|
// If abilityIndex is not provided, determine it based on species and hidden ability
|
|
if (species.abilityHidden && hasHiddenAbility) {
|
|
// If the species has a hidden ability and the hidden ability is present
|
|
this.abilityIndex = species.ability2 ? 2 : 1; // Use ability index 2 if species has a second ability, otherwise use 1
|
|
} else {
|
|
// If there is no hidden ability or species does not have a hidden ability
|
|
this.abilityIndex = species.ability2 ? randAbilityIndex : 0; // Use random ability index if species has a second ability, otherwise use 0
|
|
}
|
|
}
|
|
if (formIndex !== undefined) {
|
|
this.formIndex = formIndex;
|
|
}
|
|
if (gender !== undefined) {
|
|
this.gender = gender;
|
|
}
|
|
if (shiny !== undefined) {
|
|
this.shiny = shiny;
|
|
}
|
|
if (variant !== undefined) {
|
|
this.variant = variant;
|
|
}
|
|
this.exp = dataSource?.exp || getLevelTotalExp(this.level, species.growthRate);
|
|
this.levelExp = dataSource?.levelExp || 0;
|
|
if (dataSource) {
|
|
this.id = dataSource.id;
|
|
this.hp = dataSource.hp;
|
|
this.stats = dataSource.stats;
|
|
this.ivs = dataSource.ivs;
|
|
this.passive = !!dataSource.passive;
|
|
if (this.variant === undefined) {
|
|
this.variant = 0;
|
|
}
|
|
this.nature = dataSource.nature || 0 as Nature;
|
|
this.nickname = dataSource.nickname;
|
|
this.natureOverride = dataSource.natureOverride !== undefined ? dataSource.natureOverride : -1;
|
|
this.moveset = dataSource.moveset;
|
|
this.status = dataSource.status;
|
|
this.friendship = dataSource.friendship !== undefined ? dataSource.friendship : this.species.baseFriendship;
|
|
this.metLevel = dataSource.metLevel || 5;
|
|
this.luck = dataSource.luck;
|
|
this.metBiome = dataSource.metBiome;
|
|
this.pauseEvolutions = dataSource.pauseEvolutions;
|
|
this.pokerus = !!dataSource.pokerus;
|
|
this.fusionSpecies = dataSource.fusionSpecies instanceof PokemonSpecies ? dataSource.fusionSpecies : getPokemonSpecies(dataSource.fusionSpecies);
|
|
this.fusionFormIndex = dataSource.fusionFormIndex;
|
|
this.fusionAbilityIndex = dataSource.fusionAbilityIndex;
|
|
this.fusionShiny = dataSource.fusionShiny;
|
|
this.fusionVariant = dataSource.fusionVariant || 0;
|
|
this.fusionGender = dataSource.fusionGender;
|
|
this.fusionLuck = dataSource.fusionLuck;
|
|
} else {
|
|
this.id = Utils.randSeedInt(4294967296);
|
|
this.ivs = ivs || Utils.getIvsFromId(this.id);
|
|
|
|
if (this.gender === undefined) {
|
|
this.generateGender();
|
|
}
|
|
|
|
if (this.formIndex === undefined) {
|
|
this.formIndex = this.scene.getSpeciesFormIndex(species, this.gender, this.nature, this.isPlayer());
|
|
}
|
|
|
|
if (this.shiny === undefined) {
|
|
this.trySetShiny();
|
|
}
|
|
|
|
if (this.variant === undefined) {
|
|
this.variant = this.shiny ? this.generateVariant() : 0;
|
|
}
|
|
|
|
if (nature !== undefined) {
|
|
this.setNature(nature);
|
|
} else {
|
|
this.generateNature();
|
|
}
|
|
|
|
this.natureOverride = -1;
|
|
|
|
this.friendship = species.baseFriendship;
|
|
this.metLevel = level;
|
|
this.metBiome = scene.currentBattle ? scene.arena.biomeType : -1;
|
|
this.pokerus = false;
|
|
|
|
if (level > 1) {
|
|
const fused = new Utils.BooleanHolder(scene.gameMode.isSplicedOnly);
|
|
if (!fused.value && !this.isPlayer() && !this.hasTrainer()) {
|
|
this.scene.applyModifier(EnemyFusionChanceModifier, false, fused);
|
|
}
|
|
|
|
if (fused.value) {
|
|
this.calculateStats();
|
|
this.generateFusionSpecies();
|
|
}
|
|
}
|
|
this.luck = (this.shiny ? this.variant + 1 : 0) + (this.fusionShiny ? this.fusionVariant + 1 : 0);
|
|
this.fusionLuck = this.luck;
|
|
}
|
|
|
|
this.generateName();
|
|
|
|
if (!species.isObtainable()) {
|
|
this.shiny = false;
|
|
}
|
|
|
|
this.calculateStats();
|
|
}
|
|
|
|
|
|
getNameToRender() {
|
|
try {
|
|
if (this.nickname) {
|
|
return decodeURIComponent(escape(atob(this.nickname)));
|
|
}
|
|
return this.name;
|
|
} catch (err) {
|
|
console.error(`Failed to decode nickname for ${this.name}`, err);
|
|
return this.name;
|
|
}
|
|
}
|
|
|
|
init(): void {
|
|
this.fieldPosition = FieldPosition.CENTER;
|
|
|
|
this.initBattleInfo();
|
|
|
|
this.scene.fieldUI.addAt(this.battleInfo, 0);
|
|
|
|
const getSprite = (hasShadow?: boolean) => {
|
|
const ret = this.scene.addPokemonSprite(this, 0, 0, `pkmn__${this.isPlayer() ? "back__" : ""}sub`, undefined, true);
|
|
ret.setOrigin(0.5, 1);
|
|
ret.setPipeline(this.scene.spritePipeline, { tone: [ 0.0, 0.0, 0.0, 0.0 ], hasShadow: !!hasShadow, teraColor: getTypeRgb(this.getTeraType()) });
|
|
return ret;
|
|
};
|
|
|
|
this.setScale(this.getSpriteScale());
|
|
|
|
const sprite = getSprite(true);
|
|
const tintSprite = getSprite();
|
|
|
|
tintSprite.setVisible(false);
|
|
|
|
this.addAt(sprite, 0);
|
|
this.addAt(tintSprite, 1);
|
|
|
|
if (this.isShiny() && !this.shinySparkle) {
|
|
this.initShinySparkle();
|
|
}
|
|
}
|
|
|
|
abstract initBattleInfo(): void;
|
|
|
|
isOnField(): boolean {
|
|
if (!this.scene) {
|
|
return false;
|
|
}
|
|
return this.scene.field.getIndex(this) > -1;
|
|
}
|
|
|
|
isFainted(checkStatus?: boolean): boolean {
|
|
return !this.hp && (!checkStatus || this.status?.effect === StatusEffect.FAINT);
|
|
}
|
|
|
|
/**
|
|
* Check if this pokemon is both not fainted and allowed to be in battle.
|
|
* This is frequently a better alternative to {@link isFainted}
|
|
* @returns {boolean} True if pokemon is allowed in battle
|
|
*/
|
|
isAllowedInBattle(): boolean {
|
|
const challengeAllowed = new Utils.BooleanHolder(true);
|
|
applyChallenges(this.scene.gameMode, ChallengeType.POKEMON_IN_BATTLE, this, challengeAllowed);
|
|
return !this.isFainted() && challengeAllowed.value;
|
|
}
|
|
|
|
isActive(onField?: boolean): boolean {
|
|
if (!this.scene) {
|
|
return false;
|
|
}
|
|
return this.isAllowedInBattle() && !!this.scene && (!onField || this.isOnField());
|
|
}
|
|
|
|
getDexAttr(): bigint {
|
|
let ret = 0n;
|
|
ret |= this.gender !== Gender.FEMALE ? DexAttr.MALE : DexAttr.FEMALE;
|
|
ret |= !this.shiny ? DexAttr.NON_SHINY : DexAttr.SHINY;
|
|
ret |= this.variant >= 2 ? DexAttr.VARIANT_3 : this.variant === 1 ? DexAttr.VARIANT_2 : DexAttr.DEFAULT_VARIANT;
|
|
ret |= this.scene.gameData.getFormAttr(this.formIndex);
|
|
return ret;
|
|
}
|
|
|
|
/**
|
|
* Sets the Pokemon's name. Only called when loading a Pokemon so this function needs to be called when
|
|
* initializing hardcoded Pokemon or else it will not display the form index name properly.
|
|
* @returns n/a
|
|
*/
|
|
generateName(): void {
|
|
if (!this.fusionSpecies) {
|
|
this.name = this.species.getName(this.formIndex);
|
|
return;
|
|
}
|
|
this.name = getFusedSpeciesName(this.species.getName(this.formIndex), this.fusionSpecies.getName(this.fusionFormIndex));
|
|
if (this.battleInfo) {
|
|
this.updateInfo(true);
|
|
}
|
|
}
|
|
|
|
abstract isPlayer(): boolean;
|
|
|
|
abstract hasTrainer(): boolean;
|
|
|
|
abstract getFieldIndex(): integer;
|
|
|
|
abstract getBattlerIndex(): BattlerIndex;
|
|
|
|
loadAssets(ignoreOverride: boolean = true): Promise<void> {
|
|
return new Promise(resolve => {
|
|
const moveIds = this.getMoveset().map(m => m.getMove().id);
|
|
Promise.allSettled(moveIds.map(m => initMoveAnim(this.scene, m)))
|
|
.then(() => {
|
|
loadMoveAnimAssets(this.scene, moveIds);
|
|
this.getSpeciesForm().loadAssets(this.scene, this.getGender() === Gender.FEMALE, this.formIndex, this.shiny, this.variant);
|
|
if (this.isPlayer() || this.getFusionSpeciesForm()) {
|
|
this.scene.loadPokemonAtlas(this.getBattleSpriteKey(true, ignoreOverride), this.getBattleSpriteAtlasPath(true, ignoreOverride));
|
|
}
|
|
if (this.getFusionSpeciesForm()) {
|
|
this.getFusionSpeciesForm().loadAssets(this.scene, this.getFusionGender() === Gender.FEMALE, this.fusionFormIndex, this.fusionShiny, this.fusionVariant);
|
|
this.scene.loadPokemonAtlas(this.getFusionBattleSpriteKey(true, ignoreOverride), this.getFusionBattleSpriteAtlasPath(true, ignoreOverride));
|
|
}
|
|
this.scene.load.once(Phaser.Loader.Events.COMPLETE, () => {
|
|
if (this.isPlayer()) {
|
|
const originalWarn = console.warn;
|
|
// Ignore warnings for missing frames, because there will be a lot
|
|
console.warn = () => {};
|
|
const battleFrameNames = this.scene.anims.generateFrameNames(this.getBattleSpriteKey(), { zeroPad: 4, suffix: ".png", start: 1, end: 400 });
|
|
console.warn = originalWarn;
|
|
if (!(this.scene.anims.exists(this.getBattleSpriteKey()))) {
|
|
this.scene.anims.create({
|
|
key: this.getBattleSpriteKey(),
|
|
frames: battleFrameNames,
|
|
frameRate: 12,
|
|
repeat: -1
|
|
});
|
|
}
|
|
}
|
|
this.playAnim();
|
|
const updateFusionPaletteAndResolve = () => {
|
|
this.updateFusionPalette();
|
|
if (this.summonData?.speciesForm) {
|
|
this.updateFusionPalette(true);
|
|
}
|
|
resolve();
|
|
};
|
|
if (this.shiny) {
|
|
const populateVariantColors = (key: string, back: boolean = false): Promise<void> => {
|
|
return new Promise(resolve => {
|
|
const battleSpritePath = this.getBattleSpriteAtlasPath(back, ignoreOverride).replace("variant/", "").replace(/_[1-3]$/, "");
|
|
let config = variantData;
|
|
const useExpSprite = this.scene.experimentalSprites && this.scene.hasExpSprite(this.getBattleSpriteKey(back, ignoreOverride));
|
|
battleSpritePath.split("/").map(p => config ? config = config[p] : null);
|
|
const variantSet: VariantSet = config as VariantSet;
|
|
if (variantSet && variantSet[this.variant] === 1) {
|
|
if (variantColorCache.hasOwnProperty(key)) {
|
|
return resolve();
|
|
}
|
|
this.scene.cachedFetch(`./images/pokemon/variant/${useExpSprite ? "exp/" : ""}${battleSpritePath}.json`).
|
|
then(res => {
|
|
// Prevent the JSON from processing if it failed to load
|
|
if (!res.ok) {
|
|
console.error(`Could not load ${res.url}!`);
|
|
return;
|
|
}
|
|
return res.json();
|
|
}).then(c => {
|
|
variantColorCache[key] = c;
|
|
resolve();
|
|
});
|
|
} else {
|
|
resolve();
|
|
}
|
|
});
|
|
};
|
|
if (this.isPlayer()) {
|
|
Promise.all([ populateVariantColors(this.getBattleSpriteKey(false)), populateVariantColors(this.getBattleSpriteKey(true), true) ]).then(() => updateFusionPaletteAndResolve());
|
|
} else {
|
|
populateVariantColors(this.getBattleSpriteKey(false)).then(() => updateFusionPaletteAndResolve());
|
|
}
|
|
} else {
|
|
updateFusionPaletteAndResolve();
|
|
}
|
|
});
|
|
if (!this.scene.load.isLoading()) {
|
|
this.scene.load.start();
|
|
}
|
|
});
|
|
});
|
|
}
|
|
|
|
getFormKey(): string {
|
|
if (!this.species.forms.length || this.species.forms.length <= this.formIndex) {
|
|
return "";
|
|
}
|
|
return this.species.forms[this.formIndex].formKey;
|
|
}
|
|
|
|
getFusionFormKey(): string {
|
|
if (!this.fusionSpecies) {
|
|
return null;
|
|
}
|
|
if (!this.fusionSpecies.forms.length || this.fusionSpecies.forms.length <= this.fusionFormIndex) {
|
|
return "";
|
|
}
|
|
return this.fusionSpecies.forms[this.fusionFormIndex].formKey;
|
|
}
|
|
|
|
getSpriteAtlasPath(ignoreOverride?: boolean): string {
|
|
const spriteId = this.getSpriteId(ignoreOverride).replace(/\_{2}/g, "/");
|
|
return `${/_[1-3]$/.test(spriteId) ? "variant/" : ""}${spriteId}`;
|
|
}
|
|
|
|
getBattleSpriteAtlasPath(back?: boolean, ignoreOverride?: boolean): string {
|
|
const spriteId = this.getBattleSpriteId(back, ignoreOverride).replace(/\_{2}/g, "/");
|
|
return `${/_[1-3]$/.test(spriteId) ? "variant/" : ""}${spriteId}`;
|
|
}
|
|
|
|
getSpriteId(ignoreOverride?: boolean): string {
|
|
return this.getSpeciesForm(ignoreOverride).getSpriteId(this.getGender(ignoreOverride) === Gender.FEMALE, this.formIndex, this.shiny, this.variant);
|
|
}
|
|
|
|
getBattleSpriteId(back?: boolean, ignoreOverride?: boolean): string {
|
|
if (back === undefined) {
|
|
back = this.isPlayer();
|
|
}
|
|
return this.getSpeciesForm(ignoreOverride).getSpriteId(this.getGender(ignoreOverride) === Gender.FEMALE, this.formIndex, this.shiny, this.variant, back);
|
|
}
|
|
|
|
getSpriteKey(ignoreOverride?: boolean): string {
|
|
return this.getSpeciesForm(ignoreOverride).getSpriteKey(this.getGender(ignoreOverride) === Gender.FEMALE, this.formIndex, this.shiny, this.variant);
|
|
}
|
|
|
|
getBattleSpriteKey(back?: boolean, ignoreOverride?: boolean): string {
|
|
return `pkmn__${this.getBattleSpriteId(back, ignoreOverride)}`;
|
|
}
|
|
|
|
getFusionSpriteId(ignoreOverride?: boolean): string {
|
|
return this.getFusionSpeciesForm(ignoreOverride).getSpriteId(this.getFusionGender(ignoreOverride) === Gender.FEMALE, this.fusionFormIndex, this.fusionShiny, this.fusionVariant);
|
|
}
|
|
|
|
getFusionBattleSpriteId(back?: boolean, ignoreOverride?: boolean): string {
|
|
if (back === undefined) {
|
|
back = this.isPlayer();
|
|
}
|
|
return this.getFusionSpeciesForm(ignoreOverride).getSpriteId(this.getFusionGender(ignoreOverride) === Gender.FEMALE, this.fusionFormIndex, this.fusionShiny, this.fusionVariant, back);
|
|
}
|
|
|
|
getFusionBattleSpriteKey(back?: boolean, ignoreOverride?: boolean): string {
|
|
return `pkmn__${this.getFusionBattleSpriteId(back, ignoreOverride)}`;
|
|
}
|
|
|
|
getFusionBattleSpriteAtlasPath(back?: boolean, ignoreOverride?: boolean): string {
|
|
return this.getFusionBattleSpriteId(back, ignoreOverride).replace(/\_{2}/g, "/");
|
|
}
|
|
|
|
getIconAtlasKey(ignoreOverride?: boolean): string {
|
|
return this.getSpeciesForm(ignoreOverride).getIconAtlasKey(this.formIndex, this.shiny, this.variant);
|
|
}
|
|
|
|
getFusionIconAtlasKey(ignoreOverride?: boolean): string {
|
|
return this.getFusionSpeciesForm(ignoreOverride).getIconAtlasKey(this.fusionFormIndex, this.fusionShiny, this.fusionVariant);
|
|
}
|
|
|
|
getIconId(ignoreOverride?: boolean): string {
|
|
return this.getSpeciesForm(ignoreOverride).getIconId(this.getGender(ignoreOverride) === Gender.FEMALE, this.formIndex, this.shiny, this.variant);
|
|
}
|
|
|
|
getFusionIconId(ignoreOverride?: boolean): string {
|
|
return this.getFusionSpeciesForm(ignoreOverride).getIconId(this.getFusionGender(ignoreOverride) === Gender.FEMALE, this.fusionFormIndex, this.fusionShiny, this.fusionVariant);
|
|
}
|
|
|
|
getSpeciesForm(ignoreOverride?: boolean): PokemonSpeciesForm {
|
|
if (!ignoreOverride && this.summonData?.speciesForm) {
|
|
return this.summonData.speciesForm;
|
|
}
|
|
if (!this.species.forms?.length) {
|
|
return this.species;
|
|
}
|
|
return this.species.forms[this.formIndex];
|
|
}
|
|
|
|
getFusionSpeciesForm(ignoreOverride?: boolean): PokemonSpeciesForm {
|
|
if (!ignoreOverride && this.summonData?.speciesForm) {
|
|
return this.summonData.fusionSpeciesForm;
|
|
}
|
|
if (!this.fusionSpecies?.forms?.length || this.fusionFormIndex >= this.fusionSpecies?.forms.length) {
|
|
return this.fusionSpecies;
|
|
}
|
|
return this.fusionSpecies?.forms[this.fusionFormIndex];
|
|
}
|
|
|
|
getSprite(): Phaser.GameObjects.Sprite {
|
|
return this.getAt(0) as Phaser.GameObjects.Sprite;
|
|
}
|
|
|
|
getTintSprite(): Phaser.GameObjects.Sprite {
|
|
return !this.maskEnabled
|
|
? this.getAt(1) as Phaser.GameObjects.Sprite
|
|
: this.maskSprite;
|
|
}
|
|
|
|
getSpriteScale(): number {
|
|
const formKey = this.getFormKey();
|
|
if (formKey.indexOf(SpeciesFormKey.GIGANTAMAX) > -1 || formKey.indexOf(SpeciesFormKey.ETERNAMAX) > -1) {
|
|
return 1.5;
|
|
}
|
|
return 1;
|
|
}
|
|
|
|
getHeldItems(): PokemonHeldItemModifier[] {
|
|
if (!this.scene) {
|
|
return [];
|
|
}
|
|
return this.scene.findModifiers(m => m instanceof PokemonHeldItemModifier && m.pokemonId === this.id, this.isPlayer()) as PokemonHeldItemModifier[];
|
|
}
|
|
|
|
updateScale(): void {
|
|
this.setScale(this.getSpriteScale());
|
|
}
|
|
|
|
updateSpritePipelineData(): void {
|
|
[ this.getSprite(), this.getTintSprite() ].map(s => s.pipelineData["teraColor"] = getTypeRgb(this.getTeraType()));
|
|
this.updateInfo(true);
|
|
}
|
|
|
|
initShinySparkle(): void {
|
|
const keySuffix = this.variant ? `_${this.variant + 1}` : "";
|
|
const key = `shiny${keySuffix}`;
|
|
const shinySparkle = this.scene.addFieldSprite(0, 0, key);
|
|
shinySparkle.setVisible(false);
|
|
shinySparkle.setOrigin(0.5, 1);
|
|
const frameNames = this.scene.anims.generateFrameNames(key, { suffix: ".png", end: 34 });
|
|
if (!(this.scene.anims.exists(`sparkle${keySuffix}`))) {
|
|
this.scene.anims.create({
|
|
key: `sparkle${keySuffix}`,
|
|
frames: frameNames,
|
|
frameRate: 32,
|
|
showOnStart: true,
|
|
hideOnComplete: true,
|
|
});
|
|
}
|
|
this.add(shinySparkle);
|
|
|
|
this.shinySparkle = shinySparkle;
|
|
}
|
|
|
|
/**
|
|
* Attempts to animate a given {@linkcode Phaser.GameObjects.Sprite}
|
|
* @see {@linkcode Phaser.GameObjects.Sprite.play}
|
|
* @param sprite {@linkcode Phaser.GameObjects.Sprite} to animate
|
|
* @param tintSprite {@linkcode Phaser.GameObjects.Sprite} placed on top of the sprite to add a color tint
|
|
* @param animConfig {@linkcode String} to pass to {@linkcode Phaser.GameObjects.Sprite.play}
|
|
* @returns true if the sprite was able to be animated
|
|
*/
|
|
tryPlaySprite(sprite: Phaser.GameObjects.Sprite, tintSprite: Phaser.GameObjects.Sprite, key: string): boolean {
|
|
// Catch errors when trying to play an animation that doesn't exist
|
|
try {
|
|
sprite.play(key);
|
|
tintSprite.play(key);
|
|
} catch (error: unknown) {
|
|
console.error(`Couldn't play animation for '${key}'!\nIs the image for this Pokemon missing?\n`, error);
|
|
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
playAnim(): void {
|
|
this.tryPlaySprite(this.getSprite(), this.getTintSprite(), this.getBattleSpriteKey());
|
|
}
|
|
|
|
getFieldPositionOffset(): [ number, number ] {
|
|
switch (this.fieldPosition) {
|
|
case FieldPosition.CENTER:
|
|
return [ 0, 0 ];
|
|
case FieldPosition.LEFT:
|
|
return [ -32, -8 ];
|
|
case FieldPosition.RIGHT:
|
|
return [ 32, 0 ];
|
|
}
|
|
}
|
|
|
|
setFieldPosition(fieldPosition: FieldPosition, duration?: integer): Promise<void> {
|
|
return new Promise(resolve => {
|
|
if (fieldPosition === this.fieldPosition) {
|
|
resolve();
|
|
return;
|
|
}
|
|
|
|
const initialOffset = this.getFieldPositionOffset();
|
|
|
|
this.fieldPosition = fieldPosition;
|
|
|
|
this.battleInfo.setMini(fieldPosition !== FieldPosition.CENTER);
|
|
this.battleInfo.setOffset(fieldPosition === FieldPosition.RIGHT);
|
|
|
|
const newOffset = this.getFieldPositionOffset();
|
|
|
|
const relX = newOffset[0] - initialOffset[0];
|
|
const relY = newOffset[1] - initialOffset[1];
|
|
|
|
if (duration) {
|
|
this.scene.tweens.add({
|
|
targets: this,
|
|
x: (_target, _key, value: number) => value + relX,
|
|
y: (_target, _key, value: number) => value + relY,
|
|
duration: duration,
|
|
ease: "Sine.easeOut",
|
|
onComplete: () => resolve()
|
|
});
|
|
} else {
|
|
this.x += relX;
|
|
this.y += relY;
|
|
}
|
|
});
|
|
}
|
|
|
|
getStat(stat: Stat): integer {
|
|
return this.stats[stat];
|
|
}
|
|
|
|
getBattleStat(stat: Stat, opponent?: Pokemon, move?: Move, isCritical: boolean = false): integer {
|
|
if (stat === Stat.HP) {
|
|
return this.getStat(Stat.HP);
|
|
}
|
|
const battleStat = (stat - 1) as BattleStat;
|
|
const statLevel = new Utils.IntegerHolder(this.summonData.battleStats[battleStat]);
|
|
if (opponent) {
|
|
if (isCritical) {
|
|
switch (stat) {
|
|
case Stat.ATK:
|
|
case Stat.SPATK:
|
|
statLevel.value = Math.max(statLevel.value, 0);
|
|
break;
|
|
case Stat.DEF:
|
|
case Stat.SPDEF:
|
|
statLevel.value = Math.min(statLevel.value, 0);
|
|
break;
|
|
}
|
|
}
|
|
applyAbAttrs(IgnoreOpponentStatChangesAbAttr, opponent, null, statLevel);
|
|
if (move) {
|
|
applyMoveAttrs(IgnoreOpponentStatChangesAttr, this, opponent, move, statLevel);
|
|
}
|
|
}
|
|
if (this.isPlayer()) {
|
|
this.scene.applyModifiers(TempBattleStatBoosterModifier, this.isPlayer(), battleStat as integer as TempBattleStat, statLevel);
|
|
}
|
|
const statValue = new Utils.NumberHolder(this.getStat(stat));
|
|
this.scene.applyModifiers(StatBoosterModifier, this.isPlayer(), this, stat, statValue);
|
|
|
|
const fieldApplied = new Utils.BooleanHolder(false);
|
|
for (const pokemon of this.scene.getField(true)) {
|
|
applyFieldBattleStatMultiplierAbAttrs(FieldMultiplyBattleStatAbAttr, pokemon, stat, statValue, this, fieldApplied);
|
|
if (fieldApplied.value) {
|
|
break;
|
|
}
|
|
}
|
|
applyBattleStatMultiplierAbAttrs(BattleStatMultiplierAbAttr, this, battleStat, statValue);
|
|
let ret = statValue.value * (Math.max(2, 2 + statLevel.value) / Math.max(2, 2 - statLevel.value));
|
|
switch (stat) {
|
|
case Stat.ATK:
|
|
if (this.getTag(BattlerTagType.SLOW_START)) {
|
|
ret >>= 1;
|
|
}
|
|
break;
|
|
case Stat.DEF:
|
|
if (this.isOfType(Type.ICE) && this.scene.arena.weather?.weatherType === WeatherType.SNOW) {
|
|
ret *= 1.5;
|
|
}
|
|
break;
|
|
case Stat.SPATK:
|
|
break;
|
|
case Stat.SPDEF:
|
|
if (this.isOfType(Type.ROCK) && this.scene.arena.weather?.weatherType === WeatherType.SANDSTORM) {
|
|
ret *= 1.5;
|
|
}
|
|
break;
|
|
case Stat.SPD:
|
|
// Check both the player and enemy to see if Tailwind should be multiplying the speed of the Pokemon
|
|
if ((this.isPlayer() && this.scene.arena.getTagOnSide(ArenaTagType.TAILWIND, ArenaTagSide.PLAYER))
|
|
|| (!this.isPlayer() && this.scene.arena.getTagOnSide(ArenaTagType.TAILWIND, ArenaTagSide.ENEMY))) {
|
|
ret *= 2;
|
|
}
|
|
|
|
if (this.getTag(BattlerTagType.SLOW_START)) {
|
|
ret >>= 1;
|
|
}
|
|
if (this.status && this.status.effect === StatusEffect.PARALYSIS) {
|
|
ret >>= 1;
|
|
}
|
|
break;
|
|
}
|
|
|
|
const highestStatBoost = this.findTag(t => t instanceof HighestStatBoostTag && (t as HighestStatBoostTag).stat === stat) as HighestStatBoostTag;
|
|
if (highestStatBoost) {
|
|
ret *= highestStatBoost.multiplier;
|
|
}
|
|
|
|
return Math.floor(ret);
|
|
}
|
|
|
|
calculateStats(): void {
|
|
if (!this.stats) {
|
|
this.stats = [ 0, 0, 0, 0, 0, 0 ];
|
|
}
|
|
const baseStats = this.getSpeciesForm().baseStats.slice(0);
|
|
if (this.fusionSpecies) {
|
|
const fusionBaseStats = this.getFusionSpeciesForm().baseStats;
|
|
for (let s = 0; s < this.stats.length; s++) {
|
|
baseStats[s] = Math.ceil((baseStats[s] + fusionBaseStats[s]) / 2);
|
|
}
|
|
} else if (this.scene.gameMode.isSplicedOnly) {
|
|
for (let s = 0; s < this.stats.length; s++) {
|
|
baseStats[s] = Math.ceil(baseStats[s] / 2);
|
|
}
|
|
}
|
|
this.scene.applyModifiers(PokemonBaseStatModifier, this.isPlayer(), this, baseStats);
|
|
const stats = Utils.getEnumValues(Stat);
|
|
for (const s of stats) {
|
|
const isHp = s === Stat.HP;
|
|
const baseStat = baseStats[s];
|
|
let value = Math.floor(((2 * baseStat + this.ivs[s]) * this.level) * 0.01);
|
|
if (isHp) {
|
|
value = value + this.level + 10;
|
|
if (this.hasAbility(Abilities.WONDER_GUARD, false, true)) {
|
|
value = 1;
|
|
}
|
|
if (this.hp > value || this.hp === undefined) {
|
|
this.hp = value;
|
|
} else if (this.hp) {
|
|
const lastMaxHp = this.getMaxHp();
|
|
if (lastMaxHp && value > lastMaxHp) {
|
|
this.hp += value - lastMaxHp;
|
|
}
|
|
}
|
|
} else {
|
|
value += 5;
|
|
const natureStatMultiplier = new Utils.NumberHolder(getNatureStatMultiplier(this.getNature(), s));
|
|
this.scene.applyModifier(PokemonNatureWeightModifier, this.isPlayer(), this, natureStatMultiplier);
|
|
if (natureStatMultiplier.value !== 1) {
|
|
value = Math.max(Math[natureStatMultiplier.value > 1 ? "ceil" : "floor"](value * natureStatMultiplier.value), 1);
|
|
}
|
|
}
|
|
this.stats[s] = value;
|
|
}
|
|
}
|
|
|
|
getNature(): Nature {
|
|
return this.natureOverride !== -1 ? this.natureOverride : this.nature;
|
|
}
|
|
|
|
setNature(nature: Nature): void {
|
|
this.nature = nature;
|
|
this.calculateStats();
|
|
}
|
|
|
|
generateNature(naturePool?: Nature[]): void {
|
|
if (naturePool === undefined) {
|
|
naturePool = Utils.getEnumValues(Nature);
|
|
}
|
|
const nature = naturePool[Utils.randSeedInt(naturePool.length)];
|
|
this.setNature(nature);
|
|
}
|
|
|
|
isFullHp(): boolean {
|
|
return this.hp >= this.getMaxHp();
|
|
}
|
|
|
|
getMaxHp(): integer {
|
|
return this.getStat(Stat.HP);
|
|
}
|
|
|
|
getInverseHp(): integer {
|
|
return this.getMaxHp() - this.hp;
|
|
}
|
|
|
|
getHpRatio(precise: boolean = false): number {
|
|
return precise
|
|
? this.hp / this.getMaxHp()
|
|
: Math.round((this.hp / this.getMaxHp()) * 100) / 100;
|
|
}
|
|
|
|
generateGender(): void {
|
|
if (this.species.malePercent === null) {
|
|
this.gender = Gender.GENDERLESS;
|
|
} else {
|
|
const genderChance = (this.id % 256) * 0.390625;
|
|
if (genderChance < this.species.malePercent) {
|
|
this.gender = Gender.MALE;
|
|
} else {
|
|
this.gender = Gender.FEMALE;
|
|
}
|
|
}
|
|
}
|
|
|
|
getGender(ignoreOverride?: boolean): Gender {
|
|
if (!ignoreOverride && this.summonData?.gender !== undefined) {
|
|
return this.summonData.gender;
|
|
}
|
|
return this.gender;
|
|
}
|
|
|
|
getFusionGender(ignoreOverride?: boolean): Gender {
|
|
if (!ignoreOverride && this.summonData?.fusionGender !== undefined) {
|
|
return this.summonData.fusionGender;
|
|
}
|
|
return this.fusionGender;
|
|
}
|
|
|
|
isShiny(): boolean {
|
|
return this.shiny || (this.isFusion() && this.fusionShiny);
|
|
}
|
|
|
|
getVariant(): Variant {
|
|
return !this.isFusion() ? this.variant : Math.max(this.variant, this.fusionVariant) as Variant;
|
|
}
|
|
|
|
getLuck(): integer {
|
|
return this.luck + (this.isFusion() ? this.fusionLuck : 0);
|
|
}
|
|
|
|
isFusion(): boolean {
|
|
return !!this.fusionSpecies;
|
|
}
|
|
|
|
abstract isBoss(): boolean;
|
|
|
|
getMoveset(ignoreOverride?: boolean): PokemonMove[] {
|
|
const ret = !ignoreOverride && this.summonData?.moveset
|
|
? this.summonData.moveset
|
|
: this.moveset;
|
|
|
|
// Overrides moveset based on arrays specified in overrides.ts
|
|
const overrideArray: Array<Moves> = this.isPlayer() ? Overrides.MOVESET_OVERRIDE : Overrides.OPP_MOVESET_OVERRIDE;
|
|
if (overrideArray.length > 0) {
|
|
overrideArray.forEach((move: Moves, index: number) => {
|
|
const ppUsed = this.moveset[index]?.ppUsed || 0;
|
|
this.moveset[index] = new PokemonMove(move, Math.min(ppUsed, allMoves[move].pp));
|
|
});
|
|
}
|
|
|
|
return ret;
|
|
}
|
|
|
|
/**
|
|
* All moves that could be relearned by this pokemon at this point. Used for memory mushrooms.
|
|
* @returns {Moves[]} The valid moves
|
|
*/
|
|
getLearnableLevelMoves(): Moves[] {
|
|
return this.getLevelMoves(1, true, false, true).map(lm => lm[1]).filter(lm => !this.moveset.filter(m => m.moveId === lm).length).filter((move: Moves, i: integer, array: Moves[]) => array.indexOf(move) === i);
|
|
}
|
|
|
|
/**
|
|
* Gets the types of a pokemon
|
|
* @param includeTeraType boolean to include tera-formed type, default false
|
|
* @param forDefend boolean if the pokemon is defending from an attack
|
|
* @param ignoreOverride boolean if true, ignore ability changing effects
|
|
* @returns array of {@linkcode Type}
|
|
*/
|
|
getTypes(includeTeraType = false, forDefend: boolean = false, ignoreOverride?: boolean): Type[] {
|
|
const types = [];
|
|
|
|
if (includeTeraType) {
|
|
const teraType = this.getTeraType();
|
|
if (teraType !== Type.UNKNOWN) {
|
|
types.push(teraType);
|
|
}
|
|
}
|
|
|
|
if (!types.length || !includeTeraType) {
|
|
if (!ignoreOverride && this.summonData?.types) {
|
|
this.summonData.types.forEach(t => types.push(t));
|
|
} else {
|
|
const speciesForm = this.getSpeciesForm(ignoreOverride);
|
|
|
|
types.push(speciesForm.type1);
|
|
|
|
const fusionSpeciesForm = this.getFusionSpeciesForm(ignoreOverride);
|
|
if (fusionSpeciesForm) {
|
|
if (fusionSpeciesForm.type2 !== null && fusionSpeciesForm.type2 !== speciesForm.type1) {
|
|
types.push(fusionSpeciesForm.type2);
|
|
} else if (fusionSpeciesForm.type1 !== speciesForm.type1) {
|
|
types.push(fusionSpeciesForm.type1);
|
|
}
|
|
}
|
|
|
|
if (types.length === 1 && speciesForm.type2 !== null) {
|
|
types.push(speciesForm.type2);
|
|
}
|
|
}
|
|
}
|
|
|
|
// this.scene potentially can be undefined for a fainted pokemon in doubles
|
|
// use optional chaining to avoid runtime errors
|
|
if (forDefend && (this.getTag(GroundedTag) || this.scene?.arena.getTag(ArenaTagType.GRAVITY))) {
|
|
const flyingIndex = types.indexOf(Type.FLYING);
|
|
if (flyingIndex > -1) {
|
|
types.splice(flyingIndex, 1);
|
|
}
|
|
}
|
|
|
|
if (!types.length) { // become UNKNOWN if no types are present
|
|
types.push(Type.UNKNOWN);
|
|
}
|
|
|
|
if (types.length > 1 && types.includes(Type.UNKNOWN)) { // remove UNKNOWN if other types are present
|
|
const index = types.indexOf(Type.UNKNOWN);
|
|
if (index !== -1) {
|
|
types.splice(index, 1);
|
|
}
|
|
}
|
|
|
|
return types;
|
|
}
|
|
|
|
isOfType(type: Type, includeTeraType: boolean = true, forDefend: boolean = false, ignoreOverride?: boolean): boolean {
|
|
return !!this.getTypes(includeTeraType, forDefend, ignoreOverride).some(t => t === type);
|
|
}
|
|
|
|
/**
|
|
* Gets the non-passive ability of the pokemon. This accounts for fusions and ability changing effects.
|
|
* This should rarely be called, most of the time {@link hasAbility} or {@link hasAbilityWithAttr} are better used as
|
|
* those check both the passive and non-passive abilities and account for ability suppression.
|
|
* @see {@link hasAbility} {@link hasAbilityWithAttr} Intended ways to check abilities in most cases
|
|
* @param {boolean} ignoreOverride If true, ignore ability changing effects
|
|
* @returns {Ability} The non-passive ability of the pokemon
|
|
*/
|
|
getAbility(ignoreOverride?: boolean): Ability {
|
|
if (!ignoreOverride && this.summonData?.ability) {
|
|
return allAbilities[this.summonData.ability];
|
|
}
|
|
if (Overrides.ABILITY_OVERRIDE && this.isPlayer()) {
|
|
return allAbilities[Overrides.ABILITY_OVERRIDE];
|
|
}
|
|
if (Overrides.OPP_ABILITY_OVERRIDE && !this.isPlayer()) {
|
|
return allAbilities[Overrides.OPP_ABILITY_OVERRIDE];
|
|
}
|
|
if (this.isFusion()) {
|
|
return allAbilities[this.getFusionSpeciesForm(ignoreOverride).getAbility(this.fusionAbilityIndex)];
|
|
}
|
|
let abilityId = this.getSpeciesForm(ignoreOverride).getAbility(this.abilityIndex);
|
|
if (abilityId === Abilities.NONE) {
|
|
abilityId = this.species.ability1;
|
|
}
|
|
return allAbilities[abilityId];
|
|
}
|
|
|
|
/**
|
|
* Gets the passive ability of the pokemon. This should rarely be called, most of the time
|
|
* {@link hasAbility} or {@link hasAbilityWithAttr} are better used as those check both the passive and
|
|
* non-passive abilities and account for ability suppression.
|
|
* @see {@link hasAbility} {@link hasAbilityWithAttr} Intended ways to check abilities in most cases
|
|
* @returns {Ability} The passive ability of the pokemon
|
|
*/
|
|
getPassiveAbility(): Ability {
|
|
if (Overrides.PASSIVE_ABILITY_OVERRIDE && this.isPlayer()) {
|
|
return allAbilities[Overrides.PASSIVE_ABILITY_OVERRIDE];
|
|
}
|
|
if (Overrides.OPP_PASSIVE_ABILITY_OVERRIDE && !this.isPlayer()) {
|
|
return allAbilities[Overrides.OPP_PASSIVE_ABILITY_OVERRIDE];
|
|
}
|
|
|
|
let starterSpeciesId = this.species.speciesId;
|
|
while (pokemonPrevolutions.hasOwnProperty(starterSpeciesId)) {
|
|
starterSpeciesId = pokemonPrevolutions[starterSpeciesId];
|
|
}
|
|
return allAbilities[starterPassiveAbilities[starterSpeciesId]];
|
|
}
|
|
|
|
/**
|
|
* Gets a list of all instances of a given ability attribute among abilities this pokemon has.
|
|
* Accounts for all the various effects which can affect whether an ability will be present or
|
|
* in effect, and both passive and non-passive.
|
|
* @param attrType {@linkcode AbAttr} The ability attribute to check for.
|
|
* @param canApply {@linkcode Boolean} If false, it doesn't check whether the ability is currently active
|
|
* @param ignoreOverride {@linkcode Boolean} If true, it ignores ability changing effects
|
|
* @returns {AbAttr[]} A list of all the ability attributes on this ability.
|
|
*/
|
|
getAbilityAttrs(attrType: { new(...args: any[]): AbAttr }, canApply: boolean = true, ignoreOverride?: boolean): AbAttr[] {
|
|
const abilityAttrs: AbAttr[] = [];
|
|
|
|
if (!canApply || this.canApplyAbility()) {
|
|
abilityAttrs.push(...this.getAbility(ignoreOverride).getAttrs(attrType));
|
|
}
|
|
|
|
if (!canApply || this.canApplyAbility(true)) {
|
|
abilityAttrs.push(...this.getPassiveAbility().getAttrs(attrType));
|
|
}
|
|
|
|
return abilityAttrs;
|
|
}
|
|
|
|
/**
|
|
* Checks if a pokemon has a passive either from:
|
|
* - bought with starter candy
|
|
* - set by override
|
|
* - is a boss pokemon
|
|
* @returns whether or not a pokemon should have a passive
|
|
*/
|
|
hasPassive(): boolean {
|
|
// returns override if valid for current case
|
|
if ((Overrides.PASSIVE_ABILITY_OVERRIDE !== Abilities.NONE && this.isPlayer()) ||
|
|
(Overrides.OPP_PASSIVE_ABILITY_OVERRIDE !== Abilities.NONE && !this.isPlayer())) {
|
|
return true;
|
|
}
|
|
return this.passive || this.isBoss();
|
|
}
|
|
|
|
/**
|
|
* Checks whether an ability of a pokemon can be currently applied. This should rarely be
|
|
* directly called, as {@link hasAbility} and {@link hasAbilityWithAttr} already call this.
|
|
* @see {@link hasAbility} {@link hasAbilityWithAttr} Intended ways to check abilities in most cases
|
|
* @param {boolean} passive If true, check if passive can be applied instead of non-passive
|
|
* @returns {Ability} The passive ability of the pokemon
|
|
*/
|
|
canApplyAbility(passive: boolean = false): boolean {
|
|
if (passive && !this.hasPassive()) {
|
|
return false;
|
|
}
|
|
const ability = (!passive ? this.getAbility() : this.getPassiveAbility());
|
|
if (this.isFusion() && ability.hasAttr(NoFusionAbilityAbAttr)) {
|
|
return false;
|
|
}
|
|
if (this.scene?.arena.ignoreAbilities && ability.isIgnorable) {
|
|
return false;
|
|
}
|
|
if (this.summonData?.abilitySuppressed && !ability.hasAttr(UnsuppressableAbilityAbAttr)) {
|
|
return false;
|
|
}
|
|
if (this.isOnField() && !ability.hasAttr(SuppressFieldAbilitiesAbAttr)) {
|
|
const suppressed = new Utils.BooleanHolder(false);
|
|
this.scene.getField(true).filter(p => p !== this).map(p => {
|
|
if (p.getAbility().hasAttr(SuppressFieldAbilitiesAbAttr) && p.canApplyAbility()) {
|
|
p.getAbility().getAttrs(SuppressFieldAbilitiesAbAttr).map(a => a.apply(this, false, suppressed, [ability]));
|
|
}
|
|
if (p.getPassiveAbility().hasAttr(SuppressFieldAbilitiesAbAttr) && p.canApplyAbility(true)) {
|
|
p.getPassiveAbility().getAttrs(SuppressFieldAbilitiesAbAttr).map(a => a.apply(this, true, suppressed, [ability]));
|
|
}
|
|
});
|
|
if (suppressed.value) {
|
|
return false;
|
|
}
|
|
}
|
|
return (this.hp || ability.isBypassFaint) && !ability.conditions.find(condition => !condition(this));
|
|
}
|
|
|
|
/**
|
|
* Checks whether a pokemon has the specified ability and it's in effect. Accounts for all the various
|
|
* effects which can affect whether an ability will be present or in effect, and both passive and
|
|
* non-passive. This is the primary way to check whether a pokemon has a particular ability.
|
|
* @param {Abilities} ability The ability to check for
|
|
* @param {boolean} canApply If false, it doesn't check whether the abiltiy is currently active
|
|
* @param {boolean} ignoreOverride If true, it ignores ability changing effects
|
|
* @returns {boolean} Whether the ability is present and active
|
|
*/
|
|
hasAbility(ability: Abilities, canApply: boolean = true, ignoreOverride?: boolean): boolean {
|
|
if ((!canApply || this.canApplyAbility()) && this.getAbility(ignoreOverride).id === ability) {
|
|
return true;
|
|
}
|
|
if (this.hasPassive() && (!canApply || this.canApplyAbility(true)) && this.getPassiveAbility().id === ability) {
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* Checks whether a pokemon has an ability with the specified attribute and it's in effect.
|
|
* Accounts for all the various effects which can affect whether an ability will be present or
|
|
* in effect, and both passive and non-passive. This is one of the two primary ways to check
|
|
* whether a pokemon has a particular ability.
|
|
* @param {AbAttr} attrType The ability attribute to check for
|
|
* @param {boolean} canApply If false, it doesn't check whether the ability is currently active
|
|
* @param {boolean} ignoreOverride If true, it ignores ability changing effects
|
|
* @returns {boolean} Whether an ability with that attribute is present and active
|
|
*/
|
|
hasAbilityWithAttr(attrType: Constructor<AbAttr>, canApply: boolean = true, ignoreOverride?: boolean): boolean {
|
|
if ((!canApply || this.canApplyAbility()) && this.getAbility(ignoreOverride).hasAttr(attrType)) {
|
|
return true;
|
|
}
|
|
if (this.hasPassive() && (!canApply || this.canApplyAbility(true)) && this.getPassiveAbility().hasAttr(attrType)) {
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
getWeight(): number {
|
|
const weight = new Utils.NumberHolder(this.species.weight);
|
|
// This will trigger the ability overlay so only call this function when necessary
|
|
applyAbAttrs(WeightMultiplierAbAttr, this, null, weight);
|
|
return weight.value;
|
|
}
|
|
|
|
/**
|
|
* Gets the tera-formed type of the pokemon, or UNKNOWN if not present
|
|
* @returns the {@linkcode Type}
|
|
*/
|
|
getTeraType(): Type {
|
|
// this.scene can be undefined for a fainted mon in doubles
|
|
if (this.scene !== undefined) {
|
|
const teraModifier = this.scene.findModifier(m => m instanceof TerastallizeModifier
|
|
&& m.pokemonId === this.id && !!m.getBattlesLeft(), this.isPlayer()) as TerastallizeModifier;
|
|
// return teraType
|
|
if (teraModifier) {
|
|
return teraModifier.teraType;
|
|
}
|
|
}
|
|
// if scene is undefined, or if teraModifier is considered false, then return unknown type
|
|
return Type.UNKNOWN;
|
|
}
|
|
|
|
isTerastallized(): boolean {
|
|
return this.getTeraType() !== Type.UNKNOWN;
|
|
}
|
|
|
|
isGrounded(): boolean {
|
|
return !!this.getTag(GroundedTag) || (!this.isOfType(Type.FLYING, true, true) && !this.hasAbility(Abilities.LEVITATE) && !this.getTag(BattlerTagType.MAGNET_RISEN) && !this.getTag(SemiInvulnerableTag));
|
|
}
|
|
|
|
/**
|
|
* Calculates the effectiveness of a move against the Pokémon.
|
|
*
|
|
* @param source - The Pokémon using the move.
|
|
* @param move - The move being used.
|
|
* @returns The type damage multiplier or undefined if it's a status move
|
|
*/
|
|
getMoveEffectiveness(source: Pokemon, move: PokemonMove): TypeDamageMultiplier | undefined {
|
|
if (move.getMove().category === MoveCategory.STATUS) {
|
|
return undefined;
|
|
}
|
|
|
|
return this.getAttackMoveEffectiveness(source, move, !this.battleData?.abilityRevealed);
|
|
}
|
|
|
|
/**
|
|
* Calculates the effectiveness of an attack move against the Pokémon.
|
|
*
|
|
* @param source - The attacking Pokémon.
|
|
* @param pokemonMove - The move being used by the attacking Pokémon.
|
|
* @param ignoreAbility - Whether to check for abilities that might affect type effectiveness or immunity.
|
|
* @returns The type damage multiplier, indicating the effectiveness of the move
|
|
*/
|
|
getAttackMoveEffectiveness(source: Pokemon, pokemonMove: PokemonMove, ignoreAbility: boolean = false): TypeDamageMultiplier {
|
|
const move = pokemonMove.getMove();
|
|
const typeless = move.hasAttr(TypelessAttr);
|
|
const typeMultiplier = new Utils.NumberHolder(this.getAttackTypeEffectiveness(move, source));
|
|
const cancelled = new Utils.BooleanHolder(false);
|
|
applyMoveAttrs(VariableMoveTypeMultiplierAttr, source, this, move, typeMultiplier);
|
|
if (!typeless && !ignoreAbility) {
|
|
applyPreDefendAbAttrs(TypeImmunityAbAttr, this, source, move, cancelled, typeMultiplier, true);
|
|
}
|
|
if (!cancelled.value && !ignoreAbility) {
|
|
applyPreDefendAbAttrs(MoveImmunityAbAttr, this, source, move, cancelled, typeMultiplier, true);
|
|
}
|
|
|
|
return (!cancelled.value ? Number(typeMultiplier.value) : 0) as TypeDamageMultiplier;
|
|
}
|
|
|
|
/**
|
|
* Calculates the type effectiveness multiplier for an attack type
|
|
* @param moveOrType The move being used, or a type if the move is unknown
|
|
* @param source the Pokemon using the move
|
|
* @param ignoreStrongWinds whether or not this ignores strong winds (anticipation, forewarn, stealth rocks)
|
|
* @param simulated tag to only apply the strong winds effect message when the move is used
|
|
* @returns a multiplier for the type effectiveness
|
|
*/
|
|
getAttackTypeEffectiveness(moveOrType: Move | Type, source?: Pokemon, ignoreStrongWinds: boolean = false, simulated: boolean = true): TypeDamageMultiplier {
|
|
const move = (moveOrType instanceof Move)
|
|
? moveOrType
|
|
: undefined;
|
|
const moveType = (moveOrType instanceof Move)
|
|
? move.type
|
|
: moveOrType;
|
|
|
|
if (moveType === Type.STELLAR) {
|
|
return this.isTerastallized() ? 2 : 1;
|
|
}
|
|
const types = this.getTypes(true, true);
|
|
|
|
let multiplier = types.map(defType => {
|
|
if (source) {
|
|
const ignoreImmunity = new Utils.BooleanHolder(false);
|
|
if (source.isActive(true) && source.hasAbilityWithAttr(IgnoreTypeImmunityAbAttr)) {
|
|
applyAbAttrs(IgnoreTypeImmunityAbAttr, source, ignoreImmunity, moveType, defType);
|
|
}
|
|
if (ignoreImmunity.value) {
|
|
return 1;
|
|
}
|
|
}
|
|
|
|
return getTypeDamageMultiplier(moveType, defType);
|
|
}).reduce((acc, cur) => acc * cur, 1) as TypeDamageMultiplier;
|
|
|
|
// Handle strong winds lowering effectiveness of types super effective against pure flying
|
|
if (!ignoreStrongWinds && this.scene.arena.weather?.weatherType === WeatherType.STRONG_WINDS && !this.scene.arena.weather.isEffectSuppressed(this.scene) && this.isOfType(Type.FLYING) && getTypeDamageMultiplier(moveType, Type.FLYING) === 2) {
|
|
multiplier /= 2;
|
|
if (!simulated) {
|
|
this.scene.queueMessage(i18next.t("weather:strongWindsEffectMessage"));
|
|
}
|
|
}
|
|
|
|
const immuneTags = this.findTags(tag => tag instanceof TypeImmuneTag && tag.immuneType === moveType);
|
|
for (const tag of immuneTags) {
|
|
if (move && !move.getAttrs(HitsTagAttr).some(attr => attr.tagType === tag.tagType)) {
|
|
multiplier = 0;
|
|
break;
|
|
}
|
|
}
|
|
|
|
return multiplier;
|
|
}
|
|
|
|
getMatchupScore(pokemon: Pokemon): number {
|
|
const types = this.getTypes(true);
|
|
const enemyTypes = pokemon.getTypes(true, true);
|
|
const outspeed = (this.isActive(true) ? this.getBattleStat(Stat.SPD, pokemon) : this.getStat(Stat.SPD)) <= pokemon.getBattleStat(Stat.SPD, this);
|
|
let atkScore = pokemon.getAttackTypeEffectiveness(types[0], this) * (outspeed ? 1.25 : 1);
|
|
let defScore = 1 / Math.max(this.getAttackTypeEffectiveness(enemyTypes[0], pokemon), 0.25);
|
|
if (types.length > 1) {
|
|
atkScore *= pokemon.getAttackTypeEffectiveness(types[1], this);
|
|
}
|
|
if (enemyTypes.length > 1) {
|
|
defScore *= (1 / Math.max(this.getAttackTypeEffectiveness(enemyTypes[1], pokemon), 0.25));
|
|
}
|
|
let hpDiffRatio = this.getHpRatio() + (1 - pokemon.getHpRatio());
|
|
if (outspeed) {
|
|
hpDiffRatio = Math.min(hpDiffRatio * 1.5, 1);
|
|
}
|
|
return (atkScore + defScore) * hpDiffRatio;
|
|
}
|
|
|
|
getEvolution(): SpeciesFormEvolution {
|
|
if (pokemonEvolutions.hasOwnProperty(this.species.speciesId)) {
|
|
const evolutions = pokemonEvolutions[this.species.speciesId];
|
|
for (const e of evolutions) {
|
|
if (!e.item && this.level >= e.level && (!e.preFormKey || this.getFormKey() === e.preFormKey)) {
|
|
if (e.condition === null || (e.condition as SpeciesEvolutionCondition).predicate(this)) {
|
|
return e;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if (this.isFusion() && pokemonEvolutions.hasOwnProperty(this.fusionSpecies.speciesId)) {
|
|
const fusionEvolutions = pokemonEvolutions[this.fusionSpecies.speciesId].map(e => new FusionSpeciesFormEvolution(this.species.speciesId, e));
|
|
for (const fe of fusionEvolutions) {
|
|
if (!fe.item && this.level >= fe.level && (!fe.preFormKey || this.getFusionFormKey() === fe.preFormKey)) {
|
|
if (fe.condition === null || (fe.condition as SpeciesEvolutionCondition).predicate(this)) {
|
|
return fe;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
/**
|
|
* Gets all level up moves in a given range for a particular pokemon.
|
|
* @param {integer} startingLevel Don't include moves below this level
|
|
* @param {boolean} includeEvolutionMoves Whether to include evolution moves
|
|
* @param {boolean} simulateEvolutionChain Whether to include moves from prior evolutions
|
|
* @param {boolean} includeRelearnerMoves Whether to include moves that would require a relearner. Note the move relearner inherently allows evolution moves
|
|
* @returns {LevelMoves} A list of moves and the levels they can be learned at
|
|
*/
|
|
getLevelMoves(startingLevel?: integer, includeEvolutionMoves: boolean = false, simulateEvolutionChain: boolean = false, includeRelearnerMoves: boolean = false): LevelMoves {
|
|
const ret: LevelMoves = [];
|
|
let levelMoves: LevelMoves = [];
|
|
if (!startingLevel) {
|
|
startingLevel = this.level;
|
|
}
|
|
if (simulateEvolutionChain) {
|
|
const evolutionChain = this.species.getSimulatedEvolutionChain(this.level, this.hasTrainer(), this.isBoss(), this.isPlayer());
|
|
for (let e = 0; e < evolutionChain.length; e++) {
|
|
// TODO: Might need to pass specific form index in simulated evolution chain
|
|
const speciesLevelMoves = getPokemonSpeciesForm(evolutionChain[e][0] as Species, this.formIndex).getLevelMoves();
|
|
if (includeRelearnerMoves) {
|
|
levelMoves.push(...speciesLevelMoves);
|
|
} else {
|
|
levelMoves.push(...speciesLevelMoves.filter(lm => (includeEvolutionMoves && lm[0] === 0) || ((!e || lm[0] > 1) && (e === evolutionChain.length - 1 || lm[0] <= evolutionChain[e + 1][1]))));
|
|
}
|
|
}
|
|
} else {
|
|
levelMoves = this.getSpeciesForm(true).getLevelMoves().filter(lm => (includeEvolutionMoves && lm[0] === 0) || (includeRelearnerMoves && lm[0] === -1) || lm[0] > 0);
|
|
}
|
|
if (this.fusionSpecies) {
|
|
if (simulateEvolutionChain) {
|
|
const fusionEvolutionChain = this.fusionSpecies.getSimulatedEvolutionChain(this.level, this.hasTrainer(), this.isBoss(), this.isPlayer());
|
|
for (let e = 0; e < fusionEvolutionChain.length; e++) {
|
|
// TODO: Might need to pass specific form index in simulated evolution chain
|
|
const speciesLevelMoves = getPokemonSpeciesForm(fusionEvolutionChain[e][0] as Species, this.fusionFormIndex).getLevelMoves();
|
|
if (includeRelearnerMoves) {
|
|
levelMoves.push(...speciesLevelMoves.filter(lm => (includeEvolutionMoves && lm[0] === 0) || lm[0] !== 0));
|
|
} else {
|
|
levelMoves.push(...speciesLevelMoves.filter(lm => (includeEvolutionMoves && lm[0] === 0) || ((!e || lm[0] > 1) && (e === fusionEvolutionChain.length - 1 || lm[0] <= fusionEvolutionChain[e + 1][1]))));
|
|
}
|
|
}
|
|
} else {
|
|
levelMoves.push(...this.getFusionSpeciesForm(true).getLevelMoves().filter(lm => (includeEvolutionMoves && lm[0] === 0) || (includeRelearnerMoves && lm[0] === -1) || lm[0] > 0));
|
|
}
|
|
}
|
|
levelMoves.sort((lma: [integer, integer], lmb: [integer, integer]) => lma[0] > lmb[0] ? 1 : lma[0] < lmb[0] ? -1 : 0);
|
|
const uniqueMoves: Moves[] = [];
|
|
levelMoves = levelMoves.filter(lm => {
|
|
if (uniqueMoves.find(m => m === lm[1])) {
|
|
return false;
|
|
}
|
|
uniqueMoves.push(lm[1]);
|
|
return true;
|
|
});
|
|
|
|
if (levelMoves) {
|
|
for (const lm of levelMoves) {
|
|
const level = lm[0];
|
|
if (!includeRelearnerMoves && ((level > 0 && level < startingLevel) || (!includeEvolutionMoves && level === 0) || level < 0)) {
|
|
continue;
|
|
} else if (level > this.level) {
|
|
break;
|
|
}
|
|
ret.push(lm);
|
|
}
|
|
}
|
|
|
|
return ret;
|
|
}
|
|
|
|
setMove(moveIndex: integer, moveId: Moves): void {
|
|
const move = moveId ? new PokemonMove(moveId) : null;
|
|
this.moveset[moveIndex] = move;
|
|
if (this.summonData?.moveset) {
|
|
this.summonData.moveset[moveIndex] = move;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Function that tries to set a Pokemon shiny based on the trainer's trainer ID and secret ID
|
|
* Endless Pokemon in the end biome are unable to be set to shiny
|
|
*
|
|
* The exact mechanic is that it calculates E as the XOR of the player's trainer ID and secret ID
|
|
* F is calculated as the XOR of the first 16 bits of the Pokemon's ID with the last 16 bits
|
|
* The XOR of E and F are then compared to the thresholdOverride (default case 32) to see whether or not to generate a shiny
|
|
* @param thresholdOverride number that is divided by 2^16 (65536) to get the shiny chance
|
|
* @returns true if the Pokemon has been set as a shiny, false otherwise
|
|
*/
|
|
trySetShiny(thresholdOverride?: integer): boolean {
|
|
// Shiny Pokemon should not spawn in the end biome in endless
|
|
if (this.scene.gameMode.isEndless && this.scene.arena.biomeType === Biome.END) {
|
|
return false;
|
|
}
|
|
|
|
const rand1 = Utils.binToDec(Utils.decToBin(this.id).substring(0, 16));
|
|
const rand2 = Utils.binToDec(Utils.decToBin(this.id).substring(16, 32));
|
|
|
|
const E = this.scene.gameData.trainerId ^ this.scene.gameData.secretId;
|
|
const F = rand1 ^ rand2;
|
|
|
|
const shinyThreshold = new Utils.IntegerHolder(32);
|
|
if (thresholdOverride === undefined) {
|
|
if (this.scene.eventManager.isEventActive()) {
|
|
shinyThreshold.value *= this.scene.eventManager.getShinyMultiplier();
|
|
}
|
|
if (!this.hasTrainer()) {
|
|
this.scene.applyModifiers(ShinyRateBoosterModifier, true, shinyThreshold);
|
|
}
|
|
} else {
|
|
shinyThreshold.value = thresholdOverride;
|
|
}
|
|
|
|
this.shiny = (E ^ F) < shinyThreshold.value;
|
|
if ((E ^ F) < 32) {
|
|
console.log("REAL SHINY!!");
|
|
}
|
|
|
|
if (this.shiny) {
|
|
this.initShinySparkle();
|
|
}
|
|
|
|
return this.shiny;
|
|
}
|
|
|
|
/**
|
|
* Generates a variant
|
|
* Has a 10% of returning 2 (epic variant)
|
|
* And a 30% of returning 1 (rare variant)
|
|
* Returns 0 (basic shiny) if there is no variant or 60% of the time otherwise
|
|
* @returns the shiny variant
|
|
*/
|
|
generateVariant(): Variant {
|
|
const formIndex: number = this.formIndex;
|
|
let variantDataIndex: string | number = this.species.speciesId;
|
|
if (this.species.forms.length > 0) {
|
|
const formKey = this.species.forms[formIndex]?.formKey;
|
|
if (formKey) {
|
|
variantDataIndex = `${variantDataIndex}-${formKey}`;
|
|
}
|
|
}
|
|
// Checks if there is no variant data for both the index or index with form
|
|
if (!this.shiny || (!variantData.hasOwnProperty(variantDataIndex) && !variantData.hasOwnProperty(this.species.speciesId))) {
|
|
return 0;
|
|
}
|
|
const rand = Utils.randSeedInt(10);
|
|
if (rand >= 4) {
|
|
return 0; // 6/10
|
|
} else if (rand >= 1) {
|
|
return 1; // 3/10
|
|
} else {
|
|
return 2; // 1/10
|
|
}
|
|
}
|
|
|
|
generateFusionSpecies(forStarter?: boolean): void {
|
|
const hiddenAbilityChance = new Utils.IntegerHolder(256);
|
|
if (!this.hasTrainer()) {
|
|
this.scene.applyModifiers(HiddenAbilityRateBoosterModifier, true, hiddenAbilityChance);
|
|
}
|
|
|
|
const hasHiddenAbility = !Utils.randSeedInt(hiddenAbilityChance.value);
|
|
const randAbilityIndex = Utils.randSeedInt(2);
|
|
|
|
const filter = !forStarter ? this.species.getCompatibleFusionSpeciesFilter()
|
|
: species => {
|
|
return pokemonEvolutions.hasOwnProperty(species.speciesId)
|
|
&& !pokemonPrevolutions.hasOwnProperty(species.speciesId)
|
|
&& !species.pseudoLegendary
|
|
&& !species.legendary
|
|
&& !species.mythical
|
|
&& !species.isTrainerForbidden()
|
|
&& species.speciesId !== this.species.speciesId;
|
|
};
|
|
|
|
this.fusionSpecies = this.scene.randomSpecies(this.scene.currentBattle?.waveIndex || 0, this.level, false, filter, true);
|
|
this.fusionAbilityIndex = (this.fusionSpecies.abilityHidden && hasHiddenAbility ? this.fusionSpecies.ability2 ? 2 : 1 : this.fusionSpecies.ability2 ? randAbilityIndex : 0);
|
|
this.fusionShiny = this.shiny;
|
|
this.fusionVariant = this.variant;
|
|
|
|
if (this.fusionSpecies.malePercent === null) {
|
|
this.fusionGender = Gender.GENDERLESS;
|
|
} else {
|
|
const genderChance = (this.id % 256) * 0.390625;
|
|
if (genderChance < this.fusionSpecies.malePercent) {
|
|
this.fusionGender = Gender.MALE;
|
|
} else {
|
|
this.fusionGender = Gender.FEMALE;
|
|
}
|
|
}
|
|
|
|
this.fusionFormIndex = this.scene.getSpeciesFormIndex(this.fusionSpecies, this.fusionGender, this.getNature(), true);
|
|
this.fusionLuck = this.luck;
|
|
|
|
this.generateName();
|
|
}
|
|
|
|
clearFusionSpecies(): void {
|
|
this.fusionSpecies = undefined;
|
|
this.fusionFormIndex = 0;
|
|
this.fusionAbilityIndex = 0;
|
|
this.fusionShiny = false;
|
|
this.fusionVariant = 0;
|
|
this.fusionGender = 0;
|
|
this.fusionLuck = 0;
|
|
|
|
this.generateName();
|
|
this.calculateStats();
|
|
}
|
|
|
|
generateAndPopulateMoveset(): void {
|
|
this.moveset = [];
|
|
let movePool: [Moves, number][] = [];
|
|
const allLevelMoves = this.getLevelMoves(1, true, true);
|
|
if (!allLevelMoves) {
|
|
console.log(this.species.speciesId, "ERROR");
|
|
return;
|
|
}
|
|
|
|
for (let m = 0; m < allLevelMoves.length; m++) {
|
|
const levelMove = allLevelMoves[m];
|
|
if (this.level < levelMove[0]) {
|
|
break;
|
|
}
|
|
let weight = levelMove[0];
|
|
if (weight === 0) { // Evo Moves
|
|
weight = 50;
|
|
}
|
|
if (weight === 1 && allMoves[levelMove[1]].power >= 80) { // Assume level 1 moves with 80+ BP are "move reminder" moves and bump their weight
|
|
weight = 40;
|
|
}
|
|
if (allMoves[levelMove[1]].name.endsWith(" (N)")) {
|
|
weight /= 100;
|
|
} // Unimplemented level up moves are possible to generate, but 1% of their normal chance.
|
|
if (!movePool.some(m => m[0] === levelMove[1])) {
|
|
movePool.push([levelMove[1], weight]);
|
|
}
|
|
}
|
|
|
|
if (this.hasTrainer()) {
|
|
const tms = Object.keys(tmSpecies);
|
|
for (const tm of tms) {
|
|
const moveId = parseInt(tm) as Moves;
|
|
let compatible = false;
|
|
for (const p of tmSpecies[tm]) {
|
|
if (Array.isArray(p)) {
|
|
if (p[0] === this.species.speciesId || (this.fusionSpecies && p[0] === this.fusionSpecies.speciesId) && p.slice(1).indexOf(this.species.forms[this.formIndex]) > -1) {
|
|
compatible = true;
|
|
break;
|
|
}
|
|
} else if (p === this.species.speciesId || (this.fusionSpecies && p === this.fusionSpecies.speciesId)) {
|
|
compatible = true;
|
|
break;
|
|
}
|
|
}
|
|
if (compatible && !movePool.some(m => m[0] === moveId) && !allMoves[moveId].name.endsWith(" (N)")) {
|
|
if (tmPoolTiers[moveId] === ModifierTier.COMMON && this.level >= 15) {
|
|
movePool.push([moveId, 4]);
|
|
} else if (tmPoolTiers[moveId] === ModifierTier.GREAT && this.level >= 30) {
|
|
movePool.push([moveId, 8]);
|
|
} else if (tmPoolTiers[moveId] === ModifierTier.ULTRA && this.level >= 50) {
|
|
movePool.push([moveId, 14]);
|
|
}
|
|
}
|
|
}
|
|
|
|
if (this.level >= 60) { // No egg moves below level 60
|
|
for (let i = 0; i < 3; i++) {
|
|
const moveId = speciesEggMoves[this.species.getRootSpeciesId()][i];
|
|
if (!movePool.some(m => m[0] === moveId) && !allMoves[moveId].name.endsWith(" (N)")) {
|
|
movePool.push([moveId, 40]);
|
|
}
|
|
}
|
|
const moveId = speciesEggMoves[this.species.getRootSpeciesId()][3];
|
|
if (this.level >= 170 && !movePool.some(m => m[0] === moveId) && !allMoves[moveId].name.endsWith(" (N)") && !this.isBoss()) { // No rare egg moves before e4
|
|
movePool.push([moveId, 30]);
|
|
}
|
|
if (this.fusionSpecies) {
|
|
for (let i = 0; i < 3; i++) {
|
|
const moveId = speciesEggMoves[this.fusionSpecies.getRootSpeciesId()][i];
|
|
if (!movePool.some(m => m[0] === moveId) && !allMoves[moveId].name.endsWith(" (N)")) {
|
|
movePool.push([moveId, 40]);
|
|
}
|
|
}
|
|
const moveId = speciesEggMoves[this.fusionSpecies.getRootSpeciesId()][3];
|
|
if (this.level >= 170 && !movePool.some(m => m[0] === moveId) && !allMoves[moveId].name.endsWith(" (N)") && !this.isBoss()) {// No rare egg moves before e4
|
|
movePool.push([moveId, 30]);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if (this.isBoss()) { // Bosses never get self ko moves
|
|
movePool = movePool.filter(m => !allMoves[m[0]].hasAttr(SacrificialAttr));
|
|
}
|
|
movePool = movePool.filter(m => !allMoves[m[0]].hasAttr(SacrificialAttrOnHit));
|
|
if (this.hasTrainer()) {
|
|
// Trainers never get OHKO moves
|
|
movePool = movePool.filter(m => !allMoves[m[0]].hasAttr(OneHitKOAttr));
|
|
// Half the weight of self KO moves
|
|
movePool = movePool.map(m => [m[0], m[1] * (!!allMoves[m[0]].hasAttr(SacrificialAttr) ? 0.5 : 1)]);
|
|
movePool = movePool.map(m => [m[0], m[1] * (!!allMoves[m[0]].hasAttr(SacrificialAttrOnHit) ? 0.5 : 1)]);
|
|
// Trainers get a weight bump to stat buffing moves
|
|
movePool = movePool.map(m => [m[0], m[1] * (allMoves[m[0]].getAttrs(StatChangeAttr).some(a => a.levels > 1 && a.selfTarget) ? 1.25 : 1)]);
|
|
// Trainers get a weight decrease to multiturn moves
|
|
movePool = movePool.map(m => [m[0], m[1] * (!!allMoves[m[0]].hasAttr(ChargeAttr) || !!allMoves[m[0]].hasAttr(RechargeAttr) ? 0.7 : 1)]);
|
|
}
|
|
|
|
// Weight towards higher power moves, by reducing the power of moves below the highest power.
|
|
// Caps max power at 90 to avoid something like hyper beam ruining the stats.
|
|
// This is a pretty soft weighting factor, although it is scaled with the weight multiplier.
|
|
const maxPower = Math.min(movePool.reduce((v, m) => Math.max(allMoves[m[0]].power, v), 40), 90);
|
|
movePool = movePool.map(m => [m[0], m[1] * (allMoves[m[0]].category === MoveCategory.STATUS ? 1 : Math.max(Math.min(allMoves[m[0]].power/maxPower, 1), 0.5))]);
|
|
|
|
// Weight damaging moves against the lower stat
|
|
const worseCategory: MoveCategory = this.stats[Stat.ATK] > this.stats[Stat.SPATK] ? MoveCategory.SPECIAL : MoveCategory.PHYSICAL;
|
|
const statRatio = worseCategory === MoveCategory.PHYSICAL ? this.stats[Stat.ATK]/this.stats[Stat.SPATK] : this.stats[Stat.SPATK]/this.stats[Stat.ATK];
|
|
movePool = movePool.map(m => [m[0], m[1] * (allMoves[m[0]].category === worseCategory ? statRatio : 1)]);
|
|
|
|
let weightMultiplier = 0.9; // The higher this is the more the game weights towards higher level moves. At 0 all moves are equal weight.
|
|
if (this.hasTrainer()) {
|
|
weightMultiplier += 0.7;
|
|
}
|
|
if (this.isBoss()) {
|
|
weightMultiplier += 0.4;
|
|
}
|
|
const baseWeights: [Moves, number][] = movePool.map(m => [m[0], Math.ceil(Math.pow(m[1], weightMultiplier)*100)]);
|
|
|
|
if (this.hasTrainer() || this.isBoss()) { // Trainers and bosses always force a stab move
|
|
const stabMovePool = baseWeights.filter(m => allMoves[m[0]].category !== MoveCategory.STATUS && this.isOfType(allMoves[m[0]].type));
|
|
|
|
if (stabMovePool.length) {
|
|
const totalWeight = stabMovePool.reduce((v, m) => v + m[1], 0);
|
|
let rand = Utils.randSeedInt(totalWeight);
|
|
let index = 0;
|
|
while (rand > stabMovePool[index][1]) {
|
|
rand -= stabMovePool[index++][1];
|
|
}
|
|
this.moveset.push(new PokemonMove(stabMovePool[index][0], 0, 0));
|
|
}
|
|
} else { // Normal wild pokemon just force a random damaging move
|
|
const attackMovePool = baseWeights.filter(m => allMoves[m[0]].category !== MoveCategory.STATUS);
|
|
if (attackMovePool.length) {
|
|
const totalWeight = attackMovePool.reduce((v, m) => v + m[1], 0);
|
|
let rand = Utils.randSeedInt(totalWeight);
|
|
let index = 0;
|
|
while (rand > attackMovePool[index][1]) {
|
|
rand -= attackMovePool[index++][1];
|
|
}
|
|
this.moveset.push(new PokemonMove(attackMovePool[index][0], 0, 0));
|
|
}
|
|
}
|
|
|
|
while (baseWeights.length > this.moveset.length && this.moveset.length < 4) {
|
|
if (this.hasTrainer()) {
|
|
// Sqrt the weight of any damaging moves with overlapping types. This is about a 0.05 - 0.1 multiplier.
|
|
// Other damaging moves 2x weight if 0-1 damaging moves, 0.5x if 2, 0.125x if 3. These weights double if STAB.
|
|
// Status moves remain unchanged on weight, this encourages 1-2
|
|
movePool = baseWeights.filter(m => !this.moveset.some(mo => m[0] === mo.moveId)).map(m => [m[0], this.moveset.some(mo => mo.getMove().category !== MoveCategory.STATUS && mo.getMove().type === allMoves[m[0]].type) ? Math.ceil(Math.sqrt(m[1])) : allMoves[m[0]].category !== MoveCategory.STATUS ? Math.ceil(m[1]/Math.max(Math.pow(4, this.moveset.filter(mo => mo.getMove().power > 1).length)/8,0.5) * (this.isOfType(allMoves[m[0]].type) ? 2 : 1)) : m[1]]);
|
|
} else { // Non-trainer pokemon just use normal weights
|
|
movePool = baseWeights.filter(m => !this.moveset.some(mo => m[0] === mo.moveId));
|
|
}
|
|
const totalWeight = movePool.reduce((v, m) => v + m[1], 0);
|
|
let rand = Utils.randSeedInt(totalWeight);
|
|
let index = 0;
|
|
while (rand > movePool[index][1]) {
|
|
rand -= movePool[index++][1];
|
|
}
|
|
this.moveset.push(new PokemonMove(movePool[index][0], 0, 0));
|
|
}
|
|
|
|
this.scene.triggerPokemonFormChange(this, SpeciesFormChangeMoveLearnedTrigger);
|
|
}
|
|
|
|
trySelectMove(moveIndex: integer, ignorePp?: boolean): boolean {
|
|
const move = this.getMoveset().length > moveIndex
|
|
? this.getMoveset()[moveIndex]
|
|
: null;
|
|
return move?.isUsable(this, ignorePp);
|
|
}
|
|
|
|
showInfo(): void {
|
|
if (!this.battleInfo.visible) {
|
|
const otherBattleInfo = this.scene.fieldUI.getAll().slice(0, 4).filter(ui => ui instanceof BattleInfo && ((ui as BattleInfo) instanceof PlayerBattleInfo) === this.isPlayer()).find(() => true);
|
|
if (!otherBattleInfo || !this.getFieldIndex()) {
|
|
this.scene.fieldUI.sendToBack(this.battleInfo);
|
|
this.scene.sendTextToBack(); // Push the top right text objects behind everything else
|
|
} else {
|
|
this.scene.fieldUI.moveAbove(this.battleInfo, otherBattleInfo);
|
|
}
|
|
this.battleInfo.setX(this.battleInfo.x + (this.isPlayer() ? 150 : !this.isBoss() ? -150 : -198));
|
|
this.battleInfo.setVisible(true);
|
|
if (this.isPlayer()) {
|
|
this.battleInfo.expMaskRect.x += 150;
|
|
}
|
|
this.scene.tweens.add({
|
|
targets: [ this.battleInfo, this.battleInfo.expMaskRect ],
|
|
x: this.isPlayer() ? "-=150" : `+=${!this.isBoss() ? 150 : 246}`,
|
|
duration: 1000,
|
|
ease: "Cubic.easeOut"
|
|
});
|
|
}
|
|
}
|
|
|
|
hideInfo(): Promise<void> {
|
|
return new Promise(resolve => {
|
|
if (this.battleInfo.visible) {
|
|
this.scene.tweens.add({
|
|
targets: [ this.battleInfo, this.battleInfo.expMaskRect ],
|
|
x: this.isPlayer() ? "+=150" : `-=${!this.isBoss() ? 150 : 246}`,
|
|
duration: 500,
|
|
ease: "Cubic.easeIn",
|
|
onComplete: () => {
|
|
if (this.isPlayer()) {
|
|
this.battleInfo.expMaskRect.x -= 150;
|
|
}
|
|
this.battleInfo.setVisible(false);
|
|
this.battleInfo.setX(this.battleInfo.x - (this.isPlayer() ? 150 : !this.isBoss() ? -150 : -198));
|
|
resolve();
|
|
}
|
|
});
|
|
} else {
|
|
resolve();
|
|
}
|
|
});
|
|
}
|
|
|
|
updateInfo(instant?: boolean): Promise<void> {
|
|
return this.battleInfo.updateInfo(this, instant);
|
|
}
|
|
|
|
/**
|
|
* Show or hide the type effectiveness multiplier window
|
|
* Passing undefined will hide the window
|
|
*/
|
|
updateEffectiveness(effectiveness?: string) {
|
|
this.battleInfo.updateEffectiveness(effectiveness);
|
|
}
|
|
|
|
toggleStats(visible: boolean): void {
|
|
this.battleInfo.toggleStats(visible);
|
|
}
|
|
|
|
toggleFlyout(visible: boolean): void {
|
|
this.battleInfo.toggleFlyout(visible);
|
|
}
|
|
|
|
addExp(exp: integer) {
|
|
const maxExpLevel = this.scene.getMaxExpLevel();
|
|
const initialExp = this.exp;
|
|
this.exp += exp;
|
|
while (this.level < maxExpLevel && this.exp >= getLevelTotalExp(this.level + 1, this.species.growthRate)) {
|
|
this.level++;
|
|
}
|
|
if (this.level >= maxExpLevel) {
|
|
console.log(initialExp, this.exp, getLevelTotalExp(this.level, this.species.growthRate));
|
|
this.exp = Math.max(getLevelTotalExp(this.level, this.species.growthRate), initialExp);
|
|
}
|
|
this.levelExp = this.exp - getLevelTotalExp(this.level, this.species.growthRate);
|
|
}
|
|
|
|
getOpponent(targetIndex: integer): Pokemon {
|
|
const ret = this.getOpponents()[targetIndex];
|
|
if (ret.summonData) {
|
|
return ret;
|
|
}
|
|
return null;
|
|
}
|
|
|
|
getOpponents(): Pokemon[] {
|
|
return ((this.isPlayer() ? this.scene.getEnemyField() : this.scene.getPlayerField()) as Pokemon[]).filter(p => p.isActive());
|
|
}
|
|
|
|
getOpponentDescriptor(): string {
|
|
const opponents = this.getOpponents();
|
|
if (opponents.length === 1) {
|
|
return opponents[0].name;
|
|
}
|
|
return this.isPlayer() ? i18next.t("arenaTag:opposingTeam") : i18next.t("arenaTag:yourTeam");
|
|
}
|
|
|
|
getAlly(): Pokemon {
|
|
return (this.isPlayer() ? this.scene.getPlayerField() : this.scene.getEnemyField())[this.getFieldIndex() ? 0 : 1];
|
|
}
|
|
|
|
/**
|
|
* Calculates the accuracy multiplier of the user against a target.
|
|
*
|
|
* This method considers various factors such as the user's accuracy level, the target's evasion level,
|
|
* abilities, and modifiers to compute the final accuracy multiplier.
|
|
*
|
|
* @param target {@linkcode Pokemon} - The target Pokémon against which the move is used.
|
|
* @param sourceMove {@linkcode Move} - The move being used by the user.
|
|
* @returns The calculated accuracy multiplier.
|
|
*/
|
|
getAccuracyMultiplier(target: Pokemon, sourceMove: Move): number {
|
|
const isOhko = sourceMove.hasAttr(OneHitKOAccuracyAttr);
|
|
if (isOhko) {
|
|
return 1;
|
|
}
|
|
|
|
const userAccuracyLevel = new Utils.IntegerHolder(this.summonData.battleStats[BattleStat.ACC]);
|
|
const targetEvasionLevel = new Utils.IntegerHolder(target.summonData.battleStats[BattleStat.EVA]);
|
|
|
|
applyAbAttrs(IgnoreOpponentStatChangesAbAttr, target, null, userAccuracyLevel);
|
|
applyAbAttrs(IgnoreOpponentStatChangesAbAttr, this, null, targetEvasionLevel);
|
|
applyAbAttrs(IgnoreOpponentEvasionAbAttr, this, null, targetEvasionLevel);
|
|
applyMoveAttrs(IgnoreOpponentStatChangesAttr, this, target, sourceMove, targetEvasionLevel);
|
|
this.scene.applyModifiers(TempBattleStatBoosterModifier, this.isPlayer(), TempBattleStat.ACC, userAccuracyLevel);
|
|
|
|
const accuracyMultiplier = new Utils.NumberHolder(1);
|
|
if (userAccuracyLevel.value !== targetEvasionLevel.value) {
|
|
accuracyMultiplier.value = userAccuracyLevel.value > targetEvasionLevel.value
|
|
? (3 + Math.min(userAccuracyLevel.value - targetEvasionLevel.value, 6)) / 3
|
|
: 3 / (3 + Math.min(targetEvasionLevel.value - userAccuracyLevel.value, 6));
|
|
}
|
|
|
|
applyBattleStatMultiplierAbAttrs(BattleStatMultiplierAbAttr, this, BattleStat.ACC, accuracyMultiplier, sourceMove);
|
|
|
|
const evasionMultiplier = new Utils.NumberHolder(1);
|
|
applyBattleStatMultiplierAbAttrs(BattleStatMultiplierAbAttr, target, BattleStat.EVA, evasionMultiplier);
|
|
|
|
accuracyMultiplier.value /= evasionMultiplier.value;
|
|
|
|
return accuracyMultiplier.value;
|
|
}
|
|
|
|
apply(source: Pokemon, move: Move): HitResult {
|
|
let result: HitResult;
|
|
const damage = new Utils.NumberHolder(0);
|
|
const defendingSidePlayField = this.isPlayer() ? this.scene.getPlayerField() : this.scene.getEnemyField();
|
|
|
|
const variableCategory = new Utils.IntegerHolder(move.category);
|
|
applyMoveAttrs(VariableMoveCategoryAttr, source, this, move, variableCategory);
|
|
const moveCategory = variableCategory.value as MoveCategory;
|
|
|
|
applyMoveAttrs(VariableMoveTypeAttr, source, this, move);
|
|
const types = this.getTypes(true, true);
|
|
|
|
const cancelled = new Utils.BooleanHolder(false);
|
|
const typeless = move.hasAttr(TypelessAttr);
|
|
const typeMultiplier = new Utils.NumberHolder(!typeless && (moveCategory !== MoveCategory.STATUS || move.getAttrs(StatusMoveTypeImmunityAttr).find(attr => types.includes(attr.immuneType)))
|
|
? this.getAttackTypeEffectiveness(move, source, false, false)
|
|
: 1);
|
|
applyMoveAttrs(VariableMoveTypeMultiplierAttr, source, this, move, typeMultiplier);
|
|
if (typeless) {
|
|
typeMultiplier.value = 1;
|
|
}
|
|
if (types.find(t => move.isTypeImmune(source, this, t))) {
|
|
typeMultiplier.value = 0;
|
|
}
|
|
|
|
// Apply arena tags for conditional protection
|
|
if (!move.checkFlag(MoveFlags.IGNORE_PROTECT, source, this) && !move.isAllyTarget()) {
|
|
const defendingSide = this.isPlayer() ? ArenaTagSide.PLAYER : ArenaTagSide.ENEMY;
|
|
this.scene.arena.applyTagsForSide(ArenaTagType.QUICK_GUARD, defendingSide, cancelled, this, move.priority);
|
|
this.scene.arena.applyTagsForSide(ArenaTagType.WIDE_GUARD, defendingSide, cancelled, this, move.moveTarget);
|
|
this.scene.arena.applyTagsForSide(ArenaTagType.MAT_BLOCK, defendingSide, cancelled, this, move.category);
|
|
this.scene.arena.applyTagsForSide(ArenaTagType.CRAFTY_SHIELD, defendingSide, cancelled, this, move.category, move.moveTarget);
|
|
}
|
|
|
|
switch (moveCategory) {
|
|
case MoveCategory.PHYSICAL:
|
|
case MoveCategory.SPECIAL:
|
|
const isPhysical = moveCategory === MoveCategory.PHYSICAL;
|
|
const power = move.calculateBattlePower(source, this);
|
|
const sourceTeraType = source.getTeraType();
|
|
|
|
if (!typeless) {
|
|
applyPreDefendAbAttrs(TypeImmunityAbAttr, this, source, move, cancelled, typeMultiplier);
|
|
applyMoveAttrs(NeutralDamageAgainstFlyingTypeMultiplierAttr, source, this, move, typeMultiplier);
|
|
}
|
|
if (!cancelled.value) {
|
|
applyPreDefendAbAttrs(MoveImmunityAbAttr, this, source, move, cancelled, typeMultiplier);
|
|
defendingSidePlayField.forEach((p) => applyPreDefendAbAttrs(FieldPriorityMoveImmunityAbAttr, p, source, move, cancelled, typeMultiplier));
|
|
}
|
|
|
|
if (cancelled.value) {
|
|
source.stopMultiHit(this);
|
|
result = HitResult.NO_EFFECT;
|
|
} else {
|
|
const typeBoost = source.findTag(t => t instanceof TypeBoostTag && t.boostedType === move.type) as TypeBoostTag;
|
|
if (typeBoost?.oneUse) {
|
|
source.removeTag(typeBoost.tagType);
|
|
}
|
|
|
|
const arenaAttackTypeMultiplier = new Utils.NumberHolder(this.scene.arena.getAttackTypeMultiplier(move.type, source.isGrounded()));
|
|
applyMoveAttrs(IgnoreWeatherTypeDebuffAttr, source, this, move, arenaAttackTypeMultiplier);
|
|
|
|
const glaiveRushModifier = new Utils.IntegerHolder(1);
|
|
if (this.getTag(BattlerTagType.RECEIVE_DOUBLE_DAMAGE)) {
|
|
glaiveRushModifier.value = 2;
|
|
}
|
|
let isCritical: boolean;
|
|
const critOnly = new Utils.BooleanHolder(false);
|
|
const critAlways = source.getTag(BattlerTagType.ALWAYS_CRIT);
|
|
applyMoveAttrs(CritOnlyAttr, source, this, move, critOnly);
|
|
applyAbAttrs(ConditionalCritAbAttr, source, null, critOnly, this, move);
|
|
if (critOnly.value || critAlways) {
|
|
isCritical = true;
|
|
} else {
|
|
const critLevel = new Utils.IntegerHolder(0);
|
|
applyMoveAttrs(HighCritAttr, source, this, move, critLevel);
|
|
this.scene.applyModifiers(CritBoosterModifier, source.isPlayer(), source, critLevel);
|
|
this.scene.applyModifiers(TempBattleStatBoosterModifier, source.isPlayer(), TempBattleStat.CRIT, critLevel);
|
|
const bonusCrit = new Utils.BooleanHolder(false);
|
|
if (applyAbAttrs(BonusCritAbAttr, source, null, bonusCrit)) {
|
|
if (bonusCrit.value) {
|
|
critLevel.value += 1;
|
|
}
|
|
}
|
|
if (source.getTag(BattlerTagType.CRIT_BOOST)) {
|
|
critLevel.value += 2;
|
|
}
|
|
console.log(`crit stage: +${critLevel.value}`);
|
|
const critChance = [24, 8, 2, 1][Math.max(0, Math.min(critLevel.value, 3))];
|
|
isCritical = !source.getTag(BattlerTagType.NO_CRIT) && (critChance === 1 || !this.scene.randBattleSeedInt(critChance));
|
|
if (Overrides.NEVER_CRIT_OVERRIDE) {
|
|
isCritical = false;
|
|
}
|
|
}
|
|
if (isCritical) {
|
|
const blockCrit = new Utils.BooleanHolder(false);
|
|
applyAbAttrs(BlockCritAbAttr, this, null, blockCrit);
|
|
if (blockCrit.value) {
|
|
isCritical = false;
|
|
}
|
|
}
|
|
const sourceAtk = new Utils.IntegerHolder(source.getBattleStat(isPhysical ? Stat.ATK : Stat.SPATK, this, null, isCritical));
|
|
const targetDef = new Utils.IntegerHolder(this.getBattleStat(isPhysical ? Stat.DEF : Stat.SPDEF, source, move, isCritical));
|
|
const criticalMultiplier = new Utils.NumberHolder(isCritical ? 1.5 : 1);
|
|
applyAbAttrs(MultCritAbAttr, source, null, criticalMultiplier);
|
|
const screenMultiplier = new Utils.NumberHolder(1);
|
|
if (!isCritical) {
|
|
this.scene.arena.applyTagsForSide(WeakenMoveScreenTag, this.isPlayer() ? ArenaTagSide.PLAYER : ArenaTagSide.ENEMY, move.category, this.scene.currentBattle.double, screenMultiplier);
|
|
}
|
|
const isTypeImmune = (typeMultiplier.value * arenaAttackTypeMultiplier.value) === 0;
|
|
const sourceTypes = source.getTypes();
|
|
const matchesSourceType = sourceTypes[0] === move.type || (sourceTypes.length > 1 && sourceTypes[1] === move.type);
|
|
const stabMultiplier = new Utils.NumberHolder(1);
|
|
if (sourceTeraType === Type.UNKNOWN && matchesSourceType) {
|
|
stabMultiplier.value += 0.5;
|
|
} else if (sourceTeraType !== Type.UNKNOWN && sourceTeraType === move.type) {
|
|
stabMultiplier.value += 0.5;
|
|
}
|
|
|
|
applyAbAttrs(StabBoostAbAttr, source, null, stabMultiplier);
|
|
|
|
if (sourceTeraType !== Type.UNKNOWN && matchesSourceType) {
|
|
stabMultiplier.value = Math.min(stabMultiplier.value + 0.5, 2.25);
|
|
}
|
|
|
|
const targetCount = getMoveTargets(source, move.id).targets.length;
|
|
const targetMultiplier = targetCount > 1 ? 0.75 : 1; // 25% damage debuff on multi-target hits (even if it's immune)
|
|
|
|
applyMoveAttrs(VariableAtkAttr, source, this, move, sourceAtk);
|
|
applyMoveAttrs(VariableDefAttr, source, this, move, targetDef);
|
|
|
|
const effectPhase = this.scene.getCurrentPhase();
|
|
let numTargets = 1;
|
|
if (effectPhase instanceof MoveEffectPhase) {
|
|
numTargets = effectPhase.getTargets().length;
|
|
}
|
|
const twoStrikeMultiplier = new Utils.NumberHolder(1);
|
|
applyPreAttackAbAttrs(AddSecondStrikeAbAttr, source, this, move, numTargets, new Utils.IntegerHolder(0), twoStrikeMultiplier);
|
|
|
|
if (!isTypeImmune) {
|
|
const levelMultiplier = (2 * source.level / 5 + 2);
|
|
const randomMultiplier = ((this.scene.randBattleSeedInt(16) + 85) / 100);
|
|
damage.value = Math.ceil((((levelMultiplier * power * sourceAtk.value / targetDef.value) / 50) + 2)
|
|
* stabMultiplier.value
|
|
* typeMultiplier.value
|
|
* arenaAttackTypeMultiplier.value
|
|
* screenMultiplier.value
|
|
* twoStrikeMultiplier.value
|
|
* targetMultiplier
|
|
* criticalMultiplier.value
|
|
* glaiveRushModifier.value
|
|
* randomMultiplier);
|
|
|
|
if (isPhysical && source.status && source.status.effect === StatusEffect.BURN) {
|
|
if (!move.hasAttr(BypassBurnDamageReductionAttr)) {
|
|
const burnDamageReductionCancelled = new Utils.BooleanHolder(false);
|
|
applyAbAttrs(BypassBurnDamageReductionAbAttr, source, burnDamageReductionCancelled);
|
|
if (!burnDamageReductionCancelled.value) {
|
|
damage.value = Math.floor(damage.value / 2);
|
|
}
|
|
}
|
|
}
|
|
|
|
applyPreAttackAbAttrs(DamageBoostAbAttr, source, this, move, damage);
|
|
|
|
/**
|
|
* For each {@link HitsTagAttr} the move has, doubles the damage of the move if:
|
|
* The target has a {@link BattlerTagType} that this move interacts with
|
|
* AND
|
|
* The move doubles damage when used against that tag
|
|
*/
|
|
move.getAttrs(HitsTagAttr).filter(hta => hta.doubleDamage).forEach(hta => {
|
|
if (this.getTag(hta.tagType)) {
|
|
damage.value *= 2;
|
|
}
|
|
});
|
|
}
|
|
|
|
if (this.scene.arena.terrain?.terrainType === TerrainType.MISTY && this.isGrounded() && move.type === Type.DRAGON) {
|
|
damage.value = Math.floor(damage.value / 2);
|
|
}
|
|
|
|
const fixedDamage = new Utils.IntegerHolder(0);
|
|
applyMoveAttrs(FixedDamageAttr, source, this, move, fixedDamage);
|
|
if (!isTypeImmune && fixedDamage.value) {
|
|
damage.value = fixedDamage.value;
|
|
isCritical = false;
|
|
result = HitResult.EFFECTIVE;
|
|
}
|
|
|
|
if (!result) {
|
|
if (!typeMultiplier.value) {
|
|
result = move.id === Moves.SHEER_COLD ? HitResult.IMMUNE : HitResult.NO_EFFECT;
|
|
} else {
|
|
const isOneHitKo = new Utils.BooleanHolder(false);
|
|
applyMoveAttrs(OneHitKOAttr, source, this, move, isOneHitKo);
|
|
if (isOneHitKo.value) {
|
|
result = HitResult.ONE_HIT_KO;
|
|
isCritical = false;
|
|
damage.value = this.hp;
|
|
} else if (typeMultiplier.value >= 2) {
|
|
result = HitResult.SUPER_EFFECTIVE;
|
|
} else if (typeMultiplier.value >= 1) {
|
|
result = HitResult.EFFECTIVE;
|
|
} else {
|
|
result = HitResult.NOT_VERY_EFFECTIVE;
|
|
}
|
|
}
|
|
}
|
|
|
|
const isOneHitKo = result === HitResult.ONE_HIT_KO;
|
|
|
|
if (!fixedDamage.value && !isOneHitKo) {
|
|
if (!source.isPlayer()) {
|
|
this.scene.applyModifiers(EnemyDamageBoosterModifier, false, damage);
|
|
}
|
|
if (!this.isPlayer()) {
|
|
this.scene.applyModifiers(EnemyDamageReducerModifier, false, damage);
|
|
}
|
|
|
|
applyPreDefendAbAttrs(ReceivedMoveDamageMultiplierAbAttr, this, source, move, cancelled, damage);
|
|
}
|
|
|
|
// This attribute may modify damage arbitrarily, so be careful about changing its order of application.
|
|
applyMoveAttrs(ModifiedDamageAttr, source, this, move, damage);
|
|
|
|
console.log("damage", damage.value, move.name, power, sourceAtk, targetDef);
|
|
|
|
// In case of fatal damage, this tag would have gotten cleared before we could lapse it.
|
|
const destinyTag = this.getTag(BattlerTagType.DESTINY_BOND);
|
|
|
|
if (damage.value) {
|
|
if (this.isFullHp()) {
|
|
applyPreDefendAbAttrs(PreDefendFullHpEndureAbAttr, this, source, move, cancelled, damage);
|
|
} else if (!this.isPlayer() && damage.value >= this.hp) {
|
|
this.scene.applyModifiers(EnemyEndureChanceModifier, false, this);
|
|
}
|
|
|
|
/**
|
|
* We explicitly require to ignore the faint phase here, as we want to show the messages
|
|
* about the critical hit and the super effective/not very effective messages before the faint phase.
|
|
*/
|
|
damage.value = this.damageAndUpdate(damage.value, result as DamageResult, isCritical, isOneHitKo, isOneHitKo, true);
|
|
this.turnData.damageTaken += damage.value;
|
|
|
|
if (isCritical) {
|
|
this.scene.queueMessage(i18next.t("battle:hitResultCriticalHit"));
|
|
}
|
|
if (source.isPlayer()) {
|
|
this.scene.validateAchvs(DamageAchv, damage);
|
|
if (damage.value > this.scene.gameData.gameStats.highestDamage) {
|
|
this.scene.gameData.gameStats.highestDamage = damage.value;
|
|
}
|
|
}
|
|
source.turnData.damageDealt += damage.value;
|
|
source.turnData.currDamageDealt = damage.value;
|
|
this.battleData.hitCount++;
|
|
const attackResult = { move: move.id, result: result as DamageResult, damage: damage.value, critical: isCritical, sourceId: source.id, attackingPosition: source.getBattlerIndex() };
|
|
this.turnData.attacksReceived.unshift(attackResult);
|
|
if (source.isPlayer() && !this.isPlayer()) {
|
|
this.scene.applyModifiers(DamageMoneyRewardModifier, true, source, damage);
|
|
}
|
|
}
|
|
|
|
// want to include is.Fainted() in case multi hit move ends early, still want to render message
|
|
if (source.turnData.hitsLeft === 1 || this.isFainted()) {
|
|
switch (result) {
|
|
case HitResult.SUPER_EFFECTIVE:
|
|
this.scene.queueMessage(i18next.t("battle:hitResultSuperEffective"));
|
|
break;
|
|
case HitResult.NOT_VERY_EFFECTIVE:
|
|
this.scene.queueMessage(i18next.t("battle:hitResultNotVeryEffective"));
|
|
break;
|
|
case HitResult.NO_EFFECT:
|
|
this.scene.queueMessage(i18next.t("battle:hitResultNoEffect", { pokemonName: getPokemonNameWithAffix(this) }));
|
|
break;
|
|
case HitResult.IMMUNE:
|
|
this.scene.queueMessage(`${this.name} is unaffected!`);
|
|
break;
|
|
case HitResult.ONE_HIT_KO:
|
|
this.scene.queueMessage(i18next.t("battle:hitResultOneHitKO"));
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (this.isFainted()) {
|
|
// set splice index here, so future scene queues happen before FaintedPhase
|
|
this.scene.setPhaseQueueSplice();
|
|
this.scene.unshiftPhase(new FaintPhase(this.scene, this.getBattlerIndex(), isOneHitKo));
|
|
this.resetSummonData();
|
|
}
|
|
|
|
if (damage) {
|
|
const attacker = this.scene.getPokemonById(source.id);
|
|
destinyTag?.lapse(attacker, BattlerTagLapseType.CUSTOM);
|
|
}
|
|
}
|
|
break;
|
|
case MoveCategory.STATUS:
|
|
if (!typeless) {
|
|
applyPreDefendAbAttrs(TypeImmunityAbAttr, this, source, move, cancelled, typeMultiplier);
|
|
}
|
|
if (!cancelled.value) {
|
|
applyPreDefendAbAttrs(MoveImmunityAbAttr, this, source, move, cancelled, typeMultiplier);
|
|
defendingSidePlayField.forEach((p) => applyPreDefendAbAttrs(FieldPriorityMoveImmunityAbAttr, p, source, move, cancelled, typeMultiplier));
|
|
}
|
|
if (!typeMultiplier.value) {
|
|
this.scene.queueMessage(i18next.t("battle:hitResultNoEffect", { pokemonName: getPokemonNameWithAffix(this) }));
|
|
}
|
|
result = cancelled.value || !typeMultiplier.value ? HitResult.NO_EFFECT : HitResult.STATUS;
|
|
break;
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
/**
|
|
* Called by damageAndUpdate()
|
|
* @param damage integer
|
|
* @param ignoreSegments boolean, not currently used
|
|
* @param preventEndure used to update damage if endure or sturdy
|
|
* @param ignoreFaintPhase flag on wheter to add FaintPhase if pokemon after applying damage faints
|
|
* @returns integer representing damage
|
|
*/
|
|
damage(damage: integer, ignoreSegments: boolean = false, preventEndure: boolean = false, ignoreFaintPhase: boolean = false): integer {
|
|
if (this.isFainted()) {
|
|
return 0;
|
|
}
|
|
const surviveDamage = new Utils.BooleanHolder(false);
|
|
|
|
if (!preventEndure && this.hp - damage <= 0) {
|
|
if (this.hp >= 1 && this.getTag(BattlerTagType.ENDURING)) {
|
|
surviveDamage.value = this.lapseTag(BattlerTagType.ENDURING);
|
|
} else if (this.hp > 1 && this.getTag(BattlerTagType.STURDY)) {
|
|
surviveDamage.value = this.lapseTag(BattlerTagType.STURDY);
|
|
}
|
|
if (!surviveDamage.value) {
|
|
this.scene.applyModifiers(SurviveDamageModifier, this.isPlayer(), this, surviveDamage);
|
|
}
|
|
if (surviveDamage.value) {
|
|
damage = this.hp - 1;
|
|
}
|
|
}
|
|
|
|
damage = Math.min(damage, this.hp);
|
|
this.hp = this.hp - damage;
|
|
if (this.isFainted() && !ignoreFaintPhase) {
|
|
/**
|
|
* When adding the FaintPhase, want to toggle future unshiftPhase() and queueMessage() calls
|
|
* to appear before the FaintPhase (as FaintPhase will potentially end the encounter and add Phases such as
|
|
* GameOverPhase, VictoryPhase, etc.. that will interfere with anything else that happens during this MoveEffectPhase)
|
|
*
|
|
* Once the MoveEffectPhase is over (and calls it's .end() function, shiftPhase() will reset the PhaseQueueSplice via clearPhaseQueueSplice() )
|
|
*/
|
|
this.scene.setPhaseQueueSplice();
|
|
this.scene.unshiftPhase(new FaintPhase(this.scene, this.getBattlerIndex(), preventEndure));
|
|
this.resetSummonData();
|
|
}
|
|
|
|
return damage;
|
|
}
|
|
|
|
/**
|
|
* Called by apply(), given the damage, adds a new DamagePhase and actually updates HP values, etc.
|
|
* @param damage integer - passed to damage()
|
|
* @param result an enum if it's super effective, not very, etc.
|
|
* @param critical boolean if move is a critical hit
|
|
* @param ignoreSegments boolean, passed to damage() and not used currently
|
|
* @param preventEndure boolean, ignore endure properties of pokemon, passed to damage()
|
|
* @param ignoreFaintPhase boolean to ignore adding a FaintPhase, passsed to damage()
|
|
* @returns integer of damage done
|
|
*/
|
|
damageAndUpdate(damage: integer, result?: DamageResult, critical: boolean = false, ignoreSegments: boolean = false, preventEndure: boolean = false, ignoreFaintPhase: boolean = false): integer {
|
|
const damagePhase = new DamagePhase(this.scene, this.getBattlerIndex(), damage, result as DamageResult, critical);
|
|
this.scene.unshiftPhase(damagePhase);
|
|
damage = this.damage(damage, ignoreSegments, preventEndure, ignoreFaintPhase);
|
|
// Damage amount may have changed, but needed to be queued before calling damage function
|
|
damagePhase.updateAmount(damage);
|
|
return damage;
|
|
}
|
|
|
|
heal(amount: integer): integer {
|
|
const healAmount = Math.min(amount, this.getMaxHp() - this.hp);
|
|
this.hp += healAmount;
|
|
return healAmount;
|
|
}
|
|
|
|
isBossImmune(): boolean {
|
|
return this.isBoss();
|
|
}
|
|
|
|
isMax(): boolean {
|
|
const maxForms = [SpeciesFormKey.GIGANTAMAX, SpeciesFormKey.GIGANTAMAX_RAPID, SpeciesFormKey.GIGANTAMAX_SINGLE, SpeciesFormKey.ETERNAMAX] as string[];
|
|
return maxForms.includes(this.getFormKey()) || maxForms.includes(this.getFusionFormKey());
|
|
}
|
|
|
|
addTag(tagType: BattlerTagType, turnCount: integer = 0, sourceMove?: Moves, sourceId?: integer): boolean {
|
|
const existingTag = this.getTag(tagType);
|
|
if (existingTag) {
|
|
existingTag.onOverlap(this);
|
|
return false;
|
|
}
|
|
|
|
const newTag = getBattlerTag(tagType, turnCount, sourceMove, sourceId);
|
|
|
|
const cancelled = new Utils.BooleanHolder(false);
|
|
applyPreApplyBattlerTagAbAttrs(PreApplyBattlerTagAbAttr, this, newTag, cancelled);
|
|
|
|
if (!cancelled.value && newTag.canAdd(this)) {
|
|
this.summonData.tags.push(newTag);
|
|
newTag.onAdd(this);
|
|
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
/** @overload */
|
|
getTag(tagType: BattlerTagType): BattlerTag;
|
|
|
|
/** @overload */
|
|
getTag<T extends BattlerTag>(tagType: Constructor<T>): T;
|
|
|
|
getTag(tagType: BattlerTagType | Constructor<BattlerTag>): BattlerTag {
|
|
if (!this.summonData) {
|
|
return null;
|
|
}
|
|
return tagType instanceof Function
|
|
? this.summonData.tags.find(t => t instanceof tagType)
|
|
: this.summonData.tags.find(t => t.tagType === tagType);
|
|
}
|
|
|
|
findTag(tagFilter: ((tag: BattlerTag) => boolean)) {
|
|
if (!this.summonData) {
|
|
return null;
|
|
}
|
|
return this.summonData.tags.find(t => tagFilter(t));
|
|
}
|
|
|
|
findTags(tagFilter: ((tag: BattlerTag) => boolean)): BattlerTag[] {
|
|
if (!this.summonData) {
|
|
return [];
|
|
}
|
|
return this.summonData.tags.filter(t => tagFilter(t));
|
|
}
|
|
|
|
lapseTag(tagType: BattlerTagType): boolean {
|
|
const tags = this.summonData.tags;
|
|
const tag = tags.find(t => t.tagType === tagType);
|
|
if (tag && !(tag.lapse(this, BattlerTagLapseType.CUSTOM))) {
|
|
tag.onRemove(this);
|
|
tags.splice(tags.indexOf(tag), 1);
|
|
}
|
|
return !!tag;
|
|
}
|
|
|
|
lapseTags(lapseType: BattlerTagLapseType): void {
|
|
const tags = this.summonData.tags;
|
|
tags.filter(t => lapseType === BattlerTagLapseType.FAINT || ((t.lapseTypes.some(lType => lType === lapseType)) && !(t.lapse(this, lapseType)))).forEach(t => {
|
|
t.onRemove(this);
|
|
tags.splice(tags.indexOf(t), 1);
|
|
});
|
|
}
|
|
|
|
removeTag(tagType: BattlerTagType): boolean {
|
|
const tags = this.summonData.tags;
|
|
const tag = tags.find(t => t.tagType === tagType);
|
|
if (tag) {
|
|
tag.onRemove(this);
|
|
tags.splice(tags.indexOf(tag), 1);
|
|
}
|
|
return !!tag;
|
|
}
|
|
|
|
findAndRemoveTags(tagFilter: ((tag: BattlerTag) => boolean)): boolean {
|
|
if (!this.summonData) {
|
|
return false;
|
|
}
|
|
const tags = this.summonData.tags;
|
|
const tagsToRemove = tags.filter(t => tagFilter(t));
|
|
for (const tag of tagsToRemove) {
|
|
tag.turnCount = 0;
|
|
tag.onRemove(this);
|
|
tags.splice(tags.indexOf(tag), 1);
|
|
}
|
|
return true;
|
|
}
|
|
|
|
removeTagsBySourceId(sourceId: integer): void {
|
|
this.findAndRemoveTags(t => t.isSourceLinked() && t.sourceId === sourceId);
|
|
}
|
|
|
|
transferTagsBySourceId(sourceId: integer, newSourceId: integer): void {
|
|
if (!this.summonData) {
|
|
return;
|
|
}
|
|
const tags = this.summonData.tags;
|
|
tags.filter(t => t.sourceId === sourceId).forEach(t => t.sourceId = newSourceId);
|
|
}
|
|
|
|
/**
|
|
* Transferring stat changes and Tags
|
|
* @param source {@linkcode Pokemon} the pokemon whose stats/Tags are to be passed on from, ie: the Pokemon using Baton Pass
|
|
*/
|
|
transferSummon(source: Pokemon): void {
|
|
const battleStats = Utils.getEnumValues(BattleStat);
|
|
for (const stat of battleStats) {
|
|
this.summonData.battleStats[stat] = source.summonData.battleStats[stat];
|
|
}
|
|
for (const tag of source.summonData.tags) {
|
|
|
|
// bypass yawn, and infatuation as those can not be passed via Baton Pass
|
|
if (tag.sourceMove === Moves.YAWN || tag.tagType === BattlerTagType.INFATUATED) {
|
|
continue;
|
|
}
|
|
|
|
this.summonData.tags.push(tag);
|
|
}
|
|
if (this instanceof PlayerPokemon && source.summonData.battleStats.find(bs => bs === 6)) {
|
|
this.scene.validateAchv(achvs.TRANSFER_MAX_BATTLE_STAT);
|
|
}
|
|
this.updateInfo();
|
|
}
|
|
|
|
getMoveHistory(): TurnMove[] {
|
|
return this.battleSummonData.moveHistory;
|
|
}
|
|
|
|
pushMoveHistory(turnMove: TurnMove) {
|
|
turnMove.turn = this.scene.currentBattle?.turn;
|
|
this.getMoveHistory().push(turnMove);
|
|
}
|
|
|
|
getLastXMoves(turnCount?: integer): TurnMove[] {
|
|
const moveHistory = this.getMoveHistory();
|
|
return moveHistory.slice(turnCount >= 0 ? Math.max(moveHistory.length - (turnCount || 1), 0) : 0, moveHistory.length).reverse();
|
|
}
|
|
|
|
getMoveQueue(): QueuedMove[] {
|
|
return this.summonData.moveQueue;
|
|
}
|
|
|
|
/**
|
|
* If this Pokemon is using a multi-hit move, cancels all subsequent strikes
|
|
* @param {Pokemon} target If specified, this only cancels subsequent strikes against the given target
|
|
*/
|
|
stopMultiHit(target?: Pokemon): void {
|
|
const effectPhase = this.scene.getCurrentPhase();
|
|
if (effectPhase instanceof MoveEffectPhase && effectPhase.getUserPokemon() === this) {
|
|
effectPhase.stopMultiHit(target);
|
|
}
|
|
}
|
|
|
|
changeForm(formChange: SpeciesFormChange): Promise<void> {
|
|
return new Promise(resolve => {
|
|
this.formIndex = Math.max(this.species.forms.findIndex(f => f.formKey === formChange.formKey), 0);
|
|
this.generateName();
|
|
const abilityCount = this.getSpeciesForm().getAbilityCount();
|
|
if (this.abilityIndex >= abilityCount) {// Shouldn't happen
|
|
this.abilityIndex = abilityCount - 1;
|
|
}
|
|
this.scene.gameData.setPokemonSeen(this, false);
|
|
this.setScale(this.getSpriteScale());
|
|
this.loadAssets().then(() => {
|
|
this.calculateStats();
|
|
this.scene.updateModifiers(this.isPlayer(), true);
|
|
Promise.all([ this.updateInfo(), this.scene.updateFieldScale() ]).then(() => resolve());
|
|
});
|
|
});
|
|
}
|
|
|
|
cry(soundConfig?: Phaser.Types.Sound.SoundConfig, sceneOverride?: BattleScene): AnySound {
|
|
const scene = sceneOverride || this.scene;
|
|
const cry = this.getSpeciesForm().cry(scene, soundConfig);
|
|
let duration = cry.totalDuration * 1000;
|
|
if (this.fusionSpecies && this.getSpeciesForm() !== this.getFusionSpeciesForm()) {
|
|
let fusionCry = this.getFusionSpeciesForm().cry(scene, soundConfig, true);
|
|
duration = Math.min(duration, fusionCry.totalDuration * 1000);
|
|
fusionCry.destroy();
|
|
scene.time.delayedCall(Utils.fixedInt(Math.ceil(duration * 0.4)), () => {
|
|
try {
|
|
SoundFade.fadeOut(scene, cry, Utils.fixedInt(Math.ceil(duration * 0.2)));
|
|
fusionCry = this.getFusionSpeciesForm().cry(scene, Object.assign({ seek: Math.max(fusionCry.totalDuration * 0.4, 0) }, soundConfig));
|
|
SoundFade.fadeIn(scene, fusionCry, Utils.fixedInt(Math.ceil(duration * 0.2)), scene.masterVolume * scene.seVolume, 0);
|
|
} catch (err) {
|
|
console.error(err);
|
|
}
|
|
});
|
|
}
|
|
|
|
return cry;
|
|
}
|
|
|
|
faintCry(callback: Function): void {
|
|
if (this.fusionSpecies && this.getSpeciesForm() !== this.getFusionSpeciesForm()) {
|
|
return this.fusionFaintCry(callback);
|
|
}
|
|
|
|
const key = this.getSpeciesForm().getCryKey(this.formIndex);
|
|
//eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
let i = 0;
|
|
let rate = 0.85;
|
|
const cry = this.scene.playSound(key, { rate: rate }) as AnySound;
|
|
const sprite = this.getSprite();
|
|
const tintSprite = this.getTintSprite();
|
|
const delay = Math.max(this.scene.sound.get(key).totalDuration * 50, 25);
|
|
|
|
let frameProgress = 0;
|
|
let frameThreshold: number;
|
|
|
|
sprite.anims.pause();
|
|
tintSprite.anims.pause();
|
|
|
|
let faintCryTimer = this.scene.time.addEvent({
|
|
delay: Utils.fixedInt(delay),
|
|
repeat: -1,
|
|
callback: () => {
|
|
++i;
|
|
frameThreshold = sprite.anims.msPerFrame / rate;
|
|
frameProgress += delay;
|
|
while (frameProgress > frameThreshold) {
|
|
if (sprite.anims.duration) {
|
|
sprite.anims.nextFrame();
|
|
tintSprite.anims.nextFrame();
|
|
}
|
|
frameProgress -= frameThreshold;
|
|
}
|
|
if (cry && !cry.pendingRemove) {
|
|
rate *= 0.99;
|
|
cry.setRate(rate);
|
|
} else {
|
|
faintCryTimer.destroy();
|
|
faintCryTimer = null;
|
|
if (callback) {
|
|
callback();
|
|
}
|
|
}
|
|
}
|
|
});
|
|
|
|
// Failsafe
|
|
this.scene.time.delayedCall(Utils.fixedInt(3000), () => {
|
|
if (!faintCryTimer || !this.scene) {
|
|
return;
|
|
}
|
|
if (cry?.isPlaying) {
|
|
cry.stop();
|
|
}
|
|
faintCryTimer.destroy();
|
|
if (callback) {
|
|
callback();
|
|
}
|
|
});
|
|
}
|
|
|
|
private fusionFaintCry(callback: Function): void {
|
|
const key = this.getSpeciesForm().getCryKey(this.formIndex);
|
|
let i = 0;
|
|
let rate = 0.85;
|
|
const cry = this.scene.playSound(key, { rate: rate }) as AnySound;
|
|
const sprite = this.getSprite();
|
|
const tintSprite = this.getTintSprite();
|
|
let duration = cry.totalDuration * 1000;
|
|
|
|
let fusionCry = this.scene.playSound(this.getFusionSpeciesForm().getCryKey(this.fusionFormIndex), { rate: rate }) as AnySound;
|
|
fusionCry.stop();
|
|
duration = Math.min(duration, fusionCry.totalDuration * 1000);
|
|
fusionCry.destroy();
|
|
|
|
const delay = Math.max(duration * 0.05, 25);
|
|
|
|
let transitionIndex = 0;
|
|
let durationProgress = 0;
|
|
|
|
const transitionThreshold = Math.ceil(duration * 0.4);
|
|
while (durationProgress < transitionThreshold) {
|
|
++i;
|
|
durationProgress += delay * rate;
|
|
rate *= 0.99;
|
|
}
|
|
|
|
transitionIndex = i;
|
|
|
|
i = 0;
|
|
rate = 0.85;
|
|
|
|
let frameProgress = 0;
|
|
let frameThreshold: number;
|
|
|
|
sprite.anims.pause();
|
|
tintSprite.anims.pause();
|
|
|
|
let faintCryTimer = this.scene.time.addEvent({
|
|
delay: Utils.fixedInt(delay),
|
|
repeat: -1,
|
|
callback: () => {
|
|
++i;
|
|
frameThreshold = sprite.anims.msPerFrame / rate;
|
|
frameProgress += delay;
|
|
while (frameProgress > frameThreshold) {
|
|
if (sprite.anims.duration) {
|
|
sprite.anims.nextFrame();
|
|
tintSprite.anims.nextFrame();
|
|
}
|
|
frameProgress -= frameThreshold;
|
|
}
|
|
if (i === transitionIndex) {
|
|
SoundFade.fadeOut(this.scene, cry, Utils.fixedInt(Math.ceil((duration / rate) * 0.2)));
|
|
fusionCry = this.scene.playSound(this.getFusionSpeciesForm().getCryKey(this.fusionFormIndex), Object.assign({ seek: Math.max(fusionCry.totalDuration * 0.4, 0), rate: rate }));
|
|
SoundFade.fadeIn(this.scene, fusionCry, Utils.fixedInt(Math.ceil((duration / rate) * 0.2)), this.scene.masterVolume * this.scene.seVolume, 0);
|
|
}
|
|
rate *= 0.99;
|
|
if (cry && !cry.pendingRemove) {
|
|
cry.setRate(rate);
|
|
}
|
|
if (fusionCry && !fusionCry.pendingRemove) {
|
|
fusionCry.setRate(rate);
|
|
}
|
|
if ((!cry || cry.pendingRemove) && (!fusionCry || fusionCry.pendingRemove)) {
|
|
faintCryTimer.destroy();
|
|
faintCryTimer = null;
|
|
if (callback) {
|
|
callback();
|
|
}
|
|
}
|
|
}
|
|
});
|
|
|
|
// Failsafe
|
|
this.scene.time.delayedCall(Utils.fixedInt(3000), () => {
|
|
if (!faintCryTimer || !this.scene) {
|
|
return;
|
|
}
|
|
if (cry?.isPlaying) {
|
|
cry.stop();
|
|
}
|
|
if (fusionCry?.isPlaying) {
|
|
fusionCry.stop();
|
|
}
|
|
faintCryTimer.destroy();
|
|
if (callback) {
|
|
callback();
|
|
}
|
|
});
|
|
}
|
|
|
|
isOppositeGender(pokemon: Pokemon): boolean {
|
|
return this.gender !== Gender.GENDERLESS && pokemon.gender === (this.gender === Gender.MALE ? Gender.FEMALE : Gender.MALE);
|
|
}
|
|
|
|
canSetStatus(effect: StatusEffect, quiet: boolean = false, overrideStatus: boolean = false, sourcePokemon: Pokemon = null): boolean {
|
|
if (effect !== StatusEffect.FAINT) {
|
|
if (overrideStatus ? this.status?.effect === effect : this.status) {
|
|
return false;
|
|
}
|
|
if (this.isGrounded() && this.scene.arena.terrain?.terrainType === TerrainType.MISTY) {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
const types = this.getTypes(true, true);
|
|
|
|
switch (effect) {
|
|
case StatusEffect.POISON:
|
|
case StatusEffect.TOXIC:
|
|
// Check if the Pokemon is immune to Poison/Toxic or if the source pokemon is canceling the immunity
|
|
const poisonImmunity = types.map(defType => {
|
|
// Check if the Pokemon is not immune to Poison/Toxic
|
|
if (defType !== Type.POISON && defType !== Type.STEEL) {
|
|
return false;
|
|
}
|
|
|
|
// Check if the source Pokemon has an ability that cancels the Poison/Toxic immunity
|
|
const cancelImmunity = new Utils.BooleanHolder(false);
|
|
if (sourcePokemon) {
|
|
applyAbAttrs(IgnoreTypeStatusEffectImmunityAbAttr, sourcePokemon, cancelImmunity, effect, defType);
|
|
if (cancelImmunity.value) {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
return true;
|
|
});
|
|
|
|
if (this.isOfType(Type.POISON) || this.isOfType(Type.STEEL)) {
|
|
if (poisonImmunity.includes(true)) {
|
|
return false;
|
|
}
|
|
}
|
|
break;
|
|
case StatusEffect.PARALYSIS:
|
|
if (this.isOfType(Type.ELECTRIC)) {
|
|
return false;
|
|
}
|
|
break;
|
|
case StatusEffect.SLEEP:
|
|
if (this.isGrounded() && this.scene.arena.terrain?.terrainType === TerrainType.ELECTRIC) {
|
|
return false;
|
|
}
|
|
break;
|
|
case StatusEffect.FREEZE:
|
|
if (this.isOfType(Type.ICE) || [WeatherType.SUNNY, WeatherType.HARSH_SUN].includes(this.scene?.arena.weather?.weatherType)) {
|
|
return false;
|
|
}
|
|
break;
|
|
case StatusEffect.BURN:
|
|
if (this.isOfType(Type.FIRE)) {
|
|
return false;
|
|
}
|
|
break;
|
|
}
|
|
|
|
const cancelled = new Utils.BooleanHolder(false);
|
|
applyPreSetStatusAbAttrs(StatusEffectImmunityAbAttr, this, effect, cancelled, quiet);
|
|
|
|
if (cancelled.value) {
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
trySetStatus(effect: StatusEffect, asPhase: boolean = false, sourcePokemon: Pokemon = null, cureTurn: integer = 0, sourceText: string = null): boolean {
|
|
if (!this.canSetStatus(effect, asPhase, false, sourcePokemon)) {
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* If this Pokemon falls asleep or freezes in the middle of a multi-hit attack,
|
|
* cancel the attack's subsequent hits.
|
|
*/
|
|
if (effect === StatusEffect.SLEEP || effect === StatusEffect.FREEZE) {
|
|
this.stopMultiHit();
|
|
}
|
|
|
|
if (asPhase) {
|
|
this.scene.unshiftPhase(new ObtainStatusEffectPhase(this.scene, this.getBattlerIndex(), effect, cureTurn, sourceText, sourcePokemon));
|
|
return true;
|
|
}
|
|
|
|
let statusCureTurn: Utils.IntegerHolder;
|
|
|
|
if (effect === StatusEffect.SLEEP) {
|
|
statusCureTurn = new Utils.IntegerHolder(this.randSeedIntRange(2, 4));
|
|
applyAbAttrs(ReduceStatusEffectDurationAbAttr, this, null, effect, statusCureTurn);
|
|
|
|
this.setFrameRate(4);
|
|
|
|
// If the user is invulnerable, lets remove their invulnerability when they fall asleep
|
|
const invulnerableTags = [
|
|
BattlerTagType.UNDERGROUND,
|
|
BattlerTagType.UNDERWATER,
|
|
BattlerTagType.HIDDEN,
|
|
BattlerTagType.FLYING
|
|
];
|
|
|
|
const tag = invulnerableTags.find((t) => this.getTag(t));
|
|
|
|
if (tag) {
|
|
this.removeTag(tag);
|
|
this.getMoveQueue().pop();
|
|
}
|
|
}
|
|
|
|
this.status = new Status(effect, 0, statusCureTurn?.value);
|
|
|
|
if (effect !== StatusEffect.FAINT) {
|
|
this.scene.triggerPokemonFormChange(this, SpeciesFormChangeStatusEffectTrigger, true);
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* Resets the status of a pokemon.
|
|
* @param revive Whether revive should be cured; defaults to true.
|
|
* @param confusion Whether resetStatus should include confusion or not; defaults to false.
|
|
* @param reloadAssets Whether to reload the assets or not; defaults to false.
|
|
*/
|
|
resetStatus(revive: boolean = true, confusion: boolean = false, reloadAssets: boolean = false): void {
|
|
const lastStatus = this.status?.effect;
|
|
if (!revive && lastStatus === StatusEffect.FAINT) {
|
|
return;
|
|
}
|
|
this.status = undefined;
|
|
if (lastStatus === StatusEffect.SLEEP) {
|
|
this.setFrameRate(12);
|
|
if (this.getTag(BattlerTagType.NIGHTMARE)) {
|
|
this.lapseTag(BattlerTagType.NIGHTMARE);
|
|
}
|
|
}
|
|
if (confusion) {
|
|
if (this.getTag(BattlerTagType.CONFUSED)) {
|
|
this.lapseTag(BattlerTagType.CONFUSED);
|
|
}
|
|
}
|
|
if (reloadAssets) {
|
|
this.loadAssets(false).then(() => this.playAnim());
|
|
}
|
|
}
|
|
|
|
primeSummonData(summonDataPrimer: PokemonSummonData): void {
|
|
this.summonDataPrimer = summonDataPrimer;
|
|
}
|
|
|
|
resetSummonData(): void {
|
|
if (this.summonData?.speciesForm) {
|
|
this.summonData.speciesForm = null;
|
|
this.updateFusionPalette();
|
|
}
|
|
this.summonData = new PokemonSummonData();
|
|
if (!this.battleData) {
|
|
this.resetBattleData();
|
|
}
|
|
this.resetBattleSummonData();
|
|
if (this.summonDataPrimer) {
|
|
for (const k of Object.keys(this.summonData)) {
|
|
if (this.summonDataPrimer[k]) {
|
|
this.summonData[k] = this.summonDataPrimer[k];
|
|
}
|
|
}
|
|
this.summonDataPrimer = null;
|
|
}
|
|
this.updateInfo();
|
|
}
|
|
|
|
resetBattleData(): void {
|
|
this.battleData = new PokemonBattleData();
|
|
}
|
|
|
|
resetBattleSummonData(): void {
|
|
this.battleSummonData = new PokemonBattleSummonData();
|
|
if (this.getTag(BattlerTagType.SEEDED)) {
|
|
this.lapseTag(BattlerTagType.SEEDED);
|
|
}
|
|
if (this.scene) {
|
|
this.scene.triggerPokemonFormChange(this, SpeciesFormChangePostMoveTrigger, true);
|
|
}
|
|
}
|
|
|
|
resetTurnData(): void {
|
|
this.turnData = new PokemonTurnData();
|
|
}
|
|
|
|
getExpValue(): integer {
|
|
// Logic to factor in victor level has been removed for balancing purposes, so the player doesn't have to focus on EXP maxxing
|
|
return ((this.getSpeciesForm().getBaseExp() * this.level) / 5 + 1);
|
|
}
|
|
|
|
setFrameRate(frameRate: integer) {
|
|
this.scene.anims.get(this.getBattleSpriteKey()).frameRate = frameRate;
|
|
this.getSprite().play(this.getBattleSpriteKey());
|
|
this.getTintSprite().play(this.getBattleSpriteKey());
|
|
}
|
|
|
|
tint(color: number, alpha?: number, duration?: integer, ease?: string) {
|
|
const tintSprite = this.getTintSprite();
|
|
tintSprite.setTintFill(color);
|
|
tintSprite.setVisible(true);
|
|
|
|
if (duration) {
|
|
tintSprite.setAlpha(0);
|
|
|
|
this.scene.tweens.add({
|
|
targets: tintSprite,
|
|
alpha: alpha || 1,
|
|
duration: duration,
|
|
ease: ease || "Linear"
|
|
});
|
|
} else {
|
|
tintSprite.setAlpha(alpha);
|
|
}
|
|
}
|
|
|
|
untint(duration: integer, ease?: string) {
|
|
const tintSprite = this.getTintSprite();
|
|
|
|
if (duration) {
|
|
this.scene.tweens.add({
|
|
targets: tintSprite,
|
|
alpha: 0,
|
|
duration: duration,
|
|
ease: ease || "Linear",
|
|
onComplete: () => {
|
|
tintSprite.setVisible(false);
|
|
tintSprite.setAlpha(1);
|
|
}
|
|
});
|
|
} else {
|
|
tintSprite.setVisible(false);
|
|
tintSprite.setAlpha(1);
|
|
}
|
|
}
|
|
|
|
enableMask() {
|
|
if (!this.maskEnabled) {
|
|
this.maskSprite = this.getTintSprite();
|
|
this.maskSprite.setVisible(true);
|
|
this.maskSprite.setPosition(this.x * this.parentContainer.scale + this.parentContainer.x,
|
|
this.y * this.parentContainer.scale + this.parentContainer.y);
|
|
this.maskSprite.setScale(this.getSpriteScale() * this.parentContainer.scale);
|
|
this.maskEnabled = true;
|
|
}
|
|
}
|
|
|
|
disableMask() {
|
|
if (this.maskEnabled) {
|
|
this.maskSprite.setVisible(false);
|
|
this.maskSprite.setPosition(0, 0);
|
|
this.maskSprite.setScale(this.getSpriteScale());
|
|
this.maskSprite = null;
|
|
this.maskEnabled = false;
|
|
}
|
|
}
|
|
|
|
sparkle(): void {
|
|
if (this.shinySparkle) {
|
|
this.shinySparkle.play(`sparkle${this.variant ? `_${this.variant + 1}` : ""}`);
|
|
this.scene.playSound("sparkle");
|
|
}
|
|
}
|
|
|
|
updateFusionPalette(ignoreOveride?: boolean): void {
|
|
if (!this.getFusionSpeciesForm(ignoreOveride)) {
|
|
[ this.getSprite(), this.getTintSprite() ].map(s => {
|
|
s.pipelineData[`spriteColors${ignoreOveride && this.summonData?.speciesForm ? "Base" : ""}`] = [];
|
|
s.pipelineData[`fusionSpriteColors${ignoreOveride && this.summonData?.speciesForm ? "Base" : ""}`] = [];
|
|
});
|
|
return;
|
|
}
|
|
|
|
const speciesForm = this.getSpeciesForm(ignoreOveride);
|
|
const fusionSpeciesForm = this.getFusionSpeciesForm(ignoreOveride);
|
|
|
|
const spriteKey = speciesForm.getSpriteKey(this.getGender(ignoreOveride) === Gender.FEMALE, speciesForm.formIndex, this.shiny, this.variant);
|
|
const backSpriteKey = speciesForm.getSpriteKey(this.getGender(ignoreOveride) === Gender.FEMALE, speciesForm.formIndex, this.shiny, this.variant).replace("pkmn__", "pkmn__back__");
|
|
const fusionSpriteKey = fusionSpeciesForm.getSpriteKey(this.getFusionGender(ignoreOveride) === Gender.FEMALE, fusionSpeciesForm.formIndex, this.fusionShiny, this.fusionVariant);
|
|
const fusionBackSpriteKey = fusionSpeciesForm.getSpriteKey(this.getFusionGender(ignoreOveride) === Gender.FEMALE, fusionSpeciesForm.formIndex, this.fusionShiny, this.fusionVariant).replace("pkmn__", "pkmn__back__");
|
|
|
|
const sourceTexture = this.scene.textures.get(spriteKey);
|
|
const sourceBackTexture = this.scene.textures.get(backSpriteKey);
|
|
const fusionTexture = this.scene.textures.get(fusionSpriteKey);
|
|
const fusionBackTexture = this.scene.textures.get(fusionBackSpriteKey);
|
|
|
|
const [ sourceFrame, sourceBackFrame, fusionFrame, fusionBackFrame ] = [ sourceTexture, sourceBackTexture, fusionTexture, fusionBackTexture ].map(texture => texture.frames[texture.firstFrame]);
|
|
const [ sourceImage, sourceBackImage, fusionImage, fusionBackImage ] = [ sourceTexture, sourceBackTexture, fusionTexture, fusionBackTexture ].map(i => i.getSourceImage() as HTMLImageElement);
|
|
|
|
const canvas = document.createElement("canvas");
|
|
const backCanvas = document.createElement("canvas");
|
|
const fusionCanvas = document.createElement("canvas");
|
|
const fusionBackCanvas = document.createElement("canvas");
|
|
|
|
const spriteColors: integer[][] = [];
|
|
const pixelData: Uint8ClampedArray[] = [];
|
|
|
|
[ canvas, backCanvas, fusionCanvas, fusionBackCanvas ].forEach((canv: HTMLCanvasElement, c: integer) => {
|
|
const context = canv.getContext("2d");
|
|
const frame = [ sourceFrame, sourceBackFrame, fusionFrame, fusionBackFrame ][c];
|
|
canv.width = frame.width;
|
|
canv.height = frame.height;
|
|
context.drawImage([ sourceImage, sourceBackImage, fusionImage, fusionBackImage ][c], frame.cutX, frame.cutY, frame.width, frame.height, 0, 0, frame.width, frame.height);
|
|
const imageData = context.getImageData(frame.cutX, frame.cutY, frame.width, frame.height);
|
|
pixelData.push(imageData.data);
|
|
});
|
|
|
|
for (let f = 0; f < 2; f++) {
|
|
const variantColors = variantColorCache[!f ? spriteKey : backSpriteKey];
|
|
const variantColorSet = new Map<integer, integer[]>();
|
|
if (this.shiny && variantColors && variantColors[this.variant]) {
|
|
Object.keys(variantColors[this.variant]).forEach(k => {
|
|
variantColorSet.set(Utils.rgbaToInt(Array.from(Object.values(Utils.rgbHexToRgba(k)))), Array.from(Object.values(Utils.rgbHexToRgba(variantColors[this.variant][k]))));
|
|
});
|
|
}
|
|
|
|
for (let i = 0; i < pixelData[f].length; i += 4) {
|
|
if (pixelData[f][i + 3]) {
|
|
const pixel = pixelData[f].slice(i, i + 4);
|
|
let [ r, g, b, a ] = pixel;
|
|
if (variantColors) {
|
|
const color = Utils.rgbaToInt([r, g, b, a]);
|
|
if (variantColorSet.has(color)) {
|
|
const mappedPixel = variantColorSet.get(color);
|
|
[ r, g, b, a ] = mappedPixel;
|
|
}
|
|
}
|
|
if (!spriteColors.find(c => c[0] === r && c[1] === g && c[2] === b)) {
|
|
spriteColors.push([ r, g, b, a ]);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
const fusionSpriteColors = JSON.parse(JSON.stringify(spriteColors));
|
|
|
|
const pixelColors = [];
|
|
for (let f = 0; f < 2; f++) {
|
|
for (let i = 0; i < pixelData[f].length; i += 4) {
|
|
const total = pixelData[f].slice(i, i + 3).reduce((total: integer, value: integer) => total + value, 0);
|
|
if (!total) {
|
|
continue;
|
|
}
|
|
pixelColors.push(argbFromRgba({ r: pixelData[f][i], g: pixelData[f][i + 1], b: pixelData[f][i + 2], a: pixelData[f][i + 3] }));
|
|
}
|
|
}
|
|
|
|
const fusionPixelColors = [];
|
|
for (let f = 0; f < 2; f++) {
|
|
const variantColors = variantColorCache[!f ? fusionSpriteKey : fusionBackSpriteKey];
|
|
const variantColorSet = new Map<integer, integer[]>();
|
|
if (this.fusionShiny && variantColors && variantColors[this.fusionVariant]) {
|
|
Object.keys(variantColors[this.fusionVariant]).forEach(k => {
|
|
variantColorSet.set(Utils.rgbaToInt(Array.from(Object.values(Utils.rgbHexToRgba(k)))), Array.from(Object.values(Utils.rgbHexToRgba(variantColors[this.fusionVariant][k]))));
|
|
});
|
|
}
|
|
for (let i = 0; i < pixelData[2 + f].length; i += 4) {
|
|
const total = pixelData[2 + f].slice(i, i + 3).reduce((total: integer, value: integer) => total + value, 0);
|
|
if (!total) {
|
|
continue;
|
|
}
|
|
let [ r, g, b, a ] = [ pixelData[2 + f][i], pixelData[2 + f][i + 1], pixelData[2 + f][i + 2], pixelData[2 + f][i + 3] ];
|
|
if (variantColors) {
|
|
const color = Utils.rgbaToInt([r, g, b, a]);
|
|
if (variantColorSet.has(color)) {
|
|
const mappedPixel = variantColorSet.get(color);
|
|
[ r, g, b, a ] = mappedPixel;
|
|
}
|
|
}
|
|
fusionPixelColors.push(argbFromRgba({ r, g, b, a }));
|
|
}
|
|
}
|
|
|
|
let paletteColors: Map<number, number>;
|
|
let fusionPaletteColors: Map<number, number>;
|
|
|
|
const originalRandom = Math.random;
|
|
Math.random = () => Phaser.Math.RND.realInRange(0, 1);
|
|
|
|
this.scene.executeWithSeedOffset(() => {
|
|
paletteColors = QuantizerCelebi.quantize(pixelColors, 4);
|
|
fusionPaletteColors = QuantizerCelebi.quantize(fusionPixelColors, 4);
|
|
}, 0, "This result should not vary");
|
|
|
|
Math.random = originalRandom;
|
|
|
|
const [ palette, fusionPalette ] = [ paletteColors, fusionPaletteColors ]
|
|
.map(paletteColors => {
|
|
let keys = Array.from(paletteColors.keys()).sort((a: integer, b: integer) => paletteColors.get(a) < paletteColors.get(b) ? 1 : -1);
|
|
let rgbaColors: Map<number, integer[]>;
|
|
let hsvColors: Map<number, number[]>;
|
|
|
|
const mappedColors = new Map<integer, integer[]>();
|
|
|
|
do {
|
|
mappedColors.clear();
|
|
|
|
rgbaColors = keys.reduce((map: Map<number, integer[]>, k: number) => {
|
|
map.set(k, Object.values(rgbaFromArgb(k))); return map;
|
|
}, new Map<number, integer[]>());
|
|
hsvColors = Array.from(rgbaColors.keys()).reduce((map: Map<number, number[]>, k: number) => {
|
|
const rgb = rgbaColors.get(k).slice(0, 3);
|
|
map.set(k, Utils.rgbToHsv(rgb[0], rgb[1], rgb[2]));
|
|
return map;
|
|
}, new Map<number, number[]>());
|
|
|
|
for (let c = keys.length - 1; c >= 0; c--) {
|
|
const hsv = hsvColors.get(keys[c]);
|
|
for (let c2 = 0; c2 < c; c2++) {
|
|
const hsv2 = hsvColors.get(keys[c2]);
|
|
const diff = Math.abs(hsv[0] - hsv2[0]);
|
|
if (diff < 30 || diff >= 330) {
|
|
if (mappedColors.has(keys[c])) {
|
|
mappedColors.get(keys[c]).push(keys[c2]);
|
|
} else {
|
|
mappedColors.set(keys[c], [ keys[c2] ]);
|
|
}
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
mappedColors.forEach((values: integer[], key: integer) => {
|
|
const keyColor = rgbaColors.get(key);
|
|
const valueColors = values.map(v => rgbaColors.get(v));
|
|
const color = keyColor.slice(0);
|
|
let count = paletteColors.get(key);
|
|
for (const value of values) {
|
|
const valueCount = paletteColors.get(value);
|
|
if (!valueCount) {
|
|
continue;
|
|
}
|
|
count += valueCount;
|
|
}
|
|
|
|
for (let c = 0; c < 3; c++) {
|
|
color[c] *= (paletteColors.get(key) / count);
|
|
values.forEach((value: integer, i: integer) => {
|
|
if (paletteColors.has(value)) {
|
|
const valueCount = paletteColors.get(value);
|
|
color[c] += valueColors[i][c] * (valueCount / count);
|
|
}
|
|
});
|
|
color[c] = Math.round(color[c]);
|
|
}
|
|
|
|
paletteColors.delete(key);
|
|
for (const value of values) {
|
|
paletteColors.delete(value);
|
|
if (mappedColors.has(value)) {
|
|
mappedColors.delete(value);
|
|
}
|
|
}
|
|
|
|
paletteColors.set(argbFromRgba({ r: color[0], g: color[1], b: color[2], a: color[3] }), count);
|
|
});
|
|
|
|
keys = Array.from(paletteColors.keys()).sort((a: integer, b: integer) => paletteColors.get(a) < paletteColors.get(b) ? 1 : -1);
|
|
} while (mappedColors.size);
|
|
|
|
return keys.map(c => Object.values(rgbaFromArgb(c)));
|
|
}
|
|
);
|
|
|
|
const paletteDeltas: number[][] = [];
|
|
|
|
spriteColors.forEach((sc: integer[], i: integer) => {
|
|
paletteDeltas.push([]);
|
|
for (let p = 0; p < palette.length; p++) {
|
|
paletteDeltas[i].push(Utils.deltaRgb(sc, palette[p]));
|
|
}
|
|
});
|
|
|
|
const easeFunc = Phaser.Tweens.Builders.GetEaseFunction("Cubic.easeIn");
|
|
|
|
for (let sc = 0; sc < spriteColors.length; sc++) {
|
|
const delta = Math.min(...paletteDeltas[sc]);
|
|
const paletteIndex = Math.min(paletteDeltas[sc].findIndex(pd => pd === delta), fusionPalette.length - 1);
|
|
if (delta < 255) {
|
|
const ratio = easeFunc(delta / 255);
|
|
const color = [ 0, 0, 0, fusionSpriteColors[sc][3] ];
|
|
for (let c = 0; c < 3; c++) {
|
|
color[c] = Math.round((fusionSpriteColors[sc][c] * ratio) + (fusionPalette[paletteIndex][c] * (1 - ratio)));
|
|
}
|
|
fusionSpriteColors[sc] = color;
|
|
}
|
|
}
|
|
|
|
[ this.getSprite(), this.getTintSprite() ].map(s => {
|
|
s.pipelineData[`spriteColors${ignoreOveride && this.summonData?.speciesForm ? "Base" : ""}`] = spriteColors;
|
|
s.pipelineData[`fusionSpriteColors${ignoreOveride && this.summonData?.speciesForm ? "Base" : ""}`] = fusionSpriteColors;
|
|
});
|
|
|
|
canvas.remove();
|
|
fusionCanvas.remove();
|
|
}
|
|
|
|
randSeedInt(range: integer, min: integer = 0): integer {
|
|
return this.scene.currentBattle
|
|
? this.scene.randBattleSeedInt(range, min)
|
|
: Utils.randSeedInt(range, min);
|
|
}
|
|
|
|
randSeedIntRange(min: integer, max: integer): integer {
|
|
return this.randSeedInt((max - min) + 1, min);
|
|
}
|
|
|
|
destroy(): void {
|
|
this.battleInfo?.destroy();
|
|
super.destroy();
|
|
}
|
|
|
|
getBattleInfo(): BattleInfo {
|
|
return this.battleInfo;
|
|
}
|
|
|
|
/**
|
|
* Checks whether or not the Pokemon's root form has the same ability
|
|
* @param abilityIndex the given ability index we are checking
|
|
* @returns true if the abilities are the same
|
|
*/
|
|
hasSameAbilityInRootForm(abilityIndex: number): boolean {
|
|
const currentAbilityIndex = this.abilityIndex;
|
|
const rootForm = getPokemonSpecies(this.species.getRootSpeciesId());
|
|
return rootForm.getAbility(abilityIndex) === rootForm.getAbility(currentAbilityIndex);
|
|
}
|
|
}
|
|
|
|
export default interface Pokemon {
|
|
scene: BattleScene
|
|
}
|
|
|
|
export class PlayerPokemon extends Pokemon {
|
|
public compatibleTms: Moves[];
|
|
|
|
constructor(scene: BattleScene, species: PokemonSpecies, level: integer, abilityIndex: integer, formIndex: integer, gender: Gender, shiny: boolean, variant: Variant, ivs: integer[], nature: Nature, dataSource: Pokemon | PokemonData) {
|
|
super(scene, 106, 148, species, level, abilityIndex, formIndex, gender, shiny, variant, ivs, nature, dataSource);
|
|
|
|
if (Overrides.STATUS_OVERRIDE) {
|
|
this.status = new Status(Overrides.STATUS_OVERRIDE);
|
|
}
|
|
|
|
if (Overrides.SHINY_OVERRIDE) {
|
|
this.shiny = true;
|
|
this.initShinySparkle();
|
|
if (Overrides.VARIANT_OVERRIDE) {
|
|
this.variant = Overrides.VARIANT_OVERRIDE;
|
|
}
|
|
}
|
|
|
|
if (!dataSource) {
|
|
this.generateAndPopulateMoveset();
|
|
}
|
|
this.generateCompatibleTms();
|
|
}
|
|
|
|
initBattleInfo(): void {
|
|
this.battleInfo = new PlayerBattleInfo(this.scene);
|
|
this.battleInfo.initInfo(this);
|
|
}
|
|
|
|
isPlayer(): boolean {
|
|
return true;
|
|
}
|
|
|
|
hasTrainer(): boolean {
|
|
return true;
|
|
}
|
|
|
|
isBoss(): boolean {
|
|
return false;
|
|
}
|
|
|
|
getFieldIndex(): integer {
|
|
return this.scene.getPlayerField().indexOf(this);
|
|
}
|
|
|
|
getBattlerIndex(): BattlerIndex {
|
|
return this.getFieldIndex();
|
|
}
|
|
|
|
generateCompatibleTms(): void {
|
|
this.compatibleTms = [];
|
|
|
|
const tms = Object.keys(tmSpecies);
|
|
for (const tm of tms) {
|
|
const moveId = parseInt(tm) as Moves;
|
|
let compatible = false;
|
|
for (const p of tmSpecies[tm]) {
|
|
if (Array.isArray(p)) {
|
|
if (p[0] === this.species.speciesId || (this.fusionSpecies && p[0] === this.fusionSpecies.speciesId) && p.slice(1).indexOf(this.species.forms[this.formIndex]) > -1) {
|
|
compatible = true;
|
|
break;
|
|
}
|
|
} else if (p === this.species.speciesId || (this.fusionSpecies && p === this.fusionSpecies.speciesId)) {
|
|
compatible = true;
|
|
break;
|
|
}
|
|
}
|
|
if (reverseCompatibleTms.indexOf(moveId) > -1) {
|
|
compatible = !compatible;
|
|
}
|
|
if (compatible) {
|
|
this.compatibleTms.push(moveId);
|
|
}
|
|
}
|
|
}
|
|
|
|
tryPopulateMoveset(moveset: StarterMoveset): boolean {
|
|
if (!this.getSpeciesForm().validateStarterMoveset(moveset, this.scene.gameData.starterData[this.species.getRootSpeciesId()].eggMoves)) {
|
|
return false;
|
|
}
|
|
|
|
this.moveset = moveset.map(m => new PokemonMove(m));
|
|
|
|
return true;
|
|
}
|
|
|
|
switchOut(batonPass: boolean, removeFromField: boolean = false): Promise<void> {
|
|
return new Promise(resolve => {
|
|
this.resetTurnData();
|
|
if (!batonPass) {
|
|
this.resetSummonData();
|
|
}
|
|
this.hideInfo();
|
|
this.setVisible(false);
|
|
|
|
this.scene.ui.setMode(Mode.PARTY, PartyUiMode.FAINT_SWITCH, this.getFieldIndex(), (slotIndex: integer, option: PartyOption) => {
|
|
if (slotIndex >= this.scene.currentBattle.getBattlerCount() && slotIndex < 6) {
|
|
this.scene.prependToPhase(new SwitchSummonPhase(this.scene, this.getFieldIndex(), slotIndex, false, batonPass), MoveEndPhase);
|
|
}
|
|
if (removeFromField) {
|
|
this.setVisible(false);
|
|
this.scene.field.remove(this);
|
|
this.scene.triggerPokemonFormChange(this, SpeciesFormChangeActiveTrigger, true);
|
|
}
|
|
this.scene.ui.setMode(Mode.MESSAGE).then(() => resolve());
|
|
}, PartyUiHandler.FilterNonFainted);
|
|
});
|
|
}
|
|
|
|
addFriendship(friendship: integer): void {
|
|
const starterSpeciesId = this.species.getRootSpeciesId();
|
|
const fusionStarterSpeciesId = this.isFusion() ? this.fusionSpecies.getRootSpeciesId() : 0;
|
|
const starterData = [
|
|
this.scene.gameData.starterData[starterSpeciesId],
|
|
fusionStarterSpeciesId ? this.scene.gameData.starterData[fusionStarterSpeciesId] : null
|
|
].filter(d => d);
|
|
const amount = new Utils.IntegerHolder(friendship);
|
|
const starterAmount = new Utils.IntegerHolder(Math.floor(friendship * (this.scene.gameMode.isClassic && friendship > 0 ? 2 : 1) / (fusionStarterSpeciesId ? 2 : 1)));
|
|
if (amount.value > 0) {
|
|
this.scene.applyModifier(PokemonFriendshipBoosterModifier, true, this, amount);
|
|
this.scene.applyModifier(PokemonFriendshipBoosterModifier, true, this, starterAmount);
|
|
|
|
this.friendship = Math.min(this.friendship + amount.value, 255);
|
|
if (this.friendship === 255) {
|
|
this.scene.validateAchv(achvs.MAX_FRIENDSHIP);
|
|
}
|
|
starterData.forEach((sd: StarterDataEntry, i: integer) => {
|
|
const speciesId = !i ? starterSpeciesId : fusionStarterSpeciesId as Species;
|
|
sd.friendship = (sd.friendship || 0) + starterAmount.value;
|
|
if (sd.friendship >= getStarterValueFriendshipCap(speciesStarters[speciesId])) {
|
|
this.scene.gameData.addStarterCandy(getPokemonSpecies(speciesId), 1);
|
|
sd.friendship = 0;
|
|
}
|
|
});
|
|
} else {
|
|
this.friendship = Math.max(this.friendship + amount.value, 0);
|
|
for (const sd of starterData) {
|
|
sd.friendship = Math.max((sd.friendship || 0) + starterAmount.value, 0);
|
|
}
|
|
}
|
|
}
|
|
/**
|
|
* Handles Revival Blessing when used by player.
|
|
* @returns Promise to revive a pokemon.
|
|
* @see {@linkcode RevivalBlessingAttr}
|
|
*/
|
|
revivalBlessing(): Promise<void> {
|
|
return new Promise(resolve => {
|
|
this.scene.ui.setMode(Mode.PARTY, PartyUiMode.REVIVAL_BLESSING, this.getFieldIndex(), (slotIndex:integer, option: PartyOption) => {
|
|
if (slotIndex >= 0 && slotIndex<6) {
|
|
const pokemon = this.scene.getParty()[slotIndex];
|
|
if (!pokemon || !pokemon.isFainted()) {
|
|
resolve();
|
|
}
|
|
|
|
pokemon.resetTurnData();
|
|
pokemon.resetStatus();
|
|
pokemon.heal(Math.min(Math.max(Math.ceil(Math.floor(0.5 * pokemon.getMaxHp())), 1), pokemon.getMaxHp()));
|
|
this.scene.queueMessage(`${pokemon.name} was revived!`,0,true);
|
|
|
|
if (this.scene.currentBattle.double && this.scene.getParty().length > 1) {
|
|
const allyPokemon = this.getAlly();
|
|
if (slotIndex<=1) {
|
|
// Revived ally pokemon
|
|
this.scene.unshiftPhase(new SwitchSummonPhase(this.scene, pokemon.getFieldIndex(), slotIndex, false, false, true));
|
|
this.scene.unshiftPhase(new ToggleDoublePositionPhase(this.scene, true));
|
|
} else if (allyPokemon.isFainted()) {
|
|
// Revived party pokemon, and ally pokemon is fainted
|
|
this.scene.unshiftPhase(new SwitchSummonPhase(this.scene, allyPokemon.getFieldIndex(), slotIndex, false, false, true));
|
|
this.scene.unshiftPhase(new ToggleDoublePositionPhase(this.scene, true));
|
|
}
|
|
}
|
|
|
|
}
|
|
this.scene.ui.setMode(Mode.MESSAGE).then(() => resolve());
|
|
}, PartyUiHandler.FilterFainted);
|
|
});
|
|
}
|
|
|
|
getPossibleEvolution(evolution: SpeciesFormEvolution): Promise<Pokemon> {
|
|
return new Promise(resolve => {
|
|
const evolutionSpecies = getPokemonSpecies(evolution.speciesId);
|
|
const isFusion = evolution instanceof FusionSpeciesFormEvolution;
|
|
let ret: PlayerPokemon;
|
|
if (isFusion) {
|
|
const originalFusionSpecies = this.fusionSpecies;
|
|
const originalFusionFormIndex = this.fusionFormIndex;
|
|
this.fusionSpecies = evolutionSpecies;
|
|
this.fusionFormIndex = evolution.evoFormKey !== null ? Math.max(evolutionSpecies.forms.findIndex(f => f.formKey === evolution.evoFormKey), 0) : this.fusionFormIndex;
|
|
ret = this.scene.addPlayerPokemon(this.species, this.level, this.abilityIndex, this.formIndex, this.gender, this.shiny, this.variant, this.ivs, this.nature, this);
|
|
this.fusionSpecies = originalFusionSpecies;
|
|
this.fusionFormIndex = originalFusionFormIndex;
|
|
} else {
|
|
const formIndex = evolution.evoFormKey !== null && !isFusion ? Math.max(evolutionSpecies.forms.findIndex(f => f.formKey === evolution.evoFormKey), 0) : this.formIndex;
|
|
ret = this.scene.addPlayerPokemon(!isFusion ? evolutionSpecies : this.species, this.level, this.abilityIndex, formIndex, this.gender, this.shiny, this.variant, this.ivs, this.nature, this);
|
|
}
|
|
ret.loadAssets().then(() => resolve(ret));
|
|
});
|
|
}
|
|
|
|
evolve(evolution: SpeciesFormEvolution, preEvolution: PokemonSpeciesForm): Promise<void> {
|
|
return new Promise(resolve => {
|
|
this.pauseEvolutions = false;
|
|
// Handles Nincada evolving into Ninjask + Shedinja
|
|
this.handleSpecialEvolutions(evolution);
|
|
const isFusion = evolution instanceof FusionSpeciesFormEvolution;
|
|
if (!isFusion) {
|
|
this.species = getPokemonSpecies(evolution.speciesId);
|
|
} else {
|
|
this.fusionSpecies = getPokemonSpecies(evolution.speciesId);
|
|
}
|
|
if (evolution.preFormKey !== null) {
|
|
const formIndex = Math.max((!isFusion ? this.species : this.fusionSpecies).forms.findIndex(f => f.formKey === evolution.evoFormKey), 0);
|
|
if (!isFusion) {
|
|
this.formIndex = formIndex;
|
|
} else {
|
|
this.fusionFormIndex = formIndex;
|
|
}
|
|
}
|
|
this.generateName();
|
|
if (!isFusion) {
|
|
const abilityCount = this.getSpeciesForm().getAbilityCount();
|
|
const preEvoAbilityCount = preEvolution.getAbilityCount();
|
|
if ([0, 1, 2].includes(this.abilityIndex)) {
|
|
// Handles cases where a Pokemon with 3 abilities evolves into a Pokemon with 2 abilities (ie: Eevee -> any Eeveelution)
|
|
if (this.abilityIndex === 2 && preEvoAbilityCount === 3 && abilityCount === 2) {
|
|
this.abilityIndex = 1;
|
|
}
|
|
} else { // Prevent pokemon with an illegal ability value from breaking things
|
|
console.warn("this.abilityIndex is somehow an illegal value, please report this");
|
|
console.warn(this.abilityIndex);
|
|
this.abilityIndex = 0;
|
|
}
|
|
} else { // Do the same as above, but for fusions
|
|
const abilityCount = this.getFusionSpeciesForm().getAbilityCount();
|
|
const preEvoAbilityCount = preEvolution.getAbilityCount();
|
|
if ([0, 1, 2].includes(this.fusionAbilityIndex)) {
|
|
if (this.fusionAbilityIndex === 2 && preEvoAbilityCount === 3 && abilityCount === 2) {
|
|
this.fusionAbilityIndex = 1;
|
|
}
|
|
} else {
|
|
console.warn("this.fusionAbilityIndex is somehow an illegal value, please report this");
|
|
console.warn(this.fusionAbilityIndex);
|
|
this.fusionAbilityIndex = 0;
|
|
}
|
|
}
|
|
this.compatibleTms.splice(0, this.compatibleTms.length);
|
|
this.generateCompatibleTms();
|
|
const updateAndResolve = () => {
|
|
this.loadAssets().then(() => {
|
|
this.calculateStats();
|
|
this.updateInfo(true).then(() => resolve());
|
|
});
|
|
};
|
|
if (!this.scene.gameMode.isDaily || this.metBiome > -1) {
|
|
this.scene.gameData.updateSpeciesDexIvs(this.species.speciesId, this.ivs);
|
|
this.scene.gameData.setPokemonSeen(this, false);
|
|
this.scene.gameData.setPokemonCaught(this, false).then(() => updateAndResolve());
|
|
} else {
|
|
updateAndResolve();
|
|
}
|
|
});
|
|
}
|
|
|
|
private handleSpecialEvolutions(evolution: SpeciesFormEvolution) {
|
|
const isFusion = evolution instanceof FusionSpeciesFormEvolution;
|
|
|
|
const evoSpecies = (!isFusion ? this.species : this.fusionSpecies);
|
|
if (evoSpecies.speciesId === Species.NINCADA && evolution.speciesId === Species.NINJASK) {
|
|
const newEvolution = pokemonEvolutions[evoSpecies.speciesId][1];
|
|
|
|
if (newEvolution.condition.predicate(this)) {
|
|
const newPokemon = this.scene.addPlayerPokemon(this.species, this.level, this.abilityIndex, this.formIndex, undefined, this.shiny, this.variant, this.ivs, this.nature);
|
|
newPokemon.natureOverride = this.natureOverride;
|
|
newPokemon.passive = this.passive;
|
|
newPokemon.moveset = this.moveset.slice();
|
|
newPokemon.moveset = this.copyMoveset();
|
|
newPokemon.luck = this.luck;
|
|
newPokemon.fusionSpecies = this.fusionSpecies;
|
|
newPokemon.fusionFormIndex = this.fusionFormIndex;
|
|
newPokemon.fusionAbilityIndex = this.fusionAbilityIndex;
|
|
newPokemon.fusionShiny = this.fusionShiny;
|
|
newPokemon.fusionVariant = this.fusionVariant;
|
|
newPokemon.fusionGender = this.fusionGender;
|
|
newPokemon.fusionLuck = this.fusionLuck;
|
|
|
|
this.scene.getParty().push(newPokemon);
|
|
newPokemon.evolve((!isFusion ? newEvolution : new FusionSpeciesFormEvolution(this.id, newEvolution)), evoSpecies);
|
|
const modifiers = this.scene.findModifiers(m => m instanceof PokemonHeldItemModifier
|
|
&& m.pokemonId === this.id, true) as PokemonHeldItemModifier[];
|
|
modifiers.forEach(m => {
|
|
const clonedModifier = m.clone() as PokemonHeldItemModifier;
|
|
clonedModifier.pokemonId = newPokemon.id;
|
|
this.scene.addModifier(clonedModifier, true);
|
|
});
|
|
this.scene.updateModifiers(true);
|
|
}
|
|
}
|
|
}
|
|
|
|
getPossibleForm(formChange: SpeciesFormChange): Promise<Pokemon> {
|
|
return new Promise(resolve => {
|
|
const formIndex = Math.max(this.species.forms.findIndex(f => f.formKey === formChange.formKey), 0);
|
|
const ret = this.scene.addPlayerPokemon(this.species, this.level, this.abilityIndex, formIndex, this.gender, this.shiny, this.variant, this.ivs, this.nature, this);
|
|
ret.loadAssets().then(() => resolve(ret));
|
|
});
|
|
}
|
|
|
|
changeForm(formChange: SpeciesFormChange): Promise<void> {
|
|
return new Promise(resolve => {
|
|
this.formIndex = Math.max(this.species.forms.findIndex(f => f.formKey === formChange.formKey), 0);
|
|
this.generateName();
|
|
const abilityCount = this.getSpeciesForm().getAbilityCount();
|
|
if (this.abilityIndex >= abilityCount) { // Shouldn't happen
|
|
this.abilityIndex = abilityCount - 1;
|
|
}
|
|
this.compatibleTms.splice(0, this.compatibleTms.length);
|
|
this.generateCompatibleTms();
|
|
const updateAndResolve = () => {
|
|
this.loadAssets().then(() => {
|
|
this.calculateStats();
|
|
this.scene.updateModifiers(true, true);
|
|
this.updateInfo(true).then(() => resolve());
|
|
});
|
|
};
|
|
if (!this.scene.gameMode.isDaily || this.metBiome > -1) {
|
|
this.scene.gameData.setPokemonSeen(this, false);
|
|
this.scene.gameData.setPokemonCaught(this, false).then(() => updateAndResolve());
|
|
} else {
|
|
updateAndResolve();
|
|
}
|
|
});
|
|
}
|
|
|
|
clearFusionSpecies(): void {
|
|
super.clearFusionSpecies();
|
|
this.generateCompatibleTms();
|
|
}
|
|
|
|
/**
|
|
* Returns a Promise to fuse two PlayerPokemon together
|
|
* @param pokemon The PlayerPokemon to fuse to this one
|
|
*/
|
|
fuse(pokemon: PlayerPokemon): Promise<void> {
|
|
return new Promise(resolve => {
|
|
this.fusionSpecies = pokemon.species;
|
|
this.fusionFormIndex = pokemon.formIndex;
|
|
this.fusionAbilityIndex = pokemon.abilityIndex;
|
|
this.fusionShiny = pokemon.shiny;
|
|
this.fusionVariant = pokemon.variant;
|
|
this.fusionGender = pokemon.gender;
|
|
this.fusionLuck = pokemon.luck;
|
|
|
|
this.scene.validateAchv(achvs.SPLICE);
|
|
this.scene.gameData.gameStats.pokemonFused++;
|
|
|
|
// Store the average HP% that each Pokemon has
|
|
const newHpPercent = ((pokemon.hp / pokemon.stats[Stat.HP]) + (this.hp / this.stats[Stat.HP])) / 2;
|
|
|
|
this.generateName();
|
|
this.calculateStats();
|
|
|
|
// Set this Pokemon's HP to the average % of both fusion components
|
|
this.hp = Math.round(this.stats[Stat.HP] * newHpPercent);
|
|
if (!this.isFainted()) {
|
|
// If this Pokemon hasn't fainted, make sure the HP wasn't set over the new maximum
|
|
this.hp = Math.min(this.hp, this.stats[Stat.HP]);
|
|
this.status = getRandomStatus(this.status, pokemon.status); // Get a random valid status between the two
|
|
} else if (!pokemon.isFainted()) {
|
|
// If this Pokemon fainted but the other hasn't, make sure the HP wasn't set to zero
|
|
this.hp = Math.max(this.hp, 1);
|
|
this.status = pokemon.status; // Inherit the other Pokemon's status
|
|
}
|
|
|
|
this.generateCompatibleTms();
|
|
this.updateInfo(true);
|
|
const fusedPartyMemberIndex = this.scene.getParty().indexOf(pokemon);
|
|
let partyMemberIndex = this.scene.getParty().indexOf(this);
|
|
if (partyMemberIndex > fusedPartyMemberIndex) {
|
|
partyMemberIndex--;
|
|
}
|
|
const fusedPartyMemberHeldModifiers = this.scene.findModifiers(m => m instanceof PokemonHeldItemModifier
|
|
&& m.pokemonId === pokemon.id, true) as PokemonHeldItemModifier[];
|
|
const transferModifiers: Promise<boolean>[] = [];
|
|
for (const modifier of fusedPartyMemberHeldModifiers) {
|
|
transferModifiers.push(this.scene.tryTransferHeldItemModifier(modifier, this, false, modifier.getStackCount(), true, true));
|
|
}
|
|
Promise.allSettled(transferModifiers).then(() => {
|
|
this.scene.updateModifiers(true, true).then(() => {
|
|
this.scene.removePartyMemberModifiers(fusedPartyMemberIndex);
|
|
this.scene.getParty().splice(fusedPartyMemberIndex, 1)[0];
|
|
const newPartyMemberIndex = this.scene.getParty().indexOf(this);
|
|
pokemon.getMoveset(true).map(m => this.scene.unshiftPhase(new LearnMovePhase(this.scene, newPartyMemberIndex, m.getMove().id)));
|
|
pokemon.destroy();
|
|
this.updateFusionPalette();
|
|
resolve();
|
|
});
|
|
});
|
|
});
|
|
}
|
|
|
|
unfuse(): Promise<void> {
|
|
return new Promise(resolve => {
|
|
this.clearFusionSpecies();
|
|
|
|
this.updateInfo(true).then(() => resolve());
|
|
this.updateFusionPalette();
|
|
});
|
|
}
|
|
|
|
/** Returns a deep copy of this Pokemon's moveset array */
|
|
copyMoveset(): PokemonMove[] {
|
|
const newMoveset = [];
|
|
this.moveset.forEach(move =>
|
|
newMoveset.push(new PokemonMove(move.moveId, 0, move.ppUp, move.virtual)));
|
|
|
|
return newMoveset;
|
|
}
|
|
}
|
|
|
|
export class EnemyPokemon extends Pokemon {
|
|
public trainerSlot: TrainerSlot;
|
|
public aiType: AiType;
|
|
public bossSegments: integer;
|
|
public bossSegmentIndex: integer;
|
|
/** To indicate of the instance was populated with a dataSource -> e.g. loaded & populated from session data */
|
|
public readonly isPopulatedFromDataSource: boolean;
|
|
|
|
constructor(scene: BattleScene, species: PokemonSpecies, level: integer, trainerSlot: TrainerSlot, boss: boolean, dataSource: PokemonData) {
|
|
super(scene, 236, 84, species, level, dataSource?.abilityIndex, dataSource?.formIndex,
|
|
dataSource?.gender, dataSource ? dataSource.shiny : false, dataSource ? dataSource.variant : undefined, null, dataSource ? dataSource.nature : undefined, dataSource);
|
|
|
|
this.trainerSlot = trainerSlot;
|
|
this.isPopulatedFromDataSource = !!dataSource; // if a dataSource is provided, then it was populated from dataSource
|
|
if (boss) {
|
|
this.setBoss(boss, dataSource?.bossSegments);
|
|
}
|
|
|
|
if (Overrides.OPP_STATUS_OVERRIDE) {
|
|
this.status = new Status(Overrides.OPP_STATUS_OVERRIDE);
|
|
}
|
|
|
|
if (!dataSource) {
|
|
this.generateAndPopulateMoveset();
|
|
|
|
this.trySetShiny();
|
|
if (Overrides.OPP_SHINY_OVERRIDE) {
|
|
this.shiny = true;
|
|
this.initShinySparkle();
|
|
}
|
|
if (this.shiny) {
|
|
this.variant = this.generateVariant();
|
|
if (Overrides.OPP_VARIANT_OVERRIDE) {
|
|
this.variant = Overrides.OPP_VARIANT_OVERRIDE;
|
|
}
|
|
}
|
|
|
|
this.luck = (this.shiny ? this.variant + 1 : 0) + (this.fusionShiny ? this.fusionVariant + 1 : 0);
|
|
|
|
let prevolution: Species;
|
|
let speciesId = species.speciesId;
|
|
while ((prevolution = pokemonPrevolutions[speciesId])) {
|
|
const evolution = pokemonEvolutions[prevolution].find(pe => pe.speciesId === speciesId && (!pe.evoFormKey || pe.evoFormKey === this.getFormKey()));
|
|
if (evolution.condition?.enforceFunc) {
|
|
evolution.condition.enforceFunc(this);
|
|
}
|
|
speciesId = prevolution;
|
|
}
|
|
}
|
|
|
|
this.aiType = boss || this.hasTrainer() ? AiType.SMART : AiType.SMART_RANDOM;
|
|
}
|
|
|
|
initBattleInfo(): void {
|
|
if (!this.battleInfo) {
|
|
this.battleInfo = new EnemyBattleInfo(this.scene);
|
|
this.battleInfo.updateBossSegments(this);
|
|
this.battleInfo.initInfo(this);
|
|
} else {
|
|
this.battleInfo.updateBossSegments(this);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Sets the pokemons boss status. If true initializes the boss segments either from the arguments
|
|
* or through the the Scene.getEncounterBossSegments function
|
|
*
|
|
* @param boss if the pokemon is a boss
|
|
* @param bossSegments amount of boss segments (health-bar segments)
|
|
*/
|
|
setBoss(boss: boolean = true, bossSegments: integer = 0): void {
|
|
if (boss) {
|
|
this.bossSegments = bossSegments || this.scene.getEncounterBossSegments(this.scene.currentBattle.waveIndex, this.level, this.species, true);
|
|
this.bossSegmentIndex = this.bossSegments - 1;
|
|
} else {
|
|
this.bossSegments = 0;
|
|
this.bossSegmentIndex = 0;
|
|
}
|
|
}
|
|
|
|
generateAndPopulateMoveset(formIndex?: integer): void {
|
|
switch (true) {
|
|
case (this.species.speciesId === Species.SMEARGLE):
|
|
this.moveset = [
|
|
new PokemonMove(Moves.SKETCH),
|
|
new PokemonMove(Moves.SKETCH),
|
|
new PokemonMove(Moves.SKETCH),
|
|
new PokemonMove(Moves.SKETCH)
|
|
];
|
|
break;
|
|
case (this.species.speciesId === Species.ETERNATUS):
|
|
this.moveset = (formIndex !== undefined ? formIndex : this.formIndex)
|
|
? [
|
|
new PokemonMove(Moves.DYNAMAX_CANNON),
|
|
new PokemonMove(Moves.CROSS_POISON),
|
|
new PokemonMove(Moves.FLAMETHROWER),
|
|
new PokemonMove(Moves.RECOVER, 0, -4)
|
|
]
|
|
: [
|
|
new PokemonMove(Moves.ETERNABEAM),
|
|
new PokemonMove(Moves.SLUDGE_BOMB),
|
|
new PokemonMove(Moves.DRAGON_DANCE),
|
|
new PokemonMove(Moves.COSMIC_POWER)
|
|
];
|
|
break;
|
|
default:
|
|
super.generateAndPopulateMoveset();
|
|
break;
|
|
}
|
|
}
|
|
|
|
getNextMove(): QueuedMove {
|
|
const queuedMove = this.getMoveQueue().length
|
|
? this.getMoveset().find(m => m.moveId === this.getMoveQueue()[0].move)
|
|
: null;
|
|
if (queuedMove) {
|
|
if (queuedMove.isUsable(this, this.getMoveQueue()[0].ignorePP)) {
|
|
return { move: queuedMove.moveId, targets: this.getMoveQueue()[0].targets, ignorePP: this.getMoveQueue()[0].ignorePP };
|
|
} else {
|
|
this.getMoveQueue().shift();
|
|
return this.getNextMove();
|
|
}
|
|
}
|
|
|
|
const movePool = this.getMoveset().filter(m => m.isUsable(this));
|
|
if (movePool.length) {
|
|
if (movePool.length === 1) {
|
|
return { move: movePool[0].moveId, targets: this.getNextTargets(movePool[0].moveId) };
|
|
}
|
|
const encoreTag = this.getTag(EncoreTag) as EncoreTag;
|
|
if (encoreTag) {
|
|
const encoreMove = movePool.find(m => m.moveId === encoreTag.moveId);
|
|
if (encoreMove) {
|
|
return { move: encoreMove.moveId, targets: this.getNextTargets(encoreMove.moveId) };
|
|
}
|
|
}
|
|
switch (this.aiType) {
|
|
case AiType.RANDOM:
|
|
const moveId = movePool[this.scene.randBattleSeedInt(movePool.length)].moveId;
|
|
return { move: moveId, targets: this.getNextTargets(moveId) };
|
|
case AiType.SMART_RANDOM:
|
|
case AiType.SMART:
|
|
const moveScores = movePool.map(() => 0);
|
|
const moveTargets = Object.fromEntries(movePool.map(m => [ m.moveId, this.getNextTargets(m.moveId) ]));
|
|
for (const m in movePool) {
|
|
const pokemonMove = movePool[m];
|
|
const move = pokemonMove.getMove();
|
|
|
|
let moveScore = moveScores[m];
|
|
const targetScores: integer[] = [];
|
|
|
|
for (const mt of moveTargets[move.id]) {
|
|
// Prevent a target score from being calculated when the target is whoever attacks the user
|
|
if (mt === BattlerIndex.ATTACKER) {
|
|
break;
|
|
}
|
|
|
|
const target = this.scene.getField()[mt];
|
|
let targetScore = move.getUserBenefitScore(this, target, move) + move.getTargetBenefitScore(this, target, move) * (mt < BattlerIndex.ENEMY === this.isPlayer() ? 1 : -1);
|
|
if (Number.isNaN(targetScore)) {
|
|
console.error(`Move ${move.name} returned score of NaN`);
|
|
targetScore = 0;
|
|
}
|
|
if ((move.name.endsWith(" (N)") || !move.applyConditions(this, target, move)) && ![Moves.SUCKER_PUNCH, Moves.UPPER_HAND, Moves.THUNDERCLAP].includes(move.id)) {
|
|
targetScore = -20;
|
|
} else if (move instanceof AttackMove) {
|
|
const effectiveness = target.getAttackMoveEffectiveness(this, pokemonMove);
|
|
if (target.isPlayer() !== this.isPlayer()) {
|
|
targetScore *= effectiveness;
|
|
if (this.isOfType(move.type)) {
|
|
targetScore *= 1.5;
|
|
}
|
|
} else if (effectiveness) {
|
|
targetScore /= effectiveness;
|
|
if (this.isOfType(move.type)) {
|
|
targetScore /= 1.5;
|
|
}
|
|
}
|
|
if (!targetScore) {
|
|
targetScore = -20;
|
|
}
|
|
}
|
|
targetScores.push(targetScore);
|
|
}
|
|
|
|
moveScore += Math.max(...targetScores);
|
|
|
|
// could make smarter by checking opponent def/spdef
|
|
moveScores[m] = moveScore;
|
|
}
|
|
|
|
console.log(moveScores);
|
|
|
|
const sortedMovePool = movePool.slice(0);
|
|
sortedMovePool.sort((a, b) => {
|
|
const scoreA = moveScores[movePool.indexOf(a)];
|
|
const scoreB = moveScores[movePool.indexOf(b)];
|
|
return scoreA < scoreB ? 1 : scoreA > scoreB ? -1 : 0;
|
|
});
|
|
let r = 0;
|
|
if (this.aiType === AiType.SMART_RANDOM) {
|
|
while (r < sortedMovePool.length - 1 && this.scene.randBattleSeedInt(8) >= 5) {
|
|
r++;
|
|
}
|
|
} else if (this.aiType === AiType.SMART) {
|
|
while (r < sortedMovePool.length - 1 && (moveScores[movePool.indexOf(sortedMovePool[r + 1])] / moveScores[movePool.indexOf(sortedMovePool[r])]) >= 0
|
|
&& this.scene.randBattleSeedInt(100) < Math.round((moveScores[movePool.indexOf(sortedMovePool[r + 1])] / moveScores[movePool.indexOf(sortedMovePool[r])]) * 50)) {
|
|
r++;
|
|
}
|
|
}
|
|
console.log(movePool.map(m => m.getName()), moveScores, r, sortedMovePool.map(m => m.getName()));
|
|
return { move: sortedMovePool[r].moveId, targets: moveTargets[sortedMovePool[r].moveId] };
|
|
}
|
|
}
|
|
|
|
return { move: Moves.STRUGGLE, targets: this.getNextTargets(Moves.STRUGGLE) };
|
|
}
|
|
|
|
getNextTargets(moveId: Moves): BattlerIndex[] {
|
|
const moveTargets = getMoveTargets(this, moveId);
|
|
const targets = this.scene.getField(true).filter(p => moveTargets.targets.indexOf(p.getBattlerIndex()) > -1);
|
|
if (moveTargets.multiple) {
|
|
return targets.map(p => p.getBattlerIndex());
|
|
}
|
|
|
|
const move = allMoves[moveId];
|
|
|
|
const benefitScores = targets
|
|
.map(p => [ p.getBattlerIndex(), move.getTargetBenefitScore(this, p, move) * (p.isPlayer() === this.isPlayer() ? 1 : -1) ]);
|
|
|
|
const sortedBenefitScores = benefitScores.slice(0);
|
|
sortedBenefitScores.sort((a, b) => {
|
|
const scoreA = a[1];
|
|
const scoreB = b[1];
|
|
return scoreA < scoreB ? 1 : scoreA > scoreB ? -1 : 0;
|
|
});
|
|
|
|
if (!sortedBenefitScores.length) {
|
|
// Set target to BattlerIndex.ATTACKER when using a counter move
|
|
// This is the same as when the player does so
|
|
if (move.hasAttr(CounterDamageAttr)) {
|
|
return [BattlerIndex.ATTACKER];
|
|
}
|
|
|
|
return [];
|
|
}
|
|
|
|
let targetWeights = sortedBenefitScores.map(s => s[1]);
|
|
const lowestWeight = targetWeights[targetWeights.length - 1];
|
|
|
|
if (lowestWeight < 1) {
|
|
for (let w = 0; w < targetWeights.length; w++) {
|
|
targetWeights[w] += Math.abs(lowestWeight - 1);
|
|
}
|
|
}
|
|
|
|
const benefitCutoffIndex = targetWeights.findIndex(s => s < targetWeights[0] / 2);
|
|
if (benefitCutoffIndex > -1) {
|
|
targetWeights = targetWeights.slice(0, benefitCutoffIndex);
|
|
}
|
|
|
|
const thresholds: integer[] = [];
|
|
let totalWeight: integer;
|
|
targetWeights.reduce((total: integer, w: integer) => {
|
|
total += w;
|
|
thresholds.push(total);
|
|
totalWeight = total;
|
|
return total;
|
|
}, 0);
|
|
|
|
const randValue = this.scene.randBattleSeedInt(totalWeight);
|
|
let targetIndex: integer;
|
|
|
|
thresholds.every((t, i) => {
|
|
if (randValue >= t) {
|
|
return true;
|
|
}
|
|
|
|
targetIndex = i;
|
|
return false;
|
|
});
|
|
|
|
return [ sortedBenefitScores[targetIndex][0] ];
|
|
}
|
|
|
|
isPlayer() {
|
|
return false;
|
|
}
|
|
|
|
hasTrainer(): boolean {
|
|
return !!this.trainerSlot;
|
|
}
|
|
|
|
isBoss(): boolean {
|
|
return !!this.bossSegments;
|
|
}
|
|
|
|
getBossSegmentIndex(): integer {
|
|
const segments = (this as EnemyPokemon).bossSegments;
|
|
const segmentSize = this.getMaxHp() / segments;
|
|
for (let s = segments - 1; s > 0; s--) {
|
|
const hpThreshold = Math.round(segmentSize * s);
|
|
if (this.hp > hpThreshold) {
|
|
return s;
|
|
}
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
damage(damage: integer, ignoreSegments: boolean = false, preventEndure: boolean = false, ignoreFaintPhase: boolean = false): integer {
|
|
if (this.isFainted()) {
|
|
return 0;
|
|
}
|
|
|
|
let clearedBossSegmentIndex = this.isBoss()
|
|
? this.bossSegmentIndex + 1
|
|
: 0;
|
|
|
|
if (this.isBoss() && !ignoreSegments) {
|
|
const segmentSize = this.getMaxHp() / this.bossSegments;
|
|
for (let s = this.bossSegmentIndex; s > 0; s--) {
|
|
const hpThreshold = segmentSize * s;
|
|
const roundedHpThreshold = Math.round(hpThreshold);
|
|
if (this.hp >= roundedHpThreshold) {
|
|
if (this.hp - damage <= roundedHpThreshold) {
|
|
const hpRemainder = this.hp - roundedHpThreshold;
|
|
let segmentsBypassed = 0;
|
|
while (segmentsBypassed < this.bossSegmentIndex && this.canBypassBossSegments(segmentsBypassed + 1) && (damage - hpRemainder) >= Math.round(segmentSize * Math.pow(2, segmentsBypassed + 1))) {
|
|
segmentsBypassed++;
|
|
//console.log('damage', damage, 'segment', segmentsBypassed + 1, 'segment size', segmentSize, 'damage needed', Math.round(segmentSize * Math.pow(2, segmentsBypassed + 1)));
|
|
}
|
|
|
|
damage = hpRemainder + Math.round(segmentSize * segmentsBypassed);
|
|
clearedBossSegmentIndex = s - segmentsBypassed;
|
|
}
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
switch (this.scene.currentBattle.battleSpec) {
|
|
case BattleSpec.FINAL_BOSS:
|
|
if (!this.formIndex && this.bossSegmentIndex < 1) {
|
|
damage = Math.min(damage, this.hp - 1);
|
|
}
|
|
}
|
|
|
|
const ret = super.damage(damage, ignoreSegments, preventEndure, ignoreFaintPhase);
|
|
|
|
if (this.isBoss()) {
|
|
if (ignoreSegments) {
|
|
const segmentSize = this.getMaxHp() / this.bossSegments;
|
|
clearedBossSegmentIndex = Math.ceil(this.hp / segmentSize);
|
|
}
|
|
if (clearedBossSegmentIndex <= this.bossSegmentIndex) {
|
|
this.handleBossSegmentCleared(clearedBossSegmentIndex);
|
|
}
|
|
this.battleInfo.updateBossSegments(this);
|
|
}
|
|
|
|
return ret;
|
|
}
|
|
|
|
canBypassBossSegments(segmentCount: integer = 1): boolean {
|
|
if (this.scene.currentBattle.battleSpec === BattleSpec.FINAL_BOSS) {
|
|
if (!this.formIndex && (this.bossSegmentIndex - segmentCount) < 1) {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
handleBossSegmentCleared(segmentIndex: integer): void {
|
|
while (segmentIndex - 1 < this.bossSegmentIndex) {
|
|
let boostedStat = BattleStat.RAND;
|
|
|
|
const battleStats = Utils.getEnumValues(BattleStat).slice(0, -3);
|
|
const statWeights = new Array().fill(battleStats.length).filter((bs: BattleStat) => this.summonData.battleStats[bs] < 6).map((bs: BattleStat) => this.getStat(bs + 1));
|
|
const statThresholds: integer[] = [];
|
|
let totalWeight = 0;
|
|
for (const bs of battleStats) {
|
|
totalWeight += statWeights[bs];
|
|
statThresholds.push(totalWeight);
|
|
}
|
|
|
|
const randInt = Utils.randSeedInt(totalWeight);
|
|
|
|
for (const bs of battleStats) {
|
|
if (randInt < statThresholds[bs]) {
|
|
boostedStat = bs;
|
|
break;
|
|
}
|
|
}
|
|
|
|
let statLevels = 1;
|
|
|
|
switch (segmentIndex) {
|
|
case 1:
|
|
if (this.bossSegments >= 3) {
|
|
statLevels++;
|
|
}
|
|
break;
|
|
case 2:
|
|
if (this.bossSegments >= 5) {
|
|
statLevels++;
|
|
}
|
|
break;
|
|
}
|
|
|
|
this.scene.unshiftPhase(new StatChangePhase(this.scene, this.getBattlerIndex(), true, [ boostedStat ], statLevels, true, true));
|
|
|
|
this.bossSegmentIndex--;
|
|
}
|
|
}
|
|
|
|
heal(amount: integer): integer {
|
|
if (this.isBoss()) {
|
|
const amountRatio = amount / this.getMaxHp();
|
|
const segmentBypassCount = Math.floor(amountRatio / (1 / this.bossSegments));
|
|
const segmentSize = this.getMaxHp() / this.bossSegments;
|
|
for (let s = 1; s < this.bossSegments; s++) {
|
|
const hpThreshold = segmentSize * s;
|
|
if (this.hp <= Math.round(hpThreshold)) {
|
|
const healAmount = Math.min(amount, this.getMaxHp() - this.hp, Math.round(hpThreshold + (segmentSize * segmentBypassCount) - this.hp));
|
|
this.hp += healAmount;
|
|
return healAmount;
|
|
} else if (s >= this.bossSegmentIndex) {
|
|
return super.heal(amount);
|
|
}
|
|
}
|
|
}
|
|
|
|
return super.heal(amount);
|
|
}
|
|
|
|
getFieldIndex(): integer {
|
|
return this.scene.getEnemyField().indexOf(this);
|
|
}
|
|
|
|
getBattlerIndex(): BattlerIndex {
|
|
return BattlerIndex.ENEMY + this.getFieldIndex();
|
|
}
|
|
|
|
addToParty(pokeballType: PokeballType) {
|
|
const party = this.scene.getParty();
|
|
let ret: PlayerPokemon = null;
|
|
|
|
if (party.length < 6) {
|
|
this.pokeball = pokeballType;
|
|
this.metLevel = this.level;
|
|
this.metBiome = this.scene.arena.biomeType;
|
|
const newPokemon = this.scene.addPlayerPokemon(this.species, this.level, this.abilityIndex, this.formIndex, this.gender, this.shiny, this.variant, this.ivs, this.nature, this);
|
|
party.push(newPokemon);
|
|
ret = newPokemon;
|
|
this.scene.triggerPokemonFormChange(newPokemon, SpeciesFormChangeActiveTrigger, true);
|
|
}
|
|
|
|
return ret;
|
|
}
|
|
}
|
|
|
|
export interface TurnMove {
|
|
move: Moves;
|
|
targets?: BattlerIndex[];
|
|
result: MoveResult;
|
|
virtual?: boolean;
|
|
turn?: integer;
|
|
}
|
|
|
|
export interface QueuedMove {
|
|
move: Moves;
|
|
targets: BattlerIndex[];
|
|
ignorePP?: boolean;
|
|
}
|
|
|
|
export interface AttackMoveResult {
|
|
move: Moves;
|
|
result: DamageResult;
|
|
damage: integer;
|
|
critical: boolean;
|
|
sourceId: integer;
|
|
attackingPosition: BattlerIndex;
|
|
}
|
|
|
|
export class PokemonSummonData {
|
|
public battleStats: integer[] = [ 0, 0, 0, 0, 0, 0, 0 ];
|
|
public moveQueue: QueuedMove[] = [];
|
|
public disabledMove: Moves = Moves.NONE;
|
|
public disabledTurns: integer = 0;
|
|
public tags: BattlerTag[] = [];
|
|
public abilitySuppressed: boolean = false;
|
|
public abilitiesApplied: Abilities[] = [];
|
|
|
|
public speciesForm: PokemonSpeciesForm;
|
|
public fusionSpeciesForm: PokemonSpeciesForm;
|
|
public ability: Abilities = Abilities.NONE;
|
|
public gender: Gender;
|
|
public fusionGender: Gender;
|
|
public stats: integer[];
|
|
public moveset: PokemonMove[];
|
|
// If not initialized this value will not be populated from save data.
|
|
public types: Type[] = null;
|
|
}
|
|
|
|
export class PokemonBattleData {
|
|
public hitCount: integer = 0;
|
|
public endured: boolean = false;
|
|
public berriesEaten: BerryType[] = [];
|
|
public abilitiesApplied: Abilities[] = [];
|
|
public abilityRevealed: boolean = false;
|
|
}
|
|
|
|
export class PokemonBattleSummonData {
|
|
/** The number of turns the pokemon has passed since entering the battle */
|
|
public turnCount: integer = 1;
|
|
/** The list of moves the pokemon has used since entering the battle */
|
|
public moveHistory: TurnMove[] = [];
|
|
}
|
|
|
|
export class PokemonTurnData {
|
|
public flinched: boolean;
|
|
public acted: boolean;
|
|
public hitCount: integer;
|
|
public hitsLeft: integer;
|
|
public damageDealt: integer = 0;
|
|
public currDamageDealt: integer = 0;
|
|
public damageTaken: integer = 0;
|
|
public attacksReceived: AttackMoveResult[] = [];
|
|
}
|
|
|
|
export enum AiType {
|
|
RANDOM,
|
|
SMART_RANDOM,
|
|
SMART
|
|
}
|
|
|
|
export enum MoveResult {
|
|
PENDING,
|
|
SUCCESS,
|
|
FAIL,
|
|
MISS,
|
|
OTHER
|
|
}
|
|
|
|
export enum HitResult {
|
|
EFFECTIVE = 1,
|
|
SUPER_EFFECTIVE,
|
|
NOT_VERY_EFFECTIVE,
|
|
ONE_HIT_KO,
|
|
NO_EFFECT,
|
|
STATUS,
|
|
HEAL,
|
|
FAIL,
|
|
MISS,
|
|
OTHER,
|
|
IMMUNE
|
|
}
|
|
|
|
export type DamageResult = HitResult.EFFECTIVE | HitResult.SUPER_EFFECTIVE | HitResult.NOT_VERY_EFFECTIVE | HitResult.ONE_HIT_KO | HitResult.OTHER;
|
|
|
|
/**
|
|
* Wrapper class for the {@linkcode Move} class for Pokemon to interact with.
|
|
* These are the moves assigned to a {@linkcode Pokemon} object.
|
|
* It links to {@linkcode Move} class via the move ID.
|
|
* Compared to {@linkcode Move}, this class also tracks if a move has received.
|
|
* PP Ups, amount of PP used, and things like that.
|
|
* @see {@linkcode isUsable} - checks if move is disabled, out of PP, or not implemented.
|
|
* @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 getMovePp} - returns amount of PP a move currently has.
|
|
* @see {@linkcode getPpRatio} - returns the current PP amount / max PP amount.
|
|
* @see {@linkcode getName} - returns name of {@linkcode Move}.
|
|
**/
|
|
export class PokemonMove {
|
|
public moveId: Moves;
|
|
public ppUsed: integer;
|
|
public ppUp: integer;
|
|
public virtual: boolean;
|
|
|
|
constructor(moveId: Moves, ppUsed?: integer, ppUp?: integer, virtual?: boolean) {
|
|
this.moveId = moveId;
|
|
this.ppUsed = ppUsed || 0;
|
|
this.ppUp = ppUp || 0;
|
|
this.virtual = !!virtual;
|
|
}
|
|
|
|
isUsable(pokemon: Pokemon, ignorePp?: boolean): boolean {
|
|
if (this.moveId && pokemon.summonData?.disabledMove === this.moveId) {
|
|
return false;
|
|
}
|
|
return (ignorePp || this.ppUsed < this.getMovePp() || this.getMove().pp === -1) && !this.getMove().name.endsWith(" (N)");
|
|
}
|
|
|
|
getMove(): Move {
|
|
return allMoves[this.moveId];
|
|
}
|
|
|
|
/**
|
|
* Sets {@link ppUsed} for this move and ensures the value does not exceed {@link getMovePp}
|
|
* @param {number} count Amount of PP to use
|
|
*/
|
|
usePp(count: number = 1) {
|
|
this.ppUsed = Math.min(this.ppUsed + count, this.getMovePp());
|
|
}
|
|
|
|
getMovePp(): integer {
|
|
return this.getMove().pp + this.ppUp * Math.max(Math.floor(this.getMove().pp / 5), 1);
|
|
}
|
|
|
|
getPpRatio(): number {
|
|
return 1 - (this.ppUsed / this.getMovePp());
|
|
}
|
|
|
|
getName(): string {
|
|
return this.getMove().name;
|
|
}
|
|
|
|
/**
|
|
* Copies an existing move or creates a valid PokemonMove object from json representing one
|
|
* @param {PokemonMove | any} source The data for the move to copy
|
|
* @return {PokemonMove} A valid pokemonmove object
|
|
*/
|
|
static loadMove(source: PokemonMove | any): PokemonMove {
|
|
return new PokemonMove(source.moveId, source.ppUsed, source.ppUp, source.virtual);
|
|
}
|
|
}
|