From 89f58961cbe6303eec8f191d206fca5bd50592c8 Mon Sep 17 00:00:00 2001 From: Frederico Santos Date: Tue, 28 May 2024 12:11:04 +0100 Subject: [PATCH] Implement Zero to hero (#1131) * Implemented Zero-To-Hero * Zero to Hero documentation and form reset on summon * Zero to Hero: form reset on biome/trainer * Updated documentation on PreSwitchOutFormChangeAbAttr apply * Faint bypass on canApplyAbility * revert zygarde * zero to hero post-merge --- src/battle-scene.ts | 3 +- src/data/ability.ts | 63 +++++++++++++++++++++++++++++++-------- src/data/pokemon-forms.ts | 4 +++ src/field/pokemon.ts | 4 +-- src/phases.ts | 13 +++----- 5 files changed, 63 insertions(+), 24 deletions(-) diff --git a/src/battle-scene.ts b/src/battle-scene.ts index 89963339a1c..f972097f4c2 100644 --- a/src/battle-scene.ts +++ b/src/battle-scene.ts @@ -17,7 +17,7 @@ import { Moves } from "./data/enums/moves"; import { allMoves } from "./data/move"; import { ModifierPoolType, getDefaultModifierTypeForTier, getEnemyModifierTypesForWave, getLuckString, getLuckTextTint, getModifierPoolForType, getPartyLuckValue } from "./modifier/modifier-type"; import AbilityBar from "./ui/ability-bar"; -import { BlockItemTheftAbAttr, DoubleBattleChanceAbAttr, IncrementMovePriorityAbAttr, applyAbAttrs } from "./data/ability"; +import { BlockItemTheftAbAttr, DoubleBattleChanceAbAttr, IncrementMovePriorityAbAttr, PostBattleInitAbAttr, applyAbAttrs, applyPostBattleInitAbAttrs } from "./data/ability"; import { allAbilities } from "./data/ability"; import Battle, { BattleType, FixedBattleConfig, fixedBattles } from "./battle"; import { GameMode, GameModes, gameModes } from "./game-mode"; @@ -979,6 +979,7 @@ export default class BattleScene extends SceneBase { if (pokemon) { if (resetArenaState) { pokemon.resetBattleData(); + applyPostBattleInitAbAttrs(PostBattleInitAbAttr, pokemon, true); } this.triggerPokemonFormChange(pokemon, SpeciesFormChangeTimeOfDayTrigger); } diff --git a/src/data/ability.ts b/src/data/ability.ts index 9d597ec8222..3189e9db878 100644 --- a/src/data/ability.ts +++ b/src/data/ability.ts @@ -1753,6 +1753,39 @@ export class PreSwitchOutHealAbAttr extends PreSwitchOutAbAttr { } } +/** + * Attribute for form changes that occur on switching out + * @extends PreSwitchOutAbAttr + * @see {@linkcode applyPreSwitchOut} + */ +export class PreSwitchOutFormChangeAbAttr extends PreSwitchOutAbAttr { + private formFunc: (p: Pokemon) => integer; + + constructor(formFunc: ((p: Pokemon) => integer)) { + super(); + + this.formFunc = formFunc; + } + + /** + * On switch out, trigger the form change to the one defined in the ability + * @param pokemon The pokemon switching out and changing form {@linkcode Pokemon} + * @param passive N/A + * @param args N/A + * @returns true if the form change was successful + */ + applyPreSwitchOut(pokemon: Pokemon, passive: boolean, args: any[]): boolean | Promise { + const formIndex = this.formFunc(pokemon); + if (formIndex !== pokemon.formIndex) { + pokemon.scene.triggerPokemonFormChange(pokemon, SpeciesFormChangeManualTrigger, false); + return true; + } + + return false; + } + +} + export class PreStatChangeAbAttr extends AbAttr { applyPreStatChange(pokemon: Pokemon, passive: boolean, stat: BattleStat, cancelled: Utils.BooleanHolder, args: any[]): boolean | Promise { return false; @@ -3038,7 +3071,7 @@ export class MoneyAbAttr extends PostBattleAbAttr { function applyAbAttrsInternal(attrType: { new(...args: any[]): TAttr }, pokemon: Pokemon, applyFunc: AbAttrApplyFunc, args: any[], isAsync: boolean = false, showAbilityInstant: boolean = false, quiet: boolean = false, passive: boolean = false): Promise { return new Promise(resolve => { - if (!pokemon.canApplyAbility(passive)) { + if (!pokemon.canApplyAbility(passive, args[0])) { if (!passive) { return applyAbAttrsInternal(attrType, pokemon, applyFunc, args, isAsync, showAbilityInstant, quiet, true).then(() => resolve()); } else { @@ -3712,7 +3745,7 @@ export function initAbilities() { .attr(PostDefendContactDamageAbAttr, 8) .bypassFaint(), new Ability(Abilities.ZEN_MODE, 5) - .attr(PostBattleInitFormChangeAbAttr, p => p.getHpRatio() <= 0.5 ? 1 : 0) + .attr(PostBattleInitFormChangeAbAttr, () => 0) .attr(PostSummonFormChangeAbAttr, p => p.getHpRatio() <= 0.5 ? 1 : 0) .attr(PostTurnFormChangeAbAttr, p => p.getHpRatio() <= 0.5 ? 1 : 0) .attr(UncopiableAbilityAbAttr) @@ -3810,7 +3843,7 @@ export function initAbilities() { new Ability(Abilities.MERCILESS, 7) .attr(ConditionalCritAbAttr, (user, target, move) => target.status?.effect === StatusEffect.TOXIC || target.status?.effect === StatusEffect.POISON), new Ability(Abilities.SHIELDS_DOWN, 7) - .attr(PostBattleInitFormChangeAbAttr, p => p.formIndex % 7 + (p.getHpRatio() <= 0.5 ? 7 : 0)) + .attr(PostBattleInitFormChangeAbAttr, () => 0) .attr(PostSummonFormChangeAbAttr, p => p.formIndex % 7 + (p.getHpRatio() <= 0.5 ? 7 : 0)) .attr(PostTurnFormChangeAbAttr, p => p.formIndex % 7 + (p.getHpRatio() <= 0.5 ? 7 : 0)) .attr(UncopiableAbilityAbAttr) @@ -3843,7 +3876,7 @@ export function initAbilities() { new Ability(Abilities.SURGE_SURFER, 7) .conditionalAttr(getTerrainCondition(TerrainType.ELECTRIC), BattleStatMultiplierAbAttr, BattleStat.SPD, 2), new Ability(Abilities.SCHOOLING, 7) - .attr(PostBattleInitFormChangeAbAttr, p => p.level < 20 || p.getHpRatio() <= 0.25 ? 0 : 1) + .attr(PostBattleInitFormChangeAbAttr, () => 0) .attr(PostSummonFormChangeAbAttr, p => p.level < 20 || p.getHpRatio() <= 0.25 ? 0 : 1) .attr(PostTurnFormChangeAbAttr, p => p.level < 20 || p.getHpRatio() <= 0.25 ? 0 : 1) .attr(UncopiableAbilityAbAttr) @@ -3853,7 +3886,7 @@ export function initAbilities() { new Ability(Abilities.DISGUISE, 7) .attr(PreDefendMovePowerToOneAbAttr, (target, user, move) => target.formIndex === 0 && target.getAttackTypeEffectiveness(move.type, user) > 0) .attr(PostSummonFormChangeAbAttr, p => p.battleData.hitCount === 0 ? 0 : 1) - .attr(PostBattleInitFormChangeAbAttr, p => p.battleData.hitCount === 0 ? 0 : 1) + .attr(PostBattleInitFormChangeAbAttr, () => 0) .attr(PostDefendFormChangeAbAttr, p => p.battleData.hitCount === 0 ? 0 : 1) .attr(PreDefendFormChangeAbAttr, p => p.battleData.hitCount === 0 ? 0 : 1) .attr(PostDefendDisguiseAbAttr) @@ -3865,13 +3898,14 @@ export function initAbilities() { .ignorable() .partial(), new Ability(Abilities.BATTLE_BOND, 7) - .attr(PostVictoryFormChangeAbAttr, p => p.getFormKey() ? 2 : 1) + .attr(PostVictoryFormChangeAbAttr, () => 2) + .attr(PostBattleInitFormChangeAbAttr, () => 1) .attr(UncopiableAbilityAbAttr) .attr(UnswappableAbilityAbAttr) .attr(UnsuppressableAbilityAbAttr) .attr(NoFusionAbilityAbAttr), new Ability(Abilities.POWER_CONSTRUCT, 7) // TODO: 10% Power Construct Zygarde isn't accounted for yet. If changed, update Zygarde's getSpeciesFormIndex entry accordingly - .attr(PostBattleInitFormChangeAbAttr, p => p.getHpRatio() <= 0.5 || p.getFormKey() === "complete" ? 4 : 2) + .attr(PostBattleInitFormChangeAbAttr, () => 2) .attr(PostSummonFormChangeAbAttr, p => p.getHpRatio() <= 0.5 || p.getFormKey() === "complete" ? 4 : 2) .attr(PostTurnFormChangeAbAttr, p => p.getHpRatio() <= 0.5 || p.getFormKey() === "complete" ? 4 : 2) .attr(UncopiableAbilityAbAttr) @@ -4103,7 +4137,8 @@ export function initAbilities() { .attr(UnsuppressableAbilityAbAttr) .attr(NoTransformAbilityAbAttr) .attr(NoFusionAbilityAbAttr) - .unimplemented(), + .attr(PostBattleInitFormChangeAbAttr, () => 0) + .attr(PreSwitchOutFormChangeAbAttr, () => 1), new Ability(Abilities.COMMANDER, 9) .attr(UncopiableAbilityAbAttr) .attr(UnswappableAbilityAbAttr) @@ -4186,22 +4221,26 @@ export function initAbilities() { .attr(PostBattleInitStatChangeAbAttr, BattleStat.SPD, 1, true) .attr(UncopiableAbilityAbAttr) .attr(UnswappableAbilityAbAttr) - .attr(NoTransformAbilityAbAttr), + .attr(NoTransformAbilityAbAttr) + .partial(), new Ability(Abilities.EMBODY_ASPECT_WELLSPRING, 9) .attr(PostBattleInitStatChangeAbAttr, BattleStat.SPDEF, 1, true) .attr(UncopiableAbilityAbAttr) .attr(UnswappableAbilityAbAttr) - .attr(NoTransformAbilityAbAttr), + .attr(NoTransformAbilityAbAttr) + .partial(), new Ability(Abilities.EMBODY_ASPECT_HEARTHFLAME, 9) .attr(PostBattleInitStatChangeAbAttr, BattleStat.ATK, 1, true) .attr(UncopiableAbilityAbAttr) .attr(UnswappableAbilityAbAttr) - .attr(NoTransformAbilityAbAttr), + .attr(NoTransformAbilityAbAttr) + .partial(), new Ability(Abilities.EMBODY_ASPECT_CORNERSTONE, 9) .attr(PostBattleInitStatChangeAbAttr, BattleStat.DEF, 1, true) .attr(UncopiableAbilityAbAttr) .attr(UnswappableAbilityAbAttr) - .attr(NoTransformAbilityAbAttr), + .attr(NoTransformAbilityAbAttr) + .partial(), new Ability(Abilities.TERA_SHIFT, 9) .attr(PostSummonFormChangeAbAttr, p => p.getFormKey() ? 0 : 1) .attr(UncopiableAbilityAbAttr) diff --git a/src/data/pokemon-forms.ts b/src/data/pokemon-forms.ts index 3a3971d4fa1..5e0d5a19ee8 100644 --- a/src/data/pokemon-forms.ts +++ b/src/data/pokemon-forms.ts @@ -570,6 +570,10 @@ export const pokemonFormChanges: PokemonFormChanges = { new SpeciesFormChange(Species.GRENINJA, "battle-bond", "ash", new SpeciesFormChangeManualTrigger(), true), new SpeciesFormChange(Species.GRENINJA, "ash", "battle-bond", new SpeciesFormChangeManualTrigger(), true) ], + [Species.PALAFIN] : [ + new SpeciesFormChange(Species.PALAFIN, "zero", "hero", new SpeciesFormChangeManualTrigger(), true), + new SpeciesFormChange(Species.PALAFIN, "hero", "zero", new SpeciesFormChangeManualTrigger(), true) + ], [Species.AEGISLASH]: [ new SpeciesFormChange(Species.AEGISLASH, "blade", "shield", new SpeciesFormChangePreMoveTrigger(Moves.KINGS_SHIELD), true, new SpeciesFormChangeCondition(p => p.hasAbility(Abilities.STANCE_CHANGE))), new SpeciesFormChange(Species.AEGISLASH, "shield", "blade", new SpeciesFormChangePreMoveTrigger(m => allMoves[m].category !== MoveCategory.STATUS), true, new SpeciesFormChangeCondition(p => p.hasAbility(Abilities.STANCE_CHANGE))), diff --git a/src/field/pokemon.ts b/src/field/pokemon.ts index 4282d7cda13..6a44c7ef31a 100644 --- a/src/field/pokemon.ts +++ b/src/field/pokemon.ts @@ -962,7 +962,7 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container { * @param {boolean} passive If true, check if passive can be applied instead of non-passive * @returns {Ability} The passive ability of the pokemon */ - canApplyAbility(passive: boolean = false): boolean { + canApplyAbility(passive: boolean = false, forceBypass: boolean = false): boolean { if (passive && !this.hasPassive()) { return false; } @@ -990,7 +990,7 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container { return false; } } - return (this.hp || ability.isBypassFaint) && !ability.conditions.find(condition => !condition(this)); + return (this.hp || ability.isBypassFaint || forceBypass) && !ability.conditions.find(condition => !condition(this)); } /** diff --git a/src/phases.ts b/src/phases.ts index 9f924cf3939..a2e3a9a34fe 100644 --- a/src/phases.ts +++ b/src/phases.ts @@ -30,7 +30,7 @@ import { Weather, WeatherType, getRandomWeatherType, getTerrainBlockMessage, get import { TempBattleStat } from "./data/temp-battle-stat"; import { ArenaTagSide, ArenaTrapTag, MistTag, TrickRoomTag } from "./data/arena-tag"; import { ArenaTagType } from "./data/enums/arena-tag-type"; -import { CheckTrappedAbAttr, IgnoreOpponentStatChangesAbAttr, IgnoreOpponentEvasionAbAttr, 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, BattleStatMultiplierAbAttr, applyBattleStatMultiplierAbAttrs, IncrementMovePriorityAbAttr, applyPostVictoryAbAttrs, PostVictoryAbAttr, applyPostBattleInitAbAttrs, PostBattleInitAbAttr, BlockNonDirectDamageAbAttr as BlockNonDirectDamageAbAttr, applyPostKnockOutAbAttrs, PostKnockOutAbAttr, PostBiomeChangeAbAttr, applyPostFaintAbAttrs, PostFaintAbAttr, IncreasePpAbAttr, PostStatChangeAbAttr, applyPostStatChangeAbAttrs, AlwaysHitAbAttr, PreventBerryUseAbAttr, StatChangeCopyAbAttr, applyPostMoveUsedAbAttrs, PostMoveUsedAbAttr } from "./data/ability"; +import { CheckTrappedAbAttr, IgnoreOpponentStatChangesAbAttr, IgnoreOpponentEvasionAbAttr, 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, BattleStatMultiplierAbAttr, applyBattleStatMultiplierAbAttrs, IncrementMovePriorityAbAttr, applyPostVictoryAbAttrs, PostVictoryAbAttr, BlockNonDirectDamageAbAttr as BlockNonDirectDamageAbAttr, applyPostKnockOutAbAttrs, PostKnockOutAbAttr, PostBiomeChangeAbAttr, applyPostFaintAbAttrs, PostFaintAbAttr, IncreasePpAbAttr, PostStatChangeAbAttr, applyPostStatChangeAbAttrs, AlwaysHitAbAttr, PreventBerryUseAbAttr, StatChangeCopyAbAttr, applyPostMoveUsedAbAttrs, PostMoveUsedAbAttr } from "./data/ability"; import { Unlockables, getUnlockableName } from "./system/unlockables"; import { getBiomeKey } from "./field/arena"; import { BattleType, BattlerIndex, TurnCommand } from "./battle"; @@ -987,18 +987,14 @@ export class EncounterPhase extends BattlePhase { if (!this.loaded) { const availablePartyMembers = this.scene.getParty().filter(p => !p.isFainted()); - if (availablePartyMembers[0].isOnField()) { - applyPostBattleInitAbAttrs(PostBattleInitAbAttr, availablePartyMembers[0]); - } else { + if (!availablePartyMembers[0].isOnField()) { this.scene.pushPhase(new SummonPhase(this.scene, 0)); } if (this.scene.currentBattle.double) { if (availablePartyMembers.length > 1) { this.scene.pushPhase(new ToggleDoublePositionPhase(this.scene, true)); - if (availablePartyMembers[1].isOnField()) { - applyPostBattleInitAbAttrs(PostBattleInitAbAttr, availablePartyMembers[1]); - } else { + if (!availablePartyMembers[1].isOnField()) { this.scene.pushPhase(new SummonPhase(this.scene, 1)); } } @@ -1475,8 +1471,6 @@ export class SwitchSummonPhase extends SummonPhase { (this.player ? this.scene.getEnemyField() : this.scene.getPlayerField()).forEach(enemyPokemon => enemyPokemon.removeTagsBySourceId(pokemon.id)); } - applyPreSwitchOutAbAttrs(PreSwitchOutAbAttr, pokemon); - this.scene.ui.showText(this.player ? i18next.t("battle:playerComeBack", { pokemonName: pokemon.name }) : i18next.t("battle:trainerComeBack", { @@ -1505,6 +1499,7 @@ export class SwitchSummonPhase extends SummonPhase { const party = this.player ? this.getParty() : this.scene.getEnemyParty(); const switchedPokemon = party[this.slotIndex]; this.lastPokemon = this.getPokemon(); + 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)); if (!this.scene.findModifier(m => m instanceof SwitchEffectTransferModifier && (m as SwitchEffectTransferModifier).pokemonId === switchedPokemon.id)) {