diff --git a/src/battle-scene.ts b/src/battle-scene.ts index 8ae2be5af43..dd983f2b397 100644 --- a/src/battle-scene.ts +++ b/src/battle-scene.ts @@ -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); diff --git a/src/data/ability.ts b/src/data/ability.ts index b07f13c18e9..3e32a624f9f 100644 --- a/src/data/ability.ts +++ b/src/data/ability.ts @@ -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, + pokemon: Pokemon, + ...args: any[] +): void { + applyAbAttrsInternal( + attrType, + pokemon, + (attr, passive) => attr.applyPreSummon(pokemon, passive, args), + (attr, passive) => attr.canApplyPreSummon(pokemon, passive, args), + args + ); +} + export function applyPreSwitchOutAbAttrs( attrType: Constructor, 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) diff --git a/src/data/moves/move.ts b/src/data/moves/move.ts index a0f68dcd5cb..962a13bb840 100644 --- a/src/data/moves/move.ts +++ b/src/data/moves/move.ts @@ -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(), diff --git a/src/field/pokemon.ts b/src/field/pokemon.ts index 8fc75ca657d..b59b7ba01fe 100644 --- a/src/field/pokemon.ts +++ b/src/field/pokemon.ts @@ -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 { + /** +   * @param useIllusion - Whether we want the illusion or not. +   */ + async loadAssets(ignoreOverride = true, useIllusion: boolean = false): Promise { /** Promises that are loading assets and can be run concurrently. */ const loadPromises: Promise[] = []; // 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 { diff --git a/src/messages.ts b/src/messages.ts index e35b48f7226..c29151a98b3 100644 --- a/src/messages.ts +++ b/src/messages.ts @@ -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); } } diff --git a/src/phases/encounter-phase.ts b/src/phases/encounter-phase.ts index 9e5edf3e1d9..15f3d102e41 100644 --- a/src/phases/encounter-phase.ts +++ b/src/phases/encounter-phase.ts @@ -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 */ diff --git a/src/phases/level-up-phase.ts b/src/phases/level-up-phase.ts index 31c7fabf451..c6ca17d583e 100644 --- a/src/phases/level-up-phase.ts +++ b/src/phases/level-up-phase.ts @@ -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)); } } diff --git a/src/phases/summon-phase.ts b/src/phases/summon-phase.ts index 621c8c8c2a9..7379d509e55 100644 --- a/src/phases/summon-phase.ts +++ b/src/phases/summon-phase.ts @@ -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())); } diff --git a/src/phases/switch-summon-phase.ts b/src/phases/switch-summon-phase.ts index e0903ada275..d63cdb90f25 100644 --- a/src/phases/switch-summon-phase.ts +++ b/src/phases/switch-summon-phase.ts @@ -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 => diff --git a/src/system/game-data.ts b/src/system/game-data.ts index 63955b02de8..53146301666 100644 --- a/src/system/game-data.ts +++ b/src/system/game-data.ts @@ -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), diff --git a/src/system/pokemon-data.ts b/src/system/pokemon-data.ts index 7cdcb0c72c3..97ce494a43a 100644 --- a/src/system/pokemon-data.ts +++ b/src/system/pokemon-data.ts @@ -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, diff --git a/src/ui/battle-info.ts b/src/ui/battle-info.ts index 2b205329ab8..06c5f7fb3f1 100644 --- a/src/ui/battle-info.ts +++ b/src/ui/battle-info.ts @@ -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) diff --git a/src/ui/fight-ui-handler.ts b/src/ui/fight-ui-handler.ts index 3775dbc2228..27985629e3d 100644 --- a/src/ui/fight-ui-handler.ts +++ b/src/ui/fight-ui-handler.ts @@ -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")); diff --git a/src/ui/party-ui-handler.ts b/src/ui/party-ui-handler.ts index 61a98d79fbb..ba90108c274 100644 --- a/src/ui/party-ui-handler.ts +++ b/src/ui/party-ui-handler.ts @@ -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); } diff --git a/src/ui/rename-form-ui-handler.ts b/src/ui/rename-form-ui-handler.ts index 91c0025d283..7083f83865b 100644 --- a/src/ui/rename-form-ui-handler.ts +++ b/src/ui/rename-form-ui-handler.ts @@ -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]; } diff --git a/src/ui/summary-ui-handler.ts b/src/ui/summary-ui-handler.ts index 1e0924aa2c5..04bcf71d7ae 100644 --- a/src/ui/summary-ui-handler.ts +++ b/src/ui/summary-ui-handler.ts @@ -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])); diff --git a/test/abilities/illusion.test.ts b/test/abilities/illusion.test.ts new file mode 100644 index 00000000000..aa77aa701b2 --- /dev/null +++ b/test/abilities/illusion.test.ts @@ -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); + }); +});