diff --git a/src/battle-scene.ts b/src/battle-scene.ts index 1712cf9236f..aad3e035d81 100644 --- a/src/battle-scene.ts +++ b/src/battle-scene.ts @@ -884,8 +884,20 @@ export default class BattleScene extends SceneBase { return true; } - public getPlayerParty(): PlayerPokemon[] { - return this.party; + public getPlayerParty(fakeShininess = true): PlayerPokemon[] { + const party = this.party; + if (!fakeShininess) { + party.map(pokemon => { + pokemon.shiny = pokemon.isShiny(); + pokemon.variant = pokemon.getVariant(); + pokemon.name = pokemon.getNameToRender(); + if (pokemon.isFusion()) { + pokemon.fusionVariant = pokemon.battleData?.illusion.basePokemon?.fusionVariant ?? pokemon.fusionVariant; + pokemon.fusionShiny = pokemon.battleData?.illusion.basePokemon?.fusionShiny ?? pokemon.fusionShiny; + } + }); + } + return party; } /** @@ -1137,7 +1149,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 0b4e5ddb2c4..215531db68b 100644 --- a/src/data/ability.ts +++ b/src/data/ability.ts @@ -2246,6 +2246,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 { + globalScene.getField(true).map(pokemon => { + pokemon.breakIllusion(); + }); + return true; + } +} + export class PostSummonUnnamedMessageAbAttr extends PostSummonAbAttr { //Attr doesn't force pokemon name on the message private message: string; @@ -2634,7 +2646,7 @@ export class PostSummonTransformAbAttr extends PostSummonAbAttr { return simulated; } - 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 @@ -2651,6 +2663,10 @@ export class PostSummonTransformAbAttr extends PostSummonAbAttr { } else { target = targets[0]; } + + if (target.battleData.illusion.active) { + return false; + } target = target!; // transforming from or into fusion pokemon causes various problems (including crashes and save corruption) @@ -4450,8 +4466,8 @@ export class MaxMultiHitAbAttr extends AbAttr { } export class PostBattleAbAttr extends AbAttr { - constructor() { - super(true); + constructor(showAbility: boolean = true) { + super(showAbility); } applyPostBattle(pokemon: Pokemon, passive: boolean, simulated: boolean, args: any[]): boolean { @@ -4980,6 +4996,75 @@ 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[]): boolean { + return false; + } +} + +export class IllusionPreSummonAbAttr extends PreSummonAbAttr { + /** + * Apply a new illusion when summoning Zoroark if the illusion is available + * + * @param {Pokemon} pokemon - The Pokémon with the Illusion ability. + * @param {boolean} passive - N/A + * @param {...any} args - N/A + * @returns {boolean} - Whether the illusion was applied. + */ + applyPreSummon(pokemon: Pokemon, passive: boolean, args: any[]): boolean { + if (pokemon.battleData.illusion.available && pokemon.canApplyAbility()) { + return pokemon.generateIllusion(); + } else { + return false; + } + } +} + +export class IllusionBreakAbAttr extends PostDefendAbAttr { + /** + * Destroy illusion if attack move deals damage to zoroark + * + * @param {Pokemon} pokemon - The Pokémon with the Illusion ability. + * @param {boolean} passive - N/A + * @param {Pokemon} attacker - The attacking Pokémon. + * @param {PokemonMove} move - The move being used. + * @param {PokemonMove} hitResult - The type of hitResult the pokemon got + * @param {...any} args - N/A + * @returns {boolean} - Whether the illusion was destroyed. + */ + applyPostDefend(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 ]; + if (!breakIllusion.includes(hitResult)) { + return false; + } + pokemon.breakIllusion(); + return true; + } +} + +export class IllusionPostBattleAbAttr extends PostBattleAbAttr { + /** + * Illusion will be available again after a battle and apply the illusion of the pokemon is already on field + * + * @param {Pokemon} pokemon - The Pokémon with the Illusion ability. + * @param {boolean} passive - N/A + * @param {...any} args - N/A + * @returns {boolean} - Whether the illusion was applied. + */ + applyPostBattle(pokemon: Pokemon, passive: boolean, simulated:boolean, args: any[]): boolean { + pokemon.breakIllusion(); + pokemon.battleData.illusion.available = true; + return true; + } +} + + /** * 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). * @@ -5714,6 +5799,18 @@ export function applyPostSummonAbAttrs( ); } +export function applyPreSummonAbAttrs( + attrType: Constructor, + pokemon: Pokemon, + ...args: any[] +): void { + applyAbAttrsInternal( + attrType, + pokemon, + (attr, passive) => attr.applyPreSummon(pokemon, passive, args), args + ); +} + export function applyPreSwitchOutAbAttrs( attrType: Constructor, pokemon: Pokemon, @@ -6484,7 +6581,13 @@ export function initAbilities() { new Ability(Abilities.ILLUSION, 5) .attr(UncopiableAbilityAbAttr) .attr(UnswappableAbilityAbAttr) - .unimplemented(), + //The pokemon generate an illusion if it's available + .conditionalAttr((pokemon) => pokemon.battleData.illusion.available, IllusionPreSummonAbAttr, false) + //The pokemon loses his illusion when he is damaged by a move + .conditionalAttr((pokemon) => pokemon.battleData.illusion.active, IllusionBreakAbAttr, true) + //Illusion is available again after a battle + .conditionalAttr((pokemon) => pokemon.isAllowedInBattle(), IllusionPostBattleAbAttr, false) + .bypassFaint(), new Ability(Abilities.IMPOSTER, 5) .attr(PostSummonTransformAbAttr) .attr(UncopiableAbilityAbAttr), @@ -6883,6 +6986,8 @@ export function initAbilities() { .attr(UncopiableAbilityAbAttr) .attr(UnswappableAbilityAbAttr) .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 f2157ab65b7..c2d9817db04 100644 --- a/src/data/moves/move.ts +++ b/src/data/moves/move.ts @@ -7332,11 +7332,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; } @@ -8612,6 +8614,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.battleData.illusion.active && !user.battleData.illusion.active) // 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 3cd25e4d10a..4ea6eb2b11e 100644 --- a/src/field/pokemon.ts +++ b/src/field/pokemon.ts @@ -537,21 +537,26 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container { } } - getNameToRender() { + /** + * @param {boolean} fakeName - Whether we want the fake name or the real name of the Pokemon (for Illusion ability). + */ + getNameToRender(fakeName: boolean = true) { + const name: string = (!fakeName && this.battleData?.illusion.active) ? this.battleData?.illusion.basePokemon!.name : this.name; + const nickname: string = (!fakeName && this.battleData?.illusion.active) ? this.battleData?.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; } } init(): void { this.fieldPosition = FieldPosition.CENTER; - + this.resetBattleData(); this.initBattleInfo(); globalScene.fieldUI.addAt(this.battleInfo, 0); @@ -585,7 +590,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(); } } @@ -684,6 +689,94 @@ 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. + * + * @param {Pokemon} pokemon - The Pokemon that will create an illusion. + * @param {Pokemon[]} party - The party of the trainer's pokemon. + */ + generateIllusion(): boolean { + if (this.hasTrainer()) { + const party: Pokemon[] = (this.isPlayer() ? globalScene.getPlayerParty() : globalScene.getEnemyParty()).filter(p => p.isAllowedInBattle()); + const lastPokemon: Pokemon = party.filter(p => p !== this).at(-1) || this; + const speciesId = lastPokemon.species.speciesId; + + if ( lastPokemon === this || this.battleData.illusion.active || + ((speciesId === Species.OGERPON || speciesId === Species.TERAPAGOS) && (lastPokemon.isTerastallized || this.isTerastallized))) { + return false; + } + + this.battleData.illusion = { + active: true, + available: true, + basePokemon: { ...this }, + species: getPokemonSpecies(speciesId), + formIndex: lastPokemon.formIndex, + gender: lastPokemon.gender, + pokeball: lastPokemon.pokeball, + fusionFormIndex: lastPokemon.fusionFormIndex, + fusionSpecies: lastPokemon.fusionSpecies || undefined, + fusionGender: lastPokemon.fusionGender + }; + + this.name = lastPokemon.name; + this.nickname = lastPokemon.nickname; + this.shiny = lastPokemon.shiny; + this.variant = lastPokemon.variant; + this.fusionVariant = lastPokemon.fusionVariant; + this.fusionShiny = lastPokemon.fusionShiny; + if (this.shiny) { + this.initShinySparkle(); + } + this.loadAssets(false, true).then(() => this.playAnim()); + this.updateInfo(); + } else { + let availables: Species[] = []; + let randomIllusion: PokemonSpecies = globalScene.arena.randomSpecies(globalScene.currentBattle.waveIndex, this.level); + if (this.isBoss()) { + availables = [ Species.ENTEI, Species.RAIKOU, Species.SUICUNE ]; + randomIllusion = getPokemonSpecies(availables[this.randSeedInt(availables.length)]); + } + + this.battleData.illusion = { + active: true, + available: true, + basePokemon: { ...this }, + species: randomIllusion, + formIndex: randomIllusion.formIndex, + gender: this.gender + }; + + this.name = randomIllusion.name; + this.loadAssets(false, true).then(() => this.playAnim()); + } + return true; + } + + breakIllusion(): boolean { + console.log("breakIllusion"); + if (!this.battleData?.illusion.active) { + return false; + } + this.name = this.battleData?.illusion.basePokemon!.name; + this.nickname = this.battleData?.illusion.basePokemon!.nickname; + this.shiny = this.battleData?.illusion.basePokemon!.shiny; + this.variant = this.battleData?.illusion.basePokemon!.variant; + this.fusionVariant = this.battleData?.illusion.basePokemon!.fusionVariant; + this.fusionShiny = this.battleData?.illusion.basePokemon!.fusionShiny; + this.battleData.illusion = { active: false, available: false, basePokemon: this }; + 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; @@ -692,54 +785,61 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container { abstract getBattlerIndex(): BattlerIndex; - loadAssets(ignoreOverride = true): Promise { + /** + * @param {boolean} useIllusion - Whether we want the illusion or not. + */ + loadAssets(ignoreOverride: boolean = true, useIllusion: boolean = false): Promise { return new Promise(resolve => { const moveIds = this.getMoveset().map(m => m!.getMove().id); // TODO: is this bang correct? Promise.allSettled(moveIds.map(m => initMoveAnim(m))).then(() => { - loadMoveAnimAssets(moveIds); - this.getSpeciesForm().loadAssets( - this.getGender() === Gender.FEMALE, - this.formIndex, - this.shiny, - this.variant, - ); - if (this.isPlayer() || this.getFusionSpeciesForm()) { - globalScene.loadPokemonAtlas( - this.getBattleSpriteKey(true, ignoreOverride), - this.getBattleSpriteAtlasPath(true, ignoreOverride), + loadMoveAnimAssets(moveIds); + const formIndex = this.battleData?.illusion.active && useIllusion ? this.battleData?.illusion.formIndex : this.formIndex; + this.getSpeciesForm(false, useIllusion).loadAssets( + this.getGender(useIllusion) === Gender.FEMALE, + formIndex, + this.isShiny(useIllusion), + this.getVariant(useIllusion) ); - } - if (this.getFusionSpeciesForm()) { - this.getFusionSpeciesForm().loadAssets( - this.getFusionGender() === Gender.FEMALE, - this.fusionFormIndex, - this.fusionShiny, - this.fusionVariant, - ); - globalScene.loadPokemonAtlas( - this.getFusionBattleSpriteKey(true, ignoreOverride), - this.getFusionBattleSpriteAtlasPath(true, ignoreOverride), - ); - } - globalScene.load.once(Phaser.Loader.Events.COMPLETE, () => { - if (this.isPlayer()) { - const originalWarn = console.warn; - // Ignore warnings for missing frames, because there will be a lot - console.warn = () => {}; - const battleFrameNames = globalScene.anims.generateFrameNames( - this.getBattleSpriteKey(), - { zeroPad: 4, suffix: ".png", start: 1, end: 400 }, + if (this.isPlayer() || this.getFusionSpeciesForm(false, useIllusion)) { + globalScene.loadPokemonAtlas( + this.getBattleSpriteKey(true, ignoreOverride), + this.getBattleSpriteAtlasPath(true, ignoreOverride) ); - console.warn = originalWarn; - if (!globalScene.anims.exists(this.getBattleSpriteKey())) { - globalScene.anims.create({ - key: this.getBattleSpriteKey(), - frames: battleFrameNames, - frameRate: 10, - repeat: -1, - }); - } } + if (this.getFusionSpeciesForm(false, useIllusion)) { + const fusionFormIndex = this.battleData?.illusion.active && useIllusion ? this.battleData?.illusion.fusionFormIndex : this.fusionFormIndex; + const fusionShiny = this.battleData?.illusion.active && !useIllusion ? this.battleData?.illusion.basePokemon!.fusionShiny : this.fusionShiny; + const fusionVariant = this.battleData?.illusion.active && !useIllusion ? this.battleData?.illusion.basePokemon!.fusionVariant : this.fusionVariant; + this.getFusionSpeciesForm(false, useIllusion).loadAssets( + this.getFusionGender(false, useIllusion) === Gender.FEMALE, + fusionFormIndex, + fusionShiny, + fusionVariant + ); + globalScene.loadPokemonAtlas( + this.getFusionBattleSpriteKey(true, ignoreOverride), + this.getFusionBattleSpriteAtlasPath(true, ignoreOverride) + ); + } + globalScene.load.once(Phaser.Loader.Events.COMPLETE, () => { + if (this.isPlayer()) { + const originalWarn = console.warn; + // Ignore warnings for missing frames, because there will be a lot + console.warn = () => {}; + const battleFrameNames = globalScene.anims.generateFrameNames( + this.getBattleSpriteKey(), + { zeroPad: 4, suffix: ".png", start: 1, end: 400 }, + ); + console.warn = originalWarn; + if (!globalScene.anims.exists(this.getBattleSpriteKey())) { + globalScene.anims.create({ + key: this.getBattleSpriteKey(), + frames: battleFrameNames, + frameRate: 10, + repeat: -1, + }); + } + } this.playAnim(); const updateFusionPaletteAndResolve = () => { this.updateFusionPalette(); @@ -908,11 +1008,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.battleData?.illusion.active ? this.battleData?.illusion.formIndex! : this.formIndex; + return this.getSpeciesForm(ignoreOverride, true).getSpriteId( + this.getGender(ignoreOverride, true) === Gender.FEMALE, + formIndex, + this.shiny, + this.variant ); } @@ -920,21 +1021,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.battleData?.illusion.active ? this.battleData?.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.battleData?.illusion.basePokemon?.shiny ?? this.shiny, + this.battleData?.illusion.basePokemon?.variant ?? this.variant ); } @@ -943,11 +1047,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.battleData?.illusion.active ? this.battleData?.illusion.fusionFormIndex! : this.fusionFormIndex; + return this.getFusionSpeciesForm(ignoreOverride, true).getSpriteId( + this.getFusionGender(ignoreOverride, true) === Gender.FEMALE, + fusionFormIndex, + this.fusionShiny, + this.fusionVariant ); } @@ -955,12 +1060,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.battleData?.illusion.active ? this.battleData?.illusion.fusionFormIndex! : this.fusionFormIndex; + + return this.getFusionSpeciesForm(ignoreOverride, true).getSpriteId( + this.getFusionGender(ignoreOverride, true) === Gender.FEMALE, + fusionFormIndex, + this.fusionShiny, + this.fusionVariant, + back ); } @@ -979,62 +1087,78 @@ 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.battleData?.illusion.active ? this.battleData?.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.battleData?.illusion.active ? this.battleData?.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.battleData?.illusion.active ? this.battleData?.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.battleData?.illusion.active ? this.battleData?.illusion.species! : this.species; + + const formIndex: integer = useIllusion && this.battleData?.illusion.active ? this.battleData?.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.battleData?.illusion.active ? this.battleData?.illusion.fusionSpecies! : this.fusionSpecies!; + const fusionFormIndex: integer = useIllusion && this.battleData?.illusion.active ? this.battleData?.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; // TODO: I don't even know how to fix this... A complete cluster of classes involved + null } - return this.fusionSpecies?.forms[this.fusionFormIndex]; + return fusionSpecies?.forms[fusionFormIndex]; } getSprite(): Phaser.GameObjects.Sprite { @@ -1691,36 +1815,94 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container { } } - getGender(ignoreOverride?: boolean): Gender { - if (!ignoreOverride && this.summonData?.gender !== undefined) { + /** + * @param {boolean} fakeGender - Whether we want the fake or real gender (illusion ability). + */ + getGender(ignoreOverride?: boolean, fakeGender: boolean = false): Gender { + if (fakeGender && this.battleData?.illusion.active) { + return this.battleData?.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} fakeGender - Whether we want the fake or real gender (illusion ability). + */ + getFusionGender(ignoreOverride?: boolean, fakeGender: boolean = false): Gender { + if (fakeGender && this.battleData?.illusion.active) { + return this.battleData?.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} fakeShininess - Whether we want the fake or real shininess (illusion ability). + */ + isShiny(fakeShininess: boolean = false): boolean { + if (!fakeShininess && this.battleData?.illusion.active) { + return this.battleData?.illusion.basePokemon?.shiny || (!!this.battleData?.illusion.fusionSpecies && this.battleData?.illusion.basePokemon?.fusionShiny) || false; + } else { + return this.shiny || (this.isFusion(fakeShininess) && this.fusionShiny); + } } - getVariant(): Variant { - return !this.isFusion() - ? this.variant - : (Math.max(this.variant, this.fusionVariant) as Variant); + isDoubleShiny(fakeShininess: boolean = false): boolean { + if (!fakeShininess && this.battleData?.illusion.active) { + return this.isFusion(false) && this.battleData?.illusion.basePokemon!.shiny && this.battleData?.illusion.basePokemon.fusionShiny; + } else { + return this.isFusion(fakeShininess) && this.shiny && this.fusionShiny; + } + } + + /** + * @param {boolean} fakeVariant - Whether we want the fake or real variant (illusion ability). + */ + getVariant(fakeVariant: boolean = false): Variant { + if (!fakeVariant && this.battleData?.illusion.active) { + return !this.isFusion(false) + ? this.battleData?.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.battleData?.illusion.active + ? this.battleData?.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.battleData?.illusion.active) { + return !!this.battleData?.illusion.fusionSpecies; + } else { + return !!this.fusionSpecies; + } + } + + /** + * @param {boolean} fakeName - Whether we want the fake name or the real name of the Pokemon (for Illusion ability). + */ + getName(fakeName: boolean = false): string { + return (!fakeName && this.battleData?.illusion.active && this.battleData?.illusion.basePokemon) + ? this.battleData?.illusion.basePokemon.name + : this.name; } /** @@ -1837,12 +2019,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 fakeType - `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, + fakeType: boolean | "AUTO" = "AUTO" ): PokemonType[] { const types: PokemonType[] = []; @@ -1856,17 +2040,19 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container { } } } - if (!types.length || !includeTeraType) { + + const doIllusion: boolean = (fakeType === "AUTO") ? !forDefend : fakeType; if ( - !ignoreOverride && - this.summonData?.types && - this.summonData.types.length > 0 + !ignoreOverride && + this.summonData?.types && + this.summonData.types.length > 0 && + (!this.battleData?.illusion.active || !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 @@ -2419,15 +2605,17 @@ 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 Utils.BooleanHolder} Stores whether the move was cancelled by a non-type-based immunity. + * @param {boolean} useFakeType - 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 */ getMoveEffectiveness( - source: Pokemon, - move: Move, - ignoreAbility = false, - simulated = true, - cancelled?: Utils.BooleanHolder, + source: Pokemon, + move: Move, + ignoreAbility: boolean = false, + simulated: boolean = true, + cancelled?: Utils.BooleanHolder, + useFakeType: boolean = false ): TypeDamageMultiplier { if (!Utils.isNullOrUndefined(this.turnData?.moveEffectiveness)) { return this.turnData?.moveEffectiveness; @@ -2439,17 +2627,17 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container { const moveType = source.getMoveType(move); const typeMultiplier = new Utils.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, + useFakeType + ) + : 1); applyMoveAttrs( VariableMoveTypeMultiplierAttr, @@ -2553,19 +2741,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 {boolean} useFakeType - 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, + useFakeType: 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, useFakeType); const arena = globalScene.arena; // Handle flying v ground type immunity without removing flying type so effective types are still effective @@ -2666,7 +2856,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) @@ -2677,9 +2868,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. @@ -2691,12 +2880,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. @@ -5603,7 +5787,12 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container { } resetBattleData(): void { + const illusionActive: boolean = this.battleData?.illusion.active; + this.breakIllusion(); this.battleData = new PokemonBattleData(); + if (illusionActive) { + this.generateIllusion(); + } } resetBattleSummonData(): void { @@ -7195,15 +7384,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)) { @@ -7600,6 +7785,61 @@ export class EnemyPokemon extends Pokemon { } } +/** + * Illusion property + */ +interface Illusion { + /** + * Whether the illusion is active or not. + * @type {boolean} + */ + active: boolean; + /** + * Whether the pokemon can generate an illusion or not. + * @type {boolean} + */ + available: boolean; + /** + * The actual Pokemon. + * @type {Pokemon} + */ + basePokemon?: Pokemon; + /** + * The species of the illusion. + * @type {PokemonSpecies} + */ + species?: PokemonSpecies; + /** + * The formIndex of the illusion + * @type {integer} + */ + formIndex?: integer; + /** + * The gender of the illusion + * @type {Gender} + */ + gender?: Gender; + /** + * The pokeball of the illusion. + */ + pokeball?: PokeballType; + /** + * The fusionned species of the illusion if it's a fusion. + * @type {PokemonSpecies} + */ + fusionSpecies?: PokemonSpecies; + /** + * The fusionFormIndex of the illusion + * @type {integer} + */ + fusionFormIndex?: integer; + /** + * The fusionGender of the illusion if it's a fusion + * @type {Gender} + */ + fusionGender?: Gender; +} + export interface TurnMove { move: Moves; targets: BattlerIndex[]; @@ -7646,7 +7886,8 @@ export class PokemonBattleData { public endured = false; public berriesEaten: BerryType[] = []; public abilitiesApplied: Abilities[] = []; - public abilityRevealed = false; + public abilityRevealed: boolean = false; + public illusion: Illusion = { active: false, available: true }; } export class PokemonBattleSummonData { diff --git a/src/messages.ts b/src/messages.ts index e35b48f7226..3cf49709b0d 100644 --- a/src/messages.ts +++ b/src/messages.ts @@ -8,7 +8,7 @@ import i18next from "i18next"; * @param pokemon {@linkcode Pokemon} name and battle context will be retrieved from this instance * @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 +18,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 5decab522b5..894346fcdf1 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,10 @@ export class EncounterPhase extends BattlePhase { } if (e < (battle.double ? 2 : 1)) { if (battle.battleType === BattleType.WILD) { + for (const pokemon of globalScene.getPlayerField()) { + applyPreSummonAbAttrs(PreSummonAbAttr, pokemon, []); + } + applyPreSummonAbAttrs(PreSummonAbAttr, enemyPokemon, []); globalScene.field.add(enemyPokemon); battle.seenEnemyPartyMemberIds.add(enemyPokemon.id); const playerPokemon = globalScene.getPlayerPokemon(); @@ -545,7 +549,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/summon-phase.ts b/src/phases/summon-phase.ts index 31cd2645e68..ae179c26248 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.battleData.illusion.pokeball ?? pokemon.pokeball), ); 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.battleData.illusion.pokeball ?? pokemon.pokeball, + ); 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.battleData.illusion.pokeball ?? pokemon.pokeball)); pokemon.untint(250, "Sine.easeIn"); globalScene.updateFieldScale(); globalScene.tweens.add({ @@ -266,7 +272,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 48bcd0c4ebd..8edbeb63e4d 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.battleData.illusion.pokeball ?? pokemon.pokeball), 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 82ad2276fef..5a867bf864f 100644 --- a/src/system/game-data.ts +++ b/src/system/game-data.ts @@ -1010,7 +1010,7 @@ export class GameData { seed: globalScene.seed, playTime: globalScene.sessionPlayTime, gameMode: globalScene.gameMode.modeId, - party: globalScene.getPlayerParty().map(p => new PokemonData(p)), + party: globalScene.getPlayerParty(false).map(p => new PokemonData(p)), enemyParty: globalScene.getEnemyParty().map(p => new PokemonData(p)), modifiers: globalScene.findModifiers(() => true).map(m => new PersistentModifierData(m, true)), enemyModifiers: globalScene.findModifiers(() => true, false).map(m => new PersistentModifierData(m, false)), diff --git a/src/ui/battle-info.ts b/src/ui/battle-info.ts index 355ab9167a1..fd916dd5947 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.battleData.illusion.active ? pokemon.battleData.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.battleData.illusion.active ? pokemon.battleData.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/party-ui-handler.ts b/src/ui/party-ui-handler.ts index 21e5f9077f4..d16bea57131 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 Utils.BooleanHolder(true); applyChallenges(globalScene.gameMode, 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; }; @@ -585,7 +577,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), @@ -598,14 +590,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(); @@ -613,7 +605,7 @@ export default class PartyUiHandler extends MessageUiHandler { this.showText( i18next.t("partyUiHandler:wasReverted", { fusionName: fusionName, - pokemonName: pokemon.name, + pokemonName: pokemon.getName(false), }), undefined, () => { @@ -639,7 +631,7 @@ export default class PartyUiHandler extends MessageUiHandler { this.blockInput = true; this.showText( i18next.t("partyUiHandler:releaseConfirmation", { - pokemonName: getPokemonNameWithAffix(pokemon), + pokemonName: getPokemonNameWithAffix(pokemon, false), }), null, () => { @@ -1287,7 +1279,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(); @@ -1497,7 +1489,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); @@ -1577,12 +1569,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); @@ -1590,7 +1582,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.battleData.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 d42572cfef4..f2294e38157 100644 --- a/src/ui/summary-ui-handler.ts +++ b/src/ui/summary-ui-handler.ts @@ -348,8 +348,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.battleData.illusion.basePokemon?.shiny ?? this.pokemon.shiny, + ); + this.pokemonSprite.setPipelineData( + "variant", + this.pokemon.battleData.illusion.basePokemon?.variant ?? this.pokemon.variant, + ); ["spriteColors", "fusionSpriteColors"].map(k => { delete this.pokemonSprite.pipelineData[`${k}Base`]; if (this.pokemon?.summonData?.speciesForm) { @@ -359,7 +365,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(); @@ -417,8 +423,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, @@ -426,7 +432,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 = @@ -446,7 +452,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.battleData.illusion.basePokemon?.fusionVariant ?? this.pokemon.fusionVariant), + ); } this.pokeball.setFrame(getPokeballAtlasKey(this.pokemon.pokeball)); @@ -829,7 +837,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..aff8b642f89 --- /dev/null +++ b/test/abilities/illusion.test.ts @@ -0,0 +1,152 @@ +import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest"; +import Phaser from "phaser"; +import GameManager from "#test/testUtils/gameManager"; +import overrides from "#app/overrides"; +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, Moves.TACKLE, Moves.TACKLE, Moves.TACKLE]); + game.override.enemyHeldItems([{ name: "WIDE_LENS", count: 3 }]); + + game.override.moveset([Moves.WORRY_SEED, Moves.SOAK, Moves.TACKLE, 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.battleData.illusion.active).equals(true); + expect(zorua.battleData.illusion.active).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.battleData.illusion.active).equals(false); + expect(zorua.battleData.illusion.available).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.battleData.illusion.active).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.battleData.illusion.active).equals(false); + }); + + it("trick the enemy AI for moves effectiveness using ILLUSION type instead of actual type", async () => { + game.override.enemyMoveset([Moves.FLAMETHROWER, Moves.PSYCHIC, Moves.TACKLE, 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("do not breaks if the pokemon takes indirect damages", async () => { + game.override.enemySpecies(Species.GIGALITH); + game.override.enemyAbility(Abilities.SAND_STREAM); + game.override.enemyMoveset([Moves.WILL_O_WISP, Moves.WILL_O_WISP, Moves.WILL_O_WISP, 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.battleData.illusion.active).equals(true); + }); + + it("copy the the name, the nickname, the gender, the shininess and the pokeball of the pokemon", async () => { + vi.spyOn(overrides, "OPP_MOVESET_OVERRIDE", "get").mockReturnValue([ + Moves.SCARY_FACE, + Moves.SCARY_FACE, + Moves.SCARY_FACE, + Moves.SCARY_FACE, + ]); + + 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.battleData.illusion.pokeball).equals(PokeballType.GREAT_BALL); + }); +});