Merge branch 'beta' into implement-new-release-process
4
package-lock.json
generated
@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "pokemon-rogue-battle",
|
||||
"version": "1.8.4",
|
||||
"version": "1.9.0",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "pokemon-rogue-battle",
|
||||
"version": "1.8.4",
|
||||
"version": "1.9.0",
|
||||
"hasInstallScript": true,
|
||||
"dependencies": {
|
||||
"@material/material-color-utilities": "^0.2.7",
|
||||
|
@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "pokemon-rogue-battle",
|
||||
"private": true,
|
||||
"version": "1.8.5",
|
||||
"version": "1.9.0",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"start": "vite",
|
||||
|
BIN
public/images/pokemon/icons/variant/4/417_2.png
Normal file
After Width: | Height: | Size: 744 B |
BIN
public/images/pokemon/icons/variant/4/417_3.png
Normal file
After Width: | Height: | Size: 747 B |
Before Width: | Height: | Size: 747 B |
BIN
public/images/pokemon/icons/variant/7/746-school_3.png
Normal file
After Width: | Height: | Size: 747 B |
Before Width: | Height: | Size: 492 B |
Before Width: | Height: | Size: 489 B After Width: | Height: | Size: 492 B |
BIN
public/images/pokemon/icons/variant/7/746_3.png
Normal file
After Width: | Height: | Size: 489 B |
BIN
public/images/pokemon/icons/variant/8/840_2.png
Normal file
After Width: | Height: | Size: 269 B |
BIN
public/images/pokemon/icons/variant/8/840_3.png
Normal file
After Width: | Height: | Size: 281 B |
BIN
public/images/pokemon/icons/variant/8/841-gigantamax_2.png
Normal file
After Width: | Height: | Size: 406 B |
BIN
public/images/pokemon/icons/variant/8/841-gigantamax_3.png
Normal file
After Width: | Height: | Size: 408 B |
BIN
public/images/pokemon/icons/variant/8/841_2.png
Normal file
After Width: | Height: | Size: 397 B |
BIN
public/images/pokemon/icons/variant/8/841_3.png
Normal file
After Width: | Height: | Size: 408 B |
BIN
public/images/pokemon/icons/variant/8/842-gigantamax_2.png
Normal file
After Width: | Height: | Size: 406 B |
BIN
public/images/pokemon/icons/variant/8/842-gigantamax_3.png
Normal file
After Width: | Height: | Size: 408 B |
BIN
public/images/pokemon/icons/variant/8/842_2.png
Normal file
After Width: | Height: | Size: 388 B |
BIN
public/images/pokemon/icons/variant/8/842_3.png
Normal file
After Width: | Height: | Size: 388 B |
Before Width: | Height: | Size: 471 B |
Before Width: | Height: | Size: 474 B After Width: | Height: | Size: 543 B |
BIN
public/images/pokemon/icons/variant/8/871_3.png
Normal file
After Width: | Height: | Size: 538 B |
BIN
public/images/pokemon/icons/variant/9/1011_2.png
Normal file
After Width: | Height: | Size: 344 B |
BIN
public/images/pokemon/icons/variant/9/1011_3.png
Normal file
After Width: | Height: | Size: 344 B |
BIN
public/images/pokemon/icons/variant/9/1019_2.png
Normal file
After Width: | Height: | Size: 492 B |
BIN
public/images/pokemon/icons/variant/9/1019_3.png
Normal file
After Width: | Height: | Size: 492 B |
Before Width: | Height: | Size: 45 KiB After Width: | Height: | Size: 46 KiB |
@ -304,7 +304,7 @@
|
||||
}
|
||||
},
|
||||
{
|
||||
"filename": "746-school_1",
|
||||
"filename": "746-school_2",
|
||||
"rotated": false,
|
||||
"trimmed": false,
|
||||
"sourceSize": {
|
||||
@ -325,7 +325,7 @@
|
||||
}
|
||||
},
|
||||
{
|
||||
"filename": "746-school_2",
|
||||
"filename": "746-school_3",
|
||||
"rotated": false,
|
||||
"trimmed": false,
|
||||
"sourceSize": {
|
||||
@ -346,7 +346,7 @@
|
||||
}
|
||||
},
|
||||
{
|
||||
"filename": "746_1",
|
||||
"filename": "746_2",
|
||||
"rotated": false,
|
||||
"trimmed": false,
|
||||
"sourceSize": {
|
||||
@ -367,7 +367,7 @@
|
||||
}
|
||||
},
|
||||
{
|
||||
"filename": "746_2",
|
||||
"filename": "746_3",
|
||||
"rotated": false,
|
||||
"trimmed": false,
|
||||
"sourceSize": {
|
||||
@ -3312,6 +3312,6 @@
|
||||
"meta": {
|
||||
"app": "https://www.codeandweb.com/texturepacker",
|
||||
"version": "3.0",
|
||||
"smartupdate": "$TexturePacker:SmartUpdate:57eaade41c16d492ffda5339ea142c4d:b96a0f88bd707a9967af73e7bdf13031:d5975df27e1e94206a68aa1fd3c2c8d0$"
|
||||
"smartupdate": "$TexturePacker:SmartUpdate:0780b00fda53c3fbd0b6e554e89a6818:b96a0f88bd707a9967af73e7bdf13031:d5975df27e1e94206a68aa1fd3c2c8d0$"
|
||||
}
|
||||
}
|
||||
|
Before Width: | Height: | Size: 46 KiB After Width: | Height: | Size: 48 KiB |
@ -3013,7 +3013,7 @@
|
||||
}
|
||||
},
|
||||
{
|
||||
"filename": "1012-counterfeit_2",
|
||||
"filename": "1011_2",
|
||||
"rotated": false,
|
||||
"trimmed": false,
|
||||
"sourceSize": {
|
||||
@ -3034,7 +3034,7 @@
|
||||
}
|
||||
},
|
||||
{
|
||||
"filename": "1012-counterfeit_3",
|
||||
"filename": "1011_3",
|
||||
"rotated": false,
|
||||
"trimmed": false,
|
||||
"sourceSize": {
|
||||
@ -3055,7 +3055,7 @@
|
||||
}
|
||||
},
|
||||
{
|
||||
"filename": "1013-unremarkable_2",
|
||||
"filename": "1012-counterfeit_2",
|
||||
"rotated": false,
|
||||
"trimmed": false,
|
||||
"sourceSize": {
|
||||
@ -3076,7 +3076,7 @@
|
||||
}
|
||||
},
|
||||
{
|
||||
"filename": "1013-unremarkable_3",
|
||||
"filename": "1012-counterfeit_3",
|
||||
"rotated": false,
|
||||
"trimmed": false,
|
||||
"sourceSize": {
|
||||
@ -3097,7 +3097,7 @@
|
||||
}
|
||||
},
|
||||
{
|
||||
"filename": "1018_2",
|
||||
"filename": "1013-unremarkable_2",
|
||||
"rotated": false,
|
||||
"trimmed": false,
|
||||
"sourceSize": {
|
||||
@ -3118,7 +3118,7 @@
|
||||
}
|
||||
},
|
||||
{
|
||||
"filename": "1018_3",
|
||||
"filename": "1013-unremarkable_3",
|
||||
"rotated": false,
|
||||
"trimmed": false,
|
||||
"sourceSize": {
|
||||
@ -3139,7 +3139,7 @@
|
||||
}
|
||||
},
|
||||
{
|
||||
"filename": "1022_2",
|
||||
"filename": "1018_2",
|
||||
"rotated": false,
|
||||
"trimmed": false,
|
||||
"sourceSize": {
|
||||
@ -3160,7 +3160,7 @@
|
||||
}
|
||||
},
|
||||
{
|
||||
"filename": "1022_3",
|
||||
"filename": "1018_3",
|
||||
"rotated": false,
|
||||
"trimmed": false,
|
||||
"sourceSize": {
|
||||
@ -3181,7 +3181,7 @@
|
||||
}
|
||||
},
|
||||
{
|
||||
"filename": "1023_2",
|
||||
"filename": "1019_2",
|
||||
"rotated": false,
|
||||
"trimmed": false,
|
||||
"sourceSize": {
|
||||
@ -3202,7 +3202,7 @@
|
||||
}
|
||||
},
|
||||
{
|
||||
"filename": "1023_3",
|
||||
"filename": "1019_3",
|
||||
"rotated": false,
|
||||
"trimmed": false,
|
||||
"sourceSize": {
|
||||
@ -3223,7 +3223,7 @@
|
||||
}
|
||||
},
|
||||
{
|
||||
"filename": "8901_1",
|
||||
"filename": "1022_2",
|
||||
"rotated": false,
|
||||
"trimmed": false,
|
||||
"sourceSize": {
|
||||
@ -3244,7 +3244,7 @@
|
||||
}
|
||||
},
|
||||
{
|
||||
"filename": "8901_2",
|
||||
"filename": "1022_3",
|
||||
"rotated": false,
|
||||
"trimmed": false,
|
||||
"sourceSize": {
|
||||
@ -3265,7 +3265,7 @@
|
||||
}
|
||||
},
|
||||
{
|
||||
"filename": "8901_3",
|
||||
"filename": "1023_2",
|
||||
"rotated": false,
|
||||
"trimmed": false,
|
||||
"sourceSize": {
|
||||
@ -3284,6 +3284,90 @@
|
||||
"w": 40,
|
||||
"h": 30
|
||||
}
|
||||
},
|
||||
{
|
||||
"filename": "1023_3",
|
||||
"rotated": false,
|
||||
"trimmed": false,
|
||||
"sourceSize": {
|
||||
"w": 40,
|
||||
"h": 30
|
||||
},
|
||||
"spriteSourceSize": {
|
||||
"x": 0,
|
||||
"y": 0,
|
||||
"w": 40,
|
||||
"h": 30
|
||||
},
|
||||
"frame": {
|
||||
"x": 80,
|
||||
"y": 420,
|
||||
"w": 40,
|
||||
"h": 30
|
||||
}
|
||||
},
|
||||
{
|
||||
"filename": "8901_1",
|
||||
"rotated": false,
|
||||
"trimmed": false,
|
||||
"sourceSize": {
|
||||
"w": 40,
|
||||
"h": 30
|
||||
},
|
||||
"spriteSourceSize": {
|
||||
"x": 0,
|
||||
"y": 0,
|
||||
"w": 40,
|
||||
"h": 30
|
||||
},
|
||||
"frame": {
|
||||
"x": 120,
|
||||
"y": 420,
|
||||
"w": 40,
|
||||
"h": 30
|
||||
}
|
||||
},
|
||||
{
|
||||
"filename": "8901_2",
|
||||
"rotated": false,
|
||||
"trimmed": false,
|
||||
"sourceSize": {
|
||||
"w": 40,
|
||||
"h": 30
|
||||
},
|
||||
"spriteSourceSize": {
|
||||
"x": 0,
|
||||
"y": 0,
|
||||
"w": 40,
|
||||
"h": 30
|
||||
},
|
||||
"frame": {
|
||||
"x": 160,
|
||||
"y": 420,
|
||||
"w": 40,
|
||||
"h": 30
|
||||
}
|
||||
},
|
||||
{
|
||||
"filename": "8901_3",
|
||||
"rotated": false,
|
||||
"trimmed": false,
|
||||
"sourceSize": {
|
||||
"w": 40,
|
||||
"h": 30
|
||||
},
|
||||
"spriteSourceSize": {
|
||||
"x": 0,
|
||||
"y": 0,
|
||||
"w": 40,
|
||||
"h": 30
|
||||
},
|
||||
"frame": {
|
||||
"x": 200,
|
||||
"y": 420,
|
||||
"w": 40,
|
||||
"h": 30
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
@ -3291,6 +3375,6 @@
|
||||
"meta": {
|
||||
"app": "https://www.codeandweb.com/texturepacker",
|
||||
"version": "3.0",
|
||||
"smartupdate": "$TexturePacker:SmartUpdate:c01add1e11aabd2f8931110a67a9222b:e7531bea9b5e1bef44def5b357c81630:3ec5c0bc286c296cfb7fa30a8b06f3da$"
|
||||
"smartupdate": "$TexturePacker:SmartUpdate:a78ab8261d4cd63caee19962a0e01d8a:cb77bcbd2cc296577c3f2ba84b4c50f2:3ec5c0bc286c296cfb7fa30a8b06f3da$"
|
||||
}
|
||||
}
|
||||
|
Before Width: | Height: | Size: 34 KiB After Width: | Height: | Size: 35 KiB |
@ -1 +1 @@
|
||||
Subproject commit 833dc40ec7409031fcea147ccbc45ec9c0ba0213
|
||||
Subproject commit a7036a07875615674ea898d0fe3b182a1080af38
|
@ -7,7 +7,6 @@ import type PokemonSpecies from "#app/data/pokemon-species";
|
||||
import { allSpecies, getPokemonSpecies } from "#app/data/pokemon-species";
|
||||
import {
|
||||
fixedInt,
|
||||
deepMergeObjects,
|
||||
getIvsFromId,
|
||||
randSeedInt,
|
||||
getEnumValues,
|
||||
@ -19,6 +18,7 @@ import {
|
||||
BooleanHolder,
|
||||
type Constructor,
|
||||
} from "#app/utils/common";
|
||||
import { deepMergeSpriteData } from "#app/utils/data";
|
||||
import type { Modifier, ModifierPredicate, TurnHeldItemTransferModifier } from "./modifier/modifier";
|
||||
import {
|
||||
ConsumableModifier,
|
||||
@ -787,7 +787,7 @@ export default class BattleScene extends SceneBase {
|
||||
return;
|
||||
}
|
||||
const expVariantData = await this.cachedFetch("./images/pokemon/variant/_exp_masterlist.json").then(r => r.json());
|
||||
deepMergeObjects(variantData, expVariantData);
|
||||
deepMergeSpriteData(variantData, expVariantData);
|
||||
}
|
||||
|
||||
cachedFetch(url: string, init?: RequestInit): Promise<Response> {
|
||||
@ -835,6 +835,7 @@ export default class BattleScene extends SceneBase {
|
||||
return this.getPlayerField().find(p => p.isActive() && (includeSwitching || p.switchOutStatus === false));
|
||||
}
|
||||
|
||||
// TODO: Add `undefined` to return type
|
||||
/**
|
||||
* Returns an array of PlayerPokemon of length 1 or 2 depending on if in a double battle or not.
|
||||
* Does not actually check if the pokemon are on the field or not.
|
||||
@ -850,9 +851,9 @@ export default class BattleScene extends SceneBase {
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns The first {@linkcode EnemyPokemon} that is {@linkcode getEnemyField on the field}
|
||||
* and {@linkcode EnemyPokemon.isActive is active}
|
||||
* (aka {@linkcode EnemyPokemon.isAllowedInBattle is allowed in battle}),
|
||||
* @returns The first {@linkcode EnemyPokemon} that is {@linkcode getEnemyField | on the field}
|
||||
* and {@linkcode EnemyPokemon.isActive | is active}
|
||||
* (aka {@linkcode EnemyPokemon.isAllowedInBattle | is allowed in battle}),
|
||||
* or `undefined` if there are no valid pokemon
|
||||
* @param includeSwitching Whether a pokemon that is currently switching out is valid, default `true`
|
||||
*/
|
||||
@ -873,8 +874,8 @@ export default class BattleScene extends SceneBase {
|
||||
/**
|
||||
* Returns an array of Pokemon on both sides of the battle - player first, then enemy.
|
||||
* Does not actually check if the pokemon are on the field or not, and always has length 4 regardless of battle type.
|
||||
* @param activeOnly Whether to consider only active pokemon
|
||||
* @returns array of {@linkcode Pokemon}
|
||||
* @param activeOnly - Whether to consider only active pokemon; default `false`
|
||||
* @returns An array of {@linkcode Pokemon}, as described above.
|
||||
*/
|
||||
public getField(activeOnly = false): Pokemon[] {
|
||||
const ret = new Array(4).fill(null);
|
||||
@ -1307,14 +1308,13 @@ export default class BattleScene extends SceneBase {
|
||||
return isNewBiome;
|
||||
}
|
||||
|
||||
// TODO: ...this never actually returns `null`, right?
|
||||
newBattle(
|
||||
waveIndex?: number,
|
||||
battleType?: BattleType,
|
||||
trainerData?: TrainerData,
|
||||
double?: boolean,
|
||||
mysteryEncounterType?: MysteryEncounterType,
|
||||
): Battle | null {
|
||||
): Battle {
|
||||
const _startingWave = Overrides.STARTING_WAVE_OVERRIDE || startingWave;
|
||||
const newWaveIndex = waveIndex || (this.currentBattle?.waveIndex || _startingWave - 1) + 1;
|
||||
let newDouble: boolean | undefined;
|
||||
@ -1496,7 +1496,7 @@ export default class BattleScene extends SceneBase {
|
||||
});
|
||||
|
||||
for (const pokemon of this.getPlayerParty()) {
|
||||
pokemon.resetBattleData();
|
||||
pokemon.resetBattleAndWaveData();
|
||||
pokemon.resetTera();
|
||||
applyPostBattleInitAbAttrs(PostBattleInitAbAttr, pokemon);
|
||||
if (
|
||||
@ -3264,6 +3264,7 @@ export default class BattleScene extends SceneBase {
|
||||
[this.modifierBar, this.enemyModifierBar].map(m => m.setVisible(visible));
|
||||
}
|
||||
|
||||
// TODO: Document this
|
||||
updateModifiers(player = true, instant?: boolean): void {
|
||||
const modifiers = player ? this.modifiers : (this.enemyModifiers as PersistentModifier[]);
|
||||
for (let m = 0; m < modifiers.length; m++) {
|
||||
@ -3316,8 +3317,8 @@ export default class BattleScene extends SceneBase {
|
||||
* gets removed. This function does NOT apply in-battle effects, such as Unburden.
|
||||
* If in-battle effects are needed, use {@linkcode Pokemon.loseHeldItem} instead.
|
||||
* @param modifier The item to be removed.
|
||||
* @param enemy If `true`, remove an item owned by the enemy. If `false`, remove an item owned by the player. Default is `false`.
|
||||
* @returns `true` if the item exists and was successfully removed, `false` otherwise.
|
||||
* @param enemy `true` to remove an item owned by the enemy rather than the player; default `false`.
|
||||
* @returns `true` if the item exists and was successfully removed, `false` otherwise
|
||||
*/
|
||||
removeModifier(modifier: PersistentModifier, enemy = false): boolean {
|
||||
const modifiers = !enemy ? this.modifiers : this.enemyModifiers;
|
||||
|
@ -6,6 +6,10 @@ export abstract class AbAttr {
|
||||
public showAbility: boolean;
|
||||
private extraCondition: AbAttrCondition;
|
||||
|
||||
/**
|
||||
* @param showAbility - Whether to show this ability as a flyout during battle; default `true`.
|
||||
* Should be kept in parity with mainline where possible.
|
||||
*/
|
||||
constructor(showAbility = true) {
|
||||
this.showAbility = showAbility;
|
||||
}
|
||||
|
@ -60,6 +60,11 @@ import { SwitchType } from "#enums/switch-type";
|
||||
import { MoveFlags } from "#enums/MoveFlags";
|
||||
import { MoveTarget } from "#enums/MoveTarget";
|
||||
import { MoveCategory } from "#enums/MoveCategory";
|
||||
import type { BerryType } from "#enums/berry-type";
|
||||
import { CommonAnimPhase } from "#app/phases/common-anim-phase";
|
||||
import { CommonAnim } from "../battle-anims";
|
||||
import { getBerryEffectFunc } from "../berry";
|
||||
import { BerryUsedEvent } from "#app/events/battle-scene";
|
||||
|
||||
|
||||
// Type imports
|
||||
@ -2675,7 +2680,7 @@ export class PostSummonCopyAllyStatsAbAttr extends PostSummonAbAttr {
|
||||
}
|
||||
|
||||
/**
|
||||
* Used by Imposter
|
||||
* Attribute used by {@linkcode Abilities.IMPOSTER} to transform into a random opposing pokemon on entry.
|
||||
*/
|
||||
export class PostSummonTransformAbAttr extends PostSummonAbAttr {
|
||||
constructor() {
|
||||
@ -2710,7 +2715,7 @@ export class PostSummonTransformAbAttr extends PostSummonAbAttr {
|
||||
const targets = pokemon.getOpponents();
|
||||
const target = this.getTarget(targets);
|
||||
|
||||
if (!!target.summonData?.illusion) {
|
||||
if (target.summonData.illusion) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@ -3292,13 +3297,13 @@ export class ConditionalUserFieldStatusEffectImmunityAbAttr extends UserFieldSta
|
||||
|
||||
/**
|
||||
* Conditionally provides immunity to stat drop effects to the user's field.
|
||||
*
|
||||
*
|
||||
* Used by {@linkcode Abilities.FLOWER_VEIL | Flower Veil}.
|
||||
*/
|
||||
export class ConditionalUserFieldProtectStatAbAttr extends PreStatStageChangeAbAttr {
|
||||
/** {@linkcode BattleStat} to protect or `undefined` if **all** {@linkcode BattleStat} are protected */
|
||||
protected protectedStat?: BattleStat;
|
||||
|
||||
|
||||
/** If the method evaluates to true, the stat will be protected. */
|
||||
protected condition: (target: Pokemon) => boolean;
|
||||
|
||||
@ -3315,7 +3320,7 @@ export class ConditionalUserFieldProtectStatAbAttr extends PreStatStageChangeAbA
|
||||
* @param stat The stat being affected
|
||||
* @param cancelled Holds whether the stat change was already prevented.
|
||||
* @param args Args[0] is the target pokemon of the stat change.
|
||||
* @returns
|
||||
* @returns
|
||||
*/
|
||||
override canApplyPreStatStageChange(pokemon: Pokemon, passive: boolean, simulated: boolean, stat: BattleStat, cancelled: BooleanHolder, args: [Pokemon, ...any]): boolean {
|
||||
const target = args[0];
|
||||
@ -3451,7 +3456,7 @@ export class BonusCritAbAttr extends AbAttr {
|
||||
|
||||
/**
|
||||
* Apply the bonus crit ability by increasing the value in the provided number holder by 1
|
||||
*
|
||||
*
|
||||
* @param pokemon The pokemon with the BonusCrit ability (unused)
|
||||
* @param passive Unused
|
||||
* @param simulated Unused
|
||||
@ -3604,7 +3609,7 @@ export class PreWeatherEffectAbAttr extends AbAttr {
|
||||
args: any[]): boolean {
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
applyPreWeatherEffect(
|
||||
pokemon: Pokemon,
|
||||
passive: boolean,
|
||||
@ -3657,14 +3662,10 @@ export class SuppressWeatherEffectAbAttr extends PreWeatherEffectAbAttr {
|
||||
* Condition function to applied to abilities related to Sheer Force.
|
||||
* Checks if last move used against target was affected by a Sheer Force user and:
|
||||
* Disables: Color Change, Pickpocket, Berserk, Anger Shell
|
||||
* @returns {AbAttrCondition} If false disables the ability which the condition is applied to.
|
||||
* @returns An {@linkcode AbAttrCondition} to disable the ability under the proper conditions.
|
||||
*/
|
||||
function getSheerForceHitDisableAbCondition(): AbAttrCondition {
|
||||
return (pokemon: Pokemon) => {
|
||||
if (!pokemon.turnData) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const lastReceivedAttack = pokemon.turnData.attacksReceived[0];
|
||||
if (!lastReceivedAttack) {
|
||||
return true;
|
||||
@ -3675,7 +3676,7 @@ function getSheerForceHitDisableAbCondition(): AbAttrCondition {
|
||||
return true;
|
||||
}
|
||||
|
||||
/**if the last move chance is greater than or equal to cero, and the last attacker's ability is sheer force*/
|
||||
/** `true` if the last move's chance is above 0 and the last attacker's ability is sheer force */
|
||||
const SheerForceAffected = allMoves[lastReceivedAttack.move].chance >= 0 && lastAttacker.hasAbility(Abilities.SHEER_FORCE);
|
||||
|
||||
return !SheerForceAffected;
|
||||
@ -3745,7 +3746,7 @@ function getAnticipationCondition(): AbAttrCondition {
|
||||
*/
|
||||
function getOncePerBattleCondition(ability: Abilities): AbAttrCondition {
|
||||
return (pokemon: Pokemon) => {
|
||||
return !pokemon.battleData?.abilitiesApplied.includes(ability);
|
||||
return !pokemon.waveData.abilitiesApplied.has(ability);
|
||||
};
|
||||
}
|
||||
|
||||
@ -4034,7 +4035,7 @@ export class PostTurnStatusHealAbAttr extends PostTurnAbAttr {
|
||||
|
||||
/**
|
||||
* After the turn ends, resets the status of either the ability holder or their ally
|
||||
* @param {boolean} allyTarget Whether to target ally, defaults to false (self-target)
|
||||
* @param allyTarget Whether to target ally, defaults to false (self-target)
|
||||
*/
|
||||
export class PostTurnResetStatusAbAttr extends PostTurnAbAttr {
|
||||
private allyTarget: boolean;
|
||||
@ -4066,79 +4067,153 @@ export class PostTurnResetStatusAbAttr extends PostTurnAbAttr {
|
||||
}
|
||||
|
||||
/**
|
||||
* After the turn ends, try to create an extra item
|
||||
* Attribute to try and restore eaten berries after the turn ends.
|
||||
* Used by {@linkcode Abilities.HARVEST}.
|
||||
*/
|
||||
export class PostTurnLootAbAttr extends PostTurnAbAttr {
|
||||
export class PostTurnRestoreBerryAbAttr extends PostTurnAbAttr {
|
||||
/**
|
||||
* @param itemType - The type of item to create
|
||||
* @param procChance - Chance to create an item
|
||||
* @see {@linkcode applyPostTurn()}
|
||||
* Array containing all {@linkcode BerryType | BerryTypes} that are under cap and able to be restored.
|
||||
* Stored inside the class for a minor performance boost
|
||||
*/
|
||||
private berriesUnderCap: BerryType[]
|
||||
|
||||
/**
|
||||
* @param procChance - function providing chance to restore an item
|
||||
* @see {@linkcode createEatenBerry()}
|
||||
*/
|
||||
constructor(
|
||||
/** Extend itemType to add more options */
|
||||
private itemType: "EATEN_BERRIES" | "HELD_BERRIES",
|
||||
private procChance: (pokemon: Pokemon) => number
|
||||
) {
|
||||
super();
|
||||
}
|
||||
|
||||
override canApplyPostTurn(pokemon: Pokemon, passive: boolean, simulated: boolean, args: any[]): boolean {
|
||||
// Clamp procChance to [0, 1]. Skip if didn't proc (less than pass)
|
||||
const pass = Phaser.Math.RND.realInRange(0, 1);
|
||||
return !(Math.max(Math.min(this.procChance(pokemon), 1), 0) < pass) && this.itemType === "EATEN_BERRIES" && !!pokemon.battleData.berriesEaten;
|
||||
}
|
||||
override canApplyPostTurn(pokemon: Pokemon, _passive: boolean, _simulated: boolean, _args: any[]): boolean {
|
||||
// Ensure we have at least 1 recoverable berry (at least 1 berry in berriesEaten is not capped)
|
||||
const cappedBerries = new Set(
|
||||
globalScene.getModifiers(BerryModifier, pokemon.isPlayer()).filter(
|
||||
bm => bm.pokemonId === pokemon.id && bm.getCountUnderMax() < 1
|
||||
).map(bm => bm.berryType)
|
||||
);
|
||||
|
||||
override applyPostTurn(pokemon: Pokemon, passive: boolean, simulated: boolean, args: any[]): void {
|
||||
this.createEatenBerry(pokemon, simulated);
|
||||
}
|
||||
this.berriesUnderCap = pokemon.battleData.berriesEaten.filter(
|
||||
bt => !cappedBerries.has(bt)
|
||||
);
|
||||
|
||||
/**
|
||||
* Create a new berry chosen randomly from the berries the pokemon ate this battle
|
||||
* @param pokemon The pokemon with this ability
|
||||
* @param simulated whether the associated ability call is simulated
|
||||
* @returns whether a new berry was created
|
||||
*/
|
||||
createEatenBerry(pokemon: Pokemon, simulated: boolean): boolean {
|
||||
const berriesEaten = pokemon.battleData.berriesEaten;
|
||||
|
||||
if (!berriesEaten.length) {
|
||||
if (!this.berriesUnderCap.length) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (simulated) {
|
||||
return true;
|
||||
// Clamp procChance to [0, 1]. Skip if didn't proc (less than pass)
|
||||
const pass = Phaser.Math.RND.realInRange(0, 1);
|
||||
return Phaser.Math.Clamp(this.procChance(pokemon), 0, 1) >= pass;
|
||||
}
|
||||
|
||||
override applyPostTurn(pokemon: Pokemon, passive: boolean, simulated: boolean, args: any[]): void {
|
||||
if (!simulated) {
|
||||
this.createEatenBerry(pokemon);
|
||||
}
|
||||
}
|
||||
|
||||
const randomIdx = randSeedInt(berriesEaten.length);
|
||||
const chosenBerryType = berriesEaten[randomIdx];
|
||||
/**
|
||||
* Create a new berry chosen randomly from all berries the pokemon ate this battle
|
||||
* @param pokemon - The {@linkcode Pokemon} with this ability
|
||||
* @returns `true` if a new berry was created
|
||||
*/
|
||||
createEatenBerry(pokemon: Pokemon): boolean {
|
||||
// Pick a random available berry to yoink
|
||||
const randomIdx = randSeedInt(this.berriesUnderCap.length);
|
||||
const chosenBerryType = this.berriesUnderCap[randomIdx];
|
||||
pokemon.battleData.berriesEaten.splice(randomIdx, 1); // Remove berry from memory
|
||||
const chosenBerry = new BerryModifierType(chosenBerryType);
|
||||
berriesEaten.splice(randomIdx); // Remove berry from memory
|
||||
|
||||
// Add the randomly chosen berry or update the existing one
|
||||
const berryModifier = globalScene.findModifier(
|
||||
(m) => m instanceof BerryModifier && m.berryType === chosenBerryType,
|
||||
(m) => m instanceof BerryModifier && m.berryType === chosenBerryType && m.pokemonId == pokemon.id,
|
||||
pokemon.isPlayer()
|
||||
) as BerryModifier | undefined;
|
||||
|
||||
if (!berryModifier) {
|
||||
if (berryModifier) {
|
||||
berryModifier.stackCount++
|
||||
} else {
|
||||
const newBerry = new BerryModifier(chosenBerry, pokemon.id, chosenBerryType, 1);
|
||||
if (pokemon.isPlayer()) {
|
||||
globalScene.addModifier(newBerry);
|
||||
} else {
|
||||
globalScene.addEnemyModifier(newBerry);
|
||||
}
|
||||
} else if (berryModifier.stackCount < berryModifier.getMaxHeldItemCount(pokemon)) {
|
||||
berryModifier.stackCount++;
|
||||
}
|
||||
|
||||
globalScene.queueMessage(i18next.t("abilityTriggers:postTurnLootCreateEatenBerry", { pokemonNameWithAffix: getPokemonNameWithAffix(pokemon), berryName: chosenBerry.name }));
|
||||
globalScene.updateModifiers(pokemon.isPlayer());
|
||||
|
||||
globalScene.queueMessage(i18next.t("abilityTriggers:postTurnLootCreateEatenBerry", { pokemonNameWithAffix: getPokemonNameWithAffix(pokemon), berryName: chosenBerry.name }));
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Attribute used for {@linkcode Abilities.MOODY}
|
||||
* Attribute to track and re-trigger last turn's berries at the end of the `BerryPhase`.
|
||||
* Used by {@linkcode Abilities.CUD_CHEW}.
|
||||
*/
|
||||
export class RepeatBerryNextTurnAbAttr extends PostTurnAbAttr {
|
||||
/**
|
||||
* @returns `true` if the pokemon ate anything last turn
|
||||
*/
|
||||
override canApply(pokemon: Pokemon, _passive: boolean, _simulated: boolean, _args: any[]): boolean {
|
||||
// force ability popup for ability triggers on normal turns.
|
||||
// Still not used if ability doesn't proc
|
||||
this.showAbility = true;
|
||||
return !!pokemon.summonData.berriesEatenLast.length;
|
||||
}
|
||||
|
||||
/**
|
||||
* Cause this {@linkcode Pokemon} to regurgitate and eat all berries inside its `berriesEatenLast` array.
|
||||
* Triggers a berry use animation, but does *not* count for other berry or item-related abilities.
|
||||
* @param pokemon - The {@linkcode Pokemon} having a bad tummy ache
|
||||
* @param _passive - N/A
|
||||
* @param _simulated - N/A
|
||||
* @param _cancelled - N/A
|
||||
* @param _args - N/A
|
||||
*/
|
||||
override apply(pokemon: Pokemon, _passive: boolean, _simulated: boolean, _cancelled: BooleanHolder | null, _args: any[]): void {
|
||||
globalScene.unshiftPhase(
|
||||
new CommonAnimPhase(pokemon.getBattlerIndex(), pokemon.getBattlerIndex(), CommonAnim.USE_ITEM),
|
||||
);
|
||||
|
||||
// Re-apply effects of all berries previously scarfed.
|
||||
// This doesn't count as "eating" a berry (for unnerve/stuff cheeks/unburden) as no item is consumed.
|
||||
for (const berryType of pokemon.summonData.berriesEatenLast) {
|
||||
getBerryEffectFunc(berryType)(pokemon);
|
||||
const bMod = new BerryModifier(new BerryModifierType(berryType), pokemon.id, berryType, 1);
|
||||
globalScene.eventTarget.dispatchEvent(new BerryUsedEvent(bMod)); // trigger message
|
||||
}
|
||||
|
||||
// uncomment to make cheek pouch work with cud chew
|
||||
// applyAbAttrs(HealFromBerryUseAbAttr, pokemon, new BooleanHolder(false));
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns always `true` as we always want to move berries into summon data
|
||||
*/
|
||||
override canApplyPostTurn(pokemon: Pokemon, _passive: boolean, _simulated: boolean, _args: any[]): boolean {
|
||||
this.showAbility = false; // don't show popup for turn end berry moving (should ideally be hidden)
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Move this {@linkcode Pokemon}'s `berriesEaten` array from `PokemonTurnData`
|
||||
* into `PokemonSummonData` on turn end.
|
||||
* Both arrays are cleared on switch.
|
||||
* @param pokemon - The {@linkcode Pokemon} having a nice snack
|
||||
* @param _passive - N/A
|
||||
* @param _simulated - N/A
|
||||
* @param _args - N/A
|
||||
*/
|
||||
override applyPostTurn(pokemon: Pokemon, _passive: boolean, _simulated: boolean, _args: any[]): void {
|
||||
pokemon.summonData.berriesEatenLast = pokemon.turnData.berriesEaten;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Attribute used for {@linkcode Abilities.MOODY} to randomly raise and lower stats at turn end.
|
||||
*/
|
||||
export class MoodyAbAttr extends PostTurnAbAttr {
|
||||
constructor() {
|
||||
@ -4232,7 +4307,7 @@ export class PostTurnHurtIfSleepingAbAttr extends PostTurnAbAttr {
|
||||
}
|
||||
/**
|
||||
* Deals damage to all sleeping opponents equal to 1/8 of their max hp (min 1)
|
||||
* @param pokemon Pokemon that has this ability
|
||||
* @param pokemon {@linkcode Pokemon} with this ability
|
||||
* @param passive N/A
|
||||
* @param simulated `true` if applying in a simulated call.
|
||||
* @param args N/A
|
||||
@ -4414,7 +4489,7 @@ export class PostItemLostAbAttr extends AbAttr {
|
||||
}
|
||||
|
||||
/**
|
||||
* Applies a Battler Tag to the Pokemon after it loses or consumes item
|
||||
* Applies a Battler Tag to the Pokemon after it loses or consumes an item
|
||||
* @extends PostItemLostAbAttr
|
||||
*/
|
||||
export class PostItemLostApplyBattlerTagAbAttr extends PostItemLostAbAttr {
|
||||
@ -4503,8 +4578,19 @@ export class DoubleBerryEffectAbAttr extends AbAttr {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Attribute to prevent opposing berry use while on the field.
|
||||
* Used by {@linkcode Abilities.UNNERVE}, {@linkcode Abilities.AS_ONE_GLASTRIER} and {@linkcode Abilities.AS_ONE_SPECTRIER}
|
||||
*/
|
||||
export class PreventBerryUseAbAttr extends AbAttr {
|
||||
override apply(pokemon: Pokemon, passive: boolean, simulated: boolean, cancelled: BooleanHolder, args: any[]): void {
|
||||
/**
|
||||
* Prevent use of opposing berries.
|
||||
* @param _pokemon - Unused
|
||||
* @param _passive - Unused
|
||||
* @param _simulated - Unused
|
||||
* @param cancelled - {@linkcode BooleanHolder} containing whether to block berry use
|
||||
*/
|
||||
override apply(_pokemon: Pokemon, _passive: boolean, _simulated: boolean, cancelled: BooleanHolder): void {
|
||||
cancelled.value = true;
|
||||
}
|
||||
}
|
||||
@ -4526,17 +4612,19 @@ export class HealFromBerryUseAbAttr extends AbAttr {
|
||||
}
|
||||
|
||||
override apply(pokemon: Pokemon, passive: boolean, simulated: boolean, ...args: [BooleanHolder, any[]]): void {
|
||||
if (simulated) {
|
||||
return;
|
||||
}
|
||||
|
||||
const { name: abilityName } = passive ? pokemon.getPassiveAbility() : pokemon.getAbility();
|
||||
if (!simulated) {
|
||||
globalScene.unshiftPhase(
|
||||
new PokemonHealPhase(
|
||||
pokemon.getBattlerIndex(),
|
||||
toDmgValue(pokemon.getMaxHp() * this.healPercent),
|
||||
i18next.t("abilityTriggers:healFromBerryUse", { pokemonNameWithAffix: getPokemonNameWithAffix(pokemon), abilityName }),
|
||||
true
|
||||
globalScene.unshiftPhase(
|
||||
new PokemonHealPhase(
|
||||
pokemon.getBattlerIndex(),
|
||||
toDmgValue(pokemon.getMaxHp() * this.healPercent),
|
||||
i18next.t("abilityTriggers:healFromBerryUse", { pokemonNameWithAffix: getPokemonNameWithAffix(pokemon), abilityName }),
|
||||
true
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -4568,7 +4656,8 @@ export class CheckTrappedAbAttr extends AbAttr {
|
||||
simulated: boolean,
|
||||
trapped: BooleanHolder,
|
||||
otherPokemon: Pokemon,
|
||||
args: any[]): boolean {
|
||||
args: any[],
|
||||
): boolean {
|
||||
return true;
|
||||
}
|
||||
|
||||
@ -5091,7 +5180,7 @@ export class PostSummonStatStageChangeOnArenaAbAttr extends PostSummonStatStageC
|
||||
/**
|
||||
* Takes no damage from the first hit of a damaging move.
|
||||
* This is used in the Disguise and Ice Face abilities.
|
||||
*
|
||||
*
|
||||
* Does not apply to a user's substitute
|
||||
* @extends ReceivedMoveDamageMultiplierAbAttr
|
||||
*/
|
||||
@ -5176,15 +5265,14 @@ export class IllusionPreSummonAbAttr extends PreSummonAbAttr {
|
||||
}
|
||||
|
||||
override canApplyPreSummon(pokemon: Pokemon, passive: boolean, args: any[]): boolean {
|
||||
pokemon.initSummondata()
|
||||
if(pokemon.hasTrainer()){
|
||||
if (pokemon.hasTrainer()) {
|
||||
const party: Pokemon[] = (pokemon.isPlayer() ? globalScene.getPlayerParty() : globalScene.getEnemyParty()).filter(p => p.isAllowedInBattle());
|
||||
const lastPokemon: Pokemon = party.filter(p => p !==pokemon).at(-1) || pokemon;
|
||||
const speciesId = lastPokemon.species.speciesId;
|
||||
|
||||
// If the last conscious Pokémon in the party is a Terastallized Ogerpon or Terapagos, Illusion will not activate.
|
||||
// Illusion will also not activate if the Pokémon with Illusion is Terastallized and the last Pokémon in the party is Ogerpon or Terapagos.
|
||||
if (
|
||||
if (
|
||||
lastPokemon === pokemon ||
|
||||
((speciesId === Species.OGERPON || speciesId === Species.TERAPAGOS) && (lastPokemon.isTerastallized || pokemon.isTerastallized))
|
||||
) {
|
||||
@ -5221,7 +5309,7 @@ export class PostDefendIllusionBreakAbAttr extends PostDefendAbAttr {
|
||||
|
||||
override canApplyPostDefend(pokemon: Pokemon, passive: boolean, simulated: boolean, attacker: Pokemon, move: Move, hitResult: HitResult, args: any[]): boolean {
|
||||
const breakIllusion: HitResult[] = [ HitResult.EFFECTIVE, HitResult.SUPER_EFFECTIVE, HitResult.NOT_VERY_EFFECTIVE, HitResult.ONE_HIT_KO ];
|
||||
return breakIllusion.includes(hitResult) && !!pokemon.summonData?.illusion
|
||||
return breakIllusion.includes(hitResult) && !!pokemon.summonData.illusion
|
||||
}
|
||||
}
|
||||
|
||||
@ -5442,11 +5530,8 @@ function applySingleAbAttrs<TAttr extends AbAttr>(
|
||||
globalScene.queueAbilityDisplay(pokemon, passive, false);
|
||||
}
|
||||
|
||||
if (pokemon.summonData && !pokemon.summonData.abilitiesApplied.includes(ability.id)) {
|
||||
pokemon.summonData.abilitiesApplied.push(ability.id);
|
||||
}
|
||||
if (pokemon.battleData && !simulated && !pokemon.battleData.abilitiesApplied.includes(ability.id)) {
|
||||
pokemon.battleData.abilitiesApplied.push(ability.id);
|
||||
if (!simulated) {
|
||||
pokemon.waveData.abilitiesApplied.add(ability.id);
|
||||
}
|
||||
|
||||
globalScene.clearPhaseQueueSplice();
|
||||
@ -5637,6 +5722,7 @@ export class PostDamageForceSwitchAbAttr extends PostDamageAbAttr {
|
||||
this.hpRatio = hpRatio;
|
||||
}
|
||||
|
||||
// TODO: Refactor to use more early returns
|
||||
public override canApplyPostDamage(
|
||||
pokemon: Pokemon,
|
||||
damage: number,
|
||||
@ -5664,6 +5750,7 @@ export class PostDamageForceSwitchAbAttr extends PostDamageAbAttr {
|
||||
if (fordbiddenDefendingMoves.includes(enemyLastMoveUsed.move) || enemyLastMoveUsed.move === Moves.SKY_DROP && enemyLastMoveUsed.result === MoveResult.OTHER) {
|
||||
return false;
|
||||
// Will not activate if the Pokémon's HP falls below half by a move affected by Sheer Force.
|
||||
// TODO: Make this use the sheer force disable condition
|
||||
} else if (allMoves[enemyLastMoveUsed.move].chance >= 0 && source.hasAbility(Abilities.SHEER_FORCE)) {
|
||||
return false;
|
||||
// Activate only after the last hit of multistrike moves
|
||||
@ -6326,17 +6413,14 @@ export function applyOnLoseAbAttrs(pokemon: Pokemon, passive = false, simulated
|
||||
|
||||
/**
|
||||
* Sets the ability of a Pokémon as revealed.
|
||||
*
|
||||
* @param pokemon - The Pokémon whose ability is being revealed.
|
||||
*/
|
||||
function setAbilityRevealed(pokemon: Pokemon): void {
|
||||
if (pokemon.battleData) {
|
||||
pokemon.battleData.abilityRevealed = true;
|
||||
}
|
||||
pokemon.waveData.abilityRevealed = true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the Pokemon with weather-based forms
|
||||
* Returns all Pokemon on field with weather-based forms
|
||||
*/
|
||||
function getPokemonWithWeatherBasedForms() {
|
||||
return globalScene.getField(true).filter(p =>
|
||||
@ -6784,8 +6868,7 @@ export function initAbilities() {
|
||||
.attr(MovePowerBoostAbAttr, (user, target, move) => move.category === MoveCategory.SPECIAL && user?.status?.effect === StatusEffect.BURN, 1.5),
|
||||
new Ability(Abilities.HARVEST, 5)
|
||||
.attr(
|
||||
PostTurnLootAbAttr,
|
||||
"EATEN_BERRIES",
|
||||
PostTurnRestoreBerryAbAttr,
|
||||
/** Rate is doubled when under sun {@link https://dex.pokemonshowdown.com/abilities/harvest} */
|
||||
(pokemon) => 0.5 * (getWeatherCondition(WeatherType.SUNNY, WeatherType.HARSH_SUN)(pokemon) ? 2 : 1)
|
||||
)
|
||||
@ -6824,6 +6907,8 @@ export function initAbilities() {
|
||||
.attr(IllusionBreakAbAttr)
|
||||
// The Pokemon loses its illusion when damaged by a move
|
||||
.attr(PostDefendIllusionBreakAbAttr, true)
|
||||
// Disable Illusion in fusions
|
||||
.attr(NoFusionAbilityAbAttr)
|
||||
// Illusion is available again after a battle
|
||||
.conditionalAttr((pokemon) => pokemon.isAllowedInBattle(), IllusionPostBattleAbAttr, false)
|
||||
.uncopiable()
|
||||
@ -6907,7 +6992,7 @@ export function initAbilities() {
|
||||
.attr(HealFromBerryUseAbAttr, 1 / 3),
|
||||
new Ability(Abilities.PROTEAN, 6)
|
||||
.attr(PokemonTypeChangeAbAttr),
|
||||
//.condition((p) => !p.summonData?.abilitiesApplied.includes(Abilities.PROTEAN)), //Gen 9 Implementation
|
||||
//.condition((p) => !p.summonData.abilitiesApplied.includes(Abilities.PROTEAN)), //Gen 9 Implementation
|
||||
new Ability(Abilities.FUR_COAT, 6)
|
||||
.attr(ReceivedMoveDamageMultiplierAbAttr, (target, user, move) => move.category === MoveCategory.PHYSICAL, 0.5)
|
||||
.ignorable(),
|
||||
@ -7153,7 +7238,7 @@ export function initAbilities() {
|
||||
.attr(PostSummonStatStageChangeAbAttr, [ Stat.DEF ], 1, true),
|
||||
new Ability(Abilities.LIBERO, 8)
|
||||
.attr(PokemonTypeChangeAbAttr),
|
||||
//.condition((p) => !p.summonData?.abilitiesApplied.includes(Abilities.LIBERO)), //Gen 9 Implementation
|
||||
//.condition((p) => !p.summonData.abilitiesApplied.includes(Abilities.LIBERO)), //Gen 9 Implementation
|
||||
new Ability(Abilities.BALL_FETCH, 8)
|
||||
.attr(FetchBallAbAttr)
|
||||
.condition(getOncePerBattleCondition(Abilities.BALL_FETCH)),
|
||||
@ -7368,7 +7453,7 @@ export function initAbilities() {
|
||||
new Ability(Abilities.OPPORTUNIST, 9)
|
||||
.attr(StatStageChangeCopyAbAttr),
|
||||
new Ability(Abilities.CUD_CHEW, 9)
|
||||
.unimplemented(),
|
||||
.attr(RepeatBerryNextTurnAbAttr),
|
||||
new Ability(Abilities.SHARPNESS, 9)
|
||||
.attr(MovePowerBoostAbAttr, (user, target, move) => move.hasFlag(MoveFlags.SLICING_MOVE), 1.5),
|
||||
new Ability(Abilities.SUPREME_OVERLORD, 9)
|
||||
|
@ -768,32 +768,27 @@ class SpikesTag extends ArenaTrapTag {
|
||||
}
|
||||
|
||||
override activateTrap(pokemon: Pokemon, simulated: boolean): boolean {
|
||||
if (pokemon.isGrounded()) {
|
||||
const cancelled = new BooleanHolder(false);
|
||||
applyAbAttrs(BlockNonDirectDamageAbAttr, pokemon, cancelled);
|
||||
|
||||
if (simulated) {
|
||||
return !cancelled.value;
|
||||
}
|
||||
|
||||
if (!cancelled.value) {
|
||||
const damageHpRatio = 1 / (10 - 2 * this.layers);
|
||||
const damage = toDmgValue(pokemon.getMaxHp() * damageHpRatio);
|
||||
|
||||
globalScene.queueMessage(
|
||||
i18next.t("arenaTag:spikesActivateTrap", {
|
||||
pokemonNameWithAffix: getPokemonNameWithAffix(pokemon),
|
||||
}),
|
||||
);
|
||||
pokemon.damageAndUpdate(damage, { result: HitResult.INDIRECT });
|
||||
if (pokemon.turnData) {
|
||||
pokemon.turnData.damageTaken += damage;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
if (!pokemon.isGrounded()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return false;
|
||||
const cancelled = new BooleanHolder(false);
|
||||
applyAbAttrs(BlockNonDirectDamageAbAttr, pokemon, cancelled);
|
||||
if (simulated || cancelled.value) {
|
||||
return !cancelled.value;
|
||||
}
|
||||
|
||||
const damageHpRatio = 1 / (10 - 2 * this.layers);
|
||||
const damage = toDmgValue(pokemon.getMaxHp() * damageHpRatio);
|
||||
|
||||
globalScene.queueMessage(
|
||||
i18next.t("arenaTag:spikesActivateTrap", {
|
||||
pokemonNameWithAffix: getPokemonNameWithAffix(pokemon),
|
||||
}),
|
||||
);
|
||||
pokemon.damageAndUpdate(damage, { result: HitResult.INDIRECT });
|
||||
pokemon.turnData.damageTaken += damage;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
@ -962,31 +957,28 @@ class StealthRockTag extends ArenaTrapTag {
|
||||
override activateTrap(pokemon: Pokemon, simulated: boolean): boolean {
|
||||
const cancelled = new BooleanHolder(false);
|
||||
applyAbAttrs(BlockNonDirectDamageAbAttr, pokemon, cancelled);
|
||||
|
||||
if (cancelled.value) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const damageHpRatio = this.getDamageHpRatio(pokemon);
|
||||
if (!damageHpRatio) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (damageHpRatio) {
|
||||
if (simulated) {
|
||||
return true;
|
||||
}
|
||||
const damage = toDmgValue(pokemon.getMaxHp() * damageHpRatio);
|
||||
globalScene.queueMessage(
|
||||
i18next.t("arenaTag:stealthRockActivateTrap", {
|
||||
pokemonNameWithAffix: getPokemonNameWithAffix(pokemon),
|
||||
}),
|
||||
);
|
||||
pokemon.damageAndUpdate(damage, { result: HitResult.INDIRECT });
|
||||
if (pokemon.turnData) {
|
||||
pokemon.turnData.damageTaken += damage;
|
||||
}
|
||||
if (simulated) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
const damage = toDmgValue(pokemon.getMaxHp() * damageHpRatio);
|
||||
globalScene.queueMessage(
|
||||
i18next.t("arenaTag:stealthRockActivateTrap", {
|
||||
pokemonNameWithAffix: getPokemonNameWithAffix(pokemon),
|
||||
}),
|
||||
);
|
||||
pokemon.damageAndUpdate(damage, { result: HitResult.INDIRECT });
|
||||
pokemon.turnData.damageTaken += damage;
|
||||
return true;
|
||||
}
|
||||
|
||||
getMatchupScoreMultiplier(pokemon: Pokemon): number {
|
||||
|
@ -66,7 +66,7 @@ export const speciesEggMoves = {
|
||||
[Species.PORYGON]: [ Moves.THUNDERCLAP, Moves.AURA_SPHERE, Moves.FLAMETHROWER, Moves.TECHNO_BLAST ],
|
||||
[Species.OMANYTE]: [ Moves.FREEZE_DRY, Moves.GIGA_DRAIN, Moves.POWER_GEM, Moves.STEAM_ERUPTION ],
|
||||
[Species.KABUTO]: [ Moves.CEASELESS_EDGE, Moves.HIGH_HORSEPOWER, Moves.CRABHAMMER, Moves.MIGHTY_CLEAVE ],
|
||||
[Species.AERODACTYL]: [ Moves.FLOATY_FALL, Moves.CLOSE_COMBAT, Moves.STONE_AXE, Moves.SWORDS_DANCE ],
|
||||
[Species.AERODACTYL]: [ Moves.FLOATY_FALL, Moves.HIGH_HORSEPOWER, Moves.STONE_AXE, Moves.SWORDS_DANCE ],
|
||||
[Species.ARTICUNO]: [ Moves.EARTH_POWER, Moves.CALM_MIND, Moves.AURORA_VEIL, Moves.AEROBLAST ],
|
||||
[Species.ZAPDOS]: [ Moves.BLEAKWIND_STORM, Moves.CALM_MIND, Moves.SANDSEAR_STORM, Moves.ELECTRO_SHOT ],
|
||||
[Species.MOLTRES]: [ Moves.EARTH_POWER, Moves.CALM_MIND, Moves.AEROBLAST, Moves.TORCH_SONG ],
|
||||
@ -360,7 +360,7 @@ export const speciesEggMoves = {
|
||||
[Species.CLAUNCHER]: [ Moves.SHELL_SMASH, Moves.ARMOR_CANNON, Moves.ENERGY_BALL, Moves.ORIGIN_PULSE ],
|
||||
[Species.HELIOPTILE]: [ Moves.WEATHER_BALL, Moves.HYDRO_STEAM, Moves.EARTH_POWER, Moves.BOOMBURST ],
|
||||
[Species.TYRUNT]: [ Moves.DRAGON_HAMMER, Moves.FLARE_BLITZ, Moves.VOLT_TACKLE, Moves.SHIFT_GEAR ],
|
||||
[Species.AMAURA]: [ Moves.RECOVER, Moves.WRING_OUT, Moves.POWER_GEM, Moves.GEOMANCY ],
|
||||
[Species.AMAURA]: [ Moves.RECOVER, Moves.TERA_STARSTORM, Moves.POWER_GEM, Moves.GEOMANCY ],
|
||||
[Species.HAWLUCHA]: [ Moves.TRIPLE_AXEL, Moves.HIGH_HORSEPOWER, Moves.FLOATY_FALL, Moves.WICKED_BLOW ],
|
||||
[Species.DEDENNE]: [ Moves.BOOMBURST, Moves.FAKE_OUT, Moves.NASTY_PLOT, Moves.REVIVAL_BLESSING ],
|
||||
[Species.CARBINK]: [ Moves.BODY_PRESS, Moves.SHORE_UP, Moves.SPARKLY_SWIRL, Moves.DIAMOND_STORM ],
|
||||
@ -436,7 +436,7 @@ export const speciesEggMoves = {
|
||||
[Species.ALOLA_RATTATA]: [ Moves.FALSE_SURRENDER, Moves.PSYCHIC_FANGS, Moves.COIL, Moves.EXTREME_SPEED ],
|
||||
[Species.ALOLA_SANDSHREW]: [ Moves.SPIKY_SHIELD, Moves.LIQUIDATION, Moves.SHIFT_GEAR, Moves.GLACIAL_LANCE ],
|
||||
[Species.ALOLA_VULPIX]: [ Moves.MOONBLAST, Moves.GLARE, Moves.MYSTICAL_FIRE, Moves.REVIVAL_BLESSING ],
|
||||
[Species.ALOLA_DIGLETT]: [ Moves.THOUSAND_WAVES, Moves.SWORDS_DANCE, Moves.TRIPLE_DIVE, Moves.MOUNTAIN_GALE ],
|
||||
[Species.ALOLA_DIGLETT]: [ Moves.THOUSAND_WAVES, Moves.SWORDS_DANCE, Moves.TRIPLE_DIVE, Moves.PYRO_BALL ],
|
||||
[Species.ALOLA_MEOWTH]: [ Moves.BADDY_BAD, Moves.BUZZY_BUZZ, Moves.PARTING_SHOT, Moves.MAKE_IT_RAIN ],
|
||||
[Species.ALOLA_GEODUDE]: [ Moves.THOUSAND_WAVES, Moves.BULK_UP, Moves.STONE_AXE, Moves.EXTREME_SPEED ],
|
||||
[Species.ALOLA_GRIMER]: [ Moves.SUCKER_PUNCH, Moves.BARB_BARRAGE, Moves.RECOVER, Moves.SURGING_STRIKES ],
|
||||
|
@ -1132,7 +1132,6 @@ export abstract class BattleAnim {
|
||||
if (priority === 0) {
|
||||
// Place the sprite in front of the pokemon on the field.
|
||||
targetSprite = globalScene.getEnemyField().find(p => p) ?? globalScene.getPlayerField().find(p => p);
|
||||
console.log(typeof targetSprite);
|
||||
moveFunc = globalScene.field.moveBelow;
|
||||
} else if (priority === 2 && this.bgSprite) {
|
||||
moveFunc = globalScene.field.moveAbove;
|
||||
|
@ -1,4 +1,5 @@
|
||||
import { globalScene } from "#app/global-scene";
|
||||
import Overrides from "#app/overrides";
|
||||
import {
|
||||
applyAbAttrs,
|
||||
BlockNonDirectDamageAbAttr,
|
||||
@ -91,7 +92,12 @@ export class BattlerTag {
|
||||
|
||||
onOverlap(_pokemon: Pokemon): void {}
|
||||
|
||||
/**
|
||||
* Tick down this {@linkcode BattlerTag}'s duration.
|
||||
* @returns `true` if the tag should be kept (`turnCount` > 0`)
|
||||
*/
|
||||
lapse(_pokemon: Pokemon, _lapseType: BattlerTagLapseType): boolean {
|
||||
// TODO: Maybe flip this (return `true` if tag needs removal)
|
||||
return --this.turnCount > 0;
|
||||
}
|
||||
|
||||
@ -108,9 +114,9 @@ export class BattlerTag {
|
||||
}
|
||||
|
||||
/**
|
||||
* When given a battler tag or json representing one, load the data for it.
|
||||
* This is meant to be inherited from by any battler tag with custom attributes
|
||||
* @param {BattlerTag | any} source A battler tag
|
||||
* Load the data for a given {@linkcode BattlerTag} or JSON representation thereof.
|
||||
* Should be inherited from by any battler tag with custom attributes.
|
||||
* @param source The battler tag to load
|
||||
*/
|
||||
loadTag(source: BattlerTag | any): void {
|
||||
this.turnCount = source.turnCount;
|
||||
@ -120,7 +126,7 @@ export class BattlerTag {
|
||||
|
||||
/**
|
||||
* Helper function that retrieves the source Pokemon object
|
||||
* @returns The source {@linkcode Pokemon} or `null` if none is found
|
||||
* @returns The source {@linkcode Pokemon}, or `null` if none is found
|
||||
*/
|
||||
public getSourcePokemon(): Pokemon | null {
|
||||
return this.sourceId ? globalScene.getPokemonById(this.sourceId) : null;
|
||||
@ -140,8 +146,8 @@ export interface TerrainBattlerTag {
|
||||
* in-game. This is not to be confused with {@linkcode Moves.DISABLE}.
|
||||
*
|
||||
* Descendants can override {@linkcode isMoveRestricted} to restrict moves that
|
||||
* match a condition. A restricted move gets cancelled before it is used. Players and enemies should not be allowed
|
||||
* to select restricted moves.
|
||||
* match a condition. A restricted move gets cancelled before it is used.
|
||||
* Players and enemies should not be allowed to select restricted moves.
|
||||
*/
|
||||
export abstract class MoveRestrictionBattlerTag extends BattlerTag {
|
||||
constructor(
|
||||
@ -746,31 +752,33 @@ export class ConfusedTag extends BattlerTag {
|
||||
}
|
||||
|
||||
lapse(pokemon: Pokemon, lapseType: BattlerTagLapseType): boolean {
|
||||
const ret = lapseType !== BattlerTagLapseType.CUSTOM && super.lapse(pokemon, lapseType);
|
||||
const shouldLapse = lapseType !== BattlerTagLapseType.CUSTOM && super.lapse(pokemon, lapseType);
|
||||
|
||||
if (ret) {
|
||||
globalScene.queueMessage(
|
||||
i18next.t("battlerTags:confusedLapse", {
|
||||
pokemonNameWithAffix: getPokemonNameWithAffix(pokemon),
|
||||
}),
|
||||
);
|
||||
globalScene.unshiftPhase(new CommonAnimPhase(pokemon.getBattlerIndex(), undefined, CommonAnim.CONFUSION));
|
||||
|
||||
// 1/3 chance of hitting self with a 40 base power move
|
||||
if (pokemon.randSeedInt(3) === 0) {
|
||||
const atk = pokemon.getEffectiveStat(Stat.ATK);
|
||||
const def = pokemon.getEffectiveStat(Stat.DEF);
|
||||
const damage = toDmgValue(
|
||||
((((2 * pokemon.level) / 5 + 2) * 40 * atk) / def / 50 + 2) * (pokemon.randSeedIntRange(85, 100) / 100),
|
||||
);
|
||||
globalScene.queueMessage(i18next.t("battlerTags:confusedLapseHurtItself"));
|
||||
pokemon.damageAndUpdate(damage, { result: HitResult.CONFUSION });
|
||||
pokemon.battleData.hitCount++;
|
||||
(globalScene.getCurrentPhase() as MovePhase).cancel();
|
||||
}
|
||||
if (!shouldLapse) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return ret;
|
||||
globalScene.queueMessage(
|
||||
i18next.t("battlerTags:confusedLapse", {
|
||||
pokemonNameWithAffix: getPokemonNameWithAffix(pokemon),
|
||||
}),
|
||||
);
|
||||
globalScene.unshiftPhase(new CommonAnimPhase(pokemon.getBattlerIndex(), undefined, CommonAnim.CONFUSION));
|
||||
|
||||
// 1/3 chance of hitting self with a 40 base power move
|
||||
if (pokemon.randSeedInt(3) === 0 || Overrides.CONFUSION_ACTIVATION_OVERRIDE === true) {
|
||||
const atk = pokemon.getEffectiveStat(Stat.ATK);
|
||||
const def = pokemon.getEffectiveStat(Stat.DEF);
|
||||
const damage = toDmgValue(
|
||||
((((2 * pokemon.level) / 5 + 2) * 40 * atk) / def / 50 + 2) * (pokemon.randSeedIntRange(85, 100) / 100),
|
||||
);
|
||||
// Intentionally don't increment rage fist's hitCount
|
||||
globalScene.queueMessage(i18next.t("battlerTags:confusedLapseHurtItself"));
|
||||
pokemon.damageAndUpdate(damage, { result: HitResult.CONFUSION });
|
||||
(globalScene.getCurrentPhase() as MovePhase).cancel();
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
getDescriptor(): string {
|
||||
@ -1117,8 +1125,8 @@ export class FrenzyTag extends BattlerTag {
|
||||
}
|
||||
|
||||
/**
|
||||
* Applies the effects of the move Encore onto the target Pokemon
|
||||
* Encore forces the target Pokemon to use its most-recent move for 3 turns
|
||||
* Applies the effects of {@linkcode Moves.ENCORE} onto the target Pokemon.
|
||||
* Encore forces the target Pokemon to use its most-recent move for 3 turns.
|
||||
*/
|
||||
export class EncoreTag extends MoveRestrictionBattlerTag {
|
||||
public moveId: Moves;
|
||||
@ -1133,10 +1141,6 @@ export class EncoreTag extends MoveRestrictionBattlerTag {
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* When given a battler tag or json representing one, load the data for it.
|
||||
* @param {BattlerTag | any} source A battler tag
|
||||
*/
|
||||
loadTag(source: BattlerTag | any): void {
|
||||
super.loadTag(source);
|
||||
this.moveId = source.moveId as Moves;
|
||||
|
@ -5,10 +5,8 @@ import { getStatusEffectHealText } from "./status-effect";
|
||||
import { NumberHolder, toDmgValue, randSeedInt } from "#app/utils/common";
|
||||
import {
|
||||
DoubleBerryEffectAbAttr,
|
||||
PostItemLostAbAttr,
|
||||
ReduceBerryUseThresholdAbAttr,
|
||||
applyAbAttrs,
|
||||
applyPostItemLostAbAttrs,
|
||||
} from "./abilities/ability";
|
||||
import i18next from "i18next";
|
||||
import { BattlerTagType } from "#enums/battler-tag-type";
|
||||
@ -70,97 +68,94 @@ export function getBerryPredicate(berryType: BerryType): BerryPredicate {
|
||||
}
|
||||
}
|
||||
|
||||
export type BerryEffectFunc = (pokemon: Pokemon, berryOwner?: Pokemon) => void;
|
||||
export type BerryEffectFunc = (consumer: Pokemon) => void;
|
||||
|
||||
export function getBerryEffectFunc(berryType: BerryType): BerryEffectFunc {
|
||||
switch (berryType) {
|
||||
case BerryType.SITRUS:
|
||||
case BerryType.ENIGMA:
|
||||
return (pokemon: Pokemon, berryOwner?: Pokemon) => {
|
||||
if (pokemon.battleData) {
|
||||
pokemon.battleData.berriesEaten.push(berryType);
|
||||
}
|
||||
const hpHealed = new NumberHolder(toDmgValue(pokemon.getMaxHp() / 4));
|
||||
applyAbAttrs(DoubleBerryEffectAbAttr, pokemon, null, false, hpHealed);
|
||||
globalScene.unshiftPhase(
|
||||
new PokemonHealPhase(
|
||||
pokemon.getBattlerIndex(),
|
||||
hpHealed.value,
|
||||
i18next.t("battle:hpHealBerry", {
|
||||
pokemonNameWithAffix: getPokemonNameWithAffix(pokemon),
|
||||
berryName: getBerryName(berryType),
|
||||
}),
|
||||
true,
|
||||
),
|
||||
);
|
||||
applyPostItemLostAbAttrs(PostItemLostAbAttr, berryOwner ?? pokemon, false);
|
||||
};
|
||||
case BerryType.LUM:
|
||||
return (pokemon: Pokemon, berryOwner?: Pokemon) => {
|
||||
if (pokemon.battleData) {
|
||||
pokemon.battleData.berriesEaten.push(berryType);
|
||||
}
|
||||
if (pokemon.status) {
|
||||
globalScene.queueMessage(getStatusEffectHealText(pokemon.status.effect, getPokemonNameWithAffix(pokemon)));
|
||||
}
|
||||
pokemon.resetStatus(true, true);
|
||||
pokemon.updateInfo();
|
||||
applyPostItemLostAbAttrs(PostItemLostAbAttr, berryOwner ?? pokemon, false);
|
||||
};
|
||||
case BerryType.LIECHI:
|
||||
case BerryType.GANLON:
|
||||
case BerryType.PETAYA:
|
||||
case BerryType.APICOT:
|
||||
case BerryType.SALAC:
|
||||
return (pokemon: Pokemon, berryOwner?: Pokemon) => {
|
||||
if (pokemon.battleData) {
|
||||
pokemon.battleData.berriesEaten.push(berryType);
|
||||
}
|
||||
// Offset BerryType such that LIECHI -> Stat.ATK = 1, GANLON -> Stat.DEF = 2, so on and so forth
|
||||
const stat: BattleStat = berryType - BerryType.ENIGMA;
|
||||
const statStages = new NumberHolder(1);
|
||||
applyAbAttrs(DoubleBerryEffectAbAttr, pokemon, null, false, statStages);
|
||||
globalScene.unshiftPhase(new StatStageChangePhase(pokemon.getBattlerIndex(), true, [stat], statStages.value));
|
||||
applyPostItemLostAbAttrs(PostItemLostAbAttr, berryOwner ?? pokemon, false);
|
||||
};
|
||||
case BerryType.LANSAT:
|
||||
return (pokemon: Pokemon, berryOwner?: Pokemon) => {
|
||||
if (pokemon.battleData) {
|
||||
pokemon.battleData.berriesEaten.push(berryType);
|
||||
}
|
||||
pokemon.addTag(BattlerTagType.CRIT_BOOST);
|
||||
applyPostItemLostAbAttrs(PostItemLostAbAttr, berryOwner ?? pokemon, false);
|
||||
};
|
||||
case BerryType.STARF:
|
||||
return (pokemon: Pokemon, berryOwner?: Pokemon) => {
|
||||
if (pokemon.battleData) {
|
||||
pokemon.battleData.berriesEaten.push(berryType);
|
||||
}
|
||||
const randStat = randSeedInt(Stat.SPD, Stat.ATK);
|
||||
const stages = new NumberHolder(2);
|
||||
applyAbAttrs(DoubleBerryEffectAbAttr, pokemon, null, false, stages);
|
||||
globalScene.unshiftPhase(new StatStageChangePhase(pokemon.getBattlerIndex(), true, [randStat], stages.value));
|
||||
applyPostItemLostAbAttrs(PostItemLostAbAttr, berryOwner ?? pokemon, false);
|
||||
};
|
||||
case BerryType.LEPPA:
|
||||
return (pokemon: Pokemon, berryOwner?: Pokemon) => {
|
||||
if (pokemon.battleData) {
|
||||
pokemon.battleData.berriesEaten.push(berryType);
|
||||
}
|
||||
const ppRestoreMove = pokemon.getMoveset().find(m => !m.getPpRatio())
|
||||
? pokemon.getMoveset().find(m => !m.getPpRatio())
|
||||
: pokemon.getMoveset().find(m => m.getPpRatio() < 1);
|
||||
if (ppRestoreMove !== undefined) {
|
||||
ppRestoreMove!.ppUsed = Math.max(ppRestoreMove!.ppUsed - 10, 0);
|
||||
globalScene.queueMessage(
|
||||
i18next.t("battle:ppHealBerry", {
|
||||
pokemonNameWithAffix: getPokemonNameWithAffix(pokemon),
|
||||
moveName: ppRestoreMove!.getName(),
|
||||
berryName: getBerryName(berryType),
|
||||
}),
|
||||
return (consumer: Pokemon) => {
|
||||
// Apply an effect pertaining to what berry we're using
|
||||
switch (berryType) {
|
||||
case BerryType.SITRUS:
|
||||
case BerryType.ENIGMA:
|
||||
{
|
||||
const hpHealed = new NumberHolder(toDmgValue(consumer.getMaxHp() / 4));
|
||||
applyAbAttrs(DoubleBerryEffectAbAttr, consumer, null, false, hpHealed);
|
||||
globalScene.unshiftPhase(
|
||||
new PokemonHealPhase(
|
||||
consumer.getBattlerIndex(),
|
||||
hpHealed.value,
|
||||
i18next.t("battle:hpHealBerry", {
|
||||
pokemonNameWithAffix: getPokemonNameWithAffix(consumer),
|
||||
berryName: getBerryName(berryType),
|
||||
}),
|
||||
true,
|
||||
),
|
||||
);
|
||||
applyPostItemLostAbAttrs(PostItemLostAbAttr, berryOwner ?? pokemon, false);
|
||||
}
|
||||
};
|
||||
}
|
||||
break;
|
||||
case BerryType.LUM:
|
||||
{
|
||||
if (consumer.status) {
|
||||
globalScene.queueMessage(
|
||||
getStatusEffectHealText(consumer.status.effect, getPokemonNameWithAffix(consumer)),
|
||||
);
|
||||
}
|
||||
consumer.resetStatus(true, true);
|
||||
consumer.updateInfo();
|
||||
}
|
||||
break;
|
||||
case BerryType.LIECHI:
|
||||
case BerryType.GANLON:
|
||||
case BerryType.PETAYA:
|
||||
case BerryType.APICOT:
|
||||
case BerryType.SALAC:
|
||||
{
|
||||
// Offset BerryType such that LIECHI --> Stat.ATK = 1, GANLON --> Stat.DEF = 2, etc etc.
|
||||
const stat: BattleStat = berryType - BerryType.ENIGMA;
|
||||
const statStages = new NumberHolder(1);
|
||||
applyAbAttrs(DoubleBerryEffectAbAttr, consumer, null, false, statStages);
|
||||
globalScene.unshiftPhase(
|
||||
new StatStageChangePhase(consumer.getBattlerIndex(), true, [stat], statStages.value),
|
||||
);
|
||||
}
|
||||
break;
|
||||
|
||||
case BerryType.LANSAT:
|
||||
{
|
||||
consumer.addTag(BattlerTagType.CRIT_BOOST);
|
||||
}
|
||||
break;
|
||||
|
||||
case BerryType.STARF:
|
||||
{
|
||||
const randStat = randSeedInt(Stat.SPD, Stat.ATK);
|
||||
const stages = new NumberHolder(2);
|
||||
applyAbAttrs(DoubleBerryEffectAbAttr, consumer, null, false, stages);
|
||||
globalScene.unshiftPhase(
|
||||
new StatStageChangePhase(consumer.getBattlerIndex(), true, [randStat], stages.value),
|
||||
);
|
||||
}
|
||||
break;
|
||||
|
||||
case BerryType.LEPPA:
|
||||
{
|
||||
// Pick the first move completely out of PP, or else the first one that has any PP missing
|
||||
const ppRestoreMove =
|
||||
consumer.getMoveset().find(m => m.ppUsed === m.getMovePp()) ??
|
||||
consumer.getMoveset().find(m => m.ppUsed < m.getMovePp());
|
||||
if (ppRestoreMove) {
|
||||
ppRestoreMove.ppUsed = Math.max(ppRestoreMove.ppUsed - 10, 0);
|
||||
globalScene.queueMessage(
|
||||
i18next.t("battle:ppHealBerry", {
|
||||
pokemonNameWithAffix: getPokemonNameWithAffix(consumer),
|
||||
moveName: ppRestoreMove.getName(),
|
||||
berryName: getBerryName(berryType),
|
||||
}),
|
||||
);
|
||||
}
|
||||
}
|
||||
break;
|
||||
default:
|
||||
console.error("Incorrect BerryType %d passed to GetBerryEffectFunc", berryType);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
@ -1,4 +1,5 @@
|
||||
import { BooleanHolder, type NumberHolder, randSeedItem, deepCopy } from "#app/utils/common";
|
||||
import { BooleanHolder, type NumberHolder, randSeedItem } from "#app/utils/common";
|
||||
import { deepCopy } from "#app/utils/data";
|
||||
import i18next from "i18next";
|
||||
import type { DexAttrProps, GameData } from "#app/system/game-data";
|
||||
import { defaultStarterSpecies } from "#app/system/game-data";
|
||||
|
@ -1,36 +1,31 @@
|
||||
import type { Abilities } from "#enums/abilities";
|
||||
import type { PokemonType } from "#enums/pokemon-type";
|
||||
import { isNullOrUndefined } from "#app/utils/common";
|
||||
import type { Nature } from "#enums/nature";
|
||||
|
||||
/**
|
||||
* Data that can customize a Pokemon in non-standard ways from its Species
|
||||
* Used by Mystery Encounters and Mints
|
||||
* Also used as a counter how often a Pokemon got hit until new arena encounter
|
||||
* Data that can customize a Pokemon in non-standard ways from its Species.
|
||||
* Includes abilities, nature, changed types, etc.
|
||||
*/
|
||||
export class CustomPokemonData {
|
||||
public spriteScale: number;
|
||||
// TODO: Change the default value for all these from -1 to something a bit more sensible
|
||||
/**
|
||||
* The scale at which to render this Pokemon's sprite.
|
||||
*/
|
||||
public spriteScale = -1;
|
||||
public ability: Abilities | -1;
|
||||
public passive: Abilities | -1;
|
||||
public nature: Nature | -1;
|
||||
public types: PokemonType[];
|
||||
/** `hitsReceivedCount` aka `hitsRecCount` saves how often the pokemon got hit until a new arena encounter (used for Rage Fist) */
|
||||
public hitsRecCount: number;
|
||||
/** Deprecated but needed for session save migration */
|
||||
// TODO: Remove this once pre-session migration is implemented
|
||||
public hitsRecCount: number | null = null;
|
||||
|
||||
constructor(data?: CustomPokemonData | Partial<CustomPokemonData>) {
|
||||
if (!isNullOrUndefined(data)) {
|
||||
Object.assign(this, data);
|
||||
}
|
||||
|
||||
this.spriteScale = this.spriteScale ?? -1;
|
||||
this.ability = this.ability ?? -1;
|
||||
this.passive = this.passive ?? -1;
|
||||
this.nature = this.nature ?? -1;
|
||||
this.types = this.types ?? [];
|
||||
this.hitsRecCount = this.hitsRecCount ?? 0;
|
||||
}
|
||||
|
||||
resetHitReceivedCount(): void {
|
||||
this.hitsRecCount = 0;
|
||||
this.spriteScale = data?.spriteScale ?? -1;
|
||||
this.ability = data?.ability ?? -1;
|
||||
this.passive = data?.passive ?? -1;
|
||||
this.nature = data?.nature ?? -1;
|
||||
this.types = data?.types ?? [];
|
||||
this.hitsRecCount = data?.hitsRecCount ?? null;
|
||||
}
|
||||
}
|
||||
|
@ -2532,10 +2532,10 @@ export class PsychoShiftEffectAttr extends MoveEffectAttr {
|
||||
return !target.status && target.canSetStatus(user.status?.effect, true, false, user) ? -10 : 0;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* The following needs to be implemented for Thief
|
||||
* "If the user faints due to the target's Ability (Rough Skin or Iron Barbs) or held Rocky Helmet, it cannot remove the target's held item."
|
||||
* "If Knock Off causes a Pokémon with the Sticky Hold Ability to faint, it can now remove that Pokémon's held item."
|
||||
* Attribute to steal items upon this move's use.
|
||||
* Used for {@linkcode Moves.THIEF} and {@linkcode Moves.COVET}.
|
||||
*/
|
||||
export class StealHeldItemChanceAttr extends MoveEffectAttr {
|
||||
private chance: number;
|
||||
@ -2550,18 +2550,22 @@ export class StealHeldItemChanceAttr extends MoveEffectAttr {
|
||||
if (rand >= this.chance) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const heldItems = this.getTargetHeldItems(target).filter((i) => i.isTransferable);
|
||||
if (heldItems.length) {
|
||||
const poolType = target.isPlayer() ? ModifierPoolType.PLAYER : target.hasTrainer() ? ModifierPoolType.TRAINER : ModifierPoolType.WILD;
|
||||
const highestItemTier = heldItems.map((m) => m.type.getOrInferTier(poolType)).reduce((highestTier, tier) => Math.max(tier!, highestTier), 0); // TODO: is the bang after tier correct?
|
||||
const tierHeldItems = heldItems.filter((m) => m.type.getOrInferTier(poolType) === highestItemTier);
|
||||
const stolenItem = tierHeldItems[user.randSeedInt(tierHeldItems.length)];
|
||||
if (globalScene.tryTransferHeldItemModifier(stolenItem, user, false)) {
|
||||
globalScene.queueMessage(i18next.t("moveTriggers:stoleItem", { pokemonName: getPokemonNameWithAffix(user), targetName: getPokemonNameWithAffix(target), itemName: stolenItem.type.name }));
|
||||
return true;
|
||||
}
|
||||
if (!heldItems.length) {
|
||||
return false;
|
||||
}
|
||||
return false;
|
||||
|
||||
const poolType = target.isPlayer() ? ModifierPoolType.PLAYER : target.hasTrainer() ? ModifierPoolType.TRAINER : ModifierPoolType.WILD;
|
||||
const highestItemTier = heldItems.map((m) => m.type.getOrInferTier(poolType)).reduce((highestTier, tier) => Math.max(tier!, highestTier), 0); // TODO: is the bang after tier correct?
|
||||
const tierHeldItems = heldItems.filter((m) => m.type.getOrInferTier(poolType) === highestItemTier);
|
||||
const stolenItem = tierHeldItems[user.randSeedInt(tierHeldItems.length)];
|
||||
if (!globalScene.tryTransferHeldItemModifier(stolenItem, user, false)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
globalScene.queueMessage(i18next.t("moveTriggers:stoleItem", { pokemonName: getPokemonNameWithAffix(user), targetName: getPokemonNameWithAffix(target), itemName: stolenItem.type.name }));
|
||||
return true;
|
||||
}
|
||||
|
||||
getTargetHeldItems(target: Pokemon): PokemonHeldItemModifier[] {
|
||||
@ -2585,58 +2589,62 @@ export class StealHeldItemChanceAttr extends MoveEffectAttr {
|
||||
* Used for Incinerate and Knock Off.
|
||||
* Not Implemented Cases: (Same applies for Thief)
|
||||
* "If the user faints due to the target's Ability (Rough Skin or Iron Barbs) or held Rocky Helmet, it cannot remove the target's held item."
|
||||
* "If Knock Off causes a Pokémon with the Sticky Hold Ability to faint, it can now remove that Pokémon's held item."
|
||||
* "If the Pokémon is knocked out by the attack, Sticky Hold does not protect the held item.""
|
||||
*/
|
||||
export class RemoveHeldItemAttr extends MoveEffectAttr {
|
||||
|
||||
/** Optional restriction for item pool to berries only i.e. Differentiating Incinerate and Knock Off */
|
||||
/** Optional restriction for item pool to berries only; i.e. Incinerate */
|
||||
private berriesOnly: boolean;
|
||||
|
||||
constructor(berriesOnly: boolean) {
|
||||
constructor(berriesOnly: boolean = false) {
|
||||
super(false);
|
||||
this.berriesOnly = berriesOnly;
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param user {@linkcode Pokemon} that used the move
|
||||
* @param target Target {@linkcode Pokemon} that the moves applies to
|
||||
* @param move {@linkcode Move} that is used
|
||||
* Attempt to permanently remove a held
|
||||
* @param user - The {@linkcode Pokemon} that used the move
|
||||
* @param target - The {@linkcode Pokemon} targeted by the move
|
||||
* @param move - N/A
|
||||
* @param args N/A
|
||||
* @returns True if an item was removed
|
||||
* @returns `true` if an item was able to be removed
|
||||
*/
|
||||
apply(user: Pokemon, target: Pokemon, move: Move, args: any[]): boolean {
|
||||
if (!this.berriesOnly && target.isPlayer()) { // "Wild Pokemon cannot knock off Player Pokemon's held items" (See Bulbapedia)
|
||||
return false;
|
||||
}
|
||||
|
||||
// Check for abilities that block item theft
|
||||
// TODO: This should not trigger if the target would faint beforehand
|
||||
const cancelled = new BooleanHolder(false);
|
||||
applyAbAttrs(BlockItemTheftAbAttr, target, cancelled); // Check for abilities that block item theft
|
||||
applyAbAttrs(BlockItemTheftAbAttr, target, cancelled);
|
||||
|
||||
if (cancelled.value === true) {
|
||||
if (cancelled.value) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Considers entire transferrable item pool by default (Knock Off). Otherwise berries only if specified (Incinerate).
|
||||
// Considers entire transferrable item pool by default (Knock Off).
|
||||
// Otherwise only consider berries (Incinerate).
|
||||
let heldItems = this.getTargetHeldItems(target).filter(i => i.isTransferable);
|
||||
|
||||
if (this.berriesOnly) {
|
||||
heldItems = heldItems.filter(m => m instanceof BerryModifier && m.pokemonId === target.id, target.isPlayer());
|
||||
}
|
||||
|
||||
if (heldItems.length) {
|
||||
const removedItem = heldItems[user.randSeedInt(heldItems.length)];
|
||||
if (!heldItems.length) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Decrease item amount and update icon
|
||||
target.loseHeldItem(removedItem);
|
||||
globalScene.updateModifiers(target.isPlayer());
|
||||
const removedItem = heldItems[user.randSeedInt(heldItems.length)];
|
||||
|
||||
// Decrease item amount and update icon
|
||||
target.loseHeldItem(removedItem);
|
||||
globalScene.updateModifiers(target.isPlayer());
|
||||
|
||||
if (this.berriesOnly) {
|
||||
globalScene.queueMessage(i18next.t("moveTriggers:incineratedItem", { pokemonName: getPokemonNameWithAffix(user), targetName: getPokemonNameWithAffix(target), itemName: removedItem.type.name }));
|
||||
} else {
|
||||
globalScene.queueMessage(i18next.t("moveTriggers:knockedOffItem", { pokemonName: getPokemonNameWithAffix(user), targetName: getPokemonNameWithAffix(target), itemName: removedItem.type.name }));
|
||||
}
|
||||
if (this.berriesOnly) {
|
||||
globalScene.queueMessage(i18next.t("moveTriggers:incineratedItem", { pokemonName: getPokemonNameWithAffix(user), targetName: getPokemonNameWithAffix(target), itemName: removedItem.type.name }));
|
||||
} else {
|
||||
globalScene.queueMessage(i18next.t("moveTriggers:knockedOffItem", { pokemonName: getPokemonNameWithAffix(user), targetName: getPokemonNameWithAffix(target), itemName: removedItem.type.name }));
|
||||
}
|
||||
|
||||
return true;
|
||||
@ -2662,17 +2670,18 @@ export class RemoveHeldItemAttr extends MoveEffectAttr {
|
||||
* Attribute that causes targets of the move to eat a berry. Used for Teatime, Stuff Cheeks
|
||||
*/
|
||||
export class EatBerryAttr extends MoveEffectAttr {
|
||||
protected chosenBerry: BerryModifier | undefined;
|
||||
protected chosenBerry: BerryModifier;
|
||||
constructor(selfTarget: boolean) {
|
||||
super(selfTarget);
|
||||
}
|
||||
|
||||
/**
|
||||
* Causes the target to eat a berry.
|
||||
* @param user {@linkcode Pokemon} Pokemon that used the move
|
||||
* @param target {@linkcode Pokemon} Pokemon that will eat a berry
|
||||
* @param move {@linkcode Move} The move being used
|
||||
* @param user The {@linkcode Pokemon} Pokemon that used the move
|
||||
* @param target The {@linkcode Pokemon} Pokemon that will eat the berry
|
||||
* @param move The {@linkcode Move} being used
|
||||
* @param args Unused
|
||||
* @returns {boolean} true if the function succeeds
|
||||
* @returns `true` if the function succeeds
|
||||
*/
|
||||
apply(user: Pokemon, target: Pokemon, move: Move, args: any[]): boolean {
|
||||
if (!super.apply(user, target, move, args)) {
|
||||
@ -2685,6 +2694,8 @@ export class EatBerryAttr extends MoveEffectAttr {
|
||||
if (heldBerries.length <= 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// pick a random berry to gobble and check if we preserve it
|
||||
this.chosenBerry = heldBerries[user.randSeedInt(heldBerries.length)];
|
||||
const preserve = new BooleanHolder(false);
|
||||
// check for berry pouch preservation
|
||||
@ -2692,7 +2703,10 @@ export class EatBerryAttr extends MoveEffectAttr {
|
||||
if (!preserve.value) {
|
||||
this.reduceBerryModifier(pokemon);
|
||||
}
|
||||
this.eatBerry(pokemon);
|
||||
|
||||
// Don't update harvest for berries preserved via Berry pouch (no item dupes lol)
|
||||
this.eatBerry(target, undefined, !preserve.value);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
@ -2708,46 +2722,64 @@ export class EatBerryAttr extends MoveEffectAttr {
|
||||
globalScene.updateModifiers(target.isPlayer());
|
||||
}
|
||||
|
||||
eatBerry(consumer: Pokemon, berryOwner?: Pokemon) {
|
||||
getBerryEffectFunc(this.chosenBerry!.berryType)(consumer, berryOwner); // consumer eats the berry
|
||||
|
||||
/**
|
||||
* Internal function to apply berry effects.
|
||||
*
|
||||
* @param consumer - The {@linkcode Pokemon} eating the berry; assumed to also be owner if `berryOwner` is omitted
|
||||
* @param berryOwner - The {@linkcode Pokemon} whose berry is being eaten; defaults to `consumer` if not specified.
|
||||
* @param updateHarvest - Whether to prevent harvest from tracking berries;
|
||||
* defaults to whether `consumer` equals `berryOwner` (i.e. consuming own berry).
|
||||
*/
|
||||
protected eatBerry(consumer: Pokemon, berryOwner: Pokemon = consumer, updateHarvest = consumer === berryOwner) {
|
||||
// consumer eats berry, owner triggers unburden and similar effects
|
||||
getBerryEffectFunc(this.chosenBerry.berryType)(consumer);
|
||||
applyPostItemLostAbAttrs(PostItemLostAbAttr, berryOwner, false);
|
||||
applyAbAttrs(HealFromBerryUseAbAttr, consumer, new BooleanHolder(false));
|
||||
consumer.recordEatenBerry(this.chosenBerry.berryType, updateHarvest);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Attribute used for moves that steal a random berry from the target. The user then eats the stolen berry.
|
||||
* Used for Pluck & Bug Bite.
|
||||
* Attribute used for moves that steal and eat a random berry from the target.
|
||||
* Used for {@linkcode Moves.PLUCK} & {@linkcode Moves.BUG_BITE}.
|
||||
*/
|
||||
export class StealEatBerryAttr extends EatBerryAttr {
|
||||
constructor() {
|
||||
super(false);
|
||||
}
|
||||
|
||||
/**
|
||||
* User steals a random berry from the target and then eats it.
|
||||
* @param user - Pokemon that used the move and will eat the stolen berry
|
||||
* @param target - Pokemon that will have its berry stolen
|
||||
* @param move - Move being used
|
||||
* @param args Unused
|
||||
* @returns true if the function succeeds
|
||||
* @param user - The {@linkcode Pokemon} using the move; will eat the stolen berry
|
||||
* @param target - The {@linkcode Pokemon} having its berry stolen
|
||||
* @param move - The {@linkcode Move} being used
|
||||
* @param args N/A
|
||||
* @returns `true` if the function succeeds
|
||||
*/
|
||||
apply(user: Pokemon, target: Pokemon, move: Move, args: any[]): boolean {
|
||||
// check for abilities that block item theft
|
||||
const cancelled = new BooleanHolder(false);
|
||||
applyAbAttrs(BlockItemTheftAbAttr, target, cancelled); // check for abilities that block item theft
|
||||
applyAbAttrs(BlockItemTheftAbAttr, target, cancelled);
|
||||
if (cancelled.value === true) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// check if the target even _has_ a berry in the first place
|
||||
// TODO: Check on cart if Pluck displays messages when used against sticky hold mons w/o berries
|
||||
const heldBerries = this.getTargetHeldBerries(target);
|
||||
if (heldBerries.length <= 0) {
|
||||
return false;
|
||||
}
|
||||
// if the target has berries, pick a random berry and steal it
|
||||
|
||||
// pick a random berry and eat it
|
||||
this.chosenBerry = heldBerries[user.randSeedInt(heldBerries.length)];
|
||||
applyPostItemLostAbAttrs(PostItemLostAbAttr, target, false);
|
||||
const message = i18next.t("battle:stealEatBerry", { pokemonName: user.name, targetName: target.name, berryName: this.chosenBerry.type.name });
|
||||
globalScene.queueMessage(message);
|
||||
this.reduceBerryModifier(target);
|
||||
this.eatBerry(user, target);
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@ -3450,7 +3482,8 @@ export class CutHpStatStageBoostAttr extends StatStageChangeAttr {
|
||||
/**
|
||||
* Attribute implementing the stat boosting effect of {@link https://bulbapedia.bulbagarden.net/wiki/Order_Up_(move) | Order Up}.
|
||||
* If the user has a Pokemon with {@link https://bulbapedia.bulbagarden.net/wiki/Commander_(Ability) | Commander} in their mouth,
|
||||
* one of the user's stats are increased by 1 stage, depending on the "commanding" Pokemon's form.
|
||||
* one of the user's stats are increased by 1 stage, depending on the "commanding" Pokemon's form. This effect does not respect
|
||||
* effect chance, but Order Up itself may be boosted by Sheer Force.
|
||||
*/
|
||||
export class OrderUpStatBoostAttr extends MoveEffectAttr {
|
||||
constructor() {
|
||||
@ -4100,30 +4133,23 @@ export class FriendshipPowerAttr extends VariablePowerAttr {
|
||||
|
||||
/**
|
||||
* This Attribute calculates the current power of {@linkcode Moves.RAGE_FIST}.
|
||||
* The counter for power calculation does not reset on every wave but on every new arena encounter
|
||||
* The counter for power calculation does not reset on every wave but on every new arena encounter.
|
||||
* Self-inflicted confusion damage and hits taken by a Subsitute are ignored.
|
||||
*/
|
||||
export class RageFistPowerAttr extends VariablePowerAttr {
|
||||
apply(user: Pokemon, target: Pokemon, move: Move, args: any[]): boolean {
|
||||
const { hitCount, prevHitCount } = user.battleData;
|
||||
/* Reasons this works correctly:
|
||||
* Confusion calls user.damageAndUpdate() directly (no counter increment),
|
||||
* Substitute hits call user.damageAndUpdate() with a damage value of 0, also causing
|
||||
no counter increment
|
||||
*/
|
||||
const hitCount = user.battleData.hitCount;
|
||||
const basePower: NumberHolder = args[0];
|
||||
|
||||
this.updateHitReceivedCount(user, hitCount, prevHitCount);
|
||||
|
||||
basePower.value = 50 + (Math.min(user.customPokemonData.hitsRecCount, 6) * 50);
|
||||
|
||||
basePower.value = 50 * (1 + Math.min(hitCount, 6));
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the number of hits the Pokemon has taken in battle
|
||||
* @param user Pokemon calling Rage Fist
|
||||
* @param hitCount The number of received hits this battle
|
||||
* @param previousHitCount The number of received hits this battle since last time Rage Fist was used
|
||||
*/
|
||||
protected updateHitReceivedCount(user: Pokemon, hitCount: number, previousHitCount: number): void {
|
||||
user.customPokemonData.hitsRecCount += (hitCount - previousHitCount);
|
||||
user.battleData.prevHitCount = hitCount;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@ -4354,10 +4380,10 @@ export class LastMoveDoublePowerAttr extends VariablePowerAttr {
|
||||
const userAlly = user.getAlly();
|
||||
const enemyAlly = enemy?.getAlly();
|
||||
|
||||
if (!isNullOrUndefined(userAlly) && userAlly.turnData.acted) {
|
||||
if (userAlly?.turnData.acted) {
|
||||
pokemonActed.push(userAlly);
|
||||
}
|
||||
if (!isNullOrUndefined(enemyAlly) && enemyAlly.turnData.acted) {
|
||||
if (enemyAlly?.turnData.acted) {
|
||||
pokemonActed.push(enemyAlly);
|
||||
}
|
||||
}
|
||||
@ -4425,13 +4451,10 @@ export class CombinedPledgeStabBoostAttr extends MoveAttr {
|
||||
* @extends VariablePowerAttr
|
||||
*/
|
||||
export class RoundPowerAttr extends VariablePowerAttr {
|
||||
override apply(user: Pokemon, target: Pokemon, move: Move, args: any[]): boolean {
|
||||
override apply(user: Pokemon, target: Pokemon, move: Move, args: [NumberHolder]): boolean {
|
||||
const power = args[0];
|
||||
if (!(power instanceof NumberHolder)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (user.turnData?.joinedRound) {
|
||||
if (user.turnData.joinedRound) {
|
||||
power.value *= 2;
|
||||
return true;
|
||||
}
|
||||
@ -7764,17 +7787,27 @@ export class StatusIfBoostedAttr extends MoveEffectAttr {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Attribute to fail move usage unless all of the user's other moves have been used at least once.
|
||||
* Used by {@linkcode Moves.LAST_RESORT}.
|
||||
*/
|
||||
export class LastResortAttr extends MoveAttr {
|
||||
// TODO: Verify behavior as Bulbapedia page is _extremely_ poorly documented
|
||||
getCondition(): MoveConditionFunc {
|
||||
return (user: Pokemon, target: Pokemon, move: Move) => {
|
||||
const uniqueUsedMoveIds = new Set<Moves>();
|
||||
const movesetMoveIds = user.getMoveset().map(m => m.moveId);
|
||||
user.getMoveHistory().map(m => {
|
||||
if (m.move !== move.id && movesetMoveIds.find(mm => mm === m.move)) {
|
||||
uniqueUsedMoveIds.add(m.move);
|
||||
}
|
||||
});
|
||||
return uniqueUsedMoveIds.size >= movesetMoveIds.length - 1;
|
||||
return (user: Pokemon, _target: Pokemon, move: Move) => {
|
||||
const movesInMoveset = new Set<Moves>(user.getMoveset().map(m => m.moveId));
|
||||
if (!movesInMoveset.delete(move.id) || !movesInMoveset.size) {
|
||||
return false; // Last resort fails if used when not in user's moveset or no other moves exist
|
||||
}
|
||||
|
||||
const movesInHistory = new Set(
|
||||
user.getMoveHistory()
|
||||
.filter(m => !m.virtual) // TODO: Change to (m) => m < MoveUseType.INDIRECT after Dancer PR refactors virtual into enum
|
||||
.map(m => m.move)
|
||||
);
|
||||
|
||||
// Since `Set.intersection()` is only present in ESNext, we have to coerce it to an array to check inclusion
|
||||
return [...movesInMoveset].every(m => movesInHistory.has(m))
|
||||
};
|
||||
}
|
||||
}
|
||||
@ -7982,13 +8015,18 @@ export class MoveCondition {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Condition to allow a move's use only on the first turn this Pokemon is sent into battle
|
||||
* (or the start of a new wave, whichever comes first).
|
||||
*/
|
||||
|
||||
export class FirstMoveCondition extends MoveCondition {
|
||||
constructor() {
|
||||
super((user, target, move) => user.battleSummonData?.waveTurnCount === 1);
|
||||
super((user, _target, _move) => user.tempSummonData.waveTurnCount === 1);
|
||||
}
|
||||
|
||||
getUserBenefitScore(user: Pokemon, target: Pokemon, move: Move): number {
|
||||
return this.apply(user, target, move) ? 10 : -20;
|
||||
getUserBenefitScore(user: Pokemon, _target: Pokemon, _move: Move): number {
|
||||
return this.apply(user, _target, _move) ? 10 : -20;
|
||||
}
|
||||
}
|
||||
|
||||
@ -8626,7 +8664,7 @@ export function initMoves() {
|
||||
new StatusMove(Moves.TRANSFORM, PokemonType.NORMAL, -1, 10, -1, 0, 1)
|
||||
.attr(TransformAttr)
|
||||
.condition((user, target, move) => !target.getTag(BattlerTagType.SUBSTITUTE))
|
||||
.condition((user, target, move) => !target.summonData?.illusion && !user.summonData?.illusion)
|
||||
.condition((user, target, move) => !target.summonData.illusion && !user.summonData.illusion)
|
||||
// transforming from or into fusion pokemon causes various problems (such as crashes)
|
||||
.condition((user, target, move) => !target.getTag(BattlerTagType.SUBSTITUTE) && !user.fusionSpecies && !target.fusionSpecies)
|
||||
.ignoresProtect()
|
||||
@ -8701,7 +8739,10 @@ export function initMoves() {
|
||||
.attr(MultiHitPowerIncrementAttr, 3)
|
||||
.checkAllHits(),
|
||||
new AttackMove(Moves.THIEF, PokemonType.DARK, MoveCategory.PHYSICAL, 60, 100, 25, -1, 0, 2)
|
||||
.attr(StealHeldItemChanceAttr, 0.3),
|
||||
.attr(StealHeldItemChanceAttr, 0.3)
|
||||
.edgeCase(),
|
||||
// Should not be able to steal held item if user faints due to Rough Skin, Iron Barbs, etc.
|
||||
// Should be able to steal items from pokemon with Sticky Hold if the damage causes them to faint
|
||||
new StatusMove(Moves.SPIDER_WEB, PokemonType.BUG, -1, 10, -1, 0, 2)
|
||||
.condition(failIfGhostTypeCondition)
|
||||
.attr(AddBattlerTagAttr, BattlerTagType.TRAPPED, false, true, 1)
|
||||
@ -8991,6 +9032,7 @@ export function initMoves() {
|
||||
.soundBased()
|
||||
.target(MoveTarget.RANDOM_NEAR_ENEMY)
|
||||
.partial(), // Does not lock the user, does not stop Pokemon from sleeping
|
||||
// Likely can make use of FrenzyAttr and an ArenaTag (just without the FrenzyMissFunc)
|
||||
new SelfStatusMove(Moves.STOCKPILE, PokemonType.NORMAL, -1, 20, -1, 0, 3)
|
||||
.condition(user => (user.getTag(StockpilingTag)?.stockpiledCount ?? 0) < 3)
|
||||
.attr(AddBattlerTagAttr, BattlerTagType.STOCKPILING, true),
|
||||
@ -9088,7 +9130,10 @@ export function initMoves() {
|
||||
.reflectable(),
|
||||
new AttackMove(Moves.KNOCK_OFF, PokemonType.DARK, MoveCategory.PHYSICAL, 65, 100, 20, -1, 0, 3)
|
||||
.attr(MovePowerMultiplierAttr, (user, target, move) => target.getHeldItems().filter(i => i.isTransferable).length > 0 ? 1.5 : 1)
|
||||
.attr(RemoveHeldItemAttr, false),
|
||||
.attr(RemoveHeldItemAttr, false)
|
||||
.edgeCase(),
|
||||
// Should not be able to remove held item if user faints due to Rough Skin, Iron Barbs, etc.
|
||||
// Should be able to remove items from pokemon with Sticky Hold if the damage causes them to faint
|
||||
new AttackMove(Moves.ENDEAVOR, PokemonType.NORMAL, MoveCategory.PHYSICAL, -1, 100, 5, -1, 0, 3)
|
||||
.attr(MatchHpAttr)
|
||||
.condition(failOnBossCondition),
|
||||
@ -9276,7 +9321,10 @@ export function initMoves() {
|
||||
.attr(HighCritAttr)
|
||||
.attr(StatusEffectAttr, StatusEffect.POISON),
|
||||
new AttackMove(Moves.COVET, PokemonType.NORMAL, MoveCategory.PHYSICAL, 60, 100, 25, -1, 0, 3)
|
||||
.attr(StealHeldItemChanceAttr, 0.3),
|
||||
.attr(StealHeldItemChanceAttr, 0.3)
|
||||
.edgeCase(),
|
||||
// Should not be able to steal held item if user faints due to Rough Skin, Iron Barbs, etc.
|
||||
// Should be able to steal items from pokemon with Sticky Hold if the damage causes them to faint
|
||||
new AttackMove(Moves.VOLT_TACKLE, PokemonType.ELECTRIC, MoveCategory.PHYSICAL, 120, 100, 15, 10, 0, 3)
|
||||
.attr(RecoilAttr, false, 0.33)
|
||||
.attr(StatusEffectAttr, StatusEffect.PARALYSIS)
|
||||
@ -9338,6 +9386,11 @@ export function initMoves() {
|
||||
new AttackMove(Moves.NATURAL_GIFT, PokemonType.NORMAL, MoveCategory.PHYSICAL, -1, 100, 15, -1, 0, 4)
|
||||
.makesContact(false)
|
||||
.unimplemented(),
|
||||
/*
|
||||
NOTE: To whoever tries to implement this, reminder to push to battleData.berriesEaten
|
||||
and enable the harvest test..
|
||||
Do NOT push to berriesEatenLast or else cud chew will puke the berry.
|
||||
*/
|
||||
new AttackMove(Moves.FEINT, PokemonType.NORMAL, MoveCategory.PHYSICAL, 30, 100, 10, -1, 2, 4)
|
||||
.attr(RemoveBattlerTagAttr, [ BattlerTagType.PROTECTED ])
|
||||
.attr(RemoveArenaTagsAttr, [ ArenaTagType.QUICK_GUARD, ArenaTagType.WIDE_GUARD, ArenaTagType.MAT_BLOCK, ArenaTagType.CRAFTY_SHIELD ], false)
|
||||
@ -9415,7 +9468,8 @@ export function initMoves() {
|
||||
.makesContact(true)
|
||||
.attr(PunishmentPowerAttr),
|
||||
new AttackMove(Moves.LAST_RESORT, PokemonType.NORMAL, MoveCategory.PHYSICAL, 140, 100, 5, -1, 0, 4)
|
||||
.attr(LastResortAttr),
|
||||
.attr(LastResortAttr)
|
||||
.edgeCase(), // May or may not need to ignore remotely called moves depending on how it works
|
||||
new StatusMove(Moves.WORRY_SEED, PokemonType.GRASS, 100, 10, -1, 0, 4)
|
||||
.attr(AbilityChangeAttr, Abilities.INSOMNIA)
|
||||
.reflectable(),
|
||||
@ -9782,7 +9836,9 @@ export function initMoves() {
|
||||
.hidesTarget(),
|
||||
new AttackMove(Moves.INCINERATE, PokemonType.FIRE, MoveCategory.SPECIAL, 60, 100, 15, -1, 0, 5)
|
||||
.target(MoveTarget.ALL_NEAR_ENEMIES)
|
||||
.attr(RemoveHeldItemAttr, true),
|
||||
.attr(RemoveHeldItemAttr, true)
|
||||
.edgeCase(),
|
||||
// Should be able to remove items from pokemon with Sticky Hold if the damage causes them to faint
|
||||
new StatusMove(Moves.QUASH, PokemonType.DARK, 100, 15, -1, 0, 5)
|
||||
.condition(failIfSingleBattle)
|
||||
.condition((user, target, move) => !target.turnData.acted)
|
||||
@ -9957,7 +10013,7 @@ export function initMoves() {
|
||||
.condition(new FirstMoveCondition())
|
||||
.condition(failIfLastCondition),
|
||||
new AttackMove(Moves.BELCH, PokemonType.POISON, MoveCategory.SPECIAL, 120, 90, 10, -1, 0, 6)
|
||||
.condition((user, target, move) => user.battleData.berriesEaten.length > 0),
|
||||
.condition((user, target, move) => user.battleData.hasEatenBerry),
|
||||
new StatusMove(Moves.ROTOTILLER, PokemonType.GROUND, -1, 10, -1, 0, 6)
|
||||
.target(MoveTarget.ALL)
|
||||
.condition((user, target, move) => {
|
||||
@ -10969,7 +11025,7 @@ export function initMoves() {
|
||||
.makesContact(false),
|
||||
new AttackMove(Moves.LUMINA_CRASH, PokemonType.PSYCHIC, MoveCategory.SPECIAL, 80, 100, 10, 100, 0, 9)
|
||||
.attr(StatStageChangeAttr, [ Stat.SPDEF ], -2),
|
||||
new AttackMove(Moves.ORDER_UP, PokemonType.DRAGON, MoveCategory.PHYSICAL, 80, 100, 10, -1, 0, 9)
|
||||
new AttackMove(Moves.ORDER_UP, PokemonType.DRAGON, MoveCategory.PHYSICAL, 80, 100, 10, 100, 0, 9)
|
||||
.attr(OrderUpStatBoostAttr)
|
||||
.makesContact(false),
|
||||
new AttackMove(Moves.JET_PUNCH, PokemonType.WATER, MoveCategory.PHYSICAL, 60, 100, 15, -1, 1, 9)
|
||||
@ -11083,7 +11139,6 @@ export function initMoves() {
|
||||
new AttackMove(Moves.TWIN_BEAM, PokemonType.PSYCHIC, MoveCategory.SPECIAL, 40, 100, 10, -1, 0, 9)
|
||||
.attr(MultiHitAttr, MultiHitType._2),
|
||||
new AttackMove(Moves.RAGE_FIST, PokemonType.GHOST, MoveCategory.PHYSICAL, 50, 100, 10, -1, 0, 9)
|
||||
.edgeCase() // Counter incorrectly increases on confusion self-hits
|
||||
.attr(RageFistPowerAttr)
|
||||
.punchingMove(),
|
||||
new AttackMove(Moves.ARMOR_CANNON, PokemonType.FIRE, MoveCategory.SPECIAL, 120, 100, 5, -1, 0, 9)
|
||||
|
@ -37,7 +37,6 @@ import type HeldModifierConfig from "#app/interfaces/held-modifier-config";
|
||||
import type { BerryType } from "#enums/berry-type";
|
||||
import { StatStageChangePhase } from "#app/phases/stat-stage-change-phase";
|
||||
import { Stat } from "#enums/stat";
|
||||
import { CLASSIC_MODE_MYSTERY_ENCOUNTER_WAVES } from "#app/constants";
|
||||
import i18next from "i18next";
|
||||
|
||||
/** the i18n namespace for this encounter */
|
||||
@ -52,8 +51,8 @@ export const AbsoluteAvariceEncounter: MysteryEncounter = MysteryEncounterBuilde
|
||||
MysteryEncounterType.ABSOLUTE_AVARICE,
|
||||
)
|
||||
.withEncounterTier(MysteryEncounterTier.GREAT)
|
||||
.withSceneWaveRangeRequirement(...CLASSIC_MODE_MYSTERY_ENCOUNTER_WAVES)
|
||||
.withSceneRequirement(new PersistentModifierRequirement("BerryModifier", 4)) // Must have at least 4 berries to spawn
|
||||
.withSceneWaveRangeRequirement(20, 180)
|
||||
.withSceneRequirement(new PersistentModifierRequirement("BerryModifier", 6)) // Must have at least 6 berries to spawn
|
||||
.withFleeAllowed(false)
|
||||
.withIntroSpriteConfigs([
|
||||
{
|
||||
@ -220,9 +219,9 @@ export const AbsoluteAvariceEncounter: MysteryEncounter = MysteryEncounterBuilde
|
||||
|
||||
// Do NOT remove the real berries yet or else it will be persisted in the session data
|
||||
|
||||
// SpDef buff below wave 50, +1 to all stats otherwise
|
||||
// +1 SpDef below wave 50, SpDef and Speed otherwise
|
||||
const statChangesForBattle: (Stat.ATK | Stat.DEF | Stat.SPATK | Stat.SPDEF | Stat.SPD | Stat.ACC | Stat.EVA)[] =
|
||||
globalScene.currentBattle.waveIndex < 50 ? [Stat.SPDEF] : [Stat.ATK, Stat.DEF, Stat.SPATK, Stat.SPDEF, Stat.SPD];
|
||||
globalScene.currentBattle.waveIndex < 50 ? [Stat.SPDEF] : [Stat.SPDEF, Stat.SPD];
|
||||
|
||||
// Calculate boss mon
|
||||
const config: EnemyPartyConfig = {
|
||||
@ -233,7 +232,7 @@ export const AbsoluteAvariceEncounter: MysteryEncounter = MysteryEncounterBuilde
|
||||
isBoss: true,
|
||||
bossSegments: 3,
|
||||
shiny: false, // Shiny lock because of consistency issues between the different options
|
||||
moveSet: [Moves.THRASH, Moves.BODY_PRESS, Moves.STUFF_CHEEKS, Moves.CRUNCH],
|
||||
moveSet: [Moves.THRASH, Moves.CRUNCH, Moves.BODY_PRESS, Moves.SLACK_OFF],
|
||||
modifierConfigs: bossModifierConfigs,
|
||||
tags: [BattlerTagType.MYSTERY_ENCOUNTER_POST_SUMMON],
|
||||
mysteryEncounterBattleEffects: (pokemon: Pokemon) => {
|
||||
|
@ -146,24 +146,34 @@ const POOL_4_POKEMON = [Species.GENESECT, Species.SLITHER_WING, Species.BUZZWOLE
|
||||
|
||||
const PHYSICAL_TUTOR_MOVES = [
|
||||
Moves.MEGAHORN,
|
||||
Moves.X_SCISSOR,
|
||||
Moves.ATTACK_ORDER,
|
||||
Moves.PIN_MISSILE,
|
||||
Moves.BUG_BITE,
|
||||
Moves.FIRST_IMPRESSION,
|
||||
Moves.LUNGE
|
||||
];
|
||||
|
||||
const SPECIAL_TUTOR_MOVES = [Moves.SILVER_WIND, Moves.BUG_BUZZ, Moves.SIGNAL_BEAM, Moves.POLLEN_PUFF];
|
||||
const SPECIAL_TUTOR_MOVES = [
|
||||
Moves.SILVER_WIND,
|
||||
Moves.SIGNAL_BEAM,
|
||||
Moves.BUG_BUZZ,
|
||||
Moves.POLLEN_PUFF,
|
||||
Moves.STRUGGLE_BUG
|
||||
];
|
||||
|
||||
const STATUS_TUTOR_MOVES = [Moves.STRING_SHOT, Moves.STICKY_WEB, Moves.SILK_TRAP, Moves.RAGE_POWDER, Moves.HEAL_ORDER];
|
||||
const STATUS_TUTOR_MOVES = [
|
||||
Moves.STRING_SHOT,
|
||||
Moves.DEFEND_ORDER,
|
||||
Moves.RAGE_POWDER,
|
||||
Moves.STICKY_WEB,
|
||||
Moves.SILK_TRAP
|
||||
];
|
||||
|
||||
const MISC_TUTOR_MOVES = [
|
||||
Moves.BUG_BITE,
|
||||
Moves.LEECH_LIFE,
|
||||
Moves.DEFEND_ORDER,
|
||||
Moves.QUIVER_DANCE,
|
||||
Moves.TAIL_GLOW,
|
||||
Moves.INFESTATION,
|
||||
Moves.U_TURN,
|
||||
Moves.HEAL_ORDER,
|
||||
Moves.QUIVER_DANCE,
|
||||
Moves.INFESTATION,
|
||||
];
|
||||
|
||||
/**
|
||||
|
@ -397,9 +397,6 @@ export const ClowningAroundEncounter: MysteryEncounter = MysteryEncounterBuilder
|
||||
newTypes.push(secondType);
|
||||
|
||||
// Apply the type changes (to both base and fusion, if pokemon is fused)
|
||||
if (!pokemon.customPokemonData) {
|
||||
pokemon.customPokemonData = new CustomPokemonData();
|
||||
}
|
||||
pokemon.customPokemonData.types = newTypes;
|
||||
if (pokemon.isFusion()) {
|
||||
if (!pokemon.fusionCustomPokemonData) {
|
||||
|
@ -684,7 +684,7 @@ function doPokemonTradeSequence(tradedPokemon: PlayerPokemon, receivedPokemon: P
|
||||
sprite.setPipelineData("shiny", tradedPokemon.shiny);
|
||||
sprite.setPipelineData("variant", tradedPokemon.variant);
|
||||
["spriteColors", "fusionSpriteColors"].map(k => {
|
||||
if (tradedPokemon.summonData?.speciesForm) {
|
||||
if (tradedPokemon.summonData.speciesForm) {
|
||||
k += "Base";
|
||||
}
|
||||
sprite.pipelineData[k] = tradedPokemon.getSprite().pipelineData[k];
|
||||
@ -710,7 +710,7 @@ function doPokemonTradeSequence(tradedPokemon: PlayerPokemon, receivedPokemon: P
|
||||
sprite.setPipelineData("shiny", receivedPokemon.shiny);
|
||||
sprite.setPipelineData("variant", receivedPokemon.variant);
|
||||
["spriteColors", "fusionSpriteColors"].map(k => {
|
||||
if (receivedPokemon.summonData?.speciesForm) {
|
||||
if (receivedPokemon.summonData.speciesForm) {
|
||||
k += "Base";
|
||||
}
|
||||
sprite.pipelineData[k] = receivedPokemon.getSprite().pipelineData[k];
|
||||
|
@ -29,8 +29,8 @@ import { Species } from "#enums/species";
|
||||
const namespace = "mysteryEncounters/mysteriousChest";
|
||||
|
||||
const RAND_LENGTH = 100;
|
||||
const TRAP_PERCENT = 35;
|
||||
const COMMON_REWARDS_PERCENT = 20;
|
||||
const TRAP_PERCENT = 30;
|
||||
const COMMON_REWARDS_PERCENT = 25;
|
||||
const ULTRA_REWARDS_PERCENT = 30;
|
||||
const ROGUE_REWARDS_PERCENT = 10;
|
||||
const MASTER_REWARDS_PERCENT = 5;
|
||||
|
@ -19,6 +19,7 @@ import {
|
||||
setEncounterRewards,
|
||||
} from "../utils/encounter-phase-utils";
|
||||
import { queueEncounterMessage } from "#app/data/mystery-encounters/utils/encounter-dialogue-utils";
|
||||
import { Nature } from "#enums/nature";
|
||||
import { Moves } from "#enums/moves";
|
||||
import { BattlerIndex } from "#app/battle";
|
||||
import { AiType, PokemonMove } from "#app/field/pokemon";
|
||||
@ -26,9 +27,10 @@ import { getPokemonSpecies } from "#app/data/pokemon-species";
|
||||
import { MysteryEncounterTier } from "#enums/mystery-encounter-tier";
|
||||
import { MysteryEncounterOptionMode } from "#enums/mystery-encounter-option-mode";
|
||||
import { PartyHealPhase } from "#app/phases/party-heal-phase";
|
||||
import { CLASSIC_MODE_MYSTERY_ENCOUNTER_WAVES } from "#app/constants";
|
||||
import { BerryType } from "#enums/berry-type";
|
||||
import { Stat } from "#enums/stat";
|
||||
import { CustomPokemonData } from "#app/data/custom-pokemon-data";
|
||||
import { randSeedInt } from "#app/utils/common";
|
||||
|
||||
/** i18n namespace for the encounter */
|
||||
const namespace = "mysteryEncounters/slumberingSnorlax";
|
||||
@ -42,7 +44,7 @@ export const SlumberingSnorlaxEncounter: MysteryEncounter = MysteryEncounterBuil
|
||||
MysteryEncounterType.SLUMBERING_SNORLAX,
|
||||
)
|
||||
.withEncounterTier(MysteryEncounterTier.GREAT)
|
||||
.withSceneWaveRangeRequirement(...CLASSIC_MODE_MYSTERY_ENCOUNTER_WAVES)
|
||||
.withSceneWaveRangeRequirement(15, 150)
|
||||
.withCatchAllowed(true)
|
||||
.withHideWildIntroMessage(true)
|
||||
.withFleeAllowed(false)
|
||||
@ -72,16 +74,26 @@ export const SlumberingSnorlaxEncounter: MysteryEncounter = MysteryEncounterBuil
|
||||
species: bossSpecies,
|
||||
isBoss: true,
|
||||
shiny: false, // Shiny lock because shiny is rolled only if the battle option is picked
|
||||
status: [StatusEffect.SLEEP, 5], // Extra turns on timer for Snorlax's start of fight moves
|
||||
moveSet: [Moves.REST, Moves.SLEEP_TALK, Moves.CRUNCH, Moves.GIGA_IMPACT],
|
||||
status: [StatusEffect.SLEEP, 6], // Extra turns on timer for Snorlax's start of fight moves
|
||||
nature: Nature.DOCILE,
|
||||
moveSet: [Moves.BODY_SLAM, Moves.CRUNCH, Moves.SLEEP_TALK, Moves.REST],
|
||||
modifierConfigs: [
|
||||
{
|
||||
modifier: generateModifierType(modifierTypes.BERRY, [BerryType.SITRUS]) as PokemonHeldItemModifierType,
|
||||
stackCount: 2,
|
||||
},
|
||||
{
|
||||
modifier: generateModifierType(modifierTypes.BERRY, [BerryType.ENIGMA]) as PokemonHeldItemModifierType,
|
||||
stackCount: 2,
|
||||
},
|
||||
{
|
||||
modifier: generateModifierType(modifierTypes.BASE_STAT_BOOSTER, [Stat.HP]) as PokemonHeldItemModifierType,
|
||||
},
|
||||
{
|
||||
modifier: generateModifierType(modifierTypes.SOOTHE_BELL) as PokemonHeldItemModifierType,
|
||||
stackCount: randSeedInt(2, 0),
|
||||
},
|
||||
{
|
||||
modifier: generateModifierType(modifierTypes.LUCKY_EGG) as PokemonHeldItemModifierType,
|
||||
stackCount: randSeedInt(2, 0),
|
||||
},
|
||||
],
|
||||
customPokemonData: new CustomPokemonData({ spriteScale: 1.25 }),
|
||||
@ -128,12 +140,6 @@ export const SlumberingSnorlaxEncounter: MysteryEncounter = MysteryEncounterBuil
|
||||
move: new PokemonMove(Moves.SNORE),
|
||||
ignorePp: true,
|
||||
},
|
||||
{
|
||||
sourceBattlerIndex: BattlerIndex.ENEMY,
|
||||
targets: [BattlerIndex.PLAYER],
|
||||
move: new PokemonMove(Moves.SNORE),
|
||||
ignorePp: true,
|
||||
},
|
||||
);
|
||||
await initBattleWithEnemyConfig(encounter.enemyPartyConfigs[0]);
|
||||
},
|
||||
|
@ -11,7 +11,6 @@ import { randSeedShuffle } from "#app/utils/common";
|
||||
import type MysteryEncounter from "../mystery-encounter";
|
||||
import { MysteryEncounterBuilder } from "../mystery-encounter";
|
||||
import { MysteryEncounterTier } from "#enums/mystery-encounter-tier";
|
||||
import { CLASSIC_MODE_MYSTERY_ENCOUNTER_WAVES } from "#app/constants";
|
||||
import { Biome } from "#enums/biome";
|
||||
import { TrainerType } from "#enums/trainer-type";
|
||||
import i18next from "i18next";
|
||||
@ -123,7 +122,7 @@ export const TheExpertPokemonBreederEncounter: MysteryEncounter = MysteryEncount
|
||||
MysteryEncounterType.THE_EXPERT_POKEMON_BREEDER,
|
||||
)
|
||||
.withEncounterTier(MysteryEncounterTier.ULTRA)
|
||||
.withSceneWaveRangeRequirement(...CLASSIC_MODE_MYSTERY_ENCOUNTER_WAVES)
|
||||
.withSceneWaveRangeRequirement(25, 180)
|
||||
.withScenePartySizeRequirement(4, 6, true) // Must have at least 4 legal pokemon in party
|
||||
.withIntroSpriteConfigs([]) // These are set in onInit()
|
||||
.withIntroDialogue([
|
||||
@ -483,9 +482,9 @@ function getPartyConfig(): EnemyPartyConfig {
|
||||
abilityIndex: 1, // Magic Guard
|
||||
shiny: false,
|
||||
nature: Nature.ADAMANT,
|
||||
moveSet: [Moves.METEOR_MASH, Moves.FIRE_PUNCH, Moves.ICE_PUNCH, Moves.THUNDER_PUNCH],
|
||||
moveSet: [Moves.FIRE_PUNCH, Moves.ICE_PUNCH, Moves.THUNDER_PUNCH, Moves.METEOR_MASH],
|
||||
ivs: [31, 31, 31, 31, 31, 31],
|
||||
tera: PokemonType.STEEL,
|
||||
tera: PokemonType.FAIRY,
|
||||
},
|
||||
],
|
||||
};
|
||||
|
@ -88,7 +88,7 @@ export const ThePokemonSalesmanEncounter: MysteryEncounter = MysteryEncounterBui
|
||||
|
||||
const r = randSeedInt(SHINY_MAGIKARP_WEIGHT);
|
||||
|
||||
const validEventEncounters = timedEventManager
|
||||
let validEventEncounters = timedEventManager
|
||||
.getEventEncounters()
|
||||
.filter(
|
||||
s =>
|
||||
@ -116,18 +116,44 @@ export const ThePokemonSalesmanEncounter: MysteryEncounter = MysteryEncounterBui
|
||||
// If you roll 1%, give shiny Magikarp with random variant
|
||||
species = getPokemonSpecies(Species.MAGIKARP);
|
||||
pokemon = new PlayerPokemon(species, 5, 2, undefined, undefined, true);
|
||||
} else if (
|
||||
}
|
||||
else if (
|
||||
(validEventEncounters.length > 0 && (r <= EVENT_THRESHOLD ||
|
||||
(isNullOrUndefined(species.abilityHidden) || species.abilityHidden === Abilities.NONE)))
|
||||
) {
|
||||
// If you roll 20%, give event encounter with 3 extra shiny rolls and its HA, if it has one
|
||||
const enc = randSeedItem(validEventEncounters);
|
||||
species = getPokemonSpecies(enc.species);
|
||||
pokemon = new PlayerPokemon(species, 5, species.abilityHidden === Abilities.NONE ? undefined : 2, enc.formIndex);
|
||||
pokemon.trySetShinySeed();
|
||||
pokemon.trySetShinySeed();
|
||||
pokemon.trySetShinySeed();
|
||||
} else {
|
||||
tries = 0;
|
||||
do {
|
||||
// If you roll 20%, give event encounter with 3 extra shiny rolls and its HA, if it has one
|
||||
const enc = randSeedItem(validEventEncounters);
|
||||
species = getPokemonSpecies(enc.species);
|
||||
pokemon = new PlayerPokemon(species, 5, species.abilityHidden === Abilities.NONE ? undefined : 2, enc.formIndex);
|
||||
pokemon.trySetShinySeed();
|
||||
pokemon.trySetShinySeed();
|
||||
pokemon.trySetShinySeed();
|
||||
if (pokemon.shiny || pokemon.abilityIndex === 2) {
|
||||
break;
|
||||
}
|
||||
tries++;
|
||||
} while (tries < 6);
|
||||
if (!pokemon.shiny && pokemon.abilityIndex !== 2) {
|
||||
// If, after 6 tries, you STILL somehow don't have an HA or shiny mon, pick from only the event mons that have an HA.
|
||||
if (validEventEncounters.some(s => !!getPokemonSpecies(s.species).abilityHidden)) {
|
||||
validEventEncounters.filter(s => !!getPokemonSpecies(s.species).abilityHidden);
|
||||
const enc = randSeedItem(validEventEncounters);
|
||||
species = getPokemonSpecies(enc.species);
|
||||
pokemon = new PlayerPokemon(species, 5, 2, enc.formIndex);
|
||||
pokemon.trySetShinySeed();
|
||||
pokemon.trySetShinySeed();
|
||||
pokemon.trySetShinySeed();
|
||||
}
|
||||
else {
|
||||
// If there's, and this would never happen, no eligible event encounters with a hidden ability, just do Magikarp
|
||||
species = getPokemonSpecies(Species.MAGIKARP);
|
||||
pokemon = new PlayerPokemon(species, 5, 2, undefined, undefined, true);
|
||||
}
|
||||
}
|
||||
}
|
||||
else {
|
||||
pokemon = new PlayerPokemon(species, 5, 2, species.formIndex);
|
||||
}
|
||||
pokemon.generateAndPopulateMoveset();
|
||||
|
@ -222,7 +222,8 @@ function endTrainerBattleAndShowDialogue(): Promise<void> {
|
||||
globalScene.triggerPokemonFormChange(pokemon, SpeciesFormChangeAbilityTrigger);
|
||||
}
|
||||
|
||||
pokemon.resetBattleData();
|
||||
// Each trainer battle is supposed to be a new fight, so reset all per-battle activation effects
|
||||
pokemon.resetBattleAndWaveData();
|
||||
applyPostBattleInitAbAttrs(PostBattleInitAbAttr, pokemon);
|
||||
}
|
||||
|
||||
|
@ -23,7 +23,6 @@ import { allSpecies, getPokemonSpecies } from "#app/data/pokemon-species";
|
||||
import type { PokemonHeldItemModifier } from "#app/modifier/modifier";
|
||||
import { HiddenAbilityRateBoosterModifier, PokemonFormChangeItemModifier } from "#app/modifier/modifier";
|
||||
import { achvs } from "#app/system/achv";
|
||||
import { CustomPokemonData } from "#app/data/custom-pokemon-data";
|
||||
import { showEncounterText } from "#app/data/mystery-encounters/utils/encounter-dialogue-utils";
|
||||
import type { PokemonHeldItemModifierType } from "#app/modifier/modifier-type";
|
||||
import { modifierTypes } from "#app/modifier/modifier-type";
|
||||
@ -601,9 +600,6 @@ async function postProcessTransformedPokemon(
|
||||
newType = randSeedInt(18) as PokemonType;
|
||||
}
|
||||
newTypes.push(newType);
|
||||
if (!newPokemon.customPokemonData) {
|
||||
newPokemon.customPokemonData = new CustomPokemonData();
|
||||
}
|
||||
newPokemon.customPokemonData.types = newTypes;
|
||||
|
||||
// Enable passive if previous had it
|
||||
|
@ -226,9 +226,9 @@ const anyBiomeEncounters: MysteryEncounterType[] = [
|
||||
*/
|
||||
export const mysteryEncountersByBiome = new Map<Biome, MysteryEncounterType[]>([
|
||||
[Biome.TOWN, []],
|
||||
[Biome.PLAINS, [MysteryEncounterType.SLUMBERING_SNORLAX, MysteryEncounterType.ABSOLUTE_AVARICE]],
|
||||
[Biome.PLAINS, [MysteryEncounterType.SLUMBERING_SNORLAX]],
|
||||
[Biome.GRASS, [MysteryEncounterType.SLUMBERING_SNORLAX, MysteryEncounterType.ABSOLUTE_AVARICE]],
|
||||
[Biome.TALL_GRASS, [MysteryEncounterType.ABSOLUTE_AVARICE]],
|
||||
[Biome.TALL_GRASS, [MysteryEncounterType.SLUMBERING_SNORLAX, MysteryEncounterType.ABSOLUTE_AVARICE]],
|
||||
[Biome.METROPOLIS, []],
|
||||
[Biome.FOREST, [MysteryEncounterType.SAFARI_ZONE, MysteryEncounterType.ABSOLUTE_AVARICE]],
|
||||
[Biome.SEA, [MysteryEncounterType.LOST_AT_SEA]],
|
||||
|
@ -10,7 +10,7 @@ import {
|
||||
import { showEncounterText } from "#app/data/mystery-encounters/utils/encounter-dialogue-utils";
|
||||
import type { AiType, PlayerPokemon } from "#app/field/pokemon";
|
||||
import type Pokemon from "#app/field/pokemon";
|
||||
import { EnemyPokemon, FieldPosition, PokemonMove, PokemonSummonData } from "#app/field/pokemon";
|
||||
import { EnemyPokemon, FieldPosition, PokemonMove } from "#app/field/pokemon";
|
||||
import type { CustomModifierSettings, ModifierType } from "#app/modifier/modifier-type";
|
||||
import {
|
||||
getPartyLuckValue,
|
||||
@ -348,11 +348,6 @@ export async function initBattleWithEnemyConfig(partyConfig: EnemyPartyConfig):
|
||||
enemyPokemon.status = new Status(status, 0, cureTurn);
|
||||
}
|
||||
|
||||
// Set summon data fields
|
||||
if (!enemyPokemon.summonData) {
|
||||
enemyPokemon.summonData = new PokemonSummonData();
|
||||
}
|
||||
|
||||
// Set ability
|
||||
if (!isNullOrUndefined(config.abilityIndex)) {
|
||||
enemyPokemon.abilityIndex = config.abilityIndex;
|
||||
@ -390,14 +385,11 @@ export async function initBattleWithEnemyConfig(partyConfig: EnemyPartyConfig):
|
||||
}
|
||||
}
|
||||
|
||||
// mysteryEncounterBattleEffects will only be used IFF MYSTERY_ENCOUNTER_POST_SUMMON tag is applied
|
||||
// mysteryEncounterBattleEffects will only be used if MYSTERY_ENCOUNTER_POST_SUMMON tag is applied
|
||||
if (config.mysteryEncounterBattleEffects) {
|
||||
enemyPokemon.mysteryEncounterBattleEffects = config.mysteryEncounterBattleEffects;
|
||||
}
|
||||
|
||||
// Requires re-priming summon data to update everything properly
|
||||
enemyPokemon.primeSummonData(enemyPokemon.summonData);
|
||||
|
||||
if (enemyPokemon.isShiny() && !enemyPokemon["shinySparkle"]) {
|
||||
enemyPokemon.initShinySparkle();
|
||||
}
|
||||
|
@ -1031,9 +1031,6 @@ export function applyAbilityOverrideToPokemon(pokemon: Pokemon, ability: Abiliti
|
||||
}
|
||||
pokemon.fusionCustomPokemonData.ability = ability;
|
||||
} else {
|
||||
if (!pokemon.customPokemonData) {
|
||||
pokemon.customPokemonData = new CustomPokemonData();
|
||||
}
|
||||
pokemon.customPokemonData.ability = ability;
|
||||
}
|
||||
}
|
||||
|
@ -88,7 +88,7 @@ export function doPokemonTransformationSequence(
|
||||
sprite.setPipelineData("shiny", previousPokemon.shiny);
|
||||
sprite.setPipelineData("variant", previousPokemon.variant);
|
||||
["spriteColors", "fusionSpriteColors"].map(k => {
|
||||
if (previousPokemon.summonData?.speciesForm) {
|
||||
if (previousPokemon.summonData.speciesForm) {
|
||||
k += "Base";
|
||||
}
|
||||
sprite.pipelineData[k] = previousPokemon.getSprite().pipelineData[k];
|
||||
@ -108,7 +108,7 @@ export function doPokemonTransformationSequence(
|
||||
sprite.setPipelineData("shiny", transformPokemon.shiny);
|
||||
sprite.setPipelineData("variant", transformPokemon.variant);
|
||||
["spriteColors", "fusionSpriteColors"].map(k => {
|
||||
if (transformPokemon.summonData?.speciesForm) {
|
||||
if (transformPokemon.summonData.speciesForm) {
|
||||
k += "Base";
|
||||
}
|
||||
sprite.pipelineData[k] = transformPokemon.getSprite().pipelineData[k];
|
||||
|
@ -33,6 +33,7 @@ import { SpeciesFormKey } from "#enums/species-form-key";
|
||||
import { starterPassiveAbilities } from "#app/data/balance/passives";
|
||||
import { loadPokemonVariantAssets } from "#app/sprites/pokemon-sprite";
|
||||
import { hasExpSprite } from "#app/sprites/sprite-utils";
|
||||
import { Gender } from "./gender";
|
||||
|
||||
export enum Region {
|
||||
NORMAL,
|
||||
@ -485,10 +486,10 @@ export abstract class PokemonSpeciesForm {
|
||||
break;
|
||||
case Species.ZACIAN:
|
||||
case Species.ZAMAZENTA:
|
||||
// biome-ignore lint/suspicious/noFallthroughSwitchClause: Falls through
|
||||
if (formSpriteKey.startsWith("behemoth")) {
|
||||
formSpriteKey = "crowned";
|
||||
}
|
||||
// biome-ignore lint/suspicious/no-fallthrough: Falls through
|
||||
default:
|
||||
ret += `-${formSpriteKey}`;
|
||||
break;
|
||||
@ -749,7 +750,7 @@ export abstract class PokemonSpeciesForm {
|
||||
let paletteColors: Map<number, number> = new Map();
|
||||
|
||||
const originalRandom = Math.random;
|
||||
Math.random = () => Phaser.Math.RND.realInRange(0, 1);
|
||||
Math.random = Phaser.Math.RND.frac;
|
||||
|
||||
globalScene.executeWithSeedOffset(
|
||||
() => {
|
||||
@ -879,6 +880,21 @@ export default class PokemonSpecies extends PokemonSpeciesForm implements Locali
|
||||
return this.name;
|
||||
}
|
||||
|
||||
/**
|
||||
* Pick and return a random {@linkcode Gender} for a {@linkcode Pokemon}.
|
||||
* @returns A randomly rolled gender based on this Species' {@linkcode malePercent}.
|
||||
*/
|
||||
generateGender(): Gender {
|
||||
if (isNullOrUndefined(this.malePercent)) {
|
||||
return Gender.GENDERLESS;
|
||||
}
|
||||
|
||||
if (Phaser.Math.RND.realInRange(0, 1) <= this.malePercent) {
|
||||
return Gender.MALE;
|
||||
}
|
||||
return Gender.FEMALE;
|
||||
}
|
||||
|
||||
/**
|
||||
* Find the name of species with proper attachments for regionals and separate starter forms (Floette, Ursaluna)
|
||||
* @returns a string with the region name or other form name attached
|
||||
|
@ -1,4 +1,5 @@
|
||||
export enum Biome {
|
||||
// TODO: Should -1 be part of the enum signature (for "unknown place")
|
||||
TOWN,
|
||||
PLAINS,
|
||||
GRASS,
|
||||
|
@ -1,5 +1,6 @@
|
||||
import Phaser from "phaser";
|
||||
import { deepCopy, getEnumValues } from "#app/utils/common";
|
||||
import { getEnumValues } from "#app/utils/common";
|
||||
import { deepCopy } from "#app/utils/data";
|
||||
import pad_generic from "./configs/inputs/pad_generic";
|
||||
import pad_unlicensedSNES from "./configs/inputs/pad_unlicensedSNES";
|
||||
import pad_xbox360 from "./configs/inputs/pad_xbox360";
|
||||
|
@ -790,6 +790,7 @@ export class BerryModifierType extends PokemonHeldItemModifierType implements Ge
|
||||
);
|
||||
|
||||
this.berryType = berryType;
|
||||
this.id = "BERRY"; // needed to prevent harvest item deletion; remove after modifier rework
|
||||
}
|
||||
|
||||
get name(): string {
|
||||
|
@ -47,7 +47,12 @@ import {
|
||||
} from "./modifier-type";
|
||||
import { Color, ShadowColor } from "#enums/color";
|
||||
import { FRIENDSHIP_GAIN_FROM_RARE_CANDY } from "#app/data/balance/starters";
|
||||
import { applyAbAttrs, CommanderAbAttr } from "#app/data/abilities/ability";
|
||||
import {
|
||||
applyAbAttrs,
|
||||
applyPostItemLostAbAttrs,
|
||||
CommanderAbAttr,
|
||||
PostItemLostAbAttr,
|
||||
} from "#app/data/abilities/ability";
|
||||
import { globalScene } from "#app/global-scene";
|
||||
|
||||
export type ModifierPredicate = (modifier: Modifier) => boolean;
|
||||
@ -232,6 +237,10 @@ export abstract class PersistentModifier extends Modifier {
|
||||
|
||||
abstract getMaxStackCount(forThreshold?: boolean): number;
|
||||
|
||||
getCountUnderMax(): number {
|
||||
return this.getMaxStackCount() - this.getStackCount();
|
||||
}
|
||||
|
||||
isIconVisible(): boolean {
|
||||
return true;
|
||||
}
|
||||
@ -653,7 +662,9 @@ export class TerastallizeAccessModifier extends PersistentModifier {
|
||||
}
|
||||
|
||||
export abstract class PokemonHeldItemModifier extends PersistentModifier {
|
||||
/** The ID of the {@linkcode Pokemon} that this item belongs to. */
|
||||
public pokemonId: number;
|
||||
/** Whether this item can be transfered to or stolen by another Pokemon. */
|
||||
public isTransferable = true;
|
||||
|
||||
constructor(type: ModifierType, pokemonId: number, stackCount?: number) {
|
||||
@ -1103,20 +1114,20 @@ export class PokemonIncrementingStatModifier extends PokemonHeldItemModifier {
|
||||
* @returns always `true`
|
||||
*/
|
||||
override apply(_pokemon: Pokemon, stat: Stat, statHolder: NumberHolder): boolean {
|
||||
// Modifies the passed in stat number holder by +1 per stack for HP, +2 per stack for other stats
|
||||
// If the Macho Brace is at max stacks (50), adds additional 5% to total HP and 10% to other stats
|
||||
// Modifies the passed in stat number holder by +2 per stack for HP, +1 per stack for other stats
|
||||
// If the Macho Brace is at max stacks (50), adds additional 10% to total HP and 5% to other stats
|
||||
const isHp = stat === Stat.HP;
|
||||
|
||||
if (isHp) {
|
||||
statHolder.value += this.stackCount;
|
||||
if (this.stackCount === this.getMaxHeldItemCount()) {
|
||||
statHolder.value = Math.floor(statHolder.value * 1.05);
|
||||
}
|
||||
} else {
|
||||
statHolder.value += 2 * this.stackCount;
|
||||
if (this.stackCount === this.getMaxHeldItemCount()) {
|
||||
statHolder.value = Math.floor(statHolder.value * 1.1);
|
||||
}
|
||||
} else {
|
||||
statHolder.value += this.stackCount;
|
||||
if (this.stackCount === this.getMaxHeldItemCount()) {
|
||||
statHolder.value = Math.floor(statHolder.value * 1.05);
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
@ -1639,14 +1650,15 @@ export class FlinchChanceModifier extends PokemonHeldItemModifier {
|
||||
}
|
||||
|
||||
/**
|
||||
* Applies {@linkcode FlinchChanceModifier}
|
||||
* @param pokemon the {@linkcode Pokemon} that holds the item
|
||||
* @param flinched {@linkcode BooleanHolder} that is `true` if the pokemon flinched
|
||||
* @returns `true` if {@linkcode FlinchChanceModifier} has been applied
|
||||
* Applies {@linkcode FlinchChanceModifier} to randomly flinch targets hit.
|
||||
* @param pokemon - The {@linkcode Pokemon} that holds the item
|
||||
* @param flinched - A {@linkcode BooleanHolder} holding whether the pokemon has flinched
|
||||
* @returns `true` if {@linkcode FlinchChanceModifier} was applied successfully
|
||||
*/
|
||||
override apply(pokemon: Pokemon, flinched: BooleanHolder): boolean {
|
||||
// The check for pokemon.battleSummonData is to ensure that a crash doesn't occur when a Pokemon with King's Rock procs a flinch
|
||||
if (pokemon.battleSummonData && !flinched.value && pokemon.randSeedInt(100) < this.getStackCount() * this.chance) {
|
||||
// The check for pokemon.summonData is to ensure that a crash doesn't occur when a Pokemon with King's Rock procs a flinch
|
||||
// TODO: Since summonData is always defined now, we can probably remove this
|
||||
if (pokemon.summonData && !flinched.value && pokemon.randSeedInt(100) < this.getStackCount() * this.chance) {
|
||||
flinched.value = true;
|
||||
return true;
|
||||
}
|
||||
@ -1772,6 +1784,7 @@ export class HitHealModifier extends PokemonHeldItemModifier {
|
||||
*/
|
||||
override apply(pokemon: Pokemon): boolean {
|
||||
if (pokemon.turnData.totalDamageDealt && !pokemon.isFullHp()) {
|
||||
// TODO: this shouldn't be undefined AFAIK
|
||||
globalScene.unshiftPhase(
|
||||
new PokemonHealPhase(
|
||||
pokemon.getBattlerIndex(),
|
||||
@ -1867,11 +1880,15 @@ export class BerryModifier extends PokemonHeldItemModifier {
|
||||
override apply(pokemon: Pokemon): boolean {
|
||||
const preserve = new BooleanHolder(false);
|
||||
globalScene.applyModifiers(PreserveBerryModifier, pokemon.isPlayer(), pokemon, preserve);
|
||||
this.consumed = !preserve.value;
|
||||
|
||||
// munch the berry and trigger unburden-like effects
|
||||
getBerryEffectFunc(this.berryType)(pokemon);
|
||||
if (!preserve.value) {
|
||||
this.consumed = true;
|
||||
}
|
||||
applyPostItemLostAbAttrs(PostItemLostAbAttr, pokemon, false);
|
||||
|
||||
// Update berry eaten trackers for Belch, Harvest, Cud Chew, etc.
|
||||
// Don't recover it if we proc berry pouch (no item duplication)
|
||||
pokemon.recordEatenBerry(this.berryType, this.consumed);
|
||||
|
||||
return true;
|
||||
}
|
||||
@ -1910,9 +1927,7 @@ export class PreserveBerryModifier extends PersistentModifier {
|
||||
* @returns always `true`
|
||||
*/
|
||||
override apply(pokemon: Pokemon, doPreserve: BooleanHolder): boolean {
|
||||
if (!doPreserve.value) {
|
||||
doPreserve.value = pokemon.randSeedInt(10) < this.getStackCount() * 3;
|
||||
}
|
||||
doPreserve.value ||= pokemon.randSeedInt(10) < this.getStackCount() * 3;
|
||||
|
||||
return true;
|
||||
}
|
||||
@ -3609,7 +3624,7 @@ export class EnemyAttackStatusEffectChanceModifier extends EnemyPersistentModifi
|
||||
super(type, stackCount);
|
||||
|
||||
this.effect = effect;
|
||||
//Hardcode temporarily
|
||||
// Hardcode temporarily
|
||||
this.chance = 0.025 * (this.effect === StatusEffect.BURN || this.effect === StatusEffect.POISON ? 2 : 1);
|
||||
}
|
||||
|
||||
@ -3716,13 +3731,13 @@ export class EnemyEndureChanceModifier extends EnemyPersistentModifier {
|
||||
* @returns `true` if {@linkcode Pokemon} endured
|
||||
*/
|
||||
override apply(target: Pokemon): boolean {
|
||||
if (target.battleData.endured || target.randSeedInt(100) >= this.chance * this.getStackCount()) {
|
||||
if (target.waveData.endured || target.randSeedInt(100) >= this.chance * this.getStackCount()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
target.addTag(BattlerTagType.ENDURE_TOKEN, 1);
|
||||
|
||||
target.battleData.endured = true;
|
||||
target.waveData.endured = true;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
@ -104,8 +104,16 @@ class DefaultOverrides {
|
||||
readonly BYPASS_TUTORIAL_SKIP_OVERRIDE: boolean = false;
|
||||
/** Set to `true` to be able to re-earn already unlocked achievements */
|
||||
readonly ACHIEVEMENTS_REUNLOCK_OVERRIDE: boolean = false;
|
||||
/** Set to `true` to force Paralysis and Freeze to always activate, or `false` to force them to not activate */
|
||||
/**
|
||||
* Set to `true` to force Paralysis and Freeze to always activate,
|
||||
* or `false` to force them to not activate (or clear for freeze).
|
||||
*/
|
||||
readonly STATUS_ACTIVATION_OVERRIDE: boolean | null = null;
|
||||
/**
|
||||
* Set to `true` to force confusion to always trigger,
|
||||
* or `false` to force it to never trigger.
|
||||
*/
|
||||
readonly CONFUSION_ACTIVATION_OVERRIDE: boolean | null = null;
|
||||
|
||||
// ----------------
|
||||
// PLAYER OVERRIDES
|
||||
|
@ -59,8 +59,8 @@ export class BattleEndPhase extends BattlePhase {
|
||||
}
|
||||
|
||||
for (const pokemon of globalScene.getField()) {
|
||||
if (pokemon?.battleSummonData) {
|
||||
pokemon.battleSummonData.waveTurnCount = 1;
|
||||
if (pokemon) {
|
||||
pokemon.tempSummonData.waveTurnCount = 1;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1,4 +1,9 @@
|
||||
import { applyAbAttrs, PreventBerryUseAbAttr, HealFromBerryUseAbAttr } from "#app/data/abilities/ability";
|
||||
import {
|
||||
applyAbAttrs,
|
||||
PreventBerryUseAbAttr,
|
||||
HealFromBerryUseAbAttr,
|
||||
RepeatBerryNextTurnAbAttr,
|
||||
} from "#app/data/abilities/ability";
|
||||
import { CommonAnim } from "#app/data/battle-anims";
|
||||
import { BerryUsedEvent } from "#app/events/battle-scene";
|
||||
import { getPokemonNameWithAffix } from "#app/messages";
|
||||
@ -8,47 +13,65 @@ import { BooleanHolder } from "#app/utils/common";
|
||||
import { FieldPhase } from "./field-phase";
|
||||
import { CommonAnimPhase } from "./common-anim-phase";
|
||||
import { globalScene } from "#app/global-scene";
|
||||
import type Pokemon from "#app/field/pokemon";
|
||||
|
||||
/** The phase after attacks where the pokemon eat berries */
|
||||
/**
|
||||
* The phase after attacks where the pokemon eat berries.
|
||||
* Also triggers Cud Chew's "repeat berry use" effects
|
||||
*/
|
||||
export class BerryPhase extends FieldPhase {
|
||||
start() {
|
||||
super.start();
|
||||
|
||||
this.executeForAll(pokemon => {
|
||||
const hasUsableBerry = !!globalScene.findModifier(m => {
|
||||
return m instanceof BerryModifier && m.shouldApply(pokemon);
|
||||
}, pokemon.isPlayer());
|
||||
|
||||
if (hasUsableBerry) {
|
||||
const cancelled = new BooleanHolder(false);
|
||||
pokemon.getOpponents().map(opp => applyAbAttrs(PreventBerryUseAbAttr, opp, cancelled));
|
||||
|
||||
if (cancelled.value) {
|
||||
globalScene.queueMessage(
|
||||
i18next.t("abilityTriggers:preventBerryUse", {
|
||||
pokemonNameWithAffix: getPokemonNameWithAffix(pokemon),
|
||||
}),
|
||||
);
|
||||
} else {
|
||||
globalScene.unshiftPhase(
|
||||
new CommonAnimPhase(pokemon.getBattlerIndex(), pokemon.getBattlerIndex(), CommonAnim.USE_ITEM),
|
||||
);
|
||||
|
||||
for (const berryModifier of globalScene.applyModifiers(BerryModifier, pokemon.isPlayer(), pokemon)) {
|
||||
if (berryModifier.consumed) {
|
||||
berryModifier.consumed = false;
|
||||
pokemon.loseHeldItem(berryModifier);
|
||||
}
|
||||
globalScene.eventTarget.dispatchEvent(new BerryUsedEvent(berryModifier)); // Announce a berry was used
|
||||
}
|
||||
|
||||
globalScene.updateModifiers(pokemon.isPlayer());
|
||||
|
||||
applyAbAttrs(HealFromBerryUseAbAttr, pokemon, new BooleanHolder(false));
|
||||
}
|
||||
}
|
||||
this.eatBerries(pokemon);
|
||||
applyAbAttrs(RepeatBerryNextTurnAbAttr, pokemon, null);
|
||||
});
|
||||
|
||||
this.end();
|
||||
}
|
||||
|
||||
/**
|
||||
* Attempt to eat all of a given {@linkcode Pokemon}'s berries once.
|
||||
* @param pokemon - The {@linkcode Pokemon} to check
|
||||
*/
|
||||
eatBerries(pokemon: Pokemon): void {
|
||||
const hasUsableBerry = !!globalScene.findModifier(
|
||||
m => m instanceof BerryModifier && m.shouldApply(pokemon),
|
||||
pokemon.isPlayer(),
|
||||
);
|
||||
|
||||
if (!hasUsableBerry) {
|
||||
return;
|
||||
}
|
||||
|
||||
// TODO: If both opponents on field have unnerve, which one displays its message?
|
||||
const cancelled = new BooleanHolder(false);
|
||||
pokemon.getOpponents().forEach(opp => applyAbAttrs(PreventBerryUseAbAttr, opp, cancelled));
|
||||
if (cancelled.value) {
|
||||
globalScene.queueMessage(
|
||||
i18next.t("abilityTriggers:preventBerryUse", {
|
||||
pokemonNameWithAffix: getPokemonNameWithAffix(pokemon),
|
||||
}),
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
globalScene.unshiftPhase(
|
||||
new CommonAnimPhase(pokemon.getBattlerIndex(), pokemon.getBattlerIndex(), CommonAnim.USE_ITEM),
|
||||
);
|
||||
|
||||
for (const berryModifier of globalScene.applyModifiers(BerryModifier, pokemon.isPlayer(), pokemon)) {
|
||||
// No need to track berries being eaten; already done inside applyModifiers
|
||||
if (berryModifier.consumed) {
|
||||
berryModifier.consumed = false;
|
||||
pokemon.loseHeldItem(berryModifier);
|
||||
}
|
||||
globalScene.eventTarget.dispatchEvent(new BerryUsedEvent(berryModifier));
|
||||
}
|
||||
globalScene.updateModifiers(pokemon.isPlayer());
|
||||
|
||||
// Abilities.CHEEK_POUCH only works once per round of nom noms
|
||||
applyAbAttrs(HealFromBerryUseAbAttr, pokemon, new BooleanHolder(false));
|
||||
}
|
||||
}
|
||||
|
@ -113,12 +113,6 @@ export class EncounterPhase extends BattlePhase {
|
||||
}
|
||||
if (!this.loaded) {
|
||||
if (battle.battleType === BattleType.TRAINER) {
|
||||
//resets hitRecCount during Trainer ecnounter
|
||||
for (const pokemon of globalScene.getPlayerParty()) {
|
||||
if (pokemon) {
|
||||
pokemon.customPokemonData.resetHitReceivedCount();
|
||||
}
|
||||
}
|
||||
battle.enemyParty[e] = battle.trainer?.genPartyMember(e)!; // TODO:: is the bang correct here?
|
||||
} else {
|
||||
let enemySpecies = globalScene.randomSpecies(battle.waveIndex, level, true);
|
||||
@ -140,7 +134,6 @@ export class EncounterPhase extends BattlePhase {
|
||||
if (globalScene.currentBattle.battleSpec === BattleSpec.FINAL_BOSS) {
|
||||
battle.enemyParty[e].ivs = new Array(6).fill(31);
|
||||
}
|
||||
// biome-ignore lint/complexity/noForEach: Improves readability
|
||||
globalScene
|
||||
.getPlayerParty()
|
||||
.slice(0, !battle.double ? 1 : 2)
|
||||
@ -195,7 +188,7 @@ export class EncounterPhase extends BattlePhase {
|
||||
];
|
||||
const moveset: string[] = [];
|
||||
for (const move of enemyPokemon.getMoveset()) {
|
||||
moveset.push(move!.getName()); // TODO: remove `!` after moveset-null removal PR
|
||||
moveset.push(move.getName());
|
||||
}
|
||||
|
||||
console.log(
|
||||
@ -288,6 +281,7 @@ export class EncounterPhase extends BattlePhase {
|
||||
});
|
||||
|
||||
if (!this.loaded && battle.battleType !== BattleType.MYSTERY_ENCOUNTER) {
|
||||
// generate modifiers for MEs, overriding prior ones as applicable
|
||||
regenerateModifierPoolThresholds(
|
||||
globalScene.getEnemyField(),
|
||||
battle.battleType === BattleType.TRAINER ? ModifierPoolType.TRAINER : ModifierPoolType.WILD,
|
||||
@ -300,8 +294,8 @@ export class EncounterPhase extends BattlePhase {
|
||||
}
|
||||
}
|
||||
|
||||
if (battle.battleType === BattleType.TRAINER) {
|
||||
globalScene.currentBattle.trainer!.genAI(globalScene.getEnemyParty());
|
||||
if (battle.battleType === BattleType.TRAINER && globalScene.currentBattle.trainer) {
|
||||
globalScene.currentBattle.trainer.genAI(globalScene.getEnemyParty());
|
||||
}
|
||||
|
||||
globalScene.ui.setMode(UiMode.MESSAGE).then(() => {
|
||||
@ -342,8 +336,10 @@ export class EncounterPhase extends BattlePhase {
|
||||
}
|
||||
|
||||
for (const pokemon of globalScene.getPlayerParty()) {
|
||||
// Currently, a new wave is not considered a new battle if there is no arena reset
|
||||
// Therefore, we only reset wave data here
|
||||
if (pokemon) {
|
||||
pokemon.resetBattleData();
|
||||
pokemon.resetWaveData();
|
||||
}
|
||||
}
|
||||
|
||||
@ -558,7 +554,7 @@ export class EncounterPhase extends BattlePhase {
|
||||
if (enemyPokemon.isShiny(true)) {
|
||||
globalScene.unshiftPhase(new ShinySparklePhase(BattlerIndex.ENEMY + e));
|
||||
}
|
||||
/** This sets Eternatus' held item to be untransferrable, preventing it from being stolen */
|
||||
/** This sets Eternatus' held item to be untransferrable, preventing it from being stolen */
|
||||
if (
|
||||
enemyPokemon.species.speciesId === Species.ETERNATUS &&
|
||||
(globalScene.gameMode.isBattleClassicFinalBoss(globalScene.currentBattle.waveIndex) ||
|
||||
|
@ -146,7 +146,7 @@ export class EvolutionPhase extends Phase {
|
||||
sprite.setPipelineData("shiny", this.pokemon.shiny);
|
||||
sprite.setPipelineData("variant", this.pokemon.variant);
|
||||
["spriteColors", "fusionSpriteColors"].map(k => {
|
||||
if (this.pokemon.summonData?.speciesForm) {
|
||||
if (this.pokemon.summonData.speciesForm) {
|
||||
k += "Base";
|
||||
}
|
||||
sprite.pipelineData[k] = this.pokemon.getSprite().pipelineData[k];
|
||||
@ -178,7 +178,7 @@ export class EvolutionPhase extends Phase {
|
||||
sprite.setPipelineData("shiny", evolvedPokemon.shiny);
|
||||
sprite.setPipelineData("variant", evolvedPokemon.variant);
|
||||
["spriteColors", "fusionSpriteColors"].map(k => {
|
||||
if (evolvedPokemon.summonData?.speciesForm) {
|
||||
if (evolvedPokemon.summonData.speciesForm) {
|
||||
k += "Base";
|
||||
}
|
||||
sprite.pipelineData[k] = evolvedPokemon.getSprite().pipelineData[k];
|
||||
|
@ -118,7 +118,7 @@ export class FaintPhase extends PokemonPhase {
|
||||
|
||||
pokemon.resetTera();
|
||||
|
||||
if (pokemon.turnData?.attacksReceived?.length) {
|
||||
if (pokemon.turnData.attacksReceived?.length) {
|
||||
const lastAttack = pokemon.turnData.attacksReceived[0];
|
||||
applyPostFaintAbAttrs(
|
||||
PostFaintAbAttr,
|
||||
@ -136,7 +136,7 @@ export class FaintPhase extends PokemonPhase {
|
||||
for (const p of alivePlayField) {
|
||||
applyPostKnockOutAbAttrs(PostKnockOutAbAttr, p, pokemon);
|
||||
}
|
||||
if (pokemon.turnData?.attacksReceived?.length) {
|
||||
if (pokemon.turnData.attacksReceived?.length) {
|
||||
const defeatSource = this.source;
|
||||
|
||||
if (defeatSource?.isOnField()) {
|
||||
|
@ -6,8 +6,7 @@ type PokemonFunc = (pokemon: Pokemon) => void;
|
||||
|
||||
export abstract class FieldPhase extends BattlePhase {
|
||||
executeForAll(func: PokemonFunc): void {
|
||||
const field = globalScene.getField(true).filter(p => p.summonData);
|
||||
for (const pokemon of field) {
|
||||
for (const pokemon of globalScene.getField(true)) {
|
||||
func(pokemon);
|
||||
}
|
||||
}
|
||||
|
@ -51,7 +51,7 @@ export class FormChangePhase extends EvolutionPhase {
|
||||
sprite.setPipelineData("shiny", transformedPokemon.shiny);
|
||||
sprite.setPipelineData("variant", transformedPokemon.variant);
|
||||
["spriteColors", "fusionSpriteColors"].map(k => {
|
||||
if (transformedPokemon.summonData?.speciesForm) {
|
||||
if (transformedPokemon.summonData.speciesForm) {
|
||||
k += "Base";
|
||||
}
|
||||
sprite.pipelineData[k] = transformedPokemon.getSprite().pipelineData[k];
|
||||
|
@ -277,9 +277,6 @@ export class MoveEffectPhase extends PokemonPhase {
|
||||
super.end();
|
||||
return;
|
||||
}
|
||||
if (isNullOrUndefined(user.turnData)) {
|
||||
user.resetTurnData();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -618,7 +618,7 @@ export class MovePhase extends BattlePhase {
|
||||
globalScene.eventTarget.dispatchEvent(new MoveUsedEvent(this.pokemon?.id, this.move.getMove(), ppUsed));
|
||||
}
|
||||
|
||||
if (this.cancelled && this.pokemon.summonData?.tags?.find(t => t.tagType === BattlerTagType.FRENZY)) {
|
||||
if (this.cancelled && this.pokemon.summonData.tags?.find(t => t.tagType === BattlerTagType.FRENZY)) {
|
||||
frenzyMissFunc(this.pokemon, this.move.getMove());
|
||||
}
|
||||
|
||||
|
@ -229,8 +229,7 @@ export class MysteryEncounterBattleStartCleanupPhase extends Phase {
|
||||
|
||||
// Lapse any residual flinches/endures but ignore all other turn-end battle tags
|
||||
const includedLapseTags = [BattlerTagType.FLINCHED, BattlerTagType.ENDURING];
|
||||
const field = globalScene.getField(true).filter(p => p.summonData);
|
||||
field.forEach(pokemon => {
|
||||
globalScene.getField(true).forEach(pokemon => {
|
||||
const tags = pokemon.summonData.tags;
|
||||
tags
|
||||
.filter(
|
||||
|
@ -7,17 +7,17 @@ export class NewBiomeEncounterPhase extends NextEncounterPhase {
|
||||
doEncounter(): void {
|
||||
globalScene.playBgm(undefined, true);
|
||||
|
||||
// Reset all battle and wave data, perform form changes, etc.
|
||||
// We do this because new biomes are considered "arena transitions" akin to MEs and trainer battles
|
||||
for (const pokemon of globalScene.getPlayerParty()) {
|
||||
if (pokemon) {
|
||||
pokemon.resetBattleData();
|
||||
pokemon.customPokemonData.resetHitReceivedCount();
|
||||
pokemon.resetBattleAndWaveData();
|
||||
if (pokemon.isOnField()) {
|
||||
applyAbAttrs(PostBiomeChangeAbAttr, pokemon, null);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (const pokemon of globalScene.getPlayerParty().filter(p => p.isOnField())) {
|
||||
applyAbAttrs(PostBiomeChangeAbAttr, pokemon, null);
|
||||
}
|
||||
|
||||
const enemyField = globalScene.getEnemyField();
|
||||
const moveTargets: any[] = [globalScene.arenaEnemy, enemyField];
|
||||
const mysteryEncounter = globalScene.currentBattle?.mysteryEncounter?.introVisuals;
|
||||
|
@ -1,6 +1,10 @@
|
||||
import { globalScene } from "#app/global-scene";
|
||||
import { EncounterPhase } from "./encounter-phase";
|
||||
|
||||
/**
|
||||
* The phase between defeating an encounter and starting another wild wave.
|
||||
* Handles generating, loading and preparing for it.
|
||||
*/
|
||||
export class NextEncounterPhase extends EncounterPhase {
|
||||
start() {
|
||||
super.start();
|
||||
@ -9,9 +13,12 @@ export class NextEncounterPhase extends EncounterPhase {
|
||||
doEncounter(): void {
|
||||
globalScene.playBgm(undefined, true);
|
||||
|
||||
// Reset all player transient wave data/intel before starting a new wild encounter.
|
||||
// We exclusively reset wave data here as wild waves are considered one continuous "battle"
|
||||
// for lack of an arena transition.
|
||||
for (const pokemon of globalScene.getPlayerParty()) {
|
||||
if (pokemon) {
|
||||
pokemon.resetBattleData();
|
||||
pokemon.resetWaveData();
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -74,7 +74,7 @@ export class QuietFormChangePhase extends BattlePhase {
|
||||
isTerastallized: this.pokemon.isTerastallized,
|
||||
});
|
||||
["spriteColors", "fusionSpriteColors"].map(k => {
|
||||
if (this.pokemon.summonData?.speciesForm) {
|
||||
if (this.pokemon.summonData.speciesForm) {
|
||||
k += "Base";
|
||||
}
|
||||
sprite.pipelineData[k] = this.pokemon.getSprite().pipelineData[k];
|
||||
|
@ -50,9 +50,7 @@ export class ShowAbilityPhase extends PokemonPhase {
|
||||
}
|
||||
|
||||
globalScene.abilityBar.showAbility(this.pokemonName, this.abilityName, this.passive, this.player).then(() => {
|
||||
if (pokemon?.battleData) {
|
||||
pokemon.battleData.abilityRevealed = true;
|
||||
}
|
||||
pokemon.waveData.abilityRevealed = true;
|
||||
|
||||
this.end();
|
||||
});
|
||||
|
@ -217,16 +217,8 @@ export class StatStageChangePhase extends PokemonPhase {
|
||||
|
||||
for (const s of filteredStats) {
|
||||
if (stages.value > 0 && pokemon.getStatStage(s) < 6) {
|
||||
if (!pokemon.turnData) {
|
||||
// Temporary fix for missing turn data struct on turn 1
|
||||
pokemon.resetTurnData();
|
||||
}
|
||||
pokemon.turnData.statStagesIncreased = true;
|
||||
} else if (stages.value < 0 && pokemon.getStatStage(s) > -6) {
|
||||
if (!pokemon.turnData) {
|
||||
// Temporary fix for missing turn data struct on turn 1
|
||||
pokemon.resetTurnData();
|
||||
}
|
||||
pokemon.turnData.statStagesDecreased = true;
|
||||
}
|
||||
|
||||
|
@ -177,11 +177,7 @@ export class SummonPhase extends PartyMemberPokemonPhase {
|
||||
}
|
||||
globalScene.currentBattle.seenEnemyPartyMemberIds.add(pokemon.id);
|
||||
}
|
||||
addPokeballOpenParticles(
|
||||
pokemon.x,
|
||||
pokemon.y - 16,
|
||||
pokemon.getPokeball(true),
|
||||
);
|
||||
addPokeballOpenParticles(pokemon.x, pokemon.y - 16, pokemon.getPokeball(true));
|
||||
globalScene.updateModifiers(this.player);
|
||||
globalScene.updateFieldScale();
|
||||
pokemon.showInfo();
|
||||
@ -200,9 +196,8 @@ export class SummonPhase extends PartyMemberPokemonPhase {
|
||||
onComplete: () => {
|
||||
pokemon.cry(pokemon.getHpRatio() > 0.25 ? undefined : { rate: 0.85 });
|
||||
pokemon.getSprite().clearTint();
|
||||
pokemon.resetSummonData();
|
||||
// necessary to stay transformed during wild waves
|
||||
if (pokemon.summonData?.speciesForm) {
|
||||
if (pokemon.summonData.speciesForm) {
|
||||
pokemon.loadAssets(false);
|
||||
}
|
||||
globalScene.time.delayedCall(1000, () => this.end());
|
||||
@ -266,7 +261,6 @@ export class SummonPhase extends PartyMemberPokemonPhase {
|
||||
onComplete: () => {
|
||||
pokemon.cry(pokemon.getHpRatio() > 0.25 ? undefined : { rate: 0.85 });
|
||||
pokemon.getSprite().clearTint();
|
||||
pokemon.resetSummonData();
|
||||
globalScene.updateFieldScale();
|
||||
globalScene.time.delayedCall(1000, () => this.end());
|
||||
},
|
||||
|
@ -33,10 +33,10 @@ export class SwitchSummonPhase extends SummonPhase {
|
||||
* @param fieldIndex - Position on the battle field
|
||||
* @param slotIndex - The index of pokemon (in party of 6) to switch into
|
||||
* @param doReturn - Whether to render "comeback" dialogue
|
||||
* @param player - (Optional) `true` if the switch is from the player
|
||||
* @param player - Whether the switch came from the player or enemy; default `true`
|
||||
*/
|
||||
constructor(switchType: SwitchType, fieldIndex: number, slotIndex: number, doReturn: boolean, player?: boolean) {
|
||||
super(fieldIndex, player !== undefined ? player : true);
|
||||
constructor(switchType: SwitchType, fieldIndex: number, slotIndex: number, doReturn: boolean, player = true) {
|
||||
super(fieldIndex, player);
|
||||
|
||||
this.switchType = switchType;
|
||||
this.slotIndex = slotIndex;
|
||||
@ -67,7 +67,8 @@ export class SwitchSummonPhase extends SummonPhase {
|
||||
!(this.player ? globalScene.getPlayerParty() : globalScene.getEnemyParty())[this.slotIndex])
|
||||
) {
|
||||
if (this.player) {
|
||||
return this.switchAndSummon();
|
||||
this.switchAndSummon();
|
||||
return;
|
||||
}
|
||||
globalScene.time.delayedCall(750, () => this.switchAndSummon());
|
||||
return;
|
||||
@ -120,14 +121,23 @@ export class SwitchSummonPhase extends SummonPhase {
|
||||
|
||||
switchAndSummon() {
|
||||
const party = this.player ? this.getParty() : globalScene.getEnemyParty();
|
||||
const switchedInPokemon = party[this.slotIndex];
|
||||
const switchedInPokemon: Pokemon | undefined = party[this.slotIndex];
|
||||
this.lastPokemon = this.getPokemon();
|
||||
|
||||
applyPreSummonAbAttrs(PreSummonAbAttr, switchedInPokemon);
|
||||
applyPreSwitchOutAbAttrs(PreSwitchOutAbAttr, this.lastPokemon);
|
||||
if (this.switchType === SwitchType.BATON_PASS && switchedInPokemon) {
|
||||
(this.player ? globalScene.getEnemyField() : globalScene.getPlayerField()).forEach(enemyPokemon =>
|
||||
if (!switchedInPokemon) {
|
||||
this.end();
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.switchType === SwitchType.BATON_PASS) {
|
||||
// If switching via baton pass, update opposing tags coming from the prior pokemon
|
||||
(this.player ? globalScene.getEnemyField() : globalScene.getPlayerField()).forEach((enemyPokemon: Pokemon) =>
|
||||
enemyPokemon.transferTagsBySourceId(this.lastPokemon.id, switchedInPokemon.id),
|
||||
);
|
||||
|
||||
// If the recipient pokemon lacks a baton, give our baton to it during the swap
|
||||
if (
|
||||
!globalScene.findModifier(
|
||||
m =>
|
||||
@ -140,14 +150,8 @@ export class SwitchSummonPhase extends SummonPhase {
|
||||
m instanceof SwitchEffectTransferModifier &&
|
||||
(m as SwitchEffectTransferModifier).pokemonId === this.lastPokemon.id,
|
||||
) as SwitchEffectTransferModifier;
|
||||
if (
|
||||
batonPassModifier &&
|
||||
!globalScene.findModifier(
|
||||
m =>
|
||||
m instanceof SwitchEffectTransferModifier &&
|
||||
(m as SwitchEffectTransferModifier).pokemonId === switchedInPokemon.id,
|
||||
)
|
||||
) {
|
||||
|
||||
if (batonPassModifier) {
|
||||
globalScene.tryTransferHeldItemModifier(
|
||||
batonPassModifier,
|
||||
switchedInPokemon,
|
||||
@ -160,49 +164,48 @@ export class SwitchSummonPhase extends SummonPhase {
|
||||
}
|
||||
}
|
||||
}
|
||||
if (switchedInPokemon) {
|
||||
party[this.slotIndex] = this.lastPokemon;
|
||||
party[this.fieldIndex] = switchedInPokemon;
|
||||
const showTextAndSummon = () => {
|
||||
globalScene.ui.showText(
|
||||
this.player
|
||||
? i18next.t("battle:playerGo", {
|
||||
pokemonName: getPokemonNameWithAffix(switchedInPokemon),
|
||||
})
|
||||
: i18next.t("battle:trainerGo", {
|
||||
trainerName: globalScene.currentBattle.trainer?.getName(
|
||||
!(this.fieldIndex % 2) ? TrainerSlot.TRAINER : TrainerSlot.TRAINER_PARTNER,
|
||||
),
|
||||
pokemonName: this.getPokemon().getNameToRender(),
|
||||
}),
|
||||
);
|
||||
/**
|
||||
* If this switch is passing a Substitute, make the switched Pokemon match the returned Pokemon's state as it left.
|
||||
* Otherwise, clear any persisting tags on the returned Pokemon.
|
||||
*/
|
||||
if (this.switchType === SwitchType.BATON_PASS || this.switchType === SwitchType.SHED_TAIL) {
|
||||
const substitute = this.lastPokemon.getTag(SubstituteTag);
|
||||
if (substitute) {
|
||||
switchedInPokemon.x += this.lastPokemon.getSubstituteOffset()[0];
|
||||
switchedInPokemon.y += this.lastPokemon.getSubstituteOffset()[1];
|
||||
switchedInPokemon.setAlpha(0.5);
|
||||
}
|
||||
} else {
|
||||
switchedInPokemon.resetSummonData();
|
||||
|
||||
party[this.slotIndex] = this.lastPokemon;
|
||||
party[this.fieldIndex] = switchedInPokemon;
|
||||
const showTextAndSummon = () => {
|
||||
globalScene.ui.showText(
|
||||
this.player
|
||||
? i18next.t("battle:playerGo", {
|
||||
pokemonName: getPokemonNameWithAffix(switchedInPokemon),
|
||||
})
|
||||
: i18next.t("battle:trainerGo", {
|
||||
trainerName: globalScene.currentBattle.trainer?.getName(
|
||||
!(this.fieldIndex % 2) ? TrainerSlot.TRAINER : TrainerSlot.TRAINER_PARTNER,
|
||||
),
|
||||
pokemonName: this.getPokemon().getNameToRender(),
|
||||
}),
|
||||
);
|
||||
|
||||
/**
|
||||
* If this switch is passing a Substitute, make the switched Pokemon matches the returned Pokemon's state as it left.
|
||||
* Otherwise, clear any persisting tags on the returned Pokemon.
|
||||
*/
|
||||
if (this.switchType === SwitchType.BATON_PASS || this.switchType === SwitchType.SHED_TAIL) {
|
||||
const substitute = this.lastPokemon.getTag(SubstituteTag);
|
||||
if (substitute) {
|
||||
switchedInPokemon.x += this.lastPokemon.getSubstituteOffset()[0];
|
||||
switchedInPokemon.y += this.lastPokemon.getSubstituteOffset()[1];
|
||||
switchedInPokemon.setAlpha(0.5);
|
||||
}
|
||||
this.summon();
|
||||
};
|
||||
if (this.player) {
|
||||
showTextAndSummon();
|
||||
} else {
|
||||
globalScene.time.delayedCall(1500, () => {
|
||||
this.hideEnemyTrainer();
|
||||
globalScene.pbTrayEnemy.hide();
|
||||
showTextAndSummon();
|
||||
});
|
||||
switchedInPokemon.resetSummonData();
|
||||
}
|
||||
this.summon();
|
||||
};
|
||||
|
||||
if (this.player) {
|
||||
showTextAndSummon();
|
||||
} else {
|
||||
this.end();
|
||||
globalScene.time.delayedCall(1500, () => {
|
||||
this.hideEnemyTrainer();
|
||||
globalScene.pbTrayEnemy.hide();
|
||||
showTextAndSummon();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@ -220,15 +223,15 @@ export class SwitchSummonPhase extends SummonPhase {
|
||||
const lastPokemonHasForceSwitchAbAttr =
|
||||
this.lastPokemon.hasAbilityWithAttr(PostDamageForceSwitchAbAttr) && !this.lastPokemon.isFainted();
|
||||
|
||||
// Compensate for turn spent summoning
|
||||
// Or compensate for force switch move if switched out pokemon is not fainted
|
||||
// Compensate for turn spent summoning/forced switch if switched out pokemon is not fainted.
|
||||
// Needed as we increment turn counters in `TurnEndPhase`.
|
||||
if (
|
||||
currentCommand === Command.POKEMON ||
|
||||
lastPokemonIsForceSwitchedAndNotFainted ||
|
||||
lastPokemonHasForceSwitchAbAttr
|
||||
) {
|
||||
pokemon.battleSummonData.turnCount--;
|
||||
pokemon.battleSummonData.waveTurnCount--;
|
||||
pokemon.tempSummonData.turnCount--;
|
||||
pokemon.tempSummonData.waveTurnCount--;
|
||||
}
|
||||
|
||||
if (this.switchType === SwitchType.BATON_PASS && pokemon) {
|
||||
@ -240,12 +243,13 @@ export class SwitchSummonPhase extends SummonPhase {
|
||||
}
|
||||
}
|
||||
|
||||
// Reset turn data if not initial switch (since it gets initialized to an empty object on turn start)
|
||||
if (this.switchType !== SwitchType.INITIAL_SWITCH) {
|
||||
pokemon.resetTurnData();
|
||||
pokemon.turnData.switchedInThisTurn = true;
|
||||
}
|
||||
|
||||
this.lastPokemon?.resetSummonData();
|
||||
this.lastPokemon.resetSummonData();
|
||||
|
||||
globalScene.triggerPokemonFormChange(pokemon, SpeciesFormChangeActiveTrigger, true);
|
||||
// Reverts to weather-based forms when weather suppressors (Cloud Nine/Air Lock) are switched out
|
||||
|
@ -54,11 +54,10 @@ export class TurnEndPhase extends FieldPhase {
|
||||
}
|
||||
|
||||
globalScene.applyModifiers(TurnStatusEffectModifier, pokemon.isPlayer(), pokemon);
|
||||
|
||||
globalScene.applyModifiers(TurnHeldItemTransferModifier, pokemon.isPlayer(), pokemon);
|
||||
|
||||
pokemon.battleSummonData.turnCount++;
|
||||
pokemon.battleSummonData.waveTurnCount++;
|
||||
pokemon.tempSummonData.turnCount++;
|
||||
pokemon.tempSummonData.waveTurnCount++;
|
||||
};
|
||||
|
||||
this.executeForAll(handlePokemon);
|
||||
|
@ -72,19 +72,16 @@ export class TurnStartPhase extends FieldPhase {
|
||||
// This occurs before the main loop because of battles with more than two Pokemon
|
||||
const battlerBypassSpeed = {};
|
||||
|
||||
globalScene
|
||||
.getField(true)
|
||||
.filter(p => p.summonData)
|
||||
.map(p => {
|
||||
const bypassSpeed = new BooleanHolder(false);
|
||||
const canCheckHeldItems = new BooleanHolder(true);
|
||||
applyAbAttrs(BypassSpeedChanceAbAttr, p, null, false, bypassSpeed);
|
||||
applyAbAttrs(PreventBypassSpeedChanceAbAttr, p, null, false, bypassSpeed, canCheckHeldItems);
|
||||
if (canCheckHeldItems.value) {
|
||||
globalScene.applyModifiers(BypassSpeedChanceModifier, p.isPlayer(), p, bypassSpeed);
|
||||
}
|
||||
battlerBypassSpeed[p.getBattlerIndex()] = bypassSpeed;
|
||||
});
|
||||
globalScene.getField(true).map(p => {
|
||||
const bypassSpeed = new BooleanHolder(false);
|
||||
const canCheckHeldItems = new BooleanHolder(true);
|
||||
applyAbAttrs(BypassSpeedChanceAbAttr, p, null, false, bypassSpeed);
|
||||
applyAbAttrs(PreventBypassSpeedChanceAbAttr, p, null, false, bypassSpeed, canCheckHeldItems);
|
||||
if (canCheckHeldItems.value) {
|
||||
globalScene.applyModifiers(BypassSpeedChanceModifier, p.isPlayer(), p, bypassSpeed);
|
||||
}
|
||||
battlerBypassSpeed[p.getBattlerIndex()] = bypassSpeed;
|
||||
});
|
||||
|
||||
// The function begins sorting orderedTargets based on command priority, move priority, and possible speed bypasses.
|
||||
// Non-FIGHT commands (SWITCH, BALL, RUN) have a higher command priority and will always occur before any FIGHT commands.
|
||||
|
@ -1145,7 +1145,7 @@ export class GameData {
|
||||
? trainerConfig?.doubleOnly || sessionData.trainer?.variant === TrainerVariant.DOUBLE
|
||||
: sessionData.enemyParty.length > 1,
|
||||
mysteryEncounterType,
|
||||
)!; // TODO: is this bang correct?
|
||||
);
|
||||
battle.enemyLevels = sessionData.enemyParty.map(p => p.level);
|
||||
|
||||
globalScene.arena.init();
|
||||
@ -1198,13 +1198,16 @@ export class GameData {
|
||||
}
|
||||
}
|
||||
|
||||
if (globalScene.modifiers.length) {
|
||||
console.warn("Existing modifiers not cleared on session load, deleting...");
|
||||
globalScene.modifiers = [];
|
||||
}
|
||||
for (const modifierData of sessionData.modifiers) {
|
||||
const modifier = modifierData.toModifier(Modifier[modifierData.className]);
|
||||
if (modifier) {
|
||||
globalScene.addModifier(modifier, true);
|
||||
}
|
||||
}
|
||||
|
||||
globalScene.updateModifiers(true);
|
||||
|
||||
for (const enemyModifierData of sessionData.enemyModifiers) {
|
||||
@ -1342,68 +1345,67 @@ export class GameData {
|
||||
}
|
||||
|
||||
parseSessionData(dataStr: string): SessionSaveData {
|
||||
// TODO: Add `null`/`undefined` to the corresponding type signatures for this
|
||||
// (or prevent them from being null)
|
||||
// If the value is able to *not exist*, it should say so in the code
|
||||
const sessionData = JSON.parse(dataStr, (k: string, v: any) => {
|
||||
if (k === "party" || k === "enemyParty") {
|
||||
const ret: PokemonData[] = [];
|
||||
if (v === null) {
|
||||
v = [];
|
||||
}
|
||||
for (const pd of v) {
|
||||
ret.push(new PokemonData(pd));
|
||||
}
|
||||
return ret;
|
||||
}
|
||||
|
||||
if (k === "trainer") {
|
||||
return v ? new TrainerData(v) : null;
|
||||
}
|
||||
|
||||
if (k === "modifiers" || k === "enemyModifiers") {
|
||||
const player = k === "modifiers";
|
||||
const ret: PersistentModifierData[] = [];
|
||||
if (v === null) {
|
||||
v = [];
|
||||
}
|
||||
for (const md of v) {
|
||||
if (md?.className === "ExpBalanceModifier") {
|
||||
// Temporarily limit EXP Balance until it gets reworked
|
||||
md.stackCount = Math.min(md.stackCount, 4);
|
||||
// TODO: Add pre-parse migrate scripts
|
||||
switch (k) {
|
||||
case "party":
|
||||
case "enemyParty": {
|
||||
const ret: PokemonData[] = [];
|
||||
for (const pd of v ?? []) {
|
||||
ret.push(new PokemonData(pd));
|
||||
}
|
||||
if (
|
||||
(md instanceof Modifier.EnemyAttackStatusEffectChanceModifier && md.effect === StatusEffect.FREEZE) ||
|
||||
md.effect === StatusEffect.SLEEP
|
||||
) {
|
||||
continue;
|
||||
return ret;
|
||||
}
|
||||
|
||||
case "trainer":
|
||||
return v ? new TrainerData(v) : null;
|
||||
|
||||
case "modifiers":
|
||||
case "enemyModifiers": {
|
||||
const ret: PersistentModifierData[] = [];
|
||||
for (const md of v ?? []) {
|
||||
if (md?.className === "ExpBalanceModifier") {
|
||||
// Temporarily limit EXP Balance until it gets reworked
|
||||
md.stackCount = Math.min(md.stackCount, 4);
|
||||
}
|
||||
|
||||
if (
|
||||
md instanceof Modifier.EnemyAttackStatusEffectChanceModifier &&
|
||||
(md.effect === StatusEffect.FREEZE || md.effect === StatusEffect.SLEEP)
|
||||
) {
|
||||
// Discard any old "sleep/freeze chance tokens".
|
||||
// TODO: make this migrate script
|
||||
continue;
|
||||
}
|
||||
|
||||
ret.push(new PersistentModifierData(md, k === "modifiers"));
|
||||
}
|
||||
ret.push(new PersistentModifierData(md, player));
|
||||
return ret;
|
||||
}
|
||||
return ret;
|
||||
}
|
||||
|
||||
if (k === "arena") {
|
||||
return new ArenaData(v);
|
||||
}
|
||||
case "arena":
|
||||
return new ArenaData(v);
|
||||
|
||||
if (k === "challenges") {
|
||||
const ret: ChallengeData[] = [];
|
||||
if (v === null) {
|
||||
v = [];
|
||||
case "challenges": {
|
||||
const ret: ChallengeData[] = [];
|
||||
for (const c of v ?? []) {
|
||||
ret.push(new ChallengeData(c));
|
||||
}
|
||||
return ret;
|
||||
}
|
||||
for (const c of v) {
|
||||
ret.push(new ChallengeData(c));
|
||||
}
|
||||
return ret;
|
||||
}
|
||||
|
||||
if (k === "mysteryEncounterType") {
|
||||
return v as MysteryEncounterType;
|
||||
}
|
||||
case "mysteryEncounterType":
|
||||
return v as MysteryEncounterType;
|
||||
|
||||
if (k === "mysteryEncounterSaveData") {
|
||||
return new MysteryEncounterSaveData(v);
|
||||
}
|
||||
case "mysteryEncounterSaveData":
|
||||
return new MysteryEncounterSaveData(v);
|
||||
|
||||
return v;
|
||||
default:
|
||||
return v;
|
||||
}
|
||||
}) as SessionSaveData;
|
||||
|
||||
applySessionVersionMigration(sessionData);
|
||||
@ -1456,7 +1458,7 @@ export class GameData {
|
||||
encrypt(JSON.stringify(sessionData), bypassLogin),
|
||||
);
|
||||
|
||||
console.debug("Session data saved");
|
||||
console.debug("Session data saved!");
|
||||
|
||||
if (!bypassLogin && sync) {
|
||||
pokerogueApi.savedata.updateAll(request).then(error => {
|
||||
|
@ -1,16 +1,15 @@
|
||||
import { BattleType } from "#enums/battle-type";
|
||||
import { globalScene } from "#app/global-scene";
|
||||
import type { Gender } from "../data/gender";
|
||||
import type { Nature } from "#enums/nature";
|
||||
import type { PokeballType } from "#enums/pokeball";
|
||||
import { Nature } from "#enums/nature";
|
||||
import { PokeballType } from "#enums/pokeball";
|
||||
import { getPokemonSpecies, getPokemonSpeciesForm } from "../data/pokemon-species";
|
||||
import { Status } from "../data/status-effect";
|
||||
import Pokemon, { EnemyPokemon, PokemonMove, PokemonSummonData } from "../field/pokemon";
|
||||
import Pokemon, { EnemyPokemon, PokemonBattleData, PokemonMove, PokemonSummonData } from "../field/pokemon";
|
||||
import { TrainerSlot } from "#enums/trainer-slot";
|
||||
import type { Variant } from "#app/sprites/variant";
|
||||
import { loadBattlerTag } from "../data/battler-tags";
|
||||
import type { Biome } from "#enums/biome";
|
||||
import { Moves } from "#enums/moves";
|
||||
import type { Moves } from "#enums/moves";
|
||||
import type { Species } from "#enums/species";
|
||||
import { CustomPokemonData } from "#app/data/custom-pokemon-data";
|
||||
import type { PokemonType } from "#enums/pokemon-type";
|
||||
@ -60,79 +59,68 @@ export default class PokemonData {
|
||||
public fusionTeraType: PokemonType;
|
||||
|
||||
public boss: boolean;
|
||||
public bossSegments?: number;
|
||||
public bossSegments: number;
|
||||
|
||||
// Effects that need to be preserved between waves
|
||||
public summonData: PokemonSummonData;
|
||||
public battleData: PokemonBattleData;
|
||||
public summonDataSpeciesFormIndex: number;
|
||||
|
||||
/** Data that can customize a Pokemon in non-standard ways from its Species */
|
||||
public customPokemonData: CustomPokemonData;
|
||||
public fusionCustomPokemonData: CustomPokemonData;
|
||||
|
||||
// Deprecated attributes, needed for now to allow SessionData migration (see PR#4619 comments)
|
||||
// TODO: Remove these once pre-session migration is implemented
|
||||
public natureOverride: Nature | -1;
|
||||
public mysteryEncounterPokemonData: CustomPokemonData | null;
|
||||
public fusionMysteryEncounterPokemonData: CustomPokemonData | null;
|
||||
|
||||
constructor(source: Pokemon | any, forHistory = false) {
|
||||
const sourcePokemon = source instanceof Pokemon ? source : null;
|
||||
/**
|
||||
* Construct a new {@linkcode PokemonData} instance out of a {@linkcode Pokemon}
|
||||
* or JSON representation thereof.
|
||||
* @param source The {@linkcode Pokemon} to convert into data (or a JSON object representing one)
|
||||
*/
|
||||
// TODO: Remove any from type signature in favor of 2 separate method funcs
|
||||
constructor(source: Pokemon | any) {
|
||||
const sourcePokemon = source instanceof Pokemon ? source : undefined;
|
||||
|
||||
this.id = source.id;
|
||||
this.player = sourcePokemon ? sourcePokemon.isPlayer() : source.player;
|
||||
this.species = sourcePokemon ? sourcePokemon.species.speciesId : source.species;
|
||||
this.nickname = sourcePokemon
|
||||
? (!!sourcePokemon.summonData?.illusion ? sourcePokemon.summonData.illusion.basePokemon.nickname : sourcePokemon.nickname)
|
||||
: source.nickname;
|
||||
this.player = sourcePokemon?.isPlayer() ?? source.player;
|
||||
this.species = sourcePokemon?.species.speciesId ?? source.species;
|
||||
this.nickname = sourcePokemon?.summonData.illusion?.basePokemon.nickname ?? source.nickname;
|
||||
this.formIndex = Math.max(Math.min(source.formIndex, getPokemonSpecies(this.species).forms.length - 1), 0);
|
||||
this.abilityIndex = source.abilityIndex;
|
||||
this.passive = source.passive;
|
||||
this.shiny = sourcePokemon ? sourcePokemon.isShiny() : source.shiny;
|
||||
this.variant = sourcePokemon ? sourcePokemon.getVariant() : source.variant;
|
||||
this.pokeball = source.pokeball;
|
||||
this.shiny = sourcePokemon?.isShiny() ?? source.shiny;
|
||||
this.variant = sourcePokemon?.getVariant() ?? source.variant;
|
||||
this.pokeball = source.pokeball ?? PokeballType.POKEBALL;
|
||||
this.level = source.level;
|
||||
this.exp = source.exp;
|
||||
if (!forHistory) {
|
||||
this.levelExp = source.levelExp;
|
||||
}
|
||||
this.levelExp = source.levelExp;
|
||||
this.gender = source.gender;
|
||||
if (!forHistory) {
|
||||
this.hp = source.hp;
|
||||
}
|
||||
this.hp = source.hp;
|
||||
this.stats = source.stats;
|
||||
this.ivs = source.ivs;
|
||||
this.nature = source.nature !== undefined ? source.nature : (0 as Nature);
|
||||
this.friendship =
|
||||
source.friendship !== undefined ? source.friendship : getPokemonSpecies(this.species).baseFriendship;
|
||||
|
||||
// TODO: Can't we move some of this verification stuff to an upgrade script?
|
||||
this.nature = source.nature ?? Nature.HARDY;
|
||||
this.moveset = source.moveset.map((m: any) => PokemonMove.loadMove(m));
|
||||
this.status = source.status
|
||||
? new Status(source.status.effect, source.status.toxicTurnCount, source.status.sleepTurnsRemaining)
|
||||
: null;
|
||||
this.friendship = source.friendship ?? getPokemonSpecies(this.species).baseFriendship;
|
||||
this.metLevel = source.metLevel || 5;
|
||||
this.metBiome = source.metBiome !== undefined ? source.metBiome : -1;
|
||||
this.metBiome = source.metBiome ?? -1;
|
||||
this.metSpecies = source.metSpecies;
|
||||
this.metWave = source.metWave ?? (this.metBiome === -1 ? -1 : 0);
|
||||
this.luck = source.luck !== undefined ? source.luck : source.shiny ? source.variant + 1 : 0;
|
||||
if (!forHistory) {
|
||||
this.pauseEvolutions = !!source.pauseEvolutions;
|
||||
this.evoCounter = source.evoCounter ?? 0;
|
||||
}
|
||||
this.luck = source.luck ?? (source.shiny ? source.variant + 1 : 0);
|
||||
this.pauseEvolutions = !!source.pauseEvolutions;
|
||||
this.pokerus = !!source.pokerus;
|
||||
this.teraType = source.teraType as PokemonType;
|
||||
this.isTerastallized = source.isTerastallized || false;
|
||||
this.stellarTypesBoosted = source.stellarTypesBoosted || [];
|
||||
|
||||
this.fusionSpecies = sourcePokemon ? sourcePokemon.fusionSpecies?.speciesId : source.fusionSpecies;
|
||||
this.fusionFormIndex = source.fusionFormIndex;
|
||||
this.fusionAbilityIndex = source.fusionAbilityIndex;
|
||||
this.fusionShiny = sourcePokemon
|
||||
? (!!sourcePokemon.summonData?.illusion ? sourcePokemon.summonData.illusion.basePokemon.fusionShiny : sourcePokemon.fusionShiny)
|
||||
: source.fusionShiny;
|
||||
this.fusionVariant = sourcePokemon
|
||||
? (!!sourcePokemon.summonData?.illusion ? sourcePokemon.summonData.illusion.basePokemon.fusionVariant : sourcePokemon.fusionVariant)
|
||||
: source.fusionVariant;
|
||||
this.fusionGender = source.fusionGender;
|
||||
this.fusionLuck =
|
||||
source.fusionLuck !== undefined ? source.fusionLuck : source.fusionShiny ? source.fusionVariant + 1 : 0;
|
||||
this.fusionCustomPokemonData = new CustomPokemonData(source.fusionCustomPokemonData);
|
||||
this.fusionTeraType = (source.fusionTeraType ?? 0) as PokemonType;
|
||||
this.usedTMs = source.usedTMs ?? [];
|
||||
|
||||
this.customPokemonData = new CustomPokemonData(source.customPokemonData);
|
||||
this.evoCounter = source.evoCounter ?? 0;
|
||||
this.teraType = source.teraType as PokemonType;
|
||||
this.isTerastallized = !!source.isTerastallized;
|
||||
this.stellarTypesBoosted = source.stellarTypesBoosted ?? [];
|
||||
|
||||
// Deprecated, but needed for session data migration
|
||||
this.natureOverride = source.natureOverride;
|
||||
@ -143,52 +131,25 @@ export default class PokemonData {
|
||||
? new CustomPokemonData(source.fusionMysteryEncounterPokemonData)
|
||||
: null;
|
||||
|
||||
if (!forHistory) {
|
||||
this.boss = (source instanceof EnemyPokemon && !!source.bossSegments) || (!this.player && !!source.boss);
|
||||
this.bossSegments = source.bossSegments;
|
||||
}
|
||||
this.fusionSpecies = sourcePokemon?.fusionSpecies?.speciesId ?? source.fusionSpecies;
|
||||
this.fusionFormIndex = source.fusionFormIndex;
|
||||
this.fusionAbilityIndex = source.fusionAbilityIndex;
|
||||
this.fusionShiny = sourcePokemon?.summonData.illusion?.basePokemon.fusionShiny ?? source.fusionShiny;
|
||||
this.fusionVariant = sourcePokemon?.summonData.illusion?.basePokemon.fusionVariant ?? source.fusionVariant;
|
||||
this.fusionGender = source.fusionGender;
|
||||
this.fusionLuck = source.fusionLuck ?? (source.fusionShiny ? source.fusionVariant + 1 : 0);
|
||||
this.fusionTeraType = (source.fusionTeraType ?? 0) as PokemonType;
|
||||
|
||||
if (sourcePokemon) {
|
||||
this.moveset = sourcePokemon.moveset;
|
||||
if (!forHistory) {
|
||||
this.status = sourcePokemon.status;
|
||||
if (this.player && sourcePokemon.summonData) {
|
||||
this.summonData = sourcePokemon.summonData;
|
||||
this.summonDataSpeciesFormIndex = this.getSummonDataSpeciesFormIndex();
|
||||
}
|
||||
}
|
||||
} else {
|
||||
this.moveset = (source.moveset || [new PokemonMove(Moves.TACKLE), new PokemonMove(Moves.GROWL)])
|
||||
.filter(m => m)
|
||||
.map((m: any) => new PokemonMove(m.moveId, m.ppUsed, m.ppUp, m.virtual, m.maxPpOverride));
|
||||
if (!forHistory) {
|
||||
this.status = source.status
|
||||
? new Status(source.status.effect, source.status.toxicTurnCount, source.status.sleepTurnsRemaining)
|
||||
: null;
|
||||
}
|
||||
this.boss = (source instanceof EnemyPokemon && !!source.bossSegments) || (!this.player && !!source.boss);
|
||||
this.bossSegments = source.bossSegments ?? 0;
|
||||
|
||||
this.summonData = new PokemonSummonData();
|
||||
if (!forHistory && source.summonData) {
|
||||
this.summonData.stats = source.summonData.stats;
|
||||
this.summonData.statStages = source.summonData.statStages;
|
||||
this.summonData.moveQueue = source.summonData.moveQueue;
|
||||
this.summonData.abilitySuppressed = source.summonData.abilitySuppressed;
|
||||
this.summonData.abilitiesApplied = source.summonData.abilitiesApplied;
|
||||
this.summonData = new PokemonSummonData(source.summonData);
|
||||
this.battleData = new PokemonBattleData(source.battleData);
|
||||
this.summonDataSpeciesFormIndex =
|
||||
sourcePokemon?.summonData.speciesForm?.formIndex ?? source.summonDataSpeciesFormIndex;
|
||||
|
||||
this.summonData.ability = source.summonData.ability;
|
||||
this.summonData.moveset = source.summonData.moveset?.map(m => PokemonMove.loadMove(m));
|
||||
this.summonData.types = source.summonData.types;
|
||||
this.summonData.speciesForm = source.summonData.speciesForm;
|
||||
this.summonDataSpeciesFormIndex = source.summonDataSpeciesFormIndex;
|
||||
this.summonData.illusionBroken = source.summonData.illusionBroken;
|
||||
|
||||
if (source.summonData.tags) {
|
||||
this.summonData.tags = source.summonData.tags?.map(t => loadBattlerTag(t));
|
||||
} else {
|
||||
this.summonData.tags = [];
|
||||
}
|
||||
}
|
||||
}
|
||||
this.customPokemonData = new CustomPokemonData(source.customPokemonData);
|
||||
this.fusionCustomPokemonData = new CustomPokemonData(source.fusionCustomPokemonData);
|
||||
}
|
||||
|
||||
toPokemon(battleType?: BattleType, partyMemberIndex = 0, double = false): Pokemon {
|
||||
@ -223,30 +184,15 @@ export default class PokemonData {
|
||||
false,
|
||||
this,
|
||||
);
|
||||
if (this.summonData) {
|
||||
// when loading from saved session, recover summonData.speciesFrom and form index species object
|
||||
// used to stay transformed on reload session
|
||||
|
||||
if (this.summonData.speciesForm) {
|
||||
this.summonData.speciesForm = getPokemonSpeciesForm(
|
||||
this.summonData.speciesForm.speciesId,
|
||||
this.summonDataSpeciesFormIndex,
|
||||
);
|
||||
}
|
||||
ret.primeSummonData(this.summonData);
|
||||
// when loading from saved session, recover summonData.speciesFrom and form index species object
|
||||
// used to stay transformed on reload session
|
||||
if (this.summonData.speciesForm) {
|
||||
this.summonData.speciesForm = getPokemonSpeciesForm(
|
||||
this.summonData.speciesForm.speciesId,
|
||||
this.summonDataSpeciesFormIndex,
|
||||
);
|
||||
}
|
||||
return ret;
|
||||
}
|
||||
|
||||
/**
|
||||
* Method to save summon data species form index
|
||||
* Necessary in case the pokemon is transformed
|
||||
* to reload the correct form
|
||||
*/
|
||||
getSummonDataSpeciesFormIndex(): number {
|
||||
if (this.summonData.speciesForm) {
|
||||
return this.summonData.speciesForm.formIndex;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
@ -59,6 +59,10 @@ import * as v1_7_0 from "./versions/v1_7_0";
|
||||
// biome-ignore lint/style/noNamespaceImport: Convenience
|
||||
import * as v1_8_3 from "./versions/v1_8_3";
|
||||
|
||||
// --- v1.9.0 PATCHES --- //
|
||||
// biome-ignore lint/style/noNamespaceImport: Convenience
|
||||
import * as v1_9_0 from "./versions/v1_9_0";
|
||||
|
||||
/** Current game version */
|
||||
const LATEST_VERSION = version;
|
||||
|
||||
@ -80,6 +84,7 @@ systemMigrators.push(...v1_8_3.systemMigrators);
|
||||
const sessionMigrators: SessionSaveMigrator[] = [];
|
||||
sessionMigrators.push(...v1_0_4.sessionMigrators);
|
||||
sessionMigrators.push(...v1_7_0.sessionMigrators);
|
||||
sessionMigrators.push(...v1_9_0.sessionMigrators);
|
||||
|
||||
/** All settings migrators */
|
||||
const settingsMigrators: SettingsSaveMigrator[] = [];
|
||||
|
47
src/system/version_migration/versions/v1_9_0.ts
Normal file
@ -0,0 +1,47 @@
|
||||
import type { SessionSaveMigrator } from "#app/@types/SessionSaveMigrator";
|
||||
import { Status } from "#app/data/status-effect";
|
||||
import { PokemonMove } from "#app/field/pokemon";
|
||||
import type { SessionSaveData } from "#app/system/game-data";
|
||||
import type PokemonData from "#app/system/pokemon-data";
|
||||
import { Moves } from "#enums/moves";
|
||||
|
||||
/**
|
||||
* Migrate all lingering rage fist data inside `CustomPokemonData`,
|
||||
* as well as enforcing default values across the board.
|
||||
* @param data - {@linkcode SystemSaveData}
|
||||
*/
|
||||
const migratePartyData: SessionSaveMigrator = {
|
||||
version: "1.9.0",
|
||||
migrate: (data: SessionSaveData): void => {
|
||||
// this stuff is copied straight from the constructor fwiw
|
||||
const mapParty = (pkmnData: PokemonData) => {
|
||||
pkmnData.status &&= new Status(
|
||||
pkmnData.status.effect,
|
||||
pkmnData.status.toxicTurnCount,
|
||||
pkmnData.status.sleepTurnsRemaining,
|
||||
);
|
||||
// remove empty moves from moveset
|
||||
pkmnData.moveset = (pkmnData.moveset ?? [new PokemonMove(Moves.TACKLE), new PokemonMove(Moves.GROWL)])
|
||||
.filter(m => !!m)
|
||||
.map(m => PokemonMove.loadMove(m));
|
||||
// only edit summondata moveset if exists
|
||||
pkmnData.summonData.moveset &&= pkmnData.summonData.moveset.filter(m => !!m).map(m => PokemonMove.loadMove(m));
|
||||
|
||||
if (
|
||||
pkmnData.customPokemonData &&
|
||||
"hitsRecCount" in pkmnData.customPokemonData &&
|
||||
typeof pkmnData.customPokemonData["hitsRecCount"] === "number"
|
||||
) {
|
||||
// transfer old hit count stat to battleData.
|
||||
pkmnData.battleData.hitCount = pkmnData.customPokemonData["hitsRecCount"];
|
||||
pkmnData.customPokemonData["hitsRecCount"] = null;
|
||||
}
|
||||
return pkmnData;
|
||||
};
|
||||
|
||||
data.party = data.party.map(mapParty);
|
||||
data.enemyParty = data.enemyParty.map(mapParty);
|
||||
},
|
||||
};
|
||||
|
||||
export const sessionMigrators: Readonly<SessionSaveMigrator[]> = [migratePartyData] as const;
|
@ -340,7 +340,6 @@ const timedEvents: TimedEvent[] = [
|
||||
{ species: Species.DEERLING, formIndex: 0 }, // Spring Deerling
|
||||
{ species: Species.CLAUNCHER },
|
||||
{ species: Species.WISHIWASHI },
|
||||
{ species: Species.MUDBRAY },
|
||||
{ species: Species.DRAMPA },
|
||||
{ species: Species.JANGMO_O },
|
||||
{ species: Species.APPLIN },
|
||||
@ -351,7 +350,7 @@ const timedEvents: TimedEvent[] = [
|
||||
{ wave: 8, type: "CATCHING_CHARM" },
|
||||
{ wave: 25, type: "SHINY_CHARM" },
|
||||
],
|
||||
}
|
||||
},
|
||||
];
|
||||
|
||||
export class TimedEventManager {
|
||||
|
@ -617,7 +617,7 @@ export default class BattleInfo extends Phaser.GameObjects.Container {
|
||||
return resolve();
|
||||
}
|
||||
|
||||
const gender: Gender = pokemon.summonData?.illusion ? pokemon.summonData?.illusion.gender : pokemon.gender;
|
||||
const gender = pokemon.summonData.illusion?.gender ?? pokemon.gender;
|
||||
|
||||
this.genderText.setText(getGenderSymbol(gender));
|
||||
this.genderText.setColor(getGenderColor(gender));
|
||||
@ -794,7 +794,7 @@ export default class BattleInfo extends Phaser.GameObjects.Container {
|
||||
const nameSizeTest = addTextObject(0, 0, displayName, TextStyle.BATTLE_INFO);
|
||||
nameTextWidth = nameSizeTest.displayWidth;
|
||||
|
||||
const gender: Gender = pokemon.summonData?.illusion ? pokemon.summonData?.illusion.gender : pokemon.gender;
|
||||
const gender = pokemon.summonData.illusion?.gender ?? pokemon.gender;
|
||||
while (
|
||||
nameTextWidth >
|
||||
(this.player || !this.boss ? 60 : 98) -
|
||||
|
@ -127,7 +127,7 @@ export default class FightUiHandler extends UiHandler implements InfoToggle {
|
||||
messageHandler.commandWindow.setVisible(false);
|
||||
messageHandler.movesWindowContainer.setVisible(true);
|
||||
const pokemon = (globalScene.getCurrentPhase() as CommandPhase).getPokemon();
|
||||
if (pokemon.battleSummonData.turnCount <= 1) {
|
||||
if (pokemon.tempSummonData.turnCount <= 1) {
|
||||
this.setCursor(0);
|
||||
} else {
|
||||
this.setCursor(this.getCursor());
|
||||
@ -305,7 +305,7 @@ export default class FightUiHandler extends UiHandler implements InfoToggle {
|
||||
const effectiveness = opponent.getMoveEffectiveness(
|
||||
pokemon,
|
||||
pokemonMove.getMove(),
|
||||
!opponent.battleData?.abilityRevealed,
|
||||
!opponent.waveData.abilityRevealed,
|
||||
undefined,
|
||||
undefined,
|
||||
true,
|
||||
@ -356,7 +356,7 @@ export default class FightUiHandler extends UiHandler implements InfoToggle {
|
||||
opponent.getMoveEffectiveness(
|
||||
pokemon,
|
||||
pokemonMove.getMove(),
|
||||
!opponent.battleData.abilityRevealed,
|
||||
!opponent.waveData.abilityRevealed,
|
||||
undefined,
|
||||
undefined,
|
||||
true,
|
||||
|
@ -1581,7 +1581,7 @@ class PartySlot extends Phaser.GameObjects.Container {
|
||||
fusionShinyStar.setOrigin(0, 0);
|
||||
fusionShinyStar.setPosition(shinyStar.x, shinyStar.y);
|
||||
fusionShinyStar.setTint(
|
||||
getVariantTint(this.pokemon.summonData?.illusion?.basePokemon.fusionVariant ?? this.pokemon.fusionVariant),
|
||||
getVariantTint(this.pokemon.summonData.illusion?.basePokemon.fusionVariant ?? this.pokemon.fusionVariant),
|
||||
);
|
||||
|
||||
slotInfoContainer.add(fusionShinyStar);
|
||||
|
@ -102,9 +102,9 @@ export default class RegistrationFormUiHandler extends FormModalUiHandler {
|
||||
// Prevent overlapping overrides on action modification
|
||||
this.submitAction = originalRegistrationAction;
|
||||
this.sanitizeInputs();
|
||||
globalScene.ui.setMode(UiMode.LOADING, { buttonActions: [] });
|
||||
globalScene.ui.setMode(UiMode.LOADING, { buttonActions: [] });
|
||||
const onFail = error => {
|
||||
globalScene.ui.setMode(UiMode.REGISTRATION_FORM, Object.assign(config, { errorMessage: error?.trim() }));
|
||||
globalScene.ui.setMode(UiMode.REGISTRATION_FORM, Object.assign(config, { errorMessage: error?.trim() }));
|
||||
globalScene.ui.playError();
|
||||
const errorMessageFontSize = languageSettings[i18next.resolvedLanguage!]?.errorMessageFontSize;
|
||||
if (errorMessageFontSize) {
|
||||
|
@ -359,15 +359,15 @@ export default class SummaryUiHandler extends UiHandler {
|
||||
this.pokemonSprite.setPipelineData("spriteKey", this.pokemon.getSpriteKey());
|
||||
this.pokemonSprite.setPipelineData(
|
||||
"shiny",
|
||||
this.pokemon.summonData?.illusion?.basePokemon.shiny ?? this.pokemon.shiny,
|
||||
this.pokemon.summonData.illusion?.basePokemon.shiny ?? this.pokemon.shiny,
|
||||
);
|
||||
this.pokemonSprite.setPipelineData(
|
||||
"variant",
|
||||
this.pokemon.summonData?.illusion?.basePokemon.variant ?? this.pokemon.variant,
|
||||
this.pokemon.summonData.illusion?.basePokemon.variant ?? this.pokemon.variant,
|
||||
);
|
||||
["spriteColors", "fusionSpriteColors"].map(k => {
|
||||
delete this.pokemonSprite.pipelineData[`${k}Base`];
|
||||
if (this.pokemon?.summonData?.speciesForm) {
|
||||
if (this.pokemon?.summonData.speciesForm) {
|
||||
k += "Base";
|
||||
}
|
||||
this.pokemonSprite.pipelineData[k] = this.pokemon?.getSprite().pipelineData[k];
|
||||
@ -462,7 +462,7 @@ export default class SummaryUiHandler extends UiHandler {
|
||||
this.fusionShinyIcon.setVisible(doubleShiny);
|
||||
if (isFusion) {
|
||||
this.fusionShinyIcon.setTint(
|
||||
getVariantTint(this.pokemon.summonData?.illusion?.basePokemon.fusionVariant ?? this.pokemon.fusionVariant),
|
||||
getVariantTint(this.pokemon.summonData.illusion?.basePokemon.fusionVariant ?? this.pokemon.fusionVariant),
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -71,7 +71,7 @@ export default class TargetSelectUiHandler extends UiHandler {
|
||||
*/
|
||||
resetCursor(cursorN: number, user: Pokemon): void {
|
||||
if (!isNullOrUndefined(cursorN)) {
|
||||
if ([BattlerIndex.PLAYER, BattlerIndex.PLAYER_2].includes(cursorN) || user.battleSummonData.waveTurnCount === 1) {
|
||||
if ([BattlerIndex.PLAYER, BattlerIndex.PLAYER_2].includes(cursorN) || user.tempSummonData.waveTurnCount === 1) {
|
||||
// Reset cursor on the first turn of a fight or if an ally was targeted last turn
|
||||
cursorN = -1;
|
||||
}
|
||||
|
@ -467,35 +467,22 @@ export function truncateString(str: string, maxLength = 10) {
|
||||
return str;
|
||||
}
|
||||
|
||||
/**
|
||||
* Perform a deep copy of an object.
|
||||
*
|
||||
* @param values - The object to be deep copied.
|
||||
* @returns A new object that is a deep copy of the input.
|
||||
*/
|
||||
export function deepCopy(values: object): object {
|
||||
// Convert the object to a JSON string and parse it back to an object to perform a deep copy
|
||||
return JSON.parse(JSON.stringify(values));
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert a space-separated string into a capitalized and underscored string.
|
||||
*
|
||||
* @param input - The string to be converted.
|
||||
* @returns The converted string with words capitalized and separated by underscores.
|
||||
*/
|
||||
export function reverseValueToKeySetting(input) {
|
||||
export function reverseValueToKeySetting(input: string) {
|
||||
// Split the input string into an array of words
|
||||
const words = input.split(" ");
|
||||
// Capitalize the first letter of each word and convert the rest to lowercase
|
||||
const capitalizedWords = words.map(word => word.charAt(0).toUpperCase() + word.slice(1).toLowerCase());
|
||||
const capitalizedWords = words.map((word: string) => word.charAt(0).toUpperCase() + word.slice(1).toLowerCase());
|
||||
// Join the capitalized words with underscores and return the result
|
||||
return capitalizedWords.join("_");
|
||||
}
|
||||
|
||||
/**
|
||||
* Capitalize a string.
|
||||
*
|
||||
* @param str - The string to be capitalized.
|
||||
* @param sep - The separator between the words of the string.
|
||||
* @param lowerFirstChar - Whether the first character of the string should be lowercase or not.
|
||||
@ -579,25 +566,3 @@ export function animationFileName(move: Moves): string {
|
||||
export function camelCaseToKebabCase(str: string): string {
|
||||
return str.replace(/[A-Z]+(?![a-z])|[A-Z]/g, (s, o) => (o ? "-" : "") + s.toLowerCase());
|
||||
}
|
||||
|
||||
/**
|
||||
* Merges the two objects, such that for each property in `b` that matches a property in `a`,
|
||||
* the value in `a` is replaced by the value in `b`. This is done recursively if the property is a non-array object
|
||||
*
|
||||
* If the property does not exist in `a` or its `typeof` evaluates differently, the property is skipped.
|
||||
* If the value of the property is an array, the array is replaced. If it is any other object, the object is merged recursively.
|
||||
*/
|
||||
// biome-ignore lint/complexity/noBannedTypes: This function is designed to merge json objects
|
||||
export function deepMergeObjects(a: Object, b: Object) {
|
||||
for (const key in b) {
|
||||
// !(key in a) is redundant here, yet makes it clear that we're explicitly interested in properties that exist in `a`
|
||||
if (!(key in a) || typeof a[key] !== typeof b[key]) {
|
||||
continue;
|
||||
}
|
||||
if (typeof b[key] === "object" && !Array.isArray(b[key])) {
|
||||
deepMergeObjects(a[key], b[key]);
|
||||
} else {
|
||||
a[key] = b[key];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
40
src/utils/data.ts
Normal file
@ -0,0 +1,40 @@
|
||||
/**
|
||||
* Perform a deep copy of an object.
|
||||
* @param values - The object to be deep copied.
|
||||
* @returns A new object that is a deep copy of the input.
|
||||
*/
|
||||
export function deepCopy(values: object): object {
|
||||
// Convert the object to a JSON string and parse it back to an object to perform a deep copy
|
||||
return JSON.parse(JSON.stringify(values));
|
||||
}
|
||||
|
||||
/**
|
||||
* Deeply merge two JSON objects' common properties together.
|
||||
* This copies all values from `source` that match properties inside `dest`,
|
||||
* checking recursively for non-null nested objects.
|
||||
|
||||
* If a property in `source` does not exist in `dest` or its `typeof` evaluates differently, it is skipped.
|
||||
* If it is a non-array object, its properties are recursed into and checked in turn.
|
||||
* All other values are copied verbatim.
|
||||
* @param dest - The object to merge values into
|
||||
* @param source - The object to source merged values from
|
||||
* @remarks Do not use for regular objects; this is specifically made for JSON copying.
|
||||
*/
|
||||
export function deepMergeSpriteData(dest: object, source: object) {
|
||||
for (const key of Object.keys(source)) {
|
||||
if (
|
||||
!(key in dest) ||
|
||||
typeof source[key] !== typeof dest[key] ||
|
||||
Array.isArray(source[key]) !== Array.isArray(dest[key])
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Pure objects get recursed into; everything else gets overwritten
|
||||
if (typeof source[key] !== "object" || source[key] === null || Array.isArray(source[key])) {
|
||||
dest[key] = source[key];
|
||||
} else {
|
||||
deepMergeSpriteData(dest[key], source[key]);
|
||||
}
|
||||
}
|
||||
}
|
322
test/abilities/cud_chew.test.ts
Normal file
@ -0,0 +1,322 @@
|
||||
import { RepeatBerryNextTurnAbAttr } from "#app/data/abilities/ability";
|
||||
import Pokemon from "#app/field/pokemon";
|
||||
import { globalScene } from "#app/global-scene";
|
||||
import { getPokemonNameWithAffix } from "#app/messages";
|
||||
import { Abilities } from "#enums/abilities";
|
||||
import { BerryType } from "#enums/berry-type";
|
||||
import { Moves } from "#enums/moves";
|
||||
import { Species } from "#enums/species";
|
||||
import { Stat } from "#enums/stat";
|
||||
import GameManager from "#test/testUtils/gameManager";
|
||||
import i18next from "i18next";
|
||||
import Phaser from "phaser";
|
||||
import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
|
||||
describe("Abilities - Cud Chew", () => {
|
||||
let phaserGame: Phaser.Game;
|
||||
let game: GameManager;
|
||||
|
||||
beforeAll(() => {
|
||||
phaserGame = new Phaser.Game({
|
||||
type: Phaser.HEADLESS,
|
||||
});
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
game.phaseInterceptor.restoreOg();
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
game = new GameManager(phaserGame);
|
||||
game.override
|
||||
.moveset([Moves.BUG_BITE, Moves.SPLASH, Moves.HYPER_VOICE, Moves.STUFF_CHEEKS])
|
||||
.startingHeldItems([{ name: "BERRY", type: BerryType.SITRUS, count: 1 }])
|
||||
.ability(Abilities.CUD_CHEW)
|
||||
.battleStyle("single")
|
||||
.disableCrits()
|
||||
.enemySpecies(Species.MAGIKARP)
|
||||
.enemyAbility(Abilities.BALL_FETCH)
|
||||
.enemyMoveset(Moves.SPLASH);
|
||||
});
|
||||
|
||||
describe("tracks berries eaten", () => {
|
||||
it("stores inside summonData at end of turn", async () => {
|
||||
await game.classicMode.startBattle([Species.FARIGIRAF]);
|
||||
|
||||
const farigiraf = game.scene.getPlayerPokemon()!;
|
||||
farigiraf.hp = 1; // needed to allow sitrus procs
|
||||
|
||||
game.move.select(Moves.SPLASH);
|
||||
await game.phaseInterceptor.to("BerryPhase");
|
||||
|
||||
// berries tracked in turnData; not moved to battleData yet
|
||||
expect(farigiraf.summonData.berriesEatenLast).toEqual([]);
|
||||
expect(farigiraf.turnData.berriesEaten).toEqual([BerryType.SITRUS]);
|
||||
|
||||
await game.phaseInterceptor.to("TurnEndPhase");
|
||||
|
||||
// berries stored in battleData; not yet cleared from turnData
|
||||
expect(farigiraf.summonData.berriesEatenLast).toEqual([BerryType.SITRUS]);
|
||||
expect(farigiraf.turnData.berriesEaten).toEqual([BerryType.SITRUS]);
|
||||
|
||||
await game.toNextTurn();
|
||||
|
||||
// turnData cleared on turn start
|
||||
expect(farigiraf.summonData.berriesEatenLast).toEqual([BerryType.SITRUS]);
|
||||
expect(farigiraf.turnData.berriesEaten).toEqual([]);
|
||||
});
|
||||
|
||||
it("shows ability popup for eating berry, even if berry is useless", async () => {
|
||||
const abDisplaySpy = vi.spyOn(globalScene, "queueAbilityDisplay");
|
||||
game.override.enemyMoveset([Moves.SPLASH, Moves.HEAL_PULSE]);
|
||||
await game.classicMode.startBattle([Species.FARIGIRAF]);
|
||||
|
||||
const farigiraf = game.scene.getPlayerPokemon()!;
|
||||
// Dip below half to eat berry
|
||||
farigiraf.hp = farigiraf.getMaxHp() / 2 - 1;
|
||||
|
||||
game.move.select(Moves.SPLASH);
|
||||
await game.forceEnemyMove(Moves.SPLASH);
|
||||
await game.phaseInterceptor.to("TurnEndPhase");
|
||||
|
||||
// doesn't trigger since cud chew hasn't eaten berry yet
|
||||
expect(farigiraf.summonData.berriesEatenLast).toContain(BerryType.SITRUS);
|
||||
expect(abDisplaySpy).not.toHaveBeenCalledWith(farigiraf);
|
||||
await game.toNextTurn();
|
||||
|
||||
// get heal pulsed back to full before the cud chew proc
|
||||
game.move.select(Moves.SPLASH);
|
||||
await game.forceEnemyMove(Moves.HEAL_PULSE);
|
||||
await game.phaseInterceptor.to("TurnEndPhase");
|
||||
|
||||
// globalScene.queueAbilityDisplay should be called twice:
|
||||
// once to show cud chew text before regurgitating berries,
|
||||
// once to hide ability text after finishing.
|
||||
expect(abDisplaySpy).toBeCalledTimes(2);
|
||||
expect(abDisplaySpy.mock.calls[0][0]).toBe(farigiraf);
|
||||
expect(abDisplaySpy.mock.calls[0][2]).toBe(true);
|
||||
expect(abDisplaySpy.mock.calls[1][0]).toBe(farigiraf);
|
||||
expect(abDisplaySpy.mock.calls[1][2]).toBe(false);
|
||||
|
||||
// should display messgae
|
||||
expect(game.textInterceptor.getLatestMessage()).toBe(
|
||||
i18next.t("battle:hpIsFull", {
|
||||
pokemonName: getPokemonNameWithAffix(farigiraf),
|
||||
}),
|
||||
);
|
||||
|
||||
// not called again at turn end
|
||||
expect(abDisplaySpy).toBeCalledTimes(2);
|
||||
});
|
||||
|
||||
it("can store multiple berries across 2 turns with teatime", async () => {
|
||||
// always eat first berry for stuff cheeks & company
|
||||
vi.spyOn(Pokemon.prototype, "randSeedInt").mockReturnValue(0);
|
||||
game.override
|
||||
.startingHeldItems([
|
||||
{ name: "BERRY", type: BerryType.PETAYA, count: 3 },
|
||||
{ name: "BERRY", type: BerryType.LIECHI, count: 3 },
|
||||
])
|
||||
.enemyMoveset(Moves.TEATIME);
|
||||
await game.classicMode.startBattle([Species.FARIGIRAF]);
|
||||
|
||||
const farigiraf = game.scene.getPlayerPokemon()!;
|
||||
farigiraf.hp = 1; // needed to allow berry procs
|
||||
|
||||
game.move.select(Moves.STUFF_CHEEKS);
|
||||
await game.toNextTurn();
|
||||
|
||||
// Ate 2 petayas from moves + 1 of each at turn end; all 4 get tallied on turn end
|
||||
expect(farigiraf.summonData.berriesEatenLast).toEqual([
|
||||
BerryType.PETAYA,
|
||||
BerryType.PETAYA,
|
||||
BerryType.PETAYA,
|
||||
BerryType.LIECHI,
|
||||
]);
|
||||
expect(farigiraf.turnData.berriesEaten).toEqual([]);
|
||||
|
||||
game.move.select(Moves.SPLASH);
|
||||
await game.toNextTurn();
|
||||
|
||||
// previous berries eaten and deleted from summon data as remaining eaten berries move to replace them
|
||||
expect(farigiraf.summonData.berriesEatenLast).toEqual([BerryType.LIECHI, BerryType.LIECHI]);
|
||||
expect(farigiraf.turnData.berriesEaten).toEqual([]);
|
||||
expect(farigiraf.getStatStage(Stat.SPATK)).toBe(6); // 3+0+3
|
||||
expect(farigiraf.getStatStage(Stat.ATK)).toBe(4); // 1+2+1
|
||||
});
|
||||
|
||||
it("should reset both arrays on switch", async () => {
|
||||
await game.classicMode.startBattle([Species.FARIGIRAF, Species.GIRAFARIG]);
|
||||
|
||||
const farigiraf = game.scene.getPlayerPokemon()!;
|
||||
farigiraf.hp = 1;
|
||||
|
||||
// eat berry turn 1, switch out turn 2
|
||||
game.move.select(Moves.SPLASH);
|
||||
await game.toNextTurn();
|
||||
|
||||
const turn1Hp = farigiraf.hp;
|
||||
game.doSwitchPokemon(1);
|
||||
await game.toNextTurn();
|
||||
|
||||
// summonData got cleared due to switch, turnData got cleared due to turn end
|
||||
expect(farigiraf.summonData.berriesEatenLast).toEqual([]);
|
||||
expect(farigiraf.turnData.berriesEaten).toEqual([]);
|
||||
expect(farigiraf.hp).toEqual(turn1Hp);
|
||||
|
||||
game.doSwitchPokemon(1);
|
||||
await game.toNextTurn();
|
||||
|
||||
// TurnData gets cleared while switching in
|
||||
expect(farigiraf.summonData.berriesEatenLast).toEqual([]);
|
||||
expect(farigiraf.turnData.berriesEaten).toEqual([]);
|
||||
expect(farigiraf.hp).toEqual(turn1Hp);
|
||||
});
|
||||
|
||||
it("clears array if disabled", async () => {
|
||||
game.override.enemyAbility(Abilities.NEUTRALIZING_GAS);
|
||||
await game.classicMode.startBattle([Species.FARIGIRAF]);
|
||||
|
||||
const farigiraf = game.scene.getPlayerPokemon()!;
|
||||
farigiraf.hp = 1;
|
||||
|
||||
game.move.select(Moves.SPLASH);
|
||||
await game.phaseInterceptor.to("BerryPhase");
|
||||
|
||||
expect(farigiraf.summonData.berriesEatenLast).toEqual([]);
|
||||
expect(farigiraf.turnData.berriesEaten).toEqual([BerryType.SITRUS]);
|
||||
|
||||
await game.toNextTurn();
|
||||
|
||||
// both arrays empty since neut gas disabled both the mid-turn and post-turn effects
|
||||
expect(farigiraf.summonData.berriesEatenLast).toEqual([]);
|
||||
expect(farigiraf.turnData.berriesEaten).toEqual([]);
|
||||
});
|
||||
});
|
||||
|
||||
describe("regurgiates berries", () => {
|
||||
it("re-triggers effects on eater without pushing to array", async () => {
|
||||
const apply = vi.spyOn(RepeatBerryNextTurnAbAttr.prototype, "apply");
|
||||
await game.classicMode.startBattle([Species.FARIGIRAF]);
|
||||
|
||||
const farigiraf = game.scene.getPlayerPokemon()!;
|
||||
farigiraf.hp = 1;
|
||||
|
||||
game.move.select(Moves.SPLASH);
|
||||
await game.toNextTurn();
|
||||
|
||||
// ate 1 sitrus the turn prior, spitball pending
|
||||
expect(farigiraf.summonData.berriesEatenLast).toEqual([BerryType.SITRUS]);
|
||||
expect(farigiraf.turnData.berriesEaten).toEqual([]);
|
||||
expect(apply.mock.lastCall).toBeUndefined();
|
||||
|
||||
const turn1Hp = farigiraf.hp;
|
||||
|
||||
game.move.select(Moves.SPLASH);
|
||||
await game.phaseInterceptor.to("TurnEndPhase");
|
||||
|
||||
// healed back up to half without adding any more to array
|
||||
expect(farigiraf.hp).toBeGreaterThan(turn1Hp);
|
||||
expect(farigiraf.summonData.berriesEatenLast).toEqual([]);
|
||||
expect(farigiraf.turnData.berriesEaten).toEqual([]);
|
||||
});
|
||||
|
||||
it("bypasses unnerve", async () => {
|
||||
game.override.enemyAbility(Abilities.UNNERVE);
|
||||
await game.classicMode.startBattle([Species.FARIGIRAF]);
|
||||
|
||||
const farigiraf = game.scene.getPlayerPokemon()!;
|
||||
farigiraf.hp = 1;
|
||||
|
||||
game.move.select(Moves.SPLASH);
|
||||
await game.toNextTurn();
|
||||
game.move.select(Moves.SPLASH);
|
||||
await game.phaseInterceptor.to("TurnEndPhase");
|
||||
|
||||
// Turn end proc set the berriesEatenLast array back to being empty
|
||||
expect(farigiraf.summonData.berriesEatenLast).toEqual([]);
|
||||
expect(farigiraf.turnData.berriesEaten).toEqual([]);
|
||||
expect(farigiraf.hp).toBeGreaterThanOrEqual(farigiraf.hp / 2);
|
||||
});
|
||||
|
||||
it("doesn't trigger on non-eating removal", async () => {
|
||||
game.override.enemyMoveset(Moves.INCINERATE);
|
||||
await game.classicMode.startBattle([Species.FARIGIRAF]);
|
||||
|
||||
const farigiraf = game.scene.getPlayerPokemon()!;
|
||||
farigiraf.hp = farigiraf.getMaxHp() / 4;
|
||||
|
||||
game.move.select(Moves.SPLASH);
|
||||
await game.toNextTurn();
|
||||
|
||||
// no berries eaten due to getting cooked
|
||||
expect(farigiraf.summonData.berriesEatenLast).toEqual([]);
|
||||
expect(farigiraf.turnData.berriesEaten).toEqual([]);
|
||||
expect(farigiraf.hp).toBeLessThan(farigiraf.getMaxHp() / 4);
|
||||
});
|
||||
|
||||
it("works with pluck", async () => {
|
||||
game.override
|
||||
.enemySpecies(Species.BLAZIKEN)
|
||||
.enemyHeldItems([{ name: "BERRY", type: BerryType.PETAYA, count: 1 }])
|
||||
.startingHeldItems([]);
|
||||
await game.classicMode.startBattle([Species.FARIGIRAF]);
|
||||
|
||||
const farigiraf = game.scene.getPlayerPokemon()!;
|
||||
|
||||
game.move.select(Moves.BUG_BITE);
|
||||
await game.toNextTurn();
|
||||
|
||||
game.move.select(Moves.SPLASH);
|
||||
await game.toNextTurn();
|
||||
|
||||
// berry effect triggered twice - once for bug bite, once for cud chew
|
||||
expect(farigiraf.getStatStage(Stat.SPATK)).toBe(2);
|
||||
});
|
||||
|
||||
it("works with Ripen", async () => {
|
||||
game.override.passiveAbility(Abilities.RIPEN);
|
||||
await game.classicMode.startBattle([Species.FARIGIRAF]);
|
||||
|
||||
const farigiraf = game.scene.getPlayerPokemon()!;
|
||||
farigiraf.hp = 1;
|
||||
|
||||
game.move.select(Moves.SPLASH);
|
||||
await game.toNextTurn();
|
||||
game.move.select(Moves.SPLASH);
|
||||
await game.toNextTurn();
|
||||
|
||||
// Rounding errors only ever cost a maximum of 4 hp
|
||||
expect(farigiraf.getInverseHp()).toBeLessThanOrEqual(3);
|
||||
});
|
||||
|
||||
it("is preserved on reload/wave clear", async () => {
|
||||
game.override.enemyLevel(1);
|
||||
await game.classicMode.startBattle([Species.FARIGIRAF]);
|
||||
|
||||
const farigiraf = game.scene.getPlayerPokemon()!;
|
||||
farigiraf.hp = 1;
|
||||
|
||||
game.move.select(Moves.HYPER_VOICE);
|
||||
await game.toNextWave();
|
||||
|
||||
// berry went yummy yummy in big fat giraffe tummy
|
||||
expect(farigiraf.summonData.berriesEatenLast).toEqual([BerryType.SITRUS]);
|
||||
expect(farigiraf.hp).toBeGreaterThan(1);
|
||||
|
||||
// reload and the berry should still be there
|
||||
await game.reload.reloadSession();
|
||||
|
||||
const farigirafReloaded = game.scene.getPlayerPokemon()!;
|
||||
expect(farigirafReloaded.summonData.berriesEatenLast).toEqual([BerryType.SITRUS]);
|
||||
|
||||
const wave1Hp = farigirafReloaded.hp;
|
||||
|
||||
// blow up next wave and we should proc the repeat eating
|
||||
game.move.select(Moves.HYPER_VOICE);
|
||||
await game.toNextWave();
|
||||
|
||||
expect(farigirafReloaded.hp).toBeGreaterThan(wave1Hp);
|
||||
});
|
||||
});
|
||||
});
|
@ -49,7 +49,7 @@ describe("Abilities - Good As Gold", () => {
|
||||
|
||||
await game.phaseInterceptor.to("BerryPhase");
|
||||
|
||||
expect(player.battleData.abilitiesApplied[0]).toBe(Abilities.GOOD_AS_GOLD);
|
||||
expect(player.waveData.abilitiesApplied).toContain(Abilities.GOOD_AS_GOLD);
|
||||
expect(player.getStatStage(Stat.ATK)).toBe(0);
|
||||
});
|
||||
|
||||
|