diff --git a/package-lock.json b/package-lock.json index 0605b299dab..35089ea0f62 100644 --- a/package-lock.json +++ b/package-lock.json @@ -6148,4 +6148,4 @@ } } } -} +} \ No newline at end of file diff --git a/public/images/items.json b/public/images/items.json index 86792e23cc0..f6d14d18dbb 100644 --- a/public/images/items.json +++ b/public/images/items.json @@ -8396,4 +8396,4 @@ "version": "3.0", "smartupdate": "$TexturePacker:SmartUpdate:b317e2cd3502364fcdae296cd439ac4d:ae80196191516a8cb098a8467c6faa2f:110e074689c9edd2c54833ce2e4d9270$" } -} +} \ No newline at end of file diff --git a/src/battle-scene.ts b/src/battle-scene.ts index 93b05a39ecb..b0967dfebda 100644 --- a/src/battle-scene.ts +++ b/src/battle-scene.ts @@ -851,7 +851,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 old mode 100644 new mode 100755 index 491a14ba621..f3e8270f17b --- a/src/data/ability.ts +++ b/src/data/ability.ts @@ -1885,7 +1885,6 @@ export class PostSummonRemoveArenaTagAbAttr extends PostSummonAbAttr { return true; } } - export class PostSummonMessageAbAttr extends PostSummonAbAttr { private messageFunc: (pokemon: Pokemon) => string; @@ -1898,6 +1897,11 @@ export class PostSummonMessageAbAttr extends PostSummonAbAttr { applyPostSummon(pokemon: Pokemon, passive: boolean, args: any[]): boolean { pokemon.scene.queueMessage(this.messageFunc(pokemon)); + pokemon.scene.getField(true).map(pokemon => { + if (pokemon.breakIllusion()) { + pokemon.scene.queueMessage(i18next.t("abilityTriggers:illusionBreak", { pokemonName: getPokemonNameWithAffix(pokemon) })); + } + }); return true; } } @@ -2262,6 +2266,10 @@ export class PostSummonTransformAbAttr extends PostSummonAbAttr { target = targets[0]; } + if (target.illusion.active) { + return false; + } + pokemon.summonData.speciesForm = target.getSpeciesForm(); pokemon.summonData.fusionSpeciesForm = target.getFusionSpeciesForm(); pokemon.summonData.ability = target.getAbility().id; @@ -3554,8 +3562,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, args: any[]): boolean { @@ -4041,6 +4049,97 @@ export class IceFaceBlockPhysicalAbAttr extends ReceivedMoveDamageMultiplierAbAt } } +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 { + let suppressed = false; + pokemon.scene.getField(true).filter(p => p !== pokemon).map(p => { + if (p.getAbility().hasAttr(SuppressFieldAbilitiesAbAttr) && p.canApplyAbility()) { + suppressed = true; + } + if (p.getPassiveAbility().hasAttr(SuppressFieldAbilitiesAbAttr) && p.canApplyAbility(true)) { + suppressed = true; + } + }); + + if (pokemon.illusion.available && !suppressed) { + 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, 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(); + pokemon.scene.queueMessage(i18next.t("abilityTriggers:illusionBreak", { pokemonName: getPokemonNameWithAffix(pokemon) })); + return true; + } +} + +export class IllusionAfterBattle 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, args: any[]): boolean { + pokemon.breakIllusion(); + pokemon.illusion.available = true; + return true; + } +} + +export class IllusionDisableAbAttr extends PostSummonAbAttr { + /** + * Illusion will be disabled if the pokemon is summoned with an illusion. + * So the pokemon can use 1 illusion per battle. + * + * @param {Pokemon} pokemon - The Pokémon with the Illusion ability. + * @param {boolean} passive - N/A + * @param {...any} args - N/A + * @returns {boolean} + */ + applyPostSummon(pokemon: Pokemon, passive: boolean, args: any[]): boolean { + pokemon.illusion.available = false; + 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). * @@ -4216,6 +4315,11 @@ export function applyPostSummonAbAttrs(attrType: Constructor, return applyAbAttrsInternal(attrType, pokemon, (attr, passive) => attr.applyPostSummon(pokemon, passive, args), args); } +export function applyPreSummonAbAttrs(attrType: Constructor, + pokemon: Pokemon, ...args: any[]) { + return applyAbAttrsInternal(attrType, pokemon, (attr, passive) => attr.applyPreSummon(pokemon, passive, args), args); +} + export function applyPreSwitchOutAbAttrs(attrType: Constructor, pokemon: Pokemon, ...args: any[]): Promise { return applyAbAttrsInternal(attrType, pokemon, (attr, passive) => attr.applyPreSwitchOut(pokemon, passive, args), args, true); @@ -4759,7 +4863,15 @@ 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.illusion.available, IllusionPreSummonAbAttr, false) + //The pokemon loses his illusion when he is damaged by a move + .conditionalAttr((pokemon) => pokemon.illusion.active, IllusionBreakAbAttr, true) + //Illusion is available again after a battle + .conditionalAttr((pokemon) => pokemon.isAllowedInBattle(), IllusionAfterBattle, false) + //Illusion is not available after summon + .attr(IllusionDisableAbAttr, false) + .bypassFaint(), new Ability(Abilities.IMPOSTER, 5) .attr(PostSummonTransformAbAttr) .attr(UncopiableAbilityAbAttr), diff --git a/src/data/move.ts b/src/data/move.ts index f90aef585d6..d68267dff37 100644 --- a/src/data/move.ts +++ b/src/data/move.ts @@ -5486,10 +5486,12 @@ export class AbilityChangeAttr extends MoveEffectAttr { return false; } - (this.selfTarget ? user : target).summonData.ability = this.ability; - - user.scene.queueMessage(i18next.t("moveTriggers:acquiredAbility", {pokemonName: getPokemonNameWithAffix((this.selfTarget ? user : target)), abilityName: allAbilities[this.ability].name})); - + const pokemon: Pokemon = this.selfTarget ? user : target; + pokemon.summonData.ability = this.ability; + if (pokemon.breakIllusion()) { + pokemon.scene.queueMessage(i18next.t("abilityTriggers:illusionBreak", { pokemonName: getPokemonNameWithAffix(pokemon) })); + } + user.scene.queueMessage(i18next.t("moveTriggers:acquiredAbility", {pokemonName: getPokemonNameWithAffix(pokemon), abilityName: allAbilities[this.ability].name})); return true; } @@ -5641,7 +5643,8 @@ export class SuppressAbilitiesIfActedAttr extends MoveEffectAttr { export class TransformAttr extends MoveEffectAttr { apply(user: Pokemon, target: Pokemon, move: Move, args: any[]): Promise { return new Promise(resolve => { - if (!super.apply(user, target, move, args)) { + if (!super.apply(user, target, move, args) || target.illusion.active || user.illusion.active) { + user.scene.queueMessage(i18next.t("battle:attackFailed")); return resolve(false); } diff --git a/src/field/pokemon.ts b/src/field/pokemon.ts index 71e89d60cbd..9224bd98cbb 100644 --- a/src/field/pokemon.ts +++ b/src/field/pokemon.ts @@ -63,6 +63,7 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container { public name: string; public nickname: string; public species: PokemonSpecies; + public illusion: Illusion; public formIndex: integer; public abilityIndex: integer; public passive: boolean; @@ -126,6 +127,8 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container { const randAbilityIndex = Utils.randSeedInt(2); this.species = species; + this.illusion = {active: false, available: true}; + this.pokeball = dataSource?.pokeball || PokeballType.POKEBALL; this.level = level; // Determine the ability index @@ -240,15 +243,17 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container { } - getNameToRender() { + getNameToRender(useIllusion: boolean = true) { + const name: string = (!useIllusion && this.illusion.active) ? this.illusion.name : this.name; + const nickname: string = (!useIllusion && this.illusion.active) ? this.illusion.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; } } @@ -276,7 +281,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(); } } @@ -337,6 +342,113 @@ 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() ? this.scene.getParty() : this.scene.getEnemyParty()).filter(p => p.isAllowedInBattle()); + const lastPokemon: Pokemon = party.at(-1); + const speciesId = lastPokemon.species.speciesId; + + if ( lastPokemon === this || this.illusion.active || + ((speciesId === Species.OGERPON || speciesId === Species.TERAPAGOS) && (lastPokemon.isTerastallized() || this.isTerastallized()))) { + return false; + } + + this.illusion = { + active: true, + available: true, + name: this.name, + nickname: this.nickname, + species: getPokemonSpecies(speciesId), + formIndex: lastPokemon.formIndex, + shiny: this.shiny, + variant: this.variant, + shinySparkle: this.shinySparkle, + gender: lastPokemon.gender, + pokeball: lastPokemon.pokeball, + fusionFormIndex: lastPokemon.fusionFormIndex, + fusionSpecies: lastPokemon.fusionSpecies, + fusionVariant: this.fusionVariant, + fusionShiny: this.fusionShiny, + 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()); + } else { + let availables: Species[] = []; + if (this.isBoss()) { + availables = [Species.ENTEI, Species.RAIKOU, Species.SUICUNE]; + } else { + const area: Species[] = [ + Species.HOUNDOUR, Species.SABLEYE, Species.PURRLOIN, Species.PAWNIARD, Species.NICKIT, + Species.IMPIDIMP, Species.MASCHIFF, Species.ABSOL, Species.SPIRITOMB, Species.DEINO, + ]; + + for (let species of area) { + for (const evolutionLevel of getPokemonSpecies(species).getEvolutionLevels()) { + if (evolutionLevel[1] && evolutionLevel[1] <= this.level) { + species = evolutionLevel[0]; + } + } + availables.push(species); + } + availables.push(this.species.name === getPokemonSpecies(Species.ZORUA).name ? Species.MURKROW : Species.HONCHKROW); + } + const randomIllusion: PokemonSpecies = getPokemonSpecies(availables[this.randSeedInt(availables.length)]); + + this.illusion = { + active: true, + available: true, + species: randomIllusion, + name: this.name, + shiny: this.shiny, + variant: this.variant, + shinySparkle: this.shinySparkle, + gender: this.gender + }; + this.name = randomIllusion.name; + this.loadAssets(false, true).then(() => this.playAnim()); + } + return true; + } + + breakIllusion(): boolean { + if (!this.illusion.active) { + return false; + } + + this.name = this.illusion.name; + this.nickname = this.illusion.nickname; + this.shiny = this.illusion.shiny; + this.variant = this.illusion.variant; + this.fusionVariant = this.illusion.fusionVariant; + this.fusionShiny = this.illusion.fusionShiny; + this.illusion = {active: false, available: false}; + if (this.isOnField) { + this.scene.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; @@ -345,18 +457,25 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container { abstract getBattlerIndex(): BattlerIndex; - loadAssets(ignoreOverride: boolean = 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); Promise.allSettled(moveIds.map(m => initMoveAnim(this.scene, m))) .then(() => { loadMoveAnimAssets(this.scene, moveIds); - this.getSpeciesForm().loadAssets(this.scene, this.getGender() === Gender.FEMALE, this.formIndex, this.shiny, this.variant); - if (this.isPlayer() || this.getFusionSpeciesForm()) { + const formIndex = this.illusion.active && useIllusion ? this.illusion.formIndex : this.formIndex; + this.getSpeciesForm(false, useIllusion).loadAssets(this.scene, this.getGender(useIllusion) === Gender.FEMALE, formIndex, this.isShiny(useIllusion), this.getVariant(useIllusion)); + if (this.isPlayer() || this.getFusionSpeciesForm(false, useIllusion)) { this.scene.loadPokemonAtlas(this.getBattleSpriteKey(true, ignoreOverride), this.getBattleSpriteAtlasPath(true, ignoreOverride)); } - if (this.getFusionSpeciesForm()) { - this.getFusionSpeciesForm().loadAssets(this.scene, this.getFusionGender() === Gender.FEMALE, this.fusionFormIndex, this.fusionShiny, this.fusionVariant); + if (this.getFusionSpeciesForm(false, useIllusion)) { + const fusionFormIndex = this.illusion.active && useIllusion ? this.illusion.fusionFormIndex : this.fusionFormIndex; + const fusionShiny = this.illusion.active && !useIllusion ? this.illusion.fusionShiny : this.fusionShiny; + const fusionVariant = this.illusion.active && !useIllusion ? this.illusion.fusionVariant : this.fusionVariant; + this.getFusionSpeciesForm(false, useIllusion).loadAssets(this.scene, this.getFusionGender(false, useIllusion) === Gender.FEMALE, fusionFormIndex, fusionShiny, fusionVariant); this.scene.loadPokemonAtlas(this.getFusionBattleSpriteKey(true, ignoreOverride), this.getFusionBattleSpriteAtlasPath(true, ignoreOverride)); } this.scene.load.once(Phaser.Loader.Events.COMPLETE, () => { @@ -456,18 +575,27 @@ 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.illusion.active ? this.illusion.formIndex : this.formIndex; + return this.getSpeciesForm(ignoreOverride, true).getSpriteId(this.getGender(ignoreOverride, true) === Gender.FEMALE, formIndex, this.shiny, this.variant); } getBattleSpriteId(back?: boolean, ignoreOverride?: boolean): string { if (back === undefined) { back = this.isPlayer(); } - return this.getSpeciesForm(ignoreOverride).getSpriteId(this.getGender(ignoreOverride) === Gender.FEMALE, this.formIndex, this.shiny, this.variant, back); + + const formIndex: integer = this.illusion.active ? this.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(this.getGender(ignoreOverride) === Gender.FEMALE, this.formIndex, this.shiny, this.variant); + return this.getSpeciesForm(ignoreOverride, false).getSpriteKey( + this.getGender(ignoreOverride) === Gender.FEMALE, + this.formIndex, + this.illusion.shiny ?? this.shiny, + this.illusion.variant ?? this.variant + ); } getBattleSpriteKey(back?: boolean, ignoreOverride?: boolean): string { @@ -475,14 +603,18 @@ 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.illusion.active ? this.illusion.fusionFormIndex : this.fusionFormIndex; + return this.getFusionSpeciesForm(ignoreOverride, true).getSpriteId(this.getFusionGender(ignoreOverride, true) === Gender.FEMALE, fusionFormIndex, this.fusionShiny, this.fusionVariant); } getFusionBattleSpriteId(back?: boolean, ignoreOverride?: boolean): string { if (back === undefined) { back = this.isPlayer(); } - return this.getFusionSpeciesForm(ignoreOverride).getSpriteId(this.getFusionGender(ignoreOverride) === Gender.FEMALE, this.fusionFormIndex, this.fusionShiny, this.fusionVariant, back); + + const fusionFormIndex: integer = this.illusion.active ? this.illusion.fusionFormIndex : this.fusionFormIndex; + + return this.getFusionSpeciesForm(ignoreOverride, true).getSpriteId(this.getFusionGender(ignoreOverride, true) === Gender.FEMALE, fusionFormIndex, this.fusionShiny, this.fusionVariant, back); } getFusionBattleSpriteKey(back?: boolean, ignoreOverride?: boolean): string { @@ -494,39 +626,56 @@ 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.illusion.active ? this.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.illusion.active ? this.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.illusion.active ? this.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.illusion.active ? this.illusion.species : this.species; + const formIndex: integer = useIllusion && this.illusion.active ? this.illusion.formIndex : this.formIndex; + if (!ignoreOverride && this.summonData?.speciesForm) { return this.summonData.speciesForm; } - if (!this.species.forms?.length) { - return this.species; + + if (!species.forms?.length || formIndex === undefined) { + return species; } - return this.species.forms[this.formIndex]; + + return species.forms[formIndex]; } - 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.illusion.active ? this.illusion.fusionSpecies : this.fusionSpecies; + const fusionFormIndex: integer = useIllusion && this.illusion.active ? this.illusion.fusionFormIndex : this.fusionFormIndex; + if (!ignoreOverride && this.summonData?.speciesForm) { return this.summonData.fusionSpeciesForm; } - if (!this.fusionSpecies?.forms?.length || this.fusionFormIndex >= this.fusionSpecies?.forms.length) { - return this.fusionSpecies; + if (!fusionSpecies?.forms?.length || fusionFormIndex >= fusionSpecies?.forms.length) { + return fusionSpecies; } - return this.fusionSpecies?.forms[this.fusionFormIndex]; + return fusionSpecies?.forms[fusionFormIndex]; } getSprite(): Phaser.GameObjects.Sprite { @@ -835,34 +984,77 @@ 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 gender of the illusion or not. + */ + getGender(ignoreOverride?: boolean, useIllusion: boolean = false): Gender { + if (useIllusion && this.illusion.active) { + return this.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 fusionGender of the illusion or not. + */ + getFusionGender(ignoreOverride?: boolean, useIllusion: boolean = false): Gender { + if (useIllusion && this.illusion.active) { + return this.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); + isShiny(useIllusion: boolean = false): boolean { + if (!useIllusion && this.illusion.active) { + return this.illusion.shiny || (!!this.illusion.fusionSpecies && this.illusion.fusionShiny); + } else { + return this.shiny || (this.isFusion(useIllusion) && this.fusionShiny); + } } - getVariant(): Variant { - return !this.isFusion() ? this.variant : Math.max(this.variant, this.fusionVariant) as Variant; + isDoubleShiny(useIllusion: boolean = false): boolean { + if (!useIllusion && this.illusion.active) { + return this.isFusion(false) && this.illusion.shiny && this.illusion.fusionShiny; + } else { + return this.isFusion(useIllusion) && this.shiny && this.fusionShiny; + } + } + + getVariant(useIllusion: boolean = false): Variant { + if (!useIllusion && this.illusion.active) { + return !this.isFusion(false) ? this.illusion.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.illusion.active ? this.illusion.variant : this.variant; + } else { + return this.getVariant(); + } } getLuck(): integer { return this.luck + (this.isFusion() ? this.fusionLuck : 0); } - isFusion(): boolean { - return !!this.fusionSpecies; + isFusion(useIllusion: boolean = false): boolean { + if (useIllusion && this.illusion.active) { + return !!this.illusion.fusionSpecies; + } else { + return !!this.fusionSpecies; + } + } + + getName(illusion: boolean = false): string { + return (!illusion && this.illusion.active) ? this.illusion.name : this.name; } abstract isBoss(): boolean; @@ -894,30 +1086,29 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container { /** * Gets the types of a pokemon - * @param includeTeraType boolean to include tera-formed type, default false - * @param forDefend boolean if the pokemon is defending from an attack - * @param ignoreOverride boolean if true, ignore ability changing effects + * @param {boolean} includeTeraType to include tera-formed type, default false + * @param {boolean} forDefend if the pokemon is defending from an attack + * @param {boolean} ignoreOverride if true, ignore ability changing effects + * @param {boolean} useIllusion whether we want he types of the illusion or not * @returns array of {@linkcode Type} */ - getTypes(includeTeraType = false, forDefend: boolean = false, ignoreOverride?: boolean): Type[] { - const types = []; - + getTypes(includeTeraType = false, forDefend: boolean = false, ignoreOverride?: boolean, useIllusion: boolean | "AUTO" = "AUTO"): Type[] { + const types: Type[] = []; if (includeTeraType) { const teraType = this.getTeraType(); if (teraType !== Type.UNKNOWN) { types.push(teraType); } } - if (!types.length || !includeTeraType) { - if (!ignoreOverride && this.summonData?.types) { + const doIllusion: boolean = useIllusion === "AUTO" ? !forDefend : useIllusion; + if (!ignoreOverride && this.summonData?.types && (!this.illusion.active || !doIllusion)) { this.summonData.types.forEach(t => types.push(t)); } else { - const speciesForm = this.getSpeciesForm(ignoreOverride); + const speciesForm = this.getSpeciesForm(ignoreOverride, doIllusion); types.push(speciesForm.type1); - - const fusionSpeciesForm = this.getFusionSpeciesForm(ignoreOverride); + const fusionSpeciesForm = this.getFusionSpeciesForm(ignoreOverride, doIllusion); if (fusionSpeciesForm) { if (fusionSpeciesForm.type2 !== null && fusionSpeciesForm.type2 !== speciesForm.type1) { types.push(fusionSpeciesForm.type2); @@ -925,7 +1116,6 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container { types.push(fusionSpeciesForm.type1); } } - if (types.length === 1 && speciesForm.type2 !== null) { types.push(speciesForm.type2); } @@ -951,7 +1141,6 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container { types.splice(index, 1); } } - return types; } @@ -1179,12 +1368,13 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container { * @param source - The attacking Pokémon. * @param pokemonMove - The move being used by the attacking Pokémon. * @param ignoreAbility - Whether to check for abilities that might affect type effectiveness or immunity. + * @param {boolean} useIllusion - Whether we want the attack move effectiveness on the illusion or not * @returns The type damage multiplier, indicating the effectiveness of the move */ - getAttackMoveEffectiveness(source: Pokemon, pokemonMove: PokemonMove, ignoreAbility: boolean = false): TypeDamageMultiplier { + getAttackMoveEffectiveness(source: Pokemon, pokemonMove: PokemonMove, ignoreAbility: boolean = false, useIllusion: boolean = false): TypeDamageMultiplier { const move = pokemonMove.getMove(); const typeless = move.hasAttr(TypelessAttr); - const typeMultiplier = new Utils.NumberHolder(this.getAttackTypeEffectiveness(move, source)); + const typeMultiplier = new Utils.NumberHolder(this.getAttackTypeEffectiveness(move, source, undefined, undefined, useIllusion)); const cancelled = new Utils.BooleanHolder(false); applyMoveAttrs(VariableMoveTypeMultiplierAttr, source, this, move, typeMultiplier); if (!typeless && !ignoreAbility) { @@ -1203,9 +1393,10 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container { * @param source the Pokemon using the move * @param ignoreStrongWinds whether or not this ignores strong winds (anticipation, forewarn, stealth rocks) * @param simulated tag to only apply the strong winds effect message when the move is used + * @param {boolean} useIllusion - Whether we want the attack type effectiveness on the illusion or not * @returns a multiplier for the type effectiveness */ - getAttackTypeEffectiveness(moveOrType: Move | Type, source?: Pokemon, ignoreStrongWinds: boolean = false, simulated: boolean = true): TypeDamageMultiplier { + getAttackTypeEffectiveness(moveOrType: Move | Type, source?: Pokemon, ignoreStrongWinds: boolean = false, simulated: boolean = true, useIllusion: boolean = false): TypeDamageMultiplier { const move = (moveOrType instanceof Move) ? moveOrType : undefined; @@ -1216,7 +1407,7 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container { if (moveType === Type.STELLAR) { return this.isTerastallized() ? 2 : 1; } - const types = this.getTypes(true, true); + const types = this.getTypes(true, true, undefined, useIllusion); let multiplier = types.map(defType => { if (source) { @@ -1253,12 +1444,12 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container { getMatchupScore(pokemon: Pokemon): number { const types = this.getTypes(true); - const enemyTypes = pokemon.getTypes(true, true); + const enemyTypes = pokemon.getTypes(true, true, false, true); const outspeed = (this.isActive(true) ? this.getBattleStat(Stat.SPD, pokemon) : this.getStat(Stat.SPD)) <= pokemon.getBattleStat(Stat.SPD, this); - let atkScore = pokemon.getAttackTypeEffectiveness(types[0], this) * (outspeed ? 1.25 : 1); + let atkScore = pokemon.getAttackTypeEffectiveness(types[0], this, false, false, true) * (outspeed ? 1.25 : 1); let defScore = 1 / Math.max(this.getAttackTypeEffectiveness(enemyTypes[0], pokemon), 0.25); if (types.length > 1) { - atkScore *= pokemon.getAttackTypeEffectiveness(types[1], this); + atkScore *= pokemon.getAttackTypeEffectiveness(types[1], this, false, false, true); } if (enemyTypes.length > 1) { defScore *= (1 / Math.max(this.getAttackTypeEffectiveness(enemyTypes[1], pokemon), 0.25)); @@ -3720,7 +3911,7 @@ export class EnemyPokemon extends Pokemon { if ((move.name.endsWith(" (N)") || !move.applyConditions(this, target, move)) && ![Moves.SUCKER_PUNCH, Moves.UPPER_HAND, Moves.THUNDERCLAP].includes(move.id)) { targetScore = -20; } else if (move instanceof AttackMove) { - const effectiveness = target.getAttackMoveEffectiveness(this, pokemonMove); + const effectiveness = target.getAttackMoveEffectiveness(this, pokemonMove, false, true); if (target.isPlayer() !== this.isPlayer()) { targetScore *= effectiveness; if (this.isOfType(move.type)) { @@ -4017,6 +4208,91 @@ 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 stored name of the pokemon. + * @type {string} + */ + name?: string; + /** + * The stored nickname of the pokemon. + * @type {string} + */ + nickname?: string; + /** + * The species of the illusion. + * @type {PokemonSpecies} + */ + species?: PokemonSpecies; + /** + * The formIndex of the illusion + * @type {integer} + */ + formIndex?: integer; + /** + * Store whether the base pokemon is shiny or not. + * @type {boolean} + */ + shiny?: boolean; + /** + * The shiny variant of the base pokemon. + * @type {Variant} + */ + variant?: Variant; + /** + * The shinysparkles of the base pokemon. + * @type {Phaser.GameObjects.Sprite} + */ + shinySparkle?: Phaser.GameObjects.Sprite; + /** + * 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; + /** + * Whether the fusionned species of the base pokemon is shiny or not. + * @type {PokemonSpecies} + */ + fusionShiny?: boolean; + /** + * The variant of the fusionned species of the base pokemon. + * @type {Variant} + */ + fusionVariant?: Variant; + /** + * The fusionGender of the illusion if it's a fusion + * @type {Gender} + */ + fusionGender?: Gender; +} + export interface TurnMove { move: Moves; targets?: BattlerIndex[]; diff --git a/src/locales/de/ability-trigger.ts b/src/locales/de/ability-trigger.ts index 88dc9f9f027..5164b294570 100644 --- a/src/locales/de/ability-trigger.ts +++ b/src/locales/de/ability-trigger.ts @@ -10,6 +10,7 @@ export const abilityTriggers: SimpleTranslationEntries = { "trace": "{{pokemonName}} kopiert {{abilityName}} von {{targetName}}!", "windPowerCharged": "Der Treffer durch {{moveName}} läd die Stärke von {{pokemonName}} auf!", "quickDraw": "Durch Schnellschuss kann {{pokemonName}} schneller handeln als sonst!", + "illusionBreak": "Das Trugbild von {{pokemonName}} verschwindet!", "blockItemTheft": "{{abilityName}} von {{pokemonNameWithAffix}} verhindert Item-Diebstahl!", "typeImmunityHeal": "{{abilityName}} von {{pokemonNameWithAffix}} füllte einige KP auf!", "nonSuperEffectiveImmunity": "{{pokemonNameWithAffix}} vermeidet Schaden mit {{abilityName}}!", diff --git a/src/locales/en/ability-trigger.ts b/src/locales/en/ability-trigger.ts index ce41a964922..c5ee9a858fd 100644 --- a/src/locales/en/ability-trigger.ts +++ b/src/locales/en/ability-trigger.ts @@ -10,6 +10,7 @@ export const abilityTriggers: SimpleTranslationEntries = { "trace": "{{pokemonName}} copied {{targetName}}'s\n{{abilityName}}!", "windPowerCharged": "Being hit by {{moveName}} charged {{pokemonName}} with power!", "quickDraw": "{{pokemonName}} can act faster than normal, thanks to its Quick Draw!", + "illusionBreak": "{{pokemonName}}'s illusion wore off!", "blockItemTheft": "{{pokemonNameWithAffix}}'s {{abilityName}}\nprevents item theft!", "typeImmunityHeal": "{{pokemonNameWithAffix}}'s {{abilityName}}\nrestored its HP a little!", "nonSuperEffectiveImmunity": "{{pokemonNameWithAffix}} avoided damage\nwith {{abilityName}}!", diff --git a/src/locales/es/ability-trigger.ts b/src/locales/es/ability-trigger.ts index 6b1f66a11e3..df0fb1b3bf5 100644 --- a/src/locales/es/ability-trigger.ts +++ b/src/locales/es/ability-trigger.ts @@ -11,6 +11,7 @@ export const abilityTriggers: SimpleTranslationEntries = { "windPowerCharged": "¡{{pokemonName}} se ha cargado de electricidad gracias a {{moveName}}!", "quickDraw": "{{pokemonName}} can act faster than normal, thanks to its Quick Draw!", "blockItemTheft": "{{pokemonNameWithAffix}}'s {{abilityName}}\nprevents item theft!", + "illusionBreak": "¡La ilusión de {{pokemonName}} se ha desvanecido!", "typeImmunityHeal": "{{pokemonNameWithAffix}}'s {{abilityName}}\nrestored its HP a little!", "nonSuperEffectiveImmunity": "{{pokemonNameWithAffix}} avoided damage\nwith {{abilityName}}!", "postDefendDisguise": "{{pokemonNameWithAffix}}'s disguise was busted!", diff --git a/src/locales/fr/ability-trigger.ts b/src/locales/fr/ability-trigger.ts index f6b9c306cd1..b97fe3877ac 100644 --- a/src/locales/fr/ability-trigger.ts +++ b/src/locales/fr/ability-trigger.ts @@ -9,6 +9,7 @@ export const abilityTriggers: SimpleTranslationEntries = { "poisonHeal": "{{abilityName}} de {{pokemonName}}\nrestaure un peu ses PV !", "trace": "{{pokemonName}} copie le talent {{abilityName}}\nde {{targetName}} !", "windPowerCharged": "{{pokemonName}} a été touché par la capacité {{moveName}} et se charge en électricité !", + "illusionBreak": "L’illusion de {{pokemonName}} se brise !", "quickDraw": "Tir Vif permet à {{pokemonName}}\nd’agir plus vite que d’habitude !", "blockItemTheft": "{{abilityName}} de {{pokemonNameWithAffix}}\nempêche son objet d’être volé !", "typeImmunityHeal": "{{abilityName}} de {{pokemonNameWithAffix}}\nrestaure un peu ses PV !", diff --git a/src/locales/it/ability-trigger.ts b/src/locales/it/ability-trigger.ts index 37472dbdeab..fa5f9d3102f 100644 --- a/src/locales/it/ability-trigger.ts +++ b/src/locales/it/ability-trigger.ts @@ -10,6 +10,7 @@ export const abilityTriggers: SimpleTranslationEntries = { "trace": "L'abilità {{abilityName}} di {{targetName}}\nviene copiata da {{pokemonName}} con Traccia!", "windPowerCharged": "Venire colpito da {{moveName}} ha caricato {{pokemonName}}!", "quickDraw":"{{pokemonName}} agisce più rapidamente del normale grazie a Colpolesto!", + "illusionBreak": "L'illusione di {{pokemonName}} si dissolve!", "blockItemTheft": "{{abilityName}} di {{pokemonNameWithAffix}}\nlo rende immune ai furti!", "typeImmunityHeal": "{{pokemonName}} recupera alcuni PS\ncon {{abilityName}}!", "nonSuperEffectiveImmunity": "{{pokemonNameWithAffix}} evita il colpo\ncon {{abilityName}}!", diff --git a/src/locales/ko/ability-trigger.ts b/src/locales/ko/ability-trigger.ts index 61be21bc7ec..0fbf17780fc 100644 --- a/src/locales/ko/ability-trigger.ts +++ b/src/locales/ko/ability-trigger.ts @@ -10,6 +10,7 @@ export const abilityTriggers: SimpleTranslationEntries = { "trace": "{{pokemonName}}[[는]] 상대 {{targetName}}의 \n{{abilityName}}[[를]] 트레이스했다!", "windPowerCharged": "{{pokemonName}}[[는]]\n{{moveName}}에 맞아 충전되었다!", "quickDraw": "{{pokemonName}}[[는]]\n퀵드로에 의해 행동이 빨라졌다!", + "illusionBreak": "{{pokemonName}}의\n일루전이 풀렸다!", "blockItemTheft": "{{pokemonNameWithAffix}}의 {{abilityName}}에 의해\n도구를 빼앗기지 않는다!", "typeImmunityHeal": "{{pokemonNameWithAffix}}[[는]]\n{{abilityName}}[[로]] 체력이 회복되었다!", "nonSuperEffectiveImmunity": "{{pokemonNameWithAffix}}[[는]] {{abilityName}} 때문에\n데미지를 입지 않는다!", diff --git a/src/locales/pt_BR/ability-trigger.ts b/src/locales/pt_BR/ability-trigger.ts index f5d9511f3f6..61342c22218 100644 --- a/src/locales/pt_BR/ability-trigger.ts +++ b/src/locales/pt_BR/ability-trigger.ts @@ -10,6 +10,7 @@ export const abilityTriggers: SimpleTranslationEntries = { "trace": "{{pokemonName}} copiou {{abilityName}}\nde {{targetName}}!", "windPowerCharged": "Ser atingido por {{moveName}} carregou {{pokemonName}} com poder!", "quickDraw": "{{pokemonName}} pode agir mais rápido que o normal\ngraças ao seu Quick Draw!", + "illusionBreak": "A ilusão de {{pokemonName}} acabou!", "blockItemTheft": "{{abilityName}} de {{pokemonNameWithAffix}}\nprevine o roubo de itens!", "typeImmunityHeal": "{{abilityName}} de {{pokemonNameWithAffix}}\nrestaurou um pouco de PS!", "nonSuperEffectiveImmunity": "{{pokemonNameWithAffix}} evitou dano\ncom {{abilityName}}!", diff --git a/src/locales/pt_BR/config.ts b/src/locales/pt_BR/config.ts index ce4576646c2..3a44105b8a4 100644 --- a/src/locales/pt_BR/config.ts +++ b/src/locales/pt_BR/config.ts @@ -1,5 +1,6 @@ import { ability } from "./ability"; import { abilityTriggers } from "./ability-trigger"; +import { arenaFlyout } from "./arena-flyout"; import { PGFachv, PGMachv } from "./achv"; import { arenaFlyout } from "./arena-flyout"; import { arenaTag } from "./arena-tag"; diff --git a/src/locales/zh_CN/ability-trigger.ts b/src/locales/zh_CN/ability-trigger.ts index 0f2201049d2..d19a0702515 100644 --- a/src/locales/zh_CN/ability-trigger.ts +++ b/src/locales/zh_CN/ability-trigger.ts @@ -10,6 +10,7 @@ export const abilityTriggers: SimpleTranslationEntries = { "trace": "{{pokemonName}}复制了{{targetName}}的\n{{abilityName}}!", "windPowerCharged": "受{{moveName}}的影响,{{pokemonName}}提升了能力!", "quickDraw":"因为速击效果发动,\n{{pokemonName}}比平常出招更快了!", + "illusionBreak": "{{pokemonName}}造成的\n幻觉被解除了!", "blockItemTheft": "{{pokemonNameWithAffix}}的{{abilityName}}\n阻止了对方夺取道具!", "typeImmunityHeal": "{{pokemonNameWithAffix}}因{{abilityName}}\n回复了少许HP!", "nonSuperEffectiveImmunity": "{{pokemonNameWithAffix}}因{{abilityName}}\n避免了伤害!", diff --git a/src/locales/zh_TW/ability-trigger.ts b/src/locales/zh_TW/ability-trigger.ts index baa20614a44..2f89c47f1ab 100644 --- a/src/locales/zh_TW/ability-trigger.ts +++ b/src/locales/zh_TW/ability-trigger.ts @@ -10,6 +10,7 @@ export const abilityTriggers: SimpleTranslationEntries = { "trace": "{{pokemonName}} 複製了 {{targetName}} 的\n{{abilityName}}!", "windPowerCharged": "受 {{moveName}} 的影響, {{pokemonName}} 提升了能力!", "quickDraw":"{{pokemonName}} can act faster than normal, thanks to its Quick Draw!", + "illusionBreak": "{{pokemonName}}造成的\n幻覺解除了!", "blockItemTheft": "{{pokemonNameWithAffix}}'s {{abilityName}}\nprevents item theft!", "typeImmunityHeal": "{{pokemonNameWithAffix}}'s {{abilityName}}\nrestored its HP a little!", "nonSuperEffectiveImmunity": "{{pokemonNameWithAffix}} avoided damage\nwith {{abilityName}}!", diff --git a/src/messages.ts b/src/messages.ts index 2259e78abfc..ed0880ef36b 100644 --- a/src/messages.ts +++ b/src/messages.ts @@ -18,23 +18,23 @@ export function getPokemonMessage(pokemon: Pokemon, content: string): string { * @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): string { +export function getPokemonNameWithAffix(pokemon: Pokemon, useIllusion: boolean = true): string { switch (pokemon.scene.currentBattle.battleSpec) { case BattleSpec.DEFAULT: 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.ts b/src/phases.ts index c41ad333b9d..d444a4ab63a 100644 --- a/src/phases.ts +++ b/src/phases.ts @@ -25,7 +25,10 @@ import { Starter } from "./ui/starter-select-ui-handler"; import { Gender } from "./data/gender"; import { Weather, WeatherType, getRandomWeatherType, getTerrainBlockMessage, getWeatherDamageMessage, getWeatherLapseMessage } from "./data/weather"; import { ArenaTagSide, ArenaTrapTag, MistTag, TrickRoomTag } from "./data/arena-tag"; -import { CheckTrappedAbAttr, PostAttackAbAttr, PostBattleAbAttr, PostDefendAbAttr, PostSummonAbAttr, PostTurnAbAttr, PostWeatherLapseAbAttr, PreSwitchOutAbAttr, PreWeatherDamageAbAttr, ProtectStatAbAttr, RedirectMoveAbAttr, BlockRedirectAbAttr, RunSuccessAbAttr, StatChangeMultiplierAbAttr, SuppressWeatherEffectAbAttr, SyncEncounterNatureAbAttr, applyAbAttrs, applyCheckTrappedAbAttrs, applyPostAttackAbAttrs, applyPostBattleAbAttrs, applyPostDefendAbAttrs, applyPostSummonAbAttrs, applyPostTurnAbAttrs, applyPostWeatherLapseAbAttrs, applyPreStatChangeAbAttrs, applyPreSwitchOutAbAttrs, applyPreWeatherEffectAbAttrs, IncrementMovePriorityAbAttr, applyPostVictoryAbAttrs, PostVictoryAbAttr, BlockNonDirectDamageAbAttr as BlockNonDirectDamageAbAttr, applyPostKnockOutAbAttrs, PostKnockOutAbAttr, PostBiomeChangeAbAttr, applyPostFaintAbAttrs, PostFaintAbAttr, IncreasePpAbAttr, PostStatChangeAbAttr, applyPostStatChangeAbAttrs, AlwaysHitAbAttr, PreventBerryUseAbAttr, StatChangeCopyAbAttr, PokemonTypeChangeAbAttr, applyPreAttackAbAttrs, applyPostMoveUsedAbAttrs, PostMoveUsedAbAttr, MaxMultiHitAbAttr, HealFromBerryUseAbAttr, IgnoreMoveEffectsAbAttr, BlockStatusDamageAbAttr, BypassSpeedChanceAbAttr, AddSecondStrikeAbAttr } from "./data/ability"; +import { + CheckTrappedAbAttr, PreSummonAbAttr, PostAttackAbAttr, PostBattleAbAttr, PostDefendAbAttr, PostSummonAbAttr, PostTurnAbAttr, PostWeatherLapseAbAttr, PreSwitchOutAbAttr, PreWeatherDamageAbAttr, ProtectStatAbAttr, RedirectMoveAbAttr, BlockRedirectAbAttr, RunSuccessAbAttr, StatChangeMultiplierAbAttr, SuppressWeatherEffectAbAttr, SyncEncounterNatureAbAttr, + applyAbAttrs, applyCheckTrappedAbAttrs, applyPreSummonAbAttrs, applyPostAttackAbAttrs, applyPostBattleAbAttrs, applyPostDefendAbAttrs, applyPostSummonAbAttrs, applyPostTurnAbAttrs, applyPostWeatherLapseAbAttrs, applyPreStatChangeAbAttrs, applyPreSwitchOutAbAttrs, applyPreWeatherEffectAbAttrs, IncrementMovePriorityAbAttr, applyPostVictoryAbAttrs, PostVictoryAbAttr, BlockNonDirectDamageAbAttr as BlockNonDirectDamageAbAttr, applyPostKnockOutAbAttrs, PostKnockOutAbAttr, PostBiomeChangeAbAttr, applyPostFaintAbAttrs, PostFaintAbAttr, IncreasePpAbAttr, PostStatChangeAbAttr, applyPostStatChangeAbAttrs, AlwaysHitAbAttr, PreventBerryUseAbAttr, StatChangeCopyAbAttr, PokemonTypeChangeAbAttr, applyPreAttackAbAttrs, applyPostMoveUsedAbAttrs, PostMoveUsedAbAttr, MaxMultiHitAbAttr, HealFromBerryUseAbAttr, IgnoreMoveEffectsAbAttr, BlockStatusDamageAbAttr, BypassSpeedChanceAbAttr, AddSecondStrikeAbAttr +} from "./data/ability"; import { Unlockables, getUnlockableName } from "./system/unlockables"; import { getBiomeKey } from "./field/arena"; import { BattleType, BattlerIndex, TurnCommand } from "./battle"; @@ -842,6 +845,7 @@ export class EncounterPhase extends BattlePhase { }); } } + const enemyPokemon = this.scene.getEnemyParty()[e]; if (e < (battle.double ? 2 : 1)) { enemyPokemon.setX(-66 + enemyPokemon.getFieldPositionOffset()[0]); @@ -895,6 +899,8 @@ export class EncounterPhase extends BattlePhase { battle.enemyParty.forEach((enemyPokemon, e) => { if (e < (battle.double ? 2 : 1)) { if (battle.battleType === BattleType.WILD) { + + applyPreSummonAbAttrs(PreSummonAbAttr, enemyPokemon, []); this.scene.field.add(enemyPokemon); battle.seenEnemyPartyMemberIds.add(enemyPokemon.id); const playerPokemon = this.scene.getPlayerPokemon(); @@ -1058,7 +1064,7 @@ export class EncounterPhase extends BattlePhase { const enemyField = this.scene.getEnemyField(); enemyField.forEach((enemyPokemon, e) => { - if (enemyPokemon.isShiny()) { + if (enemyPokemon.isShiny(true)) { this.scene.unshiftPhase(new ShinySparklePhase(this.scene, BattlerIndex.ENEMY + e)); } }); @@ -1378,7 +1384,8 @@ export class SummonPhase extends PartyMemberPokemonPhase { start() { super.start(); - this.preSummon(); + const pokemon = this.getPokemon(); + applyPreSummonAbAttrs(PreSummonAbAttr, pokemon).then(() => this.preSummon()); } /** @@ -1447,8 +1454,7 @@ export class SummonPhase extends PartyMemberPokemonPhase { summon(): void { const pokemon = this.getPokemon(); - - const pokeball = this.scene.addFieldSprite(this.player ? 36 : 248, this.player ? 80 : 44, "pb", getPokeballAtlasKey(pokemon.pokeball)); + const pokeball = this.scene.addFieldSprite(this.player ? 36 : 248, this.player ? 80 : 44, "pb", getPokeballAtlasKey(pokemon.illusion.pokeball ?? pokemon.pokeball)); pokeball.setVisible(false); pokeball.setOrigin(0.5, 0.625); this.scene.field.add(pokeball); @@ -1494,7 +1500,7 @@ export class SummonPhase extends PartyMemberPokemonPhase { } this.scene.currentBattle.seenEnemyPartyMemberIds.add(pokemon.id); } - addPokeballOpenParticles(this.scene, pokemon.x, pokemon.y - 16, pokemon.pokeball); + addPokeballOpenParticles(this.scene, pokemon.x, pokemon.y - 16, pokemon.illusion.pokeball ?? pokemon.pokeball); this.scene.updateModifiers(this.player); this.scene.updateFieldScale(); pokemon.showInfo(); @@ -1502,7 +1508,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.illusion.pokeball ?? pokemon.pokeball)); pokemon.untint(250, "Sine.easeIn"); this.scene.updateFieldScale(); this.scene.tweens.add({ @@ -1526,7 +1532,7 @@ export class SummonPhase extends PartyMemberPokemonPhase { onEnd(): void { const pokemon = this.getPokemon(); - if (pokemon.isShiny()) { + if (pokemon.isShiny(true)) { this.scene.unshiftPhase(new ShinySparklePhase(this.scene, pokemon.getBattlerIndex())); } @@ -1587,7 +1593,10 @@ export class SwitchSummonPhase extends SummonPhase { this.scene.pbTrayEnemy.showPbTray(this.scene.getEnemyParty()); } } + const pokemon: Pokemon = this.getPokemon(); + // if doReturn === False OR slotIndex !== -1 (slotIndex is valid) and the pokemon doesn't exist/is false + // then switchAndSummon(), manually pick pokemon to switch into if (!this.doReturn || (this.slotIndex !== -1 && !(this.player ? this.scene.getParty() : this.scene.getEnemyParty())[this.slotIndex])) { if (this.player) { return this.switchAndSummon(); @@ -1597,8 +1606,6 @@ export class SwitchSummonPhase extends SummonPhase { } } - const pokemon = this.getPokemon(); - if (!this.batonPass) { (this.player ? this.scene.getEnemyField() : this.scene.getPlayerField()).forEach(enemyPokemon => enemyPokemon.removeTagsBySourceId(pokemon.id)); } @@ -1612,7 +1619,7 @@ export class SwitchSummonPhase extends SummonPhase { ); this.scene.playSound("pb_rel"); pokemon.hideInfo(); - pokemon.tint(getPokeballTintColor(pokemon.pokeball), 1, 250, "Sine.easeIn"); + pokemon.tint(getPokeballTintColor(pokemon.illusion.pokeball ?? pokemon.pokeball), 1, 250, "Sine.easeIn"); this.scene.tweens.add({ targets: pokemon, duration: 250, @@ -1631,6 +1638,8 @@ export class SwitchSummonPhase extends SummonPhase { const party = this.player ? this.getParty() : this.scene.getEnemyParty(); const switchedPokemon = party[this.slotIndex]; this.lastPokemon = this.getPokemon(); + + applyPreSummonAbAttrs(PreSummonAbAttr, switchedPokemon); applyPreSwitchOutAbAttrs(PreSwitchOutAbAttr, this.lastPokemon); if (this.batonPass && switchedPokemon) { (this.player ? this.scene.getEnemyField() : this.scene.getPlayerField()).forEach(enemyPokemon => enemyPokemon.transferTagsBySourceId(this.lastPokemon.id, switchedPokemon.id)); @@ -4470,6 +4479,7 @@ export class SwitchPhase extends BattlePhase { } this.scene.ui.setMode(Mode.MESSAGE).then(() => super.end()); }, PartyUiHandler.FilterNonFainted); + } } diff --git a/src/test/abilities/illusion.test.ts b/src/test/abilities/illusion.test.ts new file mode 100644 index 00000000000..39683b956ec --- /dev/null +++ b/src/test/abilities/illusion.test.ts @@ -0,0 +1,127 @@ +import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest"; +import Phaser from "phaser"; +import GameManager from "#app/test/utils/gameManager"; +import overrides from "#app/overrides"; +import { Species } from "#enums/species"; +import { Gender } from "../../data/gender"; +import { PokeballType } from "../../data/pokeball"; +import { + TurnEndPhase, +} from "#app/phases"; +import { Moves } from "#enums/moves"; +import { getMovePosition } from "#app/test/utils/gameManagerUtils"; +import { Abilities } from "#enums/abilities"; + +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); + vi.spyOn(overrides, "BATTLE_TYPE_OVERRIDE", "get").mockReturnValue("single"); + vi.spyOn(overrides, "OPP_SPECIES_OVERRIDE", "get").mockReturnValue(Species.ZORUA); + vi.spyOn(overrides, "OPP_ABILITY_OVERRIDE", "get").mockReturnValue(Abilities.ILLUSION); + vi.spyOn(overrides, "OPP_MOVESET_OVERRIDE", "get").mockReturnValue([Moves.TACKLE, Moves.TACKLE, Moves.TACKLE, Moves.TACKLE]); + vi.spyOn(overrides, "OPP_HELD_ITEMS_OVERRIDE", "get").mockReturnValue([{name: "WIDE_LENS", count: 3}]); + + vi.spyOn(overrides, "MOVESET_OVERRIDE", "get").mockReturnValue([Moves.WORRY_SEED, Moves.SOAK, Moves.TACKLE, Moves.TACKLE]); + vi.spyOn(overrides, "STARTING_HELD_ITEMS_OVERRIDE", "get").mockReturnValue([{name: "WIDE_LENS", count: 3}]); + }); + + it("create illusion at the start", async () => { + await game.startBattle([Species.ZOROARK, Species.AXEW]); + + const zoroark = game.scene.getPlayerPokemon(); + const zorua = game.scene.getEnemyPokemon(); + + expect(zoroark.illusion.active).equals(true); + expect(zorua.illusion.active).equals(true); + expect(zoroark.illusion.available).equals(false); + + }); + + it("disappear after receiving damaging move and changing ability move", async () => { + await game.startBattle([Species.ZOROARK, Species.AXEW]); + game.doAttack(getMovePosition(game.scene, 0, Moves.WORRY_SEED)); + + await game.phaseInterceptor.to(TurnEndPhase); + + const zoroark = game.scene.getPlayerPokemon(); + const zorua = game.scene.getEnemyPokemon(); + + expect(zorua.illusion.active).equals(false); + expect(zoroark.illusion.active).equals(false); + }); + + it("disappear if the ability is suppressed", async () => { + vi.spyOn(overrides, "ABILITY_OVERRIDE", "get").mockReturnValue(Abilities.NEUTRALIZING_GAS); + await game.startBattle([Species.KOFFING]); + + const zorua = game.scene.getEnemyPokemon(); + + expect(zorua.illusion.active).equals(false); + }); + + it("trick the enemy AI", async () => { + vi.spyOn(overrides, "OPP_MOVESET_OVERRIDE", "get").mockReturnValue([Moves.FLAMETHROWER, Moves.PSYCHIC, Moves.TACKLE, Moves.TACKLE]); + await game.startBattle([Species.ZOROARK, Species.AXEW]); + + const enemy = game.scene.getEnemyPokemon(); + const zoroark = game.scene.getPlayerPokemon(); + + const flameThwowerEffectiveness = zoroark.getAttackMoveEffectiveness(enemy, enemy.getMoveset()[0], false, true); + const psychicEffectiveness = zoroark.getAttackMoveEffectiveness(enemy, enemy.getMoveset()[1], false, true); + + expect(psychicEffectiveness).above(flameThwowerEffectiveness); + }); + + it("do not disappear if the pokemon takes indirect damages", async () => { + vi.spyOn(overrides, "OPP_SPECIES_OVERRIDE", "get").mockReturnValue(Species.GIGALITH); + vi.spyOn(overrides, "OPP_ABILITY_OVERRIDE", "get").mockReturnValue(Abilities.SAND_STREAM); + vi.spyOn(overrides, "OPP_MOVESET_OVERRIDE", "get").mockReturnValue([Moves.WILL_O_WISP, Moves.WILL_O_WISP, Moves.WILL_O_WISP, Moves.WILL_O_WISP]); + vi.spyOn(overrides, "MOVESET_OVERRIDE", "get").mockReturnValue([Moves.FLARE_BLITZ]); + + await game.startBattle([Species.ZOROARK, Species.AZUMARILL]); + + game.doAttack(getMovePosition(game.scene, 0, Moves.FLARE_BLITZ)); + + await game.phaseInterceptor.to(TurnEndPhase); + + const zoroark = game.scene.getPlayerPokemon(); + + expect(zoroark.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.startBattle([Species.ABRA, Species.ZOROARK, Species.AXEW]); + + const axew = game.scene.getParty().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.illusion.pokeball).equals(PokeballType.GREAT_BALL); + }); +}); diff --git a/src/ui/battle-info.ts b/src/ui/battle-info.ts index d78b05a569f..efd589aee9c 100644 --- a/src/ui/battle-info.ts +++ b/src/ui/battle-info.ts @@ -298,7 +298,7 @@ export default class BattleInfo extends Phaser.GameObjects.Container { }); this.teraIcon.on("pointerout", () => (this.scene as BattleScene).ui.hideTooltip()); - const isFusion = pokemon.isFusion(); + const isFusion = pokemon.isFusion(true); this.splicedIcon.setPositionRelative(this.nameText, nameTextWidth + this.genderText.displayWidth + 1 + (this.teraIcon.visible ? this.teraIcon.displayWidth + 1 : 0), 2.5); this.splicedIcon.setVisible(isFusion); @@ -308,7 +308,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, nameTextWidth + this.genderText.displayWidth + 1 + (this.teraIcon.visible ? this.teraIcon.displayWidth + 1 : 0) + (this.splicedIcon.visible ? this.splicedIcon.displayWidth + 1 : 0), 2.5); this.shinyIcon.setTexture(`shiny_star${doubleShiny ? "_1" : ""}`); @@ -510,6 +510,11 @@ export default class BattleInfo extends Phaser.GameObjects.Container { return resolve(); } + const gender: Gender = pokemon.illusion.active ? pokemon.illusion.gender : pokemon.gender; + + this.genderText.setText(getGenderSymbol(gender)); + this.genderText.setColor(getGenderColor(gender)); + const nameUpdated = this.lastName !== pokemon.getNameToRender(); if (nameUpdated) { @@ -527,8 +532,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, this.nameText.displayWidth + this.genderText.displayWidth + 1, 2); this.splicedIcon.setPositionRelative(this.nameText, this.nameText.displayWidth + this.genderText.displayWidth + 1 + (this.teraIcon.visible ? this.teraIcon.displayWidth + 1 : 0), 1.5); @@ -630,7 +637,17 @@ export default class BattleInfo extends Phaser.GameObjects.Container { this.lastBattleStats = battleStatsStr; } - 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(); }); @@ -643,7 +660,8 @@ export default class BattleInfo extends Phaser.GameObjects.Container { const nameSizeTest = addTextObject(this.scene, 0, 0, displayName, TextStyle.BATTLE_INFO); nameTextWidth = nameSizeTest.displayWidth; - while (nameTextWidth > (this.player || !this.boss ? 60 : 98) - ((pokemon.gender !== Gender.GENDERLESS ? 6 : 0) + (pokemon.fusionSpecies ? 8 : 0) + (pokemon.isShiny() ? 8 : 0) + (Math.min(pokemon.level.toString().length, 3) - 3) * 8)) { + const gender: Gender = pokemon.illusion.active ? pokemon.illusion.gender : pokemon.gender; + while (nameTextWidth > (this.player || !this.boss ? 60 : 98) - ((gender !== Gender.GENDERLESS ? 6 : 0) + (pokemon.fusionSpecies ? 8 : 0) + (pokemon.isShiny() ? 8 : 0) + (Math.min(pokemon.level.toString().length, 3) - 3) * 8)) { displayName = `${displayName.slice(0, displayName.endsWith(".") ? -2 : -1).trimEnd()}.`; nameSizeTest.setText(displayName); nameTextWidth = nameSizeTest.displayWidth; diff --git a/src/ui/party-ui-handler.ts b/src/ui/party-ui-handler.ts index 9bb8162ce2a..6c3dfda2141 100644 --- a/src/ui/party-ui-handler.ts +++ b/src/ui/party-ui-handler.ts @@ -117,14 +117,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; }; @@ -138,7 +138,7 @@ export default class PartyUiHandler extends MessageUiHandler { const challengeAllowed = new Utils.BooleanHolder(true); applyChallenges(this.scene.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; }; @@ -148,7 +148,7 @@ export default class PartyUiHandler extends MessageUiHandler { public static FilterItemMaxStacks = (pokemon: PlayerPokemon, modifier: PokemonHeldItemModifier) => { const matchingModifier = pokemon.scene.findModifier(m => m instanceof PokemonHeldItemModifier && m.pokemonId === pokemon.id && m.matchType(modifier)) as PokemonHeldItemModifier; if (matchingModifier && matchingModifier.stackCount === matchingModifier.getMaxStackCount(pokemon.scene)) { - return i18next.t("partyUiHandler:tooManyItems", { pokemonName: getPokemonNameWithAffix(pokemon) }); + return i18next.t("partyUiHandler:tooManyItems", { pokemonName: getPokemonNameWithAffix(pokemon, false) }); } return null; }; @@ -380,18 +380,18 @@ export default class PartyUiHandler extends MessageUiHandler { this.clearOptions(); ui.playSelect(); pokemon.pauseEvolutions = false; - this.showText(i18next.t("partyUiHandler:unpausedEvolutions", { pokemonName: getPokemonNameWithAffix(pokemon) }), null, () => this.showText(null, 0), null, true); + this.showText(i18next.t("partyUiHandler:unpausedEvolutions", { pokemonName: getPokemonNameWithAffix(pokemon, false) }), null, () => this.showText(null, 0), null, true); } else if (option === PartyOption.UNSPLICE) { this.clearOptions(); ui.playSelect(); - this.showText(i18next.t("partyUiHandler:unspliceConfirmation", { fusionName: pokemon.fusionSpecies.name, pokemonName: pokemon.name }), null, () => { + this.showText(i18next.t("partyUiHandler:unspliceConfirmation", { fusionName: pokemon.fusionSpecies.name, pokemonName: pokemon.getName() }), null, () => { ui.setModeWithoutClear(Mode.CONFIRM, () => { - const fusionName = pokemon.name; + const fusionName = pokemon.getName(false); pokemon.unfuse().then(() => { this.clearPartySlots(); this.populatePartySlots(); ui.setMode(Mode.PARTY); - this.showText(i18next.t("partyUiHandler:wasReverted", { fusionName: fusionName, pokemonName: pokemon.name }), null, () => { + this.showText(i18next.t("partyUiHandler:wasReverted", { fusionName: fusionName, pokemonName: pokemon.getName() }), null, () => { ui.setMode(Mode.PARTY); this.showText(null, 0); }, null, true); @@ -405,7 +405,7 @@ export default class PartyUiHandler extends MessageUiHandler { this.clearOptions(); ui.playSelect(); if (this.cursor >= this.scene.currentBattle.getBattlerCount() || !pokemon.isAllowedInBattle()) { - this.showText(i18next.t("partyUiHandler:releaseConfirmation", { pokemonName: getPokemonNameWithAffix(pokemon) }), null, () => { + this.showText(i18next.t("partyUiHandler:releaseConfirmation", { pokemonName: getPokemonNameWithAffix(pokemon, false) }), null, () => { ui.setModeWithoutClear(Mode.CONFIRM, () => { ui.setMode(Mode.PARTY); this.doRelease(this.cursor); @@ -950,7 +950,7 @@ export default class PartyUiHandler extends MessageUiHandler { } doRelease(slotIndex: integer): void { - this.showText(this.getReleaseMessage(getPokemonNameWithAffix(this.scene.getParty()[slotIndex])), null, () => { + this.showText(this.getReleaseMessage(getPokemonNameWithAffix(this.scene.getParty()[slotIndex], false)), null, () => { this.clearPartySlots(); this.scene.removePartyMemberModifiers(slotIndex); const releasedPokemon = this.scene.getParty().splice(slotIndex, 1)[0]; @@ -1081,7 +1081,7 @@ class PartySlot extends Phaser.GameObjects.Container { const slotInfoContainer = this.scene.add.container(0, 0); this.add(slotInfoContainer); - let displayName = this.pokemon.getNameToRender(); + let displayName = this.pokemon.getNameToRender(false); let nameTextWidth: number; const nameSizeTest = addTextObject(this.scene, 0, 0, displayName, TextStyle.PARTY); @@ -1148,12 +1148,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 = this.scene.add.image(0, 0, `shiny_star_small${doubleShiny ? "_1" : ""}`); shinyStar.setOrigin(0, 0); shinyStar.setPositionRelative(slotName, -9, 3); - shinyStar.setTint(getVariantTint(!doubleShiny ? this.pokemon.getVariant() : this.pokemon.variant)); + shinyStar.setTint(getVariantTint(this.pokemon.getBaseVariant(doubleShiny))); slotInfoContainer.add(shinyStar); @@ -1161,7 +1161,7 @@ class PartySlot extends Phaser.GameObjects.Container { const fusionShinyStar = this.scene.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.illusion.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 35127564b60..2c229442ed1 100644 --- a/src/ui/rename-form-ui-handler.ts +++ b/src/ui/rename-form-ui-handler.ts @@ -36,7 +36,7 @@ export default class RenameFormUiHandler extends FormModalUiHandler { show(args: any[]): boolean { if (super.show(args)) { const config = args[0] as ModalConfig; - this.inputs[0].text = (args[1] as PlayerPokemon).getNameToRender(); + this.inputs[0].text = (args[1] as PlayerPokemon).getNameToRender(false); this.submitAction = (_) => { this.sanitizeInputs(); diff --git a/src/ui/summary-ui-handler.ts b/src/ui/summary-ui-handler.ts index 405429fcd3f..8c45d767ae0 100644 --- a/src/ui/summary-ui-handler.ts +++ b/src/ui/summary-ui-handler.ts @@ -301,8 +301,8 @@ export default class SummaryUiHandler extends UiHandler { this.pokemonSprite.setPipelineData("teraColor", getTypeRgb(this.pokemon.getTeraType())); 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.illusion.shiny ?? this.pokemon.shiny); + this.pokemonSprite.setPipelineData("variant", this.pokemon.illusion.variant ?? this.pokemon.variant); [ "spriteColors", "fusionSpriteColors" ].map(k => { delete this.pokemonSprite.pipelineData[`${k}Base`]; if (this.pokemon.summonData?.speciesForm) { @@ -312,7 +312,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(); @@ -346,12 +346,12 @@ export default class SummaryUiHandler extends UiHandler { this.candyShadow.setCrop(0,0,16, candyCropY); - 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, this.nameText.displayWidth + (this.splicedIcon.visible ? this.splicedIcon.displayWidth + 1 : 0) + 1, 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 = doubleShiny || baseVariant ? @@ -364,7 +364,7 @@ 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.illusion.fusionVariant ?? this.pokemon.fusionVariant)); } this.pokeball.setFrame(getPokeballAtlasKey(this.pokemon.pokeball)); @@ -727,7 +727,7 @@ export default class SummaryUiHandler extends UiHandler { return typeIcon; }; - const types = this.pokemon.getTypes(false, false, true); + const types = this.pokemon.getTypes(false, false, true, false); profileContainer.add(getTypeIcon(0, types[0])); if (types.length > 1) { profileContainer.add(getTypeIcon(1, types[1]));