[Ability] Implement Illusion (#3273)

* implement illusion ability with unit test and localizations

* try removing whitespace change on unnecessary files

* nit corrections

* nit update src/field/pokemon.ts

Co-authored-by: Adrian T. <68144167+torranx@users.noreply.github.com>

* nit update src/phases.ts

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

* illusion test correction

* unexpected error correction

* refactor property pokemon.illusion to pokemon.battleData.illusion

* nit

* nit

* update unit test up-to-date

* add docs

* merge up to date

* bugfix

* bugfix

* merge up to date

* refactor field illusion out of battleData

* fix nit

* fix nit

* Zoroark change illusion after lastPokemon update

* Zoroark change illusion after lastPokemon update

* refactor bug fix

* bugfix

* bug fix on tests

* Update src/field/pokemon.ts

Co-authored-by: innerthunder <168692175+innerthunder@users.noreply.github.com>

* use GetFailedText

* remove useless import

* add condition 'no illusion' into transform move

* wild Zoroark creates an illusion according to the current biome

* wild Zoroark creates an illusion according to the current biome

* delete console.log()

* add doc

* Update src/field/pokemon.ts

Co-authored-by: innerthunder <168692175+innerthunder@users.noreply.github.com>

* fix tests

* update locales submodule

* update Illusion interface

* bug fix

* bug fix

* bugfix

* rename some params for future implementations

* Zoroark keep illusion between battles

* Zoroark keep illusion between battles

* delete draft

* merge up-to-date

* bugfix

* merge

* merge

* implement canApplyPresummon method

* Update test/abilities/illusion.test.ts

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

* Update src/data/ability.ts

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

* Update src/data/ability.ts

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

* Update test/abilities/illusion.test.ts

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

* Update test/abilities/illusion.test.ts

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

* nit

* review corrections

* nit

* type hints affected by enemy illusion

* type hints affected by enemy illusionin fight-ui-handler

* nit

* rename some parameters back in useIllusion

* Update src/field/pokemon.ts

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

* Update src/field/pokemon.ts

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

* refactor battleData.illusion as summonData.illusion and delete oncePerBattleClause

* add comments

* illusion will break before evolution

* Update src/field/pokemon.ts

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

* Update src/data/ability.ts

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

* Update src/data/ability.ts

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

* Update src/data/ability.ts

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

* Update src/data/ability.ts

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

* bug fix

* g

* get submodule back

* get submodule back

* bug fix to save illusion status

* add pokemon.getPokeball()

* Update src/field/pokemon.ts

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

* Update src/field/pokemon.ts

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

* Update src/field/pokemon.ts

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

* Update src/data/ability.ts

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

* Update src/field/pokemon.ts

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

* Update src/field/pokemon.ts

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

* Update src/field/pokemon.ts

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

* Update src/field/pokemon.ts

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

---------

Co-authored-by: Adrian T. <68144167+torranx@users.noreply.github.com>
Co-authored-by: Amani H. <109637146+xsn34kzx@users.noreply.github.com>
Co-authored-by: innerthunder <168692175+innerthunder@users.noreply.github.com>
Co-authored-by: flx-sta <50131232+flx-sta@users.noreply.github.com>
Co-authored-by: Sirz Benjie <142067137+SirzBenjie@users.noreply.github.com>
Co-authored-by: NightKev <34855794+DayKev@users.noreply.github.com>
This commit is contained in:
Lylian BALL 2025-04-13 03:22:04 +02:00 committed by GitHub
parent f9ff4abfb0
commit 15e535a1a0
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
17 changed files with 757 additions and 198 deletions

View File

@ -1072,7 +1072,7 @@ export default class BattleScene extends SceneBase {
container.add(icon);
if (pokemon.isFusion()) {
if (pokemon.isFusion(true)) {
const fusionIcon = this.add.sprite(0, 0, pokemon.getFusionIconAtlasKey(ignoreOverride));
fusionIcon.setName("sprite-fusion-icon");
fusionIcon.setOrigin(0.5, 0);

View File

@ -2370,6 +2370,18 @@ export class PostSummonMessageAbAttr extends PostSummonAbAttr {
}
}
/**
* Removes illusions when a Pokemon is summoned.
*/
export class PostSummonRemoveIllusionAbAttr extends PostSummonAbAttr {
applyPostSummon(pokemon: Pokemon, passive: boolean, simulated: boolean, args: any[]): boolean {
for (const pokemon of globalScene.getField(true)) {
pokemon.breakIllusion();
}
return true;
}
}
export class PostSummonUnnamedMessageAbAttr extends PostSummonAbAttr {
//Attr doesn't force pokemon name on the message
private message: string;
@ -2812,7 +2824,7 @@ export class PostSummonTransformAbAttr extends PostSummonAbAttr {
}
private getTarget(targets: Pokemon[]): Pokemon {
let target: Pokemon;
let target: Pokemon = targets[0];
if (targets.length > 1) {
globalScene.executeWithSeedOffset(() => {
// in a double battle, if one of the opposing pokemon is fused the other one will be chosen
@ -2829,6 +2841,7 @@ export class PostSummonTransformAbAttr extends PostSummonAbAttr {
} else {
target = targets[0];
}
target = target!;
return target;
@ -2836,6 +2849,12 @@ export class PostSummonTransformAbAttr extends PostSummonAbAttr {
override canApplyPostSummon(pokemon: Pokemon, passive: boolean, simulated: boolean, args: any[]): boolean {
const targets = pokemon.getOpponents();
const target = this.getTarget(targets);
if (!!target.summonData?.illusion) {
return false;
}
if (simulated || !targets.length) {
return simulated;
}
@ -4741,8 +4760,8 @@ export class MaxMultiHitAbAttr extends AbAttr {
}
export class PostBattleAbAttr extends AbAttr {
constructor() {
super(true);
constructor(showAbility: boolean = true) {
super(showAbility);
}
canApplyPostBattle(pokemon: Pokemon, passive: boolean, simulated: boolean, args: any[]): boolean {
@ -5259,6 +5278,92 @@ export class FormBlockDamageAbAttr extends ReceivedMoveDamageMultiplierAbAttr {
}
}
/**
* Base class for defining {@linkcode Ability} attributes before summon
* (should use {@linkcode PostSummonAbAttr} for most ability)
* @see {@linkcode applyPreSummon()}
*/
export class PreSummonAbAttr extends AbAttr {
applyPreSummon(pokemon: Pokemon, passive: boolean, args: any[]): void {}
canApplyPreSummon(pokemon: Pokemon, passive: boolean, args: any[]): boolean {
return true;
}
}
export class IllusionPreSummonAbAttr extends PreSummonAbAttr {
/**
* Apply a new illusion when summoning Zoroark if the illusion is available
*
* @param pokemon - The Pokémon with the Illusion ability.
* @param passive - N/A
* @param args - N/A
* @returns Whether the illusion was applied.
*/
override applyPreSummon(pokemon: Pokemon, passive: boolean, args: any[]): void {
const party: Pokemon[] = (pokemon.isPlayer() ? globalScene.getPlayerParty() : globalScene.getEnemyParty()).filter(p => p.isAllowedInBattle());
const lastPokemon: Pokemon = party.filter(p => p !==pokemon).at(-1) || pokemon;
pokemon.setIllusion(lastPokemon);
}
override canApplyPreSummon(pokemon: Pokemon, passive: boolean, args: any[]): boolean {
pokemon.initSummondata()
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 (
lastPokemon === pokemon ||
((speciesId === Species.OGERPON || speciesId === Species.TERAPAGOS) && (lastPokemon.isTerastallized || pokemon.isTerastallized))
) {
return false;
}
}
return !pokemon.summonData.illusionBroken;
}
}
export class IllusionBreakAbAttr extends PostDefendAbAttr {
/**
* Destroy the illusion upon taking damage
*
* @param pokemon - The Pokémon with the Illusion ability.
* @param passive - unused
* @param attacker - The attacking Pokémon.
* @param move - The move being used.
* @param hitResult - The type of hitResult the pokemon got
* @param args - unused
* @returns - Whether the illusion was destroyed.
*/
override applyPostDefend(pokemon: Pokemon, passive: boolean, simulated: boolean, attacker: Pokemon, move: Move, hitResult: HitResult, args: any[]): void {
pokemon.breakIllusion();
pokemon.summonData.illusionBroken = true;
}
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
}
}
export class IllusionPostBattleAbAttr extends PostBattleAbAttr {
/**
* Break the illusion once the battle ends
*
* @param pokemon - The Pokémon with the Illusion ability.
* @param passive - Unused
* @param args - Unused
* @returns - Whether the illusion was applied.
*/
override applyPostBattle(pokemon: Pokemon, passive: boolean, simulated:boolean, args: any[]): void {
pokemon.breakIllusion()
}
}
/**
* If a Pokémon with this Ability selects a damaging move, it has a 30% chance of going first in its priority bracket. If the Ability activates, this is announced at the start of the turn (after move selection).
*
@ -6017,6 +6122,20 @@ export function applyPostSummonAbAttrs(
);
}
export function applyPreSummonAbAttrs(
attrType: Constructor<PreSummonAbAttr>,
pokemon: Pokemon,
...args: any[]
): void {
applyAbAttrsInternal<PreSummonAbAttr>(
attrType,
pokemon,
(attr, passive) => attr.applyPreSummon(pokemon, passive, args),
(attr, passive) => attr.canApplyPreSummon(pokemon, passive, args),
args
);
}
export function applyPreSwitchOutAbAttrs(
attrType: Constructor<PreSwitchOutAbAttr>,
pokemon: Pokemon,
@ -6811,8 +6930,14 @@ export function initAbilities() {
return isNullOrUndefined(movePhase);
}, 1.3),
new Ability(Abilities.ILLUSION, 5)
//The pokemon generate an illusion if it's available
.attr(IllusionPreSummonAbAttr, false)
//The pokemon loses his illusion when he is damaged by a move
.attr(IllusionBreakAbAttr, true)
//Illusion is available again after a battle
.conditionalAttr((pokemon) => pokemon.isAllowedInBattle(), IllusionPostBattleAbAttr, false)
.uncopiable()
.unimplemented(),
.bypassFaint(),
new Ability(Abilities.IMPOSTER, 5)
.attr(PostSummonTransformAbAttr)
.uncopiable(),
@ -7223,6 +7348,8 @@ export function initAbilities() {
.attr(PreLeaveFieldRemoveSuppressAbilitiesSourceAbAttr)
.uncopiable()
.attr(NoTransformAbilityAbAttr)
.attr(PostSummonMessageAbAttr, (pokemon: Pokemon) => i18next.t("abilityTriggers:postSummonNeutralizingGas", { pokemonNameWithAffix: getPokemonNameWithAffix(pokemon) }))
.attr(PostSummonRemoveIllusionAbAttr)
.bypassFaint(),
new Ability(Abilities.PASTEL_VEIL, 8)
.attr(PostSummonUserFieldRemoveStatusEffectAbAttr, StatusEffect.POISON, StatusEffect.TOXIC)

View File

@ -7389,11 +7389,13 @@ export class AbilityChangeAttr extends MoveEffectAttr {
const moveTarget = this.selfTarget ? user : target;
globalScene.queueMessage(i18next.t("moveTriggers:acquiredAbility", { pokemonName: getPokemonNameWithAffix((this.selfTarget ? user : target)), abilityName: allAbilities[this.ability].name }));
globalScene.triggerPokemonFormChange(moveTarget, SpeciesFormChangeRevertWeatherFormTrigger);
if (moveTarget.breakIllusion()) {
globalScene.queueMessage(i18next.t("abilityTriggers:illusionBreak", { pokemonName: getPokemonNameWithAffix(moveTarget) }));
}
globalScene.queueMessage(i18next.t("moveTriggers:acquiredAbility", { pokemonName: getPokemonNameWithAffix(moveTarget), abilityName: allAbilities[this.ability].name }));
moveTarget.setTempAbility(allAbilities[this.ability]);
globalScene.triggerPokemonFormChange(moveTarget, SpeciesFormChangeRevertWeatherFormTrigger);
return true;
}
@ -8673,6 +8675,8 @@ export function initMoves() {
.makesContact(false),
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)
// 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(),

View File

@ -536,21 +536,33 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container {
}
}
getNameToRender() {
/**
* @param {boolean} useIllusion - Whether we want the fake name or the real name of the Pokemon (for Illusion ability).
*/
getNameToRender(useIllusion: boolean = true) {
const name: string = (!useIllusion && !!this.summonData?.illusion) ? this.summonData?.illusion.basePokemon!.name : this.name;
const nickname: string = (!useIllusion && !!this.summonData?.illusion) ? this.summonData?.illusion.basePokemon!.nickname : this.nickname;
try {
if (this.nickname) {
return decodeURIComponent(escape(atob(this.nickname)));
if (nickname) {
return decodeURIComponent(escape(atob(nickname)));
}
return this.name;
return name;
} catch (err) {
console.error(`Failed to decode nickname for ${this.name}`, err);
return this.name;
console.error(`Failed to decode nickname for ${name}`, err);
return name;
}
}
getPokeball(useIllusion = false){
if(useIllusion){
return this.summonData?.illusion?.pokeball ?? this.pokeball
} else {
return this.pokeball
}
}
init(): void {
this.fieldPosition = FieldPosition.CENTER;
this.initBattleInfo();
globalScene.fieldUI.addAt(this.battleInfo, 0);
@ -584,7 +596,7 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container {
this.addAt(sprite, 0);
this.addAt(tintSprite, 1);
if (this.isShiny() && !this.shinySparkle) {
if (this.isShiny(true) && !this.shinySparkle) {
this.initShinySparkle();
}
}
@ -682,6 +694,92 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container {
}
}
/**
* Generate an illusion of the last pokemon in the party, as other wild pokemon in the area.
*/
setIllusion(pokemon: Pokemon): boolean {
if(!!this.summonData?.illusion){
this.breakIllusion();
}
if (this.hasTrainer()) {
const speciesId = pokemon.species.speciesId;
this.summonData.illusion = {
basePokemon: {
name: this.name,
nickname: this.nickname,
shiny: this.shiny,
variant: this.variant,
fusionShiny: this.fusionShiny,
fusionVariant: this.fusionVariant
},
species: speciesId,
formIndex: pokemon.formIndex,
gender: pokemon.gender,
pokeball: pokemon.pokeball,
fusionFormIndex: pokemon.fusionFormIndex,
fusionSpecies: pokemon.fusionSpecies || undefined,
fusionGender: pokemon.fusionGender
};
this.name = pokemon.name;
this.nickname = pokemon.nickname;
this.shiny = pokemon.shiny;
this.variant = pokemon.variant;
this.fusionVariant = pokemon.fusionVariant;
this.fusionShiny = pokemon.fusionShiny;
if (this.shiny) {
this.initShinySparkle();
}
this.loadAssets(false, true).then(() => this.playAnim());
this.updateInfo();
} else {
const randomIllusion: PokemonSpecies = globalScene.arena.randomSpecies(globalScene.currentBattle.waveIndex, this.level);
this.summonData.illusion = {
basePokemon: {
name: this.name,
nickname: this.nickname,
shiny: this.shiny,
variant: this.variant,
fusionShiny: this.fusionShiny,
fusionVariant: this.fusionVariant
},
species: randomIllusion.speciesId,
formIndex: randomIllusion.formIndex,
gender: this.gender,
pokeball: this.pokeball
};
this.name = randomIllusion.name;
this.loadAssets(false, true).then(() => this.playAnim());
}
return true;
}
breakIllusion(): boolean {
if (!this.summonData?.illusion) {
return false;
} else {
this.name = this.summonData?.illusion.basePokemon.name;
this.nickname = this.summonData?.illusion.basePokemon.nickname;
this.shiny = this.summonData?.illusion.basePokemon.shiny;
this.variant = this.summonData?.illusion.basePokemon.variant;
this.fusionVariant = this.summonData?.illusion.basePokemon.fusionVariant;
this.fusionShiny = this.summonData?.illusion.basePokemon.fusionShiny;
this.summonData.illusion = null;
}
if (this.isOnField()) {
globalScene.playSound("PRSFX- Transform");
}
if (this.shiny) {
this.initShinySparkle();
}
this.loadAssets(false).then(() => this.playAnim());
this.updateInfo(true);
return true;
}
abstract isPlayer(): boolean;
abstract hasTrainer(): boolean;
@ -690,29 +788,41 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container {
abstract getBattlerIndex(): BattlerIndex;
async loadAssets(ignoreOverride = true): Promise<void> {
/**
   * @param useIllusion - Whether we want the illusion or not.
   */
async loadAssets(ignoreOverride = true, useIllusion: boolean = false): Promise<void> {
/** Promises that are loading assets and can be run concurrently. */
const loadPromises: Promise<void>[] = [];
// Assets for moves
loadPromises.push(loadMoveAnimations(this.getMoveset().map(m => m.getMove().id)));
// Load the assets for the species form
const formIndex = !!this.summonData?.illusion && useIllusion ? this.summonData?.illusion.formIndex : this.formIndex;
loadPromises.push(
this.getSpeciesForm().loadAssets(this.getGender() === Gender.FEMALE, this.formIndex, this.shiny, this.variant),
this.getSpeciesForm(false, useIllusion).loadAssets(
this.getGender(useIllusion) === Gender.FEMALE,
formIndex,
this.isShiny(useIllusion),
this.getVariant(useIllusion)
),
);
if (this.isPlayer() || this.getFusionSpeciesForm()) {
if (this.isPlayer() || this.getFusionSpeciesForm(false, useIllusion)) {
globalScene.loadPokemonAtlas(
this.getBattleSpriteKey(true, ignoreOverride),
this.getBattleSpriteAtlasPath(true, ignoreOverride),
);
}
if (this.getFusionSpeciesForm()) {
loadPromises.push(this.getFusionSpeciesForm().loadAssets(
this.getFusionGender() === Gender.FEMALE,
this.fusionFormIndex,
this.fusionShiny,
this.fusionVariant,
const fusionFormIndex = !!this.summonData?.illusion && useIllusion ? this.summonData?.illusion.fusionFormIndex : this.fusionFormIndex;
const fusionShiny = !!this.summonData?.illusion && !useIllusion ? this.summonData?.illusion.basePokemon!.fusionShiny : this.fusionShiny;
const fusionVariant = !!this.summonData?.illusion && !useIllusion ? this.summonData?.illusion.basePokemon!.fusionVariant : this.fusionVariant;
loadPromises.push(this.getFusionSpeciesForm(false, useIllusion).loadAssets(
this.getFusionGender(false, useIllusion) === Gender.FEMALE,
fusionFormIndex,
fusionShiny,
fusionVariant
));
globalScene.loadPokemonAtlas(
this.getFusionBattleSpriteKey(true, ignoreOverride),
@ -720,7 +830,7 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container {
);
}
if (this.shiny) {
if (this.isShiny(true)) {
loadPromises.push(populateVariantColors(this, false, ignoreOverride))
if (this.isPlayer()) {
loadPromises.push(populateVariantColors(this, true, ignoreOverride));
@ -870,11 +980,12 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container {
}
getSpriteId(ignoreOverride?: boolean): string {
return this.getSpeciesForm(ignoreOverride).getSpriteId(
this.getGender(ignoreOverride) === Gender.FEMALE,
this.formIndex,
this.shiny,
this.variant,
const formIndex: integer = !!this.summonData?.illusion ? this.summonData?.illusion.formIndex! : this.formIndex;
return this.getSpeciesForm(ignoreOverride, true).getSpriteId(
this.getGender(ignoreOverride, true) === Gender.FEMALE,
formIndex,
this.shiny,
this.variant
);
}
@ -882,21 +993,24 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container {
if (back === undefined) {
back = this.isPlayer();
}
return this.getSpeciesForm(ignoreOverride).getSpriteId(
this.getGender(ignoreOverride) === Gender.FEMALE,
this.formIndex,
this.shiny,
this.variant,
back,
const formIndex: integer = !!this.summonData?.illusion ? this.summonData?.illusion.formIndex! : this.formIndex;
return this.getSpeciesForm(ignoreOverride, true).getSpriteId(
this.getGender(ignoreOverride, true) === Gender.FEMALE,
formIndex,
this.shiny,
this.variant,
back
);
}
getSpriteKey(ignoreOverride?: boolean): string {
return this.getSpeciesForm(ignoreOverride).getSpriteKey(
return this.getSpeciesForm(ignoreOverride, false).getSpriteKey(
this.getGender(ignoreOverride) === Gender.FEMALE,
this.formIndex,
this.shiny,
this.variant,
this.summonData?.illusion?.basePokemon.shiny ?? this.shiny,
this.summonData?.illusion?.basePokemon.variant ?? this.variant
);
}
@ -905,11 +1019,12 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container {
}
getFusionSpriteId(ignoreOverride?: boolean): string {
return this.getFusionSpeciesForm(ignoreOverride).getSpriteId(
this.getFusionGender(ignoreOverride) === Gender.FEMALE,
this.fusionFormIndex,
this.fusionShiny,
this.fusionVariant,
const fusionFormIndex: integer = !!this.summonData?.illusion ? this.summonData?.illusion.fusionFormIndex! : this.fusionFormIndex;
return this.getFusionSpeciesForm(ignoreOverride, true).getSpriteId(
this.getFusionGender(ignoreOverride, true) === Gender.FEMALE,
fusionFormIndex,
this.fusionShiny,
this.fusionVariant
);
}
@ -917,12 +1032,15 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container {
if (back === undefined) {
back = this.isPlayer();
}
return this.getFusionSpeciesForm(ignoreOverride).getSpriteId(
this.getFusionGender(ignoreOverride) === Gender.FEMALE,
this.fusionFormIndex,
this.fusionShiny,
this.fusionVariant,
back,
const fusionFormIndex: integer = !!this.summonData?.illusion ? this.summonData?.illusion.fusionFormIndex! : this.fusionFormIndex;
return this.getFusionSpeciesForm(ignoreOverride, true).getSpriteId(
this.getFusionGender(ignoreOverride, true) === Gender.FEMALE,
fusionFormIndex,
this.fusionShiny,
this.fusionVariant,
back
);
}
@ -941,62 +1059,77 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container {
}
getIconAtlasKey(ignoreOverride?: boolean): string {
return this.getSpeciesForm(ignoreOverride).getIconAtlasKey(
this.formIndex,
this.shiny,
this.variant,
const formIndex: integer = !!this.summonData?.illusion ? this.summonData?.illusion.formIndex : this.formIndex;
return this.getSpeciesForm(ignoreOverride, true).getIconAtlasKey(
formIndex,
this.shiny,
this.variant
);
}
getFusionIconAtlasKey(ignoreOverride?: boolean): string {
return this.getFusionSpeciesForm(ignoreOverride).getIconAtlasKey(
this.fusionFormIndex,
this.fusionShiny,
this.fusionVariant,
return this.getFusionSpeciesForm(ignoreOverride, true).getIconAtlasKey(
this.fusionFormIndex,
this.fusionShiny,
this.fusionVariant
);
}
getIconId(ignoreOverride?: boolean): string {
return this.getSpeciesForm(ignoreOverride).getIconId(
this.getGender(ignoreOverride) === Gender.FEMALE,
this.formIndex,
this.shiny,
this.variant,
const formIndex: integer = !!this.summonData?.illusion ? this.summonData?.illusion.formIndex : this.formIndex;
return this.getSpeciesForm(ignoreOverride, true).getIconId(
this.getGender(ignoreOverride, true) === Gender.FEMALE,
formIndex,
this.shiny,
this.variant
);
}
getFusionIconId(ignoreOverride?: boolean): string {
return this.getFusionSpeciesForm(ignoreOverride).getIconId(
this.getFusionGender(ignoreOverride) === Gender.FEMALE,
this.fusionFormIndex,
this.fusionShiny,
this.fusionVariant,
const fusionFormIndex: integer = !!this.summonData?.illusion ? this.summonData?.illusion.fusionFormIndex! : this.fusionFormIndex;
return this.getFusionSpeciesForm(ignoreOverride, true).getIconId(
this.getFusionGender(ignoreOverride, true) === Gender.FEMALE,
fusionFormIndex,
this.fusionShiny,
this.fusionVariant
);
}
getSpeciesForm(ignoreOverride?: boolean): PokemonSpeciesForm {
/**
* @param {boolean} useIllusion - Whether we want the speciesForm of the illusion or not.
*/
getSpeciesForm(ignoreOverride?: boolean, useIllusion: boolean = false): PokemonSpeciesForm {
const species: PokemonSpecies = useIllusion && !!this.summonData?.illusion ? getPokemonSpecies(this.summonData?.illusion.species) : this.species;
const formIndex: integer = useIllusion && !!this.summonData?.illusion ? this.summonData?.illusion.formIndex : this.formIndex;
if (!ignoreOverride && this.summonData?.speciesForm) {
return this.summonData.speciesForm;
}
if (this.species.forms && this.species.forms.length > 0) {
return this.species.forms[this.formIndex];
if (species.forms && species.forms.length > 0) {
return species.forms[formIndex];
}
return this.species;
return species;
}
getFusionSpeciesForm(ignoreOverride?: boolean): PokemonSpeciesForm {
/**
* @param {boolean} useIllusion - Whether we want the fusionSpeciesForm of the illusion or not.
*/
getFusionSpeciesForm(ignoreOverride?: boolean, useIllusion: boolean = false): PokemonSpeciesForm {
const fusionSpecies: PokemonSpecies = useIllusion && !!this.summonData?.illusion ? this.summonData?.illusion.fusionSpecies! : this.fusionSpecies!;
const fusionFormIndex: integer = useIllusion && !!this.summonData?.illusion ? this.summonData?.illusion.fusionFormIndex! : this.fusionFormIndex;
if (!ignoreOverride && this.summonData?.speciesForm) {
return this.summonData.fusionSpeciesForm;
}
if (
!this.fusionSpecies?.forms?.length ||
this.fusionFormIndex >= this.fusionSpecies?.forms.length
!fusionSpecies?.forms?.length ||
fusionFormIndex >= fusionSpecies?.forms.length
) {
//@ts-ignore
return this.fusionSpecies; // TODO: I don't even know how to fix this... A complete cluster of classes involved + null
return fusionSpecies;
}
return this.fusionSpecies?.forms[this.fusionFormIndex];
return fusionSpecies?.forms[fusionFormIndex];
}
getSprite(): Phaser.GameObjects.Sprite {
@ -1652,36 +1785,98 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container {
}
}
getGender(ignoreOverride?: boolean): Gender {
if (!ignoreOverride && this.summonData?.gender !== undefined) {
/**
* @param {boolean} useIllusion - Whether we want the fake or real gender (illusion ability).
*/
getGender(ignoreOverride?: boolean, useIllusion: boolean = false): Gender {
if (useIllusion && !!this.summonData?.illusion) {
return this.summonData?.illusion.gender!;
} else if (!ignoreOverride && this.summonData?.gender !== undefined) {
return this.summonData.gender;
}
return this.gender;
}
getFusionGender(ignoreOverride?: boolean): Gender {
if (!ignoreOverride && this.summonData?.fusionGender !== undefined) {
/**
* @param {boolean} useIllusion - Whether we want the fake or real gender (illusion ability).
*/
getFusionGender(ignoreOverride?: boolean, useIllusion: boolean = false): Gender {
if (useIllusion && !!this.summonData?.illusion) {
return this.summonData?.illusion.fusionGender!;
} else if (!ignoreOverride && this.summonData?.fusionGender !== undefined) {
return this.summonData.fusionGender;
}
return this.fusionGender;
}
isShiny(): boolean {
return this.shiny || (this.isFusion() && this.fusionShiny);
/**
* @param {boolean} useIllusion - Whether we want the fake or real shininess (illusion ability).
*/
isShiny(useIllusion: boolean = false): boolean {
if (!useIllusion && !!this.summonData?.illusion) {
return this.summonData?.illusion.basePokemon?.shiny || (!!this.summonData?.illusion.fusionSpecies && this.summonData?.illusion.basePokemon?.fusionShiny) || false;
} else {
return this.shiny || (this.isFusion(useIllusion) && this.fusionShiny);
}
}
getVariant(): Variant {
return !this.isFusion()
? this.variant
: (Math.max(this.variant, this.fusionVariant) as Variant);
/**
*
* @param useIllusion - Whether we want the fake or real shininess (illusion ability).
* @returns `true` if the {@linkcode Pokemon} is shiny and the fusion is shiny as well, `false` otherwise
*/
isDoubleShiny(useIllusion: boolean = false): boolean {
if (!useIllusion && !!this.summonData?.illusion) {
return this.isFusion(false) && this.summonData?.illusion.basePokemon.shiny && this.summonData?.illusion.basePokemon.fusionShiny;
} else {
return this.isFusion(useIllusion) && this.shiny && this.fusionShiny;
}
}
/**
* @param {boolean} useIllusion - Whether we want the fake or real variant (illusion ability).
*/
getVariant(useIllusion: boolean = false): Variant {
if (!useIllusion && !!this.summonData?.illusion) {
return !this.isFusion(false)
? this.summonData?.illusion.basePokemon!.variant
: Math.max(this.variant, this.fusionVariant) as Variant;
} else {
return !this.isFusion(true)
? this.variant
: Math.max(this.variant, this.fusionVariant) as Variant;
}
}
getBaseVariant(doubleShiny: boolean): Variant {
if (doubleShiny) {
return !!this.summonData?.illusion
? this.summonData?.illusion.basePokemon!.variant
: this.variant;
} else {
return this.getVariant();
}
}
getLuck(): number {
return this.luck + (this.isFusion() ? this.fusionLuck : 0);
}
isFusion(): boolean {
return !!this.fusionSpecies;
isFusion(useIllusion: boolean = false): boolean {
if (useIllusion && !!this.summonData?.illusion) {
return !!this.summonData?.illusion.fusionSpecies;
} else {
return !!this.fusionSpecies;
}
}
/**
* @param {boolean} useIllusion - Whether we want the fake name or the real name of the Pokemon (for Illusion ability).
*/
getName(useIllusion: boolean = false): string {
return (!useIllusion && !!this.summonData?.illusion && this.summonData?.illusion.basePokemon)
? this.summonData?.illusion.basePokemon.name
: this.name;
}
/**
@ -1796,12 +1991,14 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container {
* @param includeTeraType - `true` to include tera-formed type; Default: `false`
* @param forDefend - `true` if the pokemon is defending from an attack; Default: `false`
* @param ignoreOverride - If `true`, ignore ability changing effects; Default: `false`
* @param useIllusion - `true` to return the types of the illusion instead of the actual types; "AUTO" will depend on forDefend param; Default: "AUTO"
* @returns array of {@linkcode PokemonType}
*/
public getTypes(
includeTeraType = false,
forDefend = false,
ignoreOverride = false,
includeTeraType = false,
forDefend: boolean = false,
ignoreOverride?: boolean,
useIllusion: boolean | "AUTO" = "AUTO"
): PokemonType[] {
const types: PokemonType[] = [];
@ -1815,17 +2012,19 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container {
}
}
}
if (!types.length || !includeTeraType) {
const doIllusion: boolean = (useIllusion === "AUTO") ? !forDefend : useIllusion;
if (
!ignoreOverride &&
this.summonData?.types &&
this.summonData.types.length > 0
!ignoreOverride &&
this.summonData?.types &&
this.summonData.types.length > 0 &&
(!this.summonData?.illusion || !doIllusion)
) {
this.summonData.types.forEach(t => types.push(t));
} else {
const speciesForm = this.getSpeciesForm(ignoreOverride);
const fusionSpeciesForm = this.getFusionSpeciesForm(ignoreOverride);
const speciesForm = this.getSpeciesForm(ignoreOverride, doIllusion);
const fusionSpeciesForm = this.getFusionSpeciesForm(ignoreOverride, doIllusion);
const customTypes = this.customPokemonData.types?.length > 0;
// First type, checking for "permanently changed" types from ME
@ -2378,6 +2577,7 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container {
* @param ignoreAbility Whether to ignore abilities that might affect type effectiveness or immunity (defaults to `false`).
* @param simulated Whether to apply abilities via simulated calls (defaults to `true`)
* @param cancelled {@linkcode BooleanHolder} Stores whether the move was cancelled by a non-type-based immunity.
* @param useIllusion - Whether we want the attack move effectiveness on the illusion or not
* Currently only used by {@linkcode Pokemon.apply} to determine whether a "No effect" message should be shown.
* @returns The type damage multiplier, indicating the effectiveness of the move
*/
@ -2387,6 +2587,7 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container {
ignoreAbility = false,
simulated = true,
cancelled?: BooleanHolder,
useIllusion: boolean = false
): TypeDamageMultiplier {
if (!isNullOrUndefined(this.turnData?.moveEffectiveness)) {
return this.turnData?.moveEffectiveness;
@ -2398,17 +2599,17 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container {
const moveType = source.getMoveType(move);
const typeMultiplier = new NumberHolder(
move.category !== MoveCategory.STATUS ||
move.hasAttr(RespectAttackTypeImmunityAttr)
? this.getAttackTypeEffectiveness(
moveType,
source,
false,
simulated,
move,
)
: 1,
);
move.category !== MoveCategory.STATUS ||
move.hasAttr(RespectAttackTypeImmunityAttr)
? this.getAttackTypeEffectiveness(
moveType,
source,
false,
simulated,
move,
useIllusion
)
: 1);
applyMoveAttrs(
VariableMoveTypeMultiplierAttr,
@ -2512,19 +2713,21 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container {
* @param ignoreStrongWinds whether or not this ignores strong winds (anticipation, forewarn, stealth rocks)
* @param simulated tag to only apply the strong winds effect message when the move is used
* @param move (optional) the move whose type effectiveness is to be checked. Used for applying {@linkcode VariableMoveTypeChartAttr}
* @param useIllusion - Whether we want the attack type effectiveness on the illusion or not
* @returns a multiplier for the type effectiveness
*/
getAttackTypeEffectiveness(
moveType: PokemonType,
source?: Pokemon,
ignoreStrongWinds = false,
simulated = true,
move?: Move,
moveType: PokemonType,
source?: Pokemon,
ignoreStrongWinds: boolean = false,
simulated: boolean = true,
move?: Move,
useIllusion: boolean = false
): TypeDamageMultiplier {
if (moveType === PokemonType.STELLAR) {
return this.isTerastallized ? 2 : 1;
}
const types = this.getTypes(true, true);
const types = this.getTypes(true, true, undefined, useIllusion);
const arena = globalScene.arena;
// Handle flying v ground type immunity without removing flying type so effective types are still effective
@ -2623,7 +2826,8 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container {
*/
getMatchupScore(opponent: Pokemon): number {
const types = this.getTypes(true);
const enemyTypes = opponent.getTypes(true, true);
const enemyTypes = opponent.getTypes(true, true, false, true);
/** Is this Pokemon faster than the opponent? */
const outspeed =
(this.isActive(true)
@ -2634,9 +2838,7 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container {
* Based on how effective this Pokemon's types are offensively against the opponent's types.
* This score is increased by 25 percent if this Pokemon is faster than the opponent.
*/
let atkScore =
opponent.getAttackTypeEffectiveness(types[0], this) *
(outspeed ? 1.25 : 1);
let atkScore = opponent.getAttackTypeEffectiveness(types[0], this, false, true, undefined, true) * (outspeed ? 1.25 : 1);
/**
* Based on how effectively this Pokemon defends against the opponent's types.
* This score cannot be higher than 4.
@ -2648,12 +2850,7 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container {
atkScore *= opponent.getAttackTypeEffectiveness(types[1], this);
}
if (enemyTypes.length > 1) {
defScore *=
1 /
Math.max(
this.getAttackTypeEffectiveness(enemyTypes[1], opponent),
0.25,
);
defScore *= (1 / Math.max(this.getAttackTypeEffectiveness(enemyTypes[1], opponent, false, false, undefined, true), 0.25));
}
/**
* Based on this Pokemon's HP ratio compared to that of the opponent.
@ -5538,7 +5735,13 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container {
this.summonDataPrimer = summonDataPrimer;
}
// For PreSummonAbAttr to get access to summonData
initSummondata(): void {
this.summonData = this.summonData ?? this.summonDataPrimer ?? new PokemonSummonData()
}
resetSummonData(): void {
const illusion: IllusionData | null = this.summonData?.illusion;
if (this.summonData?.speciesForm) {
this.summonData.speciesForm = null;
this.updateFusionPalette();
@ -5574,6 +5777,7 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container {
}
this.summonDataPrimer = null;
}
this.summonData.illusion = illusion
this.updateInfo();
}
@ -7146,15 +7350,11 @@ export class EnemyPokemon extends Pokemon {
) {
targetScore = -20;
} else if (move instanceof AttackMove) {
/**
* Attack moves are given extra multipliers to their base benefit score based on
* the move's type effectiveness against the target and whether the move is a STAB move.
*/
const effectiveness = target.getMoveEffectiveness(
this,
move,
!target.battleData?.abilityRevealed,
);
/**
* Attack moves are given extra multipliers to their base benefit score based on
* the move's type effectiveness against the target and whether the move is a STAB move.
*/
const effectiveness = target.getMoveEffectiveness(this, move, !target.battleData?.abilityRevealed, undefined, undefined, true);
if (target.isPlayer() !== this.isPlayer()) {
targetScore *= effectiveness;
if (this.isOfType(move.type)) {
@ -7543,6 +7743,42 @@ export class EnemyPokemon extends Pokemon {
}
}
/**
* Illusion property
*/
interface IllusionData {
basePokemon: {
/** The actual name of the Pokemon */
name: string;
/** The actual nickname of the Pokemon */
nickname: string;
/** Whether the base pokemon is shiny or not */
shiny: boolean;
/** The shiny variant of the base pokemon */
variant: Variant;
/** Whether the fusion species of the base pokemon is shiny or not */
fusionShiny: boolean;
/** The variant of the fusion species of the base pokemon */
fusionVariant: Variant;
};
/** The species of the illusion */
species: Species;
/** The formIndex of the illusion */
formIndex: number;
/** The gender of the illusion */
gender: Gender;
/** The pokeball of the illusion */
pokeball: PokeballType;
/** The fusion species of the illusion if it's a fusion */
fusionSpecies?: PokemonSpecies;
/** The fusionFormIndex of the illusion */
fusionFormIndex?: number;
/** The fusionGender of the illusion if it's a fusion */
fusionGender?: Gender;
/** The level of the illusion (not used currently) */
level?: number
}
export interface TurnMove {
move: Moves;
targets: BattlerIndex[];
@ -7576,9 +7812,12 @@ export class PokemonSummonData {
public fusionGender: Gender;
public stats: number[] = [0, 0, 0, 0, 0, 0];
public moveset: PokemonMove[];
public illusionBroken: boolean = false;
// If not initialized this value will not be populated from save data.
public types: PokemonType[] = [];
public addedType: PokemonType | null = null;
public illusion: IllusionData | null = null;
}
export class PokemonBattleData {
@ -7589,7 +7828,7 @@ export class PokemonBattleData {
public endured = false;
public berriesEaten: BerryType[] = [];
public abilitiesApplied: Abilities[] = [];
public abilityRevealed = false;
public abilityRevealed: boolean = false;
}
export class PokemonBattleSummonData {

View File

@ -6,9 +6,10 @@ import i18next from "i18next";
/**
* Retrieves the Pokemon's name, potentially with an affix indicating its role (wild or foe) in the current battle context, translated
* @param pokemon {@linkcode Pokemon} name and battle context will be retrieved from this instance
* @param {boolean} useIllusion - Whether we want the name of the illusion or not. Default value : true
* @returns {string} ex: "Wild Gengar", "Ectoplasma sauvage"
*/
export function getPokemonNameWithAffix(pokemon: Pokemon | undefined): string {
export function getPokemonNameWithAffix(pokemon: Pokemon | undefined, useIllusion = true): string {
if (!pokemon) {
return "Missigno";
}
@ -18,19 +19,17 @@ export function getPokemonNameWithAffix(pokemon: Pokemon | undefined): string {
return !pokemon.isPlayer()
? pokemon.hasTrainer()
? i18next.t("battle:foePokemonWithAffix", {
pokemonName: pokemon.getNameToRender(),
pokemonName: pokemon.getNameToRender(useIllusion),
})
: i18next.t("battle:wildPokemonWithAffix", {
pokemonName: pokemon.getNameToRender(),
pokemonName: pokemon.getNameToRender(useIllusion),
})
: pokemon.getNameToRender();
: pokemon.getNameToRender(useIllusion);
case BattleSpec.FINAL_BOSS:
return !pokemon.isPlayer()
? i18next.t("battle:foePokemonWithAffix", {
pokemonName: pokemon.getNameToRender(),
})
: pokemon.getNameToRender();
? i18next.t("battle:foePokemonWithAffix", { pokemonName: pokemon.getNameToRender(useIllusion) })
: pokemon.getNameToRender(useIllusion);
default:
return pokemon.getNameToRender();
return pokemon.getNameToRender(useIllusion);
}
}

View File

@ -1,7 +1,7 @@
import { BattlerIndex, BattleType } from "#app/battle";
import { globalScene } from "#app/global-scene";
import { PLAYER_PARTY_MAX_SIZE } from "#app/constants";
import { applyAbAttrs, SyncEncounterNatureAbAttr } from "#app/data/ability";
import { applyAbAttrs, SyncEncounterNatureAbAttr, applyPreSummonAbAttrs, PreSummonAbAttr } from "#app/data/ability";
import { initEncounterAnims, loadEncounterAnimAssets } from "#app/data/battle-anims";
import { getCharVariantFromDialogue } from "#app/data/dialogue";
import { getEncounterText } from "#app/data/mystery-encounters/utils/encounter-dialogue-utils";
@ -259,6 +259,9 @@ export class EncounterPhase extends BattlePhase {
}
if (e < (battle.double ? 2 : 1)) {
if (battle.battleType === BattleType.WILD) {
for (const pokemon of globalScene.getField()) {
applyPreSummonAbAttrs(PreSummonAbAttr, pokemon, []);
}
globalScene.field.add(enemyPokemon);
battle.seenEnemyPartyMemberIds.add(enemyPokemon.id);
const playerPokemon = globalScene.getPlayerPokemon();
@ -545,7 +548,7 @@ export class EncounterPhase extends BattlePhase {
const enemyField = globalScene.getEnemyField();
enemyField.forEach((enemyPokemon, e) => {
if (enemyPokemon.isShiny()) {
if (enemyPokemon.isShiny(true)) {
globalScene.unshiftPhase(new ShinySparklePhase(BattlerIndex.ENEMY + e));
}
/** This sets Eternatus' held item to be untransferrable, preventing it from being stolen */

View File

@ -71,6 +71,7 @@ export class LevelUpPhase extends PlayerPartyMemberPokemonPhase {
if (!this.pokemon.pauseEvolutions) {
const evolution = this.pokemon.getEvolution();
if (evolution) {
this.pokemon.breakIllusion()
globalScene.unshiftPhase(new EvolutionPhase(this.pokemon, evolution, this.lastLevel));
}
}

View File

@ -13,6 +13,7 @@ import { PostSummonPhase } from "./post-summon-phase";
import { GameOverPhase } from "./game-over-phase";
import { ShinySparklePhase } from "./shiny-sparkle-phase";
import { MysteryEncounterMode } from "#enums/mystery-encounter-mode";
import { applyPreSummonAbAttrs, PreSummonAbAttr } from "#app/data/ability";
import { globalScene } from "#app/global-scene";
export class SummonPhase extends PartyMemberPokemonPhase {
@ -27,6 +28,7 @@ export class SummonPhase extends PartyMemberPokemonPhase {
start() {
super.start();
applyPreSummonAbAttrs(PreSummonAbAttr, this.getPokemon());
this.preSummon();
}
@ -126,7 +128,7 @@ export class SummonPhase extends PartyMemberPokemonPhase {
this.player ? 36 : 248,
this.player ? 80 : 44,
"pb",
getPokeballAtlasKey(pokemon.pokeball),
getPokeballAtlasKey(pokemon.getPokeball(true)),
);
pokeball.setVisible(false);
pokeball.setOrigin(0.5, 0.625);
@ -175,7 +177,11 @@ export class SummonPhase extends PartyMemberPokemonPhase {
}
globalScene.currentBattle.seenEnemyPartyMemberIds.add(pokemon.id);
}
addPokeballOpenParticles(pokemon.x, pokemon.y - 16, pokemon.pokeball);
addPokeballOpenParticles(
pokemon.x,
pokemon.y - 16,
pokemon.getPokeball(true),
);
globalScene.updateModifiers(this.player);
globalScene.updateFieldScale();
pokemon.showInfo();
@ -183,7 +189,7 @@ export class SummonPhase extends PartyMemberPokemonPhase {
pokemon.setVisible(true);
pokemon.getSprite().setVisible(true);
pokemon.setScale(0.5);
pokemon.tint(getPokeballTintColor(pokemon.pokeball));
pokemon.tint(getPokeballTintColor(pokemon.getPokeball(true)));
pokemon.untint(250, "Sine.easeIn");
globalScene.updateFieldScale();
globalScene.tweens.add({
@ -270,7 +276,7 @@ export class SummonPhase extends PartyMemberPokemonPhase {
onEnd(): void {
const pokemon = this.getPokemon();
if (pokemon.isShiny()) {
if (pokemon.isShiny(true)) {
globalScene.unshiftPhase(new ShinySparklePhase(pokemon.getBattlerIndex()));
}

View File

@ -1,5 +1,11 @@
import { globalScene } from "#app/global-scene";
import { applyPreSwitchOutAbAttrs, PostDamageForceSwitchAbAttr, PreSwitchOutAbAttr } from "#app/data/ability";
import {
applyPreSummonAbAttrs,
applyPreSwitchOutAbAttrs,
PostDamageForceSwitchAbAttr,
PreSummonAbAttr,
PreSwitchOutAbAttr,
} from "#app/data/ability";
import { allMoves, ForceSwitchOutAttr } from "#app/data/moves/move";
import { getPokeballTintColor } from "#app/data/pokeball";
import { SpeciesFormChangeActiveTrigger } from "#app/data/pokemon-forms";
@ -99,7 +105,7 @@ export class SwitchSummonPhase extends SummonPhase {
);
globalScene.playSound("se/pb_rel");
pokemon.hideInfo();
pokemon.tint(getPokeballTintColor(pokemon.pokeball), 1, 250, "Sine.easeIn");
pokemon.tint(getPokeballTintColor(pokemon.getPokeball(true)), 1, 250, "Sine.easeIn");
globalScene.tweens.add({
targets: pokemon,
duration: 250,
@ -116,6 +122,7 @@ export class SwitchSummonPhase extends SummonPhase {
const party = this.player ? this.getParty() : globalScene.getEnemyParty();
const switchedInPokemon = 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 =>

View File

@ -1426,12 +1426,11 @@ export class GameData {
),
) // TODO: is this bang correct?
: this.getSessionSaveData();
const maxIntAttrValue = 0x80000000;
const systemData = useCachedSystem
? this.parseSystemData(decrypt(localStorage.getItem(`data_${loggedInUser?.username}`)!, bypassLogin))
: this.getSystemSaveData(); // TODO: is this bang correct?
const request = {
system: systemData,
session: sessionData,
@ -1448,7 +1447,6 @@ export class GameData {
bypassLogin,
),
);
localStorage.setItem(
`sessionData${globalScene.sessionSlotId ? globalScene.sessionSlotId : ""}_${loggedInUser?.username}`,
encrypt(JSON.stringify(sessionData), bypassLogin),

View File

@ -79,12 +79,14 @@ export default class PokemonData {
this.id = source.id;
this.player = sourcePokemon ? sourcePokemon.isPlayer() : source.player;
this.species = sourcePokemon ? sourcePokemon.species.speciesId : source.species;
this.nickname = sourcePokemon ? sourcePokemon.nickname : source.nickname;
this.nickname = sourcePokemon
? (!!sourcePokemon.summonData?.illusion ? sourcePokemon.summonData.illusion.basePokemon.nickname : sourcePokemon.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 = source.shiny;
this.variant = source.variant;
this.shiny = sourcePokemon ? sourcePokemon.isShiny() : source.shiny;
this.variant = sourcePokemon ? sourcePokemon.getVariant() : source.variant;
this.pokeball = source.pokeball;
this.level = source.level;
this.exp = source.exp;
@ -117,8 +119,12 @@ export default class PokemonData {
this.fusionSpecies = sourcePokemon ? sourcePokemon.fusionSpecies?.speciesId : source.fusionSpecies;
this.fusionFormIndex = source.fusionFormIndex;
this.fusionAbilityIndex = source.fusionAbilityIndex;
this.fusionShiny = source.fusionShiny;
this.fusionVariant = source.fusionVariant;
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;
@ -174,6 +180,7 @@ export default class PokemonData {
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));
@ -219,6 +226,7 @@ export default class PokemonData {
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,

View File

@ -356,7 +356,7 @@ export default class BattleInfo extends Phaser.GameObjects.Container {
});
this.teraIcon.on("pointerout", () => globalScene.ui.hideTooltip());
const isFusion = pokemon.isFusion();
const isFusion = pokemon.isFusion(true);
this.splicedIcon.setPositionRelative(
this.nameText,
@ -375,7 +375,7 @@ export default class BattleInfo extends Phaser.GameObjects.Container {
}
const doubleShiny = isFusion && pokemon.shiny && pokemon.fusionShiny;
const baseVariant = !doubleShiny ? pokemon.getVariant() : pokemon.variant;
const baseVariant = !doubleShiny ? pokemon.getVariant(true) : pokemon.variant;
this.shinyIcon.setPositionRelative(
this.nameText,
@ -617,6 +617,11 @@ export default class BattleInfo extends Phaser.GameObjects.Container {
return resolve();
}
const gender: Gender = !!pokemon.summonData?.illusion ? pokemon.summonData?.illusion.gender : pokemon.gender;
this.genderText.setText(getGenderSymbol(gender));
this.genderText.setColor(getGenderColor(gender));
const nameUpdated = this.lastName !== pokemon.getNameToRender();
if (nameUpdated) {
@ -638,8 +643,10 @@ export default class BattleInfo extends Phaser.GameObjects.Container {
this.lastTeraType = teraType;
}
const isFusion = pokemon.isFusion(true);
if (nameUpdated || teraTypeUpdated) {
this.splicedIcon.setVisible(!!pokemon.fusionSpecies);
this.splicedIcon.setVisible(isFusion);
this.teraIcon.setPositionRelative(
this.nameText,
@ -764,7 +771,17 @@ export default class BattleInfo extends Phaser.GameObjects.Container {
this.lastStats = statsStr;
}
this.shinyIcon.setVisible(pokemon.isShiny());
this.shinyIcon.setVisible(pokemon.isShiny(true));
const doubleShiny = isFusion && pokemon.shiny && pokemon.fusionShiny;
const baseVariant = !doubleShiny ? pokemon.getVariant(true) : pokemon.variant;
this.shinyIcon.setTint(getVariantTint(baseVariant));
this.fusionShinyIcon.setVisible(doubleShiny);
if (isFusion) {
this.fusionShinyIcon.setTint(getVariantTint(pokemon.fusionVariant));
}
this.fusionShinyIcon.setPosition(this.shinyIcon.x, this.shinyIcon.y);
resolve();
});
@ -777,10 +794,11 @@ 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;
while (
nameTextWidth >
(this.player || !this.boss ? 60 : 98) -
((pokemon.gender !== Gender.GENDERLESS ? 6 : 0) +
((gender !== Gender.GENDERLESS ? 6 : 0) +
(pokemon.fusionSpecies ? 8 : 0) +
(pokemon.isShiny() ? 8 : 0) +
(Math.min(pokemon.level.toString().length, 3) - 3) * 8)

View File

@ -306,6 +306,9 @@ export default class FightUiHandler extends UiHandler implements InfoToggle {
pokemon,
pokemonMove.getMove(),
!opponent.battleData?.abilityRevealed,
undefined,
undefined,
true
);
if (effectiveness === undefined) {
return undefined;
@ -350,7 +353,7 @@ export default class FightUiHandler extends UiHandler implements InfoToggle {
const moveColors = opponents
.map(opponent =>
opponent.getMoveEffectiveness(pokemon, pokemonMove.getMove(), !opponent.battleData.abilityRevealed),
opponent.getMoveEffectiveness(pokemon, pokemonMove.getMove(), !opponent.battleData.abilityRevealed, undefined, undefined, true),
)
.sort((a, b) => b - a)
.map(effectiveness => getTypeDamageMultiplierColor(effectiveness ?? 0, "offense"));

View File

@ -193,18 +193,14 @@ export default class PartyUiHandler extends MessageUiHandler {
public static FilterNonFainted = (pokemon: PlayerPokemon) => {
if (pokemon.isFainted()) {
return i18next.t("partyUiHandler:noEnergy", {
pokemonName: getPokemonNameWithAffix(pokemon),
});
return i18next.t("partyUiHandler:noEnergy", { pokemonName: getPokemonNameWithAffix(pokemon, false) });
}
return null;
};
public static FilterFainted = (pokemon: PlayerPokemon) => {
if (!pokemon.isFainted()) {
return i18next.t("partyUiHandler:hasEnergy", {
pokemonName: getPokemonNameWithAffix(pokemon),
});
return i18next.t("partyUiHandler:hasEnergy", { pokemonName: getPokemonNameWithAffix(pokemon, false) });
}
return null;
};
@ -218,9 +214,7 @@ export default class PartyUiHandler extends MessageUiHandler {
const challengeAllowed = new BooleanHolder(true);
applyChallenges(ChallengeType.POKEMON_IN_BATTLE, pokemon, challengeAllowed);
if (!challengeAllowed.value) {
return i18next.t("partyUiHandler:cantBeUsed", {
pokemonName: getPokemonNameWithAffix(pokemon),
});
return i18next.t("partyUiHandler:cantBeUsed", { pokemonName: getPokemonNameWithAffix(pokemon, false) });
}
return null;
};
@ -232,9 +226,7 @@ export default class PartyUiHandler extends MessageUiHandler {
m => m instanceof PokemonHeldItemModifier && m.pokemonId === pokemon.id && m.matchType(modifier),
) as PokemonHeldItemModifier;
if (matchingModifier && matchingModifier.stackCount === matchingModifier.getMaxStackCount()) {
return i18next.t("partyUiHandler:tooManyItems", {
pokemonName: getPokemonNameWithAffix(pokemon),
});
return i18next.t("partyUiHandler:tooManyItems", { pokemonName: getPokemonNameWithAffix(pokemon, false) });
}
return null;
};
@ -583,7 +575,7 @@ export default class PartyUiHandler extends MessageUiHandler {
this.showText(
i18next.t(
pokemon.pauseEvolutions ? "partyUiHandler:pausedEvolutions" : "partyUiHandler:unpausedEvolutions",
{ pokemonName: getPokemonNameWithAffix(pokemon) },
{ pokemonName: getPokemonNameWithAffix(pokemon, false) },
),
undefined,
() => this.showText("", 0),
@ -596,14 +588,14 @@ export default class PartyUiHandler extends MessageUiHandler {
this.showText(
i18next.t("partyUiHandler:unspliceConfirmation", {
fusionName: pokemon.fusionSpecies?.name,
pokemonName: pokemon.name,
pokemonName: pokemon.getName(),
}),
null,
() => {
ui.setModeWithoutClear(
Mode.CONFIRM,
() => {
const fusionName = pokemon.name;
const fusionName = pokemon.getName();
pokemon.unfuse().then(() => {
this.clearPartySlots();
this.populatePartySlots();
@ -611,7 +603,7 @@ export default class PartyUiHandler extends MessageUiHandler {
this.showText(
i18next.t("partyUiHandler:wasReverted", {
fusionName: fusionName,
pokemonName: pokemon.name,
pokemonName: pokemon.getName(false),
}),
undefined,
() => {
@ -637,7 +629,7 @@ export default class PartyUiHandler extends MessageUiHandler {
this.blockInput = true;
this.showText(
i18next.t("partyUiHandler:releaseConfirmation", {
pokemonName: getPokemonNameWithAffix(pokemon),
pokemonName: getPokemonNameWithAffix(pokemon, false),
}),
null,
() => {
@ -1285,7 +1277,7 @@ export default class PartyUiHandler extends MessageUiHandler {
doRelease(slotIndex: number): void {
this.showText(
this.getReleaseMessage(getPokemonNameWithAffix(globalScene.getPlayerParty()[slotIndex])),
this.getReleaseMessage(getPokemonNameWithAffix(globalScene.getPlayerParty()[slotIndex], false)),
null,
() => {
this.clearPartySlots();
@ -1495,7 +1487,7 @@ class PartySlot extends Phaser.GameObjects.Container {
const slotInfoContainer = globalScene.add.container(0, 0);
this.add(slotInfoContainer);
let displayName = this.pokemon.getNameToRender();
let displayName = this.pokemon.getNameToRender(false);
let nameTextWidth: number;
const nameSizeTest = addTextObject(0, 0, displayName, TextStyle.PARTY);
@ -1575,12 +1567,12 @@ class PartySlot extends Phaser.GameObjects.Container {
}
if (this.pokemon.isShiny()) {
const doubleShiny = this.pokemon.isFusion() && this.pokemon.shiny && this.pokemon.fusionShiny;
const doubleShiny = this.pokemon.isDoubleShiny(false);
const shinyStar = globalScene.add.image(0, 0, `shiny_star_small${doubleShiny ? "_1" : ""}`);
shinyStar.setOrigin(0, 0);
shinyStar.setPositionRelative(this.slotName, -9, 3);
shinyStar.setTint(getVariantTint(!doubleShiny ? this.pokemon.getVariant() : this.pokemon.variant));
shinyStar.setTint(getVariantTint(this.pokemon.getBaseVariant(doubleShiny)));
slotInfoContainer.add(shinyStar);
@ -1588,7 +1580,9 @@ class PartySlot extends Phaser.GameObjects.Container {
const fusionShinyStar = globalScene.add.image(0, 0, "shiny_star_small_2");
fusionShinyStar.setOrigin(0, 0);
fusionShinyStar.setPosition(shinyStar.x, shinyStar.y);
fusionShinyStar.setTint(getVariantTint(this.pokemon.fusionVariant));
fusionShinyStar.setTint(
getVariantTint(this.pokemon.summonData?.illusion?.basePokemon.fusionVariant ?? this.pokemon.fusionVariant),
);
slotInfoContainer.add(fusionShinyStar);
}

View File

@ -38,7 +38,7 @@ export default class RenameFormUiHandler extends FormModalUiHandler {
if (super.show(args)) {
const config = args[0] as ModalConfig;
if (args[1] && typeof (args[1] as PlayerPokemon).getNameToRender === "function") {
this.inputs[0].text = (args[1] as PlayerPokemon).getNameToRender();
this.inputs[0].text = (args[1] as PlayerPokemon).getNameToRender(false);
} else {
this.inputs[0].text = args[1];
}

View File

@ -357,8 +357,14 @@ export default class SummaryUiHandler extends UiHandler {
this.pokemonSprite.setPipelineData("isTerastallized", this.pokemon.isTerastallized);
this.pokemonSprite.setPipelineData("ignoreTimeTint", true);
this.pokemonSprite.setPipelineData("spriteKey", this.pokemon.getSpriteKey());
this.pokemonSprite.setPipelineData("shiny", this.pokemon.shiny);
this.pokemonSprite.setPipelineData("variant", this.pokemon.variant);
this.pokemonSprite.setPipelineData(
"shiny",
this.pokemon.summonData?.illusion?.basePokemon.shiny ?? this.pokemon.shiny,
);
this.pokemonSprite.setPipelineData(
"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) {
@ -368,7 +374,7 @@ export default class SummaryUiHandler extends UiHandler {
});
this.pokemon.cry();
this.nameText.setText(this.pokemon.getNameToRender());
this.nameText.setText(this.pokemon.getNameToRender(false));
const isFusion = this.pokemon.isFusion();
@ -426,8 +432,8 @@ export default class SummaryUiHandler extends UiHandler {
this.friendshipShadow.setCrop(0, 0, 16, 16 - 16 * ((this.pokemon?.friendship || 0) / 255));
const doubleShiny = isFusion && this.pokemon.shiny && this.pokemon.fusionShiny;
const baseVariant = !doubleShiny ? this.pokemon.getVariant() : this.pokemon.variant;
const doubleShiny = this.pokemon.isDoubleShiny(false);
const baseVariant = this.pokemon.getBaseVariant(doubleShiny);
this.shinyIcon.setPositionRelative(
this.nameText,
@ -435,7 +441,7 @@ export default class SummaryUiHandler extends UiHandler {
3,
);
this.shinyIcon.setTexture(`shiny_star${doubleShiny ? "_1" : ""}`);
this.shinyIcon.setVisible(this.pokemon.isShiny());
this.shinyIcon.setVisible(this.pokemon.isShiny(false));
this.shinyIcon.setTint(getVariantTint(baseVariant));
if (this.shinyIcon.visible) {
const shinyDescriptor =
@ -455,7 +461,9 @@ export default class SummaryUiHandler extends UiHandler {
this.fusionShinyIcon.setPosition(this.shinyIcon.x, this.shinyIcon.y);
this.fusionShinyIcon.setVisible(doubleShiny);
if (isFusion) {
this.fusionShinyIcon.setTint(getVariantTint(this.pokemon.fusionVariant));
this.fusionShinyIcon.setTint(
getVariantTint(this.pokemon.summonData?.illusion?.basePokemon.fusionVariant ?? this.pokemon.fusionVariant),
);
}
this.pokeball.setFrame(getPokeballAtlasKey(this.pokemon.pokeball));
@ -838,7 +846,7 @@ export default class SummaryUiHandler extends UiHandler {
return typeIcon;
};
const types = this.pokemon?.getTypes(false, false, true)!; // TODO: is this bang correct?
const types = this.pokemon?.getTypes(false, false, true, false)!; // TODO: is this bang correct?
profileContainer.add(getTypeIcon(0, types[0]));
if (types.length > 1) {
profileContainer.add(getTypeIcon(1, types[1]));

View File

@ -0,0 +1,144 @@
import { afterEach, beforeAll, beforeEach, describe, expect, it } from "vitest";
import Phaser from "phaser";
import GameManager from "#test/testUtils/gameManager";
import { Species } from "#enums/species";
import { TurnEndPhase } from "#app/phases/turn-end-phase";
import { Moves } from "#enums/moves";
import { Abilities } from "#enums/abilities";
import { PokeballType } from "#app/enums/pokeball";
import { Gender } from "#app/data/gender";
describe("Abilities - Illusion", () => {
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.battleType("single");
game.override.enemySpecies(Species.ZORUA);
game.override.enemyAbility(Abilities.ILLUSION);
game.override.enemyMoveset(Moves.TACKLE);
game.override.enemyHeldItems([{ name: "WIDE_LENS", count: 3 }]);
game.override.moveset([Moves.WORRY_SEED, Moves.SOAK, Moves.TACKLE]);
game.override.startingHeldItems([{ name: "WIDE_LENS", count: 3 }]);
});
it("creates illusion at the start", async () => {
await game.classicMode.startBattle([Species.ZOROARK, Species.AXEW]);
const zoroark = game.scene.getPlayerPokemon()!;
const zorua = game.scene.getEnemyPokemon()!;
expect(!!zoroark.summonData?.illusion).equals(true);
expect(!!zorua.summonData?.illusion).equals(true);
});
it("break after receiving damaging move", async () => {
await game.classicMode.startBattle([Species.AXEW]);
game.move.select(Moves.TACKLE);
await game.phaseInterceptor.to(TurnEndPhase);
const zorua = game.scene.getEnemyPokemon()!;
expect(!!zorua.summonData?.illusion).equals(false);
expect(zorua.name).equals("Zorua");
});
it("break after getting ability changed", async () => {
await game.classicMode.startBattle([Species.AXEW]);
game.move.select(Moves.WORRY_SEED);
await game.phaseInterceptor.to(TurnEndPhase);
const zorua = game.scene.getEnemyPokemon()!;
expect(!!zorua.summonData?.illusion).equals(false);
});
it("break if the ability is suppressed", async () => {
game.override.enemyAbility(Abilities.NEUTRALIZING_GAS);
await game.classicMode.startBattle([Species.KOFFING]);
const zorua = game.scene.getEnemyPokemon()!;
expect(!!zorua.summonData?.illusion).equals(false);
});
it("causes enemy AI to consider the illusion's type instead of the actual type when considering move effectiveness", async () => {
game.override.enemyMoveset([Moves.FLAMETHROWER, Moves.PSYCHIC, Moves.TACKLE]);
await game.classicMode.startBattle([Species.ZOROARK, Species.AXEW]);
const enemy = game.scene.getEnemyPokemon()!;
const zoroark = game.scene.getPlayerPokemon()!;
const flameThrower = enemy.getMoveset()[0]!.getMove();
const psychic = enemy.getMoveset()[1]!.getMove();
const flameThrowerEffectiveness = zoroark.getAttackTypeEffectiveness(
flameThrower.type,
enemy,
undefined,
undefined,
flameThrower,
true,
);
const psychicEffectiveness = zoroark.getAttackTypeEffectiveness(
psychic.type,
enemy,
undefined,
undefined,
psychic,
true,
);
expect(psychicEffectiveness).above(flameThrowerEffectiveness);
});
it("does not break from indirect damage", async () => {
game.override.enemySpecies(Species.GIGALITH);
game.override.enemyAbility(Abilities.SAND_STREAM);
game.override.enemyMoveset(Moves.WILL_O_WISP);
game.override.moveset([Moves.FLARE_BLITZ]);
await game.classicMode.startBattle([Species.ZOROARK, Species.AZUMARILL]);
game.move.select(Moves.FLARE_BLITZ);
await game.phaseInterceptor.to(TurnEndPhase);
const zoroark = game.scene.getPlayerPokemon()!;
expect(!!zoroark.summonData?.illusion).equals(true);
});
it("copies the the name, nickname, gender, shininess, and pokeball from the illusion source", async () => {
game.override.enemyMoveset(Moves.SPLASH);
await game.classicMode.startBattle([Species.ABRA, Species.ZOROARK, Species.AXEW]);
const axew = game.scene.getPlayerParty().at(2)!;
axew.shiny = true;
axew.nickname = btoa(unescape(encodeURIComponent("axew nickname")));
axew.gender = Gender.FEMALE;
axew.pokeball = PokeballType.GREAT_BALL;
game.doSwitchPokemon(1);
await game.phaseInterceptor.to(TurnEndPhase);
const zoroark = game.scene.getPlayerPokemon()!;
expect(zoroark.name).equals("Axew");
expect(zoroark.getNameToRender()).equals("axew nickname");
expect(zoroark.getGender(false, true)).equals(Gender.FEMALE);
expect(zoroark.isShiny(true)).equals(true);
expect(zoroark.getPokeball(true)).equals(PokeballType.GREAT_BALL);
});
});