From ca4f9297a8cabb44c3032260a462b3795370e717 Mon Sep 17 00:00:00 2001 From: Flashfyre Date: Tue, 31 Oct 2023 14:09:33 -0400 Subject: [PATCH] Implement switch moves --- src/battle-phases.ts | 78 ++++++++++-------- src/data/battle-anims.ts | 24 +++++- src/data/move.ts | 169 ++++++++++++++++++++++++++++++--------- src/pokemon.ts | 23 +++++- src/utils.ts | 4 + 5 files changed, 224 insertions(+), 74 deletions(-) diff --git a/src/battle-phases.ts b/src/battle-phases.ts index 87452fa3258..3f218f7c11a 100644 --- a/src/battle-phases.ts +++ b/src/battle-phases.ts @@ -643,6 +643,7 @@ export class SummonPhase extends PartyMemberPokemonPhase { pokemon.showInfo(); pokemon.playAnim(); pokemon.setVisible(true); + pokemon.getSprite().setVisible(true); pokemon.setScale(0.5); pokemon.tint(getPokeballTintColor(pokemon.pokeball)); pokemon.untint(250, 'Sine.easeIn'); @@ -738,7 +739,8 @@ export class SwitchSummonPhase extends SummonPhase { if (!this.scene.findModifier(m => m instanceof SwitchEffectTransferModifier && (m as SwitchEffectTransferModifier).pokemonId === switchedPokemon.id)) { const batonPassModifier = this.scene.findModifier(m => m instanceof SwitchEffectTransferModifier && (m as SwitchEffectTransferModifier).pokemonId === this.lastPokemon.id) as SwitchEffectTransferModifier; - this.scene.tryTransferHeldItemModifier(batonPassModifier, switchedPokemon, false, false); + if (batonPassModifier) + this.scene.tryTransferHeldItemModifier(batonPassModifier, switchedPokemon, false, false); } } if (switchedPokemon) { @@ -1537,6 +1539,8 @@ class MoveEffectPhase extends PokemonPhase { return; } + const applyAttrs: Promise[] = []; + // Move animation only needs one target new MoveAnim(this.move.getMove().id as Moves, user, this.getTarget()?.getBattlerIndex()).play(this.scene, () => { for (let target of targets) { @@ -1556,39 +1560,42 @@ class MoveEffectPhase extends PokemonPhase { const hitResult = !isProtected ? target.apply(user, this.move) : HitResult.NO_EFFECT; - applyFilteredMoveAttrs((attr: MoveAttr) => attr instanceof MoveEffectAttr && (attr as MoveEffectAttr).trigger === MoveEffectTrigger.PRE_APPLY, - user, target, this.move.getMove()); - - if (hitResult !== HitResult.FAIL) { - const chargeEffect = !!this.move.getMove().getAttrs(ChargeAttr).find(ca => (ca as ChargeAttr).chargeEffect); - // Charge attribute with charge effect takes all effect attributes and applies them to charge stage, so ignore them if this is present - if (!chargeEffect) - applyFilteredMoveAttrs((attr: MoveAttr) => attr instanceof MoveEffectAttr && (attr as MoveEffectAttr).trigger === MoveEffectTrigger.POST_APPLY - && (attr as MoveEffectAttr).selfTarget, user, target, this.move.getMove()); - if (hitResult !== HitResult.NO_EFFECT) { - applyFilteredMoveAttrs((attr: MoveAttr) => attr instanceof MoveEffectAttr && (attr as MoveEffectAttr).trigger === MoveEffectTrigger.POST_APPLY - && !(attr as MoveEffectAttr).selfTarget, user, target, this.move.getMove()); - if (hitResult < HitResult.NO_EFFECT) { - const flinched = new Utils.BooleanHolder(false); - user.scene.applyModifiers(FlinchChanceModifier, user.isPlayer(), user, flinched); - if (flinched.value) - target.addTag(BattlerTagType.FLINCHED, undefined, this.move.moveId, user.id); + applyAttrs.push(new Promise(resolve => { + applyFilteredMoveAttrs((attr: MoveAttr) => attr instanceof MoveEffectAttr && (attr as MoveEffectAttr).trigger === MoveEffectTrigger.PRE_APPLY, + user, target, this.move.getMove()).then(() => { + if (hitResult !== HitResult.FAIL) { + const chargeEffect = !!this.move.getMove().getAttrs(ChargeAttr).find(ca => (ca as ChargeAttr).chargeEffect); + // Charge attribute with charge effect takes all effect attributes and applies them to charge stage, so ignore them if this is present + Utils.executeIf(!chargeEffect, () => applyFilteredMoveAttrs((attr: MoveAttr) => attr instanceof MoveEffectAttr && (attr as MoveEffectAttr).trigger === MoveEffectTrigger.POST_APPLY + && (attr as MoveEffectAttr).selfTarget, user, target, this.move.getMove())).then(() => { + if (hitResult !== HitResult.NO_EFFECT) { + applyFilteredMoveAttrs((attr: MoveAttr) => attr instanceof MoveEffectAttr && (attr as MoveEffectAttr).trigger === MoveEffectTrigger.POST_APPLY + && !(attr as MoveEffectAttr).selfTarget, user, target, this.move.getMove()).then(() => { + if (hitResult < HitResult.NO_EFFECT) { + const flinched = new Utils.BooleanHolder(false); + user.scene.applyModifiers(FlinchChanceModifier, user.isPlayer(), user, flinched); + if (flinched.value) + target.addTag(BattlerTagType.FLINCHED, undefined, this.move.moveId, user.id); + } + Utils.executeIf(!isProtected && !chargeEffect, () => applyFilteredMoveAttrs((attr: MoveAttr) => attr instanceof MoveEffectAttr && (attr as MoveEffectAttr).trigger === MoveEffectTrigger.HIT, + user, target, this.move.getMove()).then(() => { + if (!target.isFainted()) { + applyPostDefendAbAttrs(PostDefendAbAttr, target, user, this.move, hitResult); + if (!user.isPlayer() && this.move instanceof AttackMove) + user.scene.applyModifiers(EnemyAttackStatusEffectChanceModifier, false, target); + } + if (this.move.getMove().hasFlag(MoveFlags.MAKES_CONTACT)) + this.scene.applyModifiers(ContactHeldItemTransferChanceModifier, this.player, user, target.getFieldIndex()); + }) + ).then(() => resolve()); + }); + } + }); } - if (!isProtected && !chargeEffect) { - applyFilteredMoveAttrs((attr: MoveAttr) => attr instanceof MoveEffectAttr && (attr as MoveEffectAttr).trigger === MoveEffectTrigger.HIT, - user, target, this.move.getMove()); - if (!target.isFainted()) { - applyPostDefendAbAttrs(PostDefendAbAttr, target, user, this.move, hitResult); - if (!user.isPlayer() && this.move instanceof AttackMove) - user.scene.applyModifiers(EnemyAttackStatusEffectChanceModifier, false, target); - } - if (this.move.getMove().hasFlag(MoveFlags.MAKES_CONTACT)) - this.scene.applyModifiers(ContactHeldItemTransferChanceModifier, this.player, user, target.getFieldIndex()); - } - } - } + }); + })); } - this.end(); + Promise.allSettled(applyAttrs).then(() => this.end()); }); }); } @@ -2801,14 +2808,17 @@ export class AttemptRunPhase extends PokemonPhase { targets: [ this.scene.arenaEnemy, enemyField ].flat(), alpha: 0, duration: 250, - ease: 'Sine.easeIn' + ease: 'Sine.easeIn', + onComplete: () => enemyField.forEach(enemyPokemon => enemyPokemon.destroy()) }); enemyField.forEach(enemyPokemon => { - enemyPokemon.hideInfo(); + enemyPokemon.hideInfo().then(() => enemyPokemon.destroy()); enemyPokemon.hp = 0; }); + this.scene.clearEnemyHeldItemModifiers(); + this.scene.pushPhase(new BattleEndPhase(this.scene)); this.scene.pushPhase(new NewBattlePhase(this.scene)); } else diff --git a/src/data/battle-anims.ts b/src/data/battle-anims.ts index 705a4681706..c65b32e1ab9 100644 --- a/src/data/battle-anims.ts +++ b/src/data/battle-anims.ts @@ -1,6 +1,6 @@ //import { battleAnimRawData } from "./battle-anim-raw-data"; import BattleScene from "../battle-scene"; -import { ChargeAttr, Moves, allMoves } from "./move"; +import { ChargeAttr, MoveFlags, Moves, allMoves } from "./move"; import Pokemon from "../pokemon"; import * as Utils from "../utils"; import { BattlerIndex } from "../battle"; @@ -560,6 +560,14 @@ export abstract class BattleAnim { abstract isOppAnim(): boolean; + protected isHideUser(): boolean { + return false; + } + + protected isHideTarget(): boolean { + return false; + } + private getGraphicFrameData(scene: BattleScene, frames: AnimFrame[]): Map> { const ret: Map> = new Map([ [AnimFrameTarget.GRAPHIC, new Map() ], @@ -795,8 +803,10 @@ export abstract class BattleAnim { targetSprite.setAlpha(1); targetSprite.pipelineData['tone'] = [ 0.0, 0.0, 0.0, 0.0 ]; targetSprite.setAngle(0); - userSprite.setVisible(true); - targetSprite.setVisible(true); + if (!this.isHideUser()) + userSprite.setVisible(true); + if (!this.isHideTarget() && (targetSprite !== userSprite || !this.isHideUser())) + targetSprite.setVisible(true); for (let ms of Object.values(spriteCache).flat()) { if (ms) ms.destroy(); @@ -858,6 +868,14 @@ export class MoveAnim extends BattleAnim { isOppAnim(): boolean { return !this.user.isPlayer() && Array.isArray(moveAnims.get(this.move)); } + + protected isHideUser(): boolean { + return allMoves[this.move].hasFlag(MoveFlags.HIDE_USER); + } + + protected isHideTarget(): boolean { + return allMoves[this.move].hasFlag(MoveFlags.HIDE_TARGET); + } } export class MoveChargeAnim extends MoveAnim { diff --git a/src/data/move.ts b/src/data/move.ts index c03ff8aed9a..6cd272849c3 100644 --- a/src/data/move.ts +++ b/src/data/move.ts @@ -1,5 +1,5 @@ import { ChargeAnim, MoveChargeAnim, initMoveAnim, loadMoveAnimAssets } from "./battle-anims"; -import { DamagePhase, MovePhase, ObtainStatusEffectPhase, PokemonHealPhase, StatChangePhase } from "../battle-phases"; +import { BattleEndPhase, DamagePhase, MovePhase, NewBattlePhase, ObtainStatusEffectPhase, PokemonHealPhase, StatChangePhase, SwitchSummonPhase } from "../battle-phases"; import { BattleStat } from "./battle-stat"; import { BattlerTagType } from "./battler-tag"; import { getPokemonMessage } from "../messages"; @@ -45,7 +45,10 @@ export enum MoveTarget { export enum MoveFlags { MAKES_CONTACT = 1, IGNORE_PROTECT = 2, - IGNORE_VIRTUAL = 4 + IGNORE_VIRTUAL = 4, + SOUND_BASED = 8, + HIDE_USER = 16, + HIDE_TARGET = 32 } type MoveCondition = (user: Pokemon, target: Pokemon, move: Move) => boolean; @@ -154,6 +157,21 @@ export default class Move { return this; } + soundBased(soundBased?: boolean): Move { + this.setFlag(MoveFlags.SOUND_BASED, soundBased); + return this; + } + + hidesUser(hidesUser?: boolean): Move { + this.setFlag(MoveFlags.HIDE_USER, hidesUser); + return this; + } + + hidesTarget(hidesTarget?: boolean): Move { + this.setFlag(MoveFlags.HIDE_TARGET, hidesTarget); + return this; + } + applyConditions(user: Pokemon, target: Pokemon, move: Move): boolean { for (let condition of this.conditions) { if (!condition(user, target, move)) @@ -1975,6 +1993,56 @@ export class AddArenaTrapTagAttr extends AddArenaTagAttr { } } +export class ForceSwitchOutAttr extends MoveEffectAttr { + private user: boolean; + private batonPass: boolean; + + constructor(user?: boolean, batonPass?: boolean) { + super(false, MoveEffectTrigger.HIT); + + this.user = !!user; + this.batonPass = !!batonPass; + } + + apply(user: Pokemon, target: Pokemon, move: Move, args: any[]): Promise { + return new Promise(resolve => { + const switchOutTarget = this.user ? user : target; + if (switchOutTarget instanceof PlayerPokemon) { + (switchOutTarget as PlayerPokemon).switchOut(this.batonPass).then(() => resolve(true)); + return; + } else if (user.scene.currentBattle.battleType) { + if ((switchOutTarget instanceof PlayerPokemon ? user.scene.getParty() : user.scene.getEnemyParty()).filter(p => !p.isFainted()).length > 1) { + switchOutTarget.resetTurnData(); + switchOutTarget.resetSummonData(); + switchOutTarget.hideInfo(); + switchOutTarget.setVisible(false); + + user.scene.unshiftPhase(new SwitchSummonPhase(user.scene, switchOutTarget.getFieldIndex(), user.scene.currentBattle.trainer.getNextSummonIndex(), false, this.batonPass, false)); + } else + return resolve(false); + } else { + switchOutTarget.hideInfo().then(() => switchOutTarget.destroy()); + switchOutTarget.hp = 0; + + user.scene.queueMessage(getPokemonMessage(switchOutTarget, ' fled!'), null, true, 500); + + if (!switchOutTarget.getAlly()?.isActive(true)) { + user.scene.clearEnemyHeldItemModifiers(); + + user.scene.pushPhase(new BattleEndPhase(user.scene)); + user.scene.pushPhase(new NewBattlePhase(user.scene)); + } + } + + resolve(true); + }); + } + + getCondition(): MoveCondition { + return (user: Pokemon, target: Pokemon, move: Move) => !user.scene.currentBattle.battleType || ((this.user ? user : target) instanceof PlayerPokemon ? user.scene.getParty() : user.scene.getEnemyParty()).filter(p => !p.isFainted()).length === 1; + } +} + export class CopyTypeAttr extends MoveEffectAttr { constructor() { super(true); @@ -2256,53 +2324,53 @@ export function getMoveTargets(user: Pokemon, move: Moves): MoveTargetSet { const moveTarget = move ? allMoves[move].moveTarget : move === undefined ? MoveTarget.NEAR_ENEMY : []; const opponents = user.getOpponents(); - let set: BattlerIndex[] = []; + let set: Pokemon[] = []; let multiple = false; switch (moveTarget) { case MoveTarget.USER: - set = [ user.getBattlerIndex() ]; + set = [ user]; break; case MoveTarget.NEAR_OTHER: case MoveTarget.OTHER: case MoveTarget.ALL_NEAR_OTHERS: case MoveTarget.ALL_OTHERS: - set = (opponents.concat([ user.getAlly() ])).map(p => p?.getBattlerIndex()); + set = (opponents.concat([ user.getAlly() ])); multiple = moveTarget === MoveTarget.ALL_NEAR_OTHERS || moveTarget === MoveTarget.ALL_OTHERS break; case MoveTarget.NEAR_ENEMY: case MoveTarget.ALL_NEAR_ENEMIES: case MoveTarget.ALL_ENEMIES: case MoveTarget.ENEMY_SIDE: - set = opponents.map(p => p.getBattlerIndex()); + set = opponents; multiple = moveTarget !== MoveTarget.NEAR_ENEMY; break; case MoveTarget.RANDOM_NEAR_ENEMY: - set = [ opponents[Utils.randInt(opponents.length)].getBattlerIndex() ]; + set = [ opponents[Utils.randInt(opponents.length)] ]; break; case MoveTarget.ATTACKER: set = user.turnData.attacksReceived.length - ? [ user.scene.getPokemonById(user.turnData.attacksReceived[0].sourceId).getBattlerIndex() ] + ? [ user.scene.getPokemonById(user.turnData.attacksReceived[0].sourceId) ] : []; break; case MoveTarget.NEAR_ALLY: case MoveTarget.ALLY: - set = [ user.getAlly()?.getBattlerIndex() ]; + set = [ user.getAlly() ]; break; case MoveTarget.USER_OR_NEAR_ALLY: case MoveTarget.USER_AND_ALLIES: case MoveTarget.USER_SIDE: - set = [ user, user.getAlly() ].map(p => p?.getBattlerIndex()); + set = [ user, user.getAlly() ]; multiple = moveTarget !== MoveTarget.USER_OR_NEAR_ALLY; break; case MoveTarget.ALL: case MoveTarget.BOTH_SIDES: - set = [ user, user.getAlly() ].concat(user.getOpponents()).map(p => p?.getBattlerIndex()); + set = [ user, user.getAlly() ].concat(user.getOpponents()); multiple = true; break; } - return { targets: set.filter(t => t !== undefined), multiple }; + return { targets: set.filter(p => p?.isActive(true)).map(p => p.getBattlerIndex()).filter(t => t !== undefined), multiple }; } export const allMoves: Move[] = [ @@ -2344,7 +2412,9 @@ export function initMoves() { .target(MoveTarget.OTHER), new AttackMove(Moves.WING_ATTACK, "Wing Attack", Type.FLYING, MoveCategory.PHYSICAL, 60, 100, 35, -1, "", -1, 0, 1) .target(MoveTarget.OTHER), - new StatusMove(Moves.WHIRLWIND, "Whirlwind (N)", Type.NORMAL, -1, 20, -1, "In battles, the opponent switches. In the wild, the Pokémon runs.", -1, -6, 1), // TODO + new StatusMove(Moves.WHIRLWIND, "Whirlwind", Type.NORMAL, -1, 20, -1, "In battles, the opponent switches. In the wild, the Pokémon runs.", -1, -6, 1) + .attr(ForceSwitchOutAttr) + .hidesTarget(), new AttackMove(Moves.FLY, "Fly", Type.FLYING, MoveCategory.PHYSICAL, 90, 95, 15, 97, "Flies up on first turn, attacks on second turn.", -1, 0, 1) .attr(ChargeAttr, ChargeAnim.FLY_CHARGING, 'flew\nup high!', BattlerTagType.FLYING) .condition(failOnGravityCondition) @@ -2406,12 +2476,18 @@ export function initMoves() { .attr(FlinchAttr), new StatusMove(Moves.GROWL, "Growl", Type.NORMAL, 100, 40, -1, "Lowers opponent's Attack.", -1, 0, 1) .attr(StatChangeAttr, BattleStat.ATK, -1) - .target(MoveTarget.ALL_NEAR_ENEMIES), - new StatusMove(Moves.ROAR, "Roar (N)", Type.NORMAL, -1, 20, -1, "In battles, the opponent switches. In the wild, the Pokémon runs.", -1, -6, 1), + .target(MoveTarget.ALL_NEAR_ENEMIES) + .soundBased(), + new StatusMove(Moves.ROAR, "Roar", Type.NORMAL, -1, 20, -1, "In battles, the opponent switches. In the wild, the Pokémon runs.", -1, -6, 1) + .attr(ForceSwitchOutAttr) + .soundBased() + .hidesTarget(), new StatusMove(Moves.SING, "Sing", Type.NORMAL, 55, 15, -1, "Puts opponent to sleep.", -1, 0, 1) - .attr(StatusEffectAttr, StatusEffect.SLEEP), + .attr(StatusEffectAttr, StatusEffect.SLEEP) + .soundBased(), new StatusMove(Moves.SUPERSONIC, "Supersonic", Type.NORMAL, 55, 20, -1, "Confuses opponent.", -1, 0, 1) - .attr(ConfuseAttr), + .attr(ConfuseAttr) + .soundBased(), new AttackMove(Moves.SONIC_BOOM, "Sonic Boom", Type.NORMAL, MoveCategory.SPECIAL, -1, 90, 20, -1, "Always inflicts 20 HP.", -1, 0, 1) .attr(FixedDamageAttr, 20), new StatusMove(Moves.DISABLE, "Disable", Type.NORMAL, 100, 20, -1, "Opponent can't use its last attack for a few turns.", -1, 0, 1) @@ -2528,14 +2604,17 @@ export function initMoves() { .attr(StatChangeAttr, BattleStat.SPD, 2, true), new AttackMove(Moves.QUICK_ATTACK, "Quick Attack", Type.NORMAL, MoveCategory.PHYSICAL, 40, 100, 30, -1, "User attacks first.", -1, 1, 1), new AttackMove(Moves.RAGE, "Rage (N)", Type.NORMAL, MoveCategory.PHYSICAL, 20, 100, 20, -1, "Raises user's Attack when hit.", -1, 0, 1), // TODO - new SelfStatusMove(Moves.TELEPORT, "Teleport (N)", Type.PSYCHIC, -1, 20, -1, "Allows user to flee wild battles.", -1, 0, 1), + new SelfStatusMove(Moves.TELEPORT, "Teleport", Type.PSYCHIC, -1, 20, -1, "Allows user to flee wild battles.", -1, -6, 1) + .attr(ForceSwitchOutAttr, true) + .hidesUser(), new AttackMove(Moves.NIGHT_SHADE, "Night Shade", Type.GHOST, MoveCategory.SPECIAL, -1, 100, 15, 42, "Inflicts damage equal to user's level.", -1, 0, 1) .attr(LevelDamageAttr), new StatusMove(Moves.MIMIC, "Mimic", Type.NORMAL, -1, 10, -1, "Copies the opponent's last move.", -1, 0, 1) .attr(MovesetCopyMoveAttr) .ignoresVirtual(), new StatusMove(Moves.SCREECH, "Screech", Type.NORMAL, 85, 40, -1, "Sharply lowers opponent's Defense.", -1, 0, 1) - .attr(StatChangeAttr, BattleStat.DEF, -2), + .attr(StatChangeAttr, BattleStat.DEF, -2) + .soundBased(), new SelfStatusMove(Moves.DOUBLE_TEAM, "Double Team", Type.NORMAL, -1, 15, -1, "Raises user's Evasiveness.", -1, 0, 1) .attr(StatChangeAttr, BattleStat.EVA, 1, true), new SelfStatusMove(Moves.RECOVER, "Recover", Type.NORMAL, -1, 5, -1, "User recovers half its max HP.", -1, 0, 1) @@ -2714,7 +2793,8 @@ export function initMoves() { new AttackMove(Moves.SNORE, "Snore", Type.NORMAL, MoveCategory.SPECIAL, 50, 100, 15, -1, "Can only be used if asleep. May cause flinching.", 30, 0, 2) .attr(BypassSleepAttr) .attr(FlinchAttr) - .condition((user: Pokemon, target: Pokemon, move: Move) => user.status?.effect === StatusEffect.SLEEP), + .condition((user: Pokemon, target: Pokemon, move: Move) => user.status?.effect === StatusEffect.SLEEP) + .soundBased(), new StatusMove(Moves.CURSE, "Curse (N)", Type.GHOST, -1, 10, -1, "Ghosts lose 50% of max HP and curse the opponent; Non-Ghosts raise Attack, Defense and lower Speed.", -1, 0, 2) .target(MoveTarget.USER), new AttackMove(Moves.FLAIL, "Flail", Type.NORMAL, MoveCategory.PHYSICAL, -1, 100, 15, -1, "The lower the user's HP, the higher the power.", -1, 0, 2) @@ -2757,7 +2837,8 @@ export function initMoves() { .ignoresProtect(), new StatusMove(Moves.PERISH_SONG, "Perish Song (N)", Type.NORMAL, -1, 5, -1, "Any Pokémon in play when this attack is used faints in 3 turns.", -1, 0, 2) .ignoresProtect() - .target(MoveTarget.ALL), + .target(MoveTarget.ALL) + .soundBased(), new AttackMove(Moves.ICY_WIND, "Icy Wind", Type.ICE, MoveCategory.SPECIAL, 55, 95, 15, 34, "Lowers opponent's Speed.", 100, 0, 2) .attr(StatChangeAttr, BattleStat.SPD, -1) .target(MoveTarget.ALL_NEAR_ENEMIES), @@ -2805,7 +2886,8 @@ export function initMoves() { .attr(RandomMovesetMoveAttr) .condition((user: Pokemon, target: Pokemon, move: Move) => user.status?.effect === StatusEffect.SLEEP), new SelfStatusMove(Moves.HEAL_BELL, "Heal Bell (N)", Type.NORMAL, -1, 5, -1, "Heals the user's party's status conditions.", -1, 0, 2) - .target(MoveTarget.USER_AND_ALLIES), + .target(MoveTarget.USER_AND_ALLIES) + .soundBased(), new AttackMove(Moves.RETURN, "Return", Type.NORMAL, MoveCategory.PHYSICAL, -1, 100, 20, -1, "Power increases with higher Friendship.", -1, 0, 2) .attr(WinCountPowerMoveAttr), new AttackMove(Moves.PRESENT, "Present (N)", Type.NORMAL, MoveCategory.PHYSICAL, -1, 90, 15, -1, "Either deals damage or heals.", -1, 0, 2) @@ -2827,7 +2909,9 @@ export function initMoves() { new AttackMove(Moves.MEGAHORN, "Megahorn", Type.BUG, MoveCategory.PHYSICAL, 120, 85, 10, -1, "", -1, 0, 2), new AttackMove(Moves.DRAGON_BREATH, "Dragon Breath", Type.DRAGON, MoveCategory.SPECIAL, 60, 100, 20, -1, "May paralyze opponent.", 30, 0, 2) .attr(StatusEffectAttr, StatusEffect.PARALYSIS), - new SelfStatusMove(Moves.BATON_PASS, "Baton Pass (N)", Type.NORMAL, -1, 40, 132, "User switches out and gives stat changes to the incoming Pokémon.", -1, 0, 2), + new SelfStatusMove(Moves.BATON_PASS, "Baton Pass", Type.NORMAL, -1, 40, 132, "User switches out and gives stat changes to the incoming Pokémon.", -1, 0, 2) + .attr(ForceSwitchOutAttr, true, true) + .hidesUser(), new StatusMove(Moves.ENCORE, "Encore (N)", Type.NORMAL, 100, 5, 122, "Forces opponent to keep using its last move for 3 turns.", -1, 0, 2), new AttackMove(Moves.PURSUIT, "Pursuit (N)", Type.DARK, MoveCategory.PHYSICAL, 40, 100, 20, -1, "Double power if the opponent is switching out.", -1, 0, 2), new AttackMove(Moves.RAPID_SPIN, "Rapid Spin", Type.NORMAL, MoveCategory.PHYSICAL, 50, 100, 40, -1, "Raises user's Speed and removes entry hazards and trap move effects.", 100, 0, 2) @@ -2883,7 +2967,8 @@ export function initMoves() { .condition((user: Pokemon, target: Pokemon, move: Move) => !user.getMoveHistory().length), new AttackMove(Moves.UPROAR, "Uproar (N)", Type.NORMAL, MoveCategory.SPECIAL, 90, 100, 10, -1, "User attacks for 3 turns and prevents sleep.", -1, 0, 3) .ignoresVirtual() - .target(MoveTarget.RANDOM_NEAR_ENEMY), + .target(MoveTarget.RANDOM_NEAR_ENEMY) + .soundBased(), new SelfStatusMove(Moves.STOCKPILE, "Stockpile (N)", Type.NORMAL, -1, 20, -1, "Stores energy for use with Spit Up and Swallow.", -1, 0, 3), new AttackMove(Moves.SPIT_UP, "Spit Up (N)", Type.NORMAL, MoveCategory.SPECIAL, -1, 100, 10, -1, "Power depends on how many times the user performed Stockpile.", -1, 0, 3), new SelfStatusMove(Moves.SWALLOW, "Swallow (N)", Type.NORMAL, -1, 10, -1, "The more times the user has performed Stockpile, the more HP is recovered.", -1, 0, 3), @@ -2981,7 +3066,8 @@ export function initMoves() { new SelfStatusMove(Moves.SLACK_OFF, "Slack Off", Type.NORMAL, -1, 5, -1, "User recovers half its max HP.", -1, 0, 3) .attr(HealAttr), new AttackMove(Moves.HYPER_VOICE, "Hyper Voice", Type.NORMAL, MoveCategory.SPECIAL, 90, 100, 10, 117, "", -1, 0, 3) - .target(MoveTarget.ALL_NEAR_ENEMIES), + .target(MoveTarget.ALL_NEAR_ENEMIES) + .soundBased(), new AttackMove(Moves.POISON_FANG, "Poison Fang", Type.POISON, MoveCategory.PHYSICAL, 50, 100, 15, -1, "May badly poison opponent.", 50, 0, 3) .attr(StatusEffectAttr, StatusEffect.TOXIC), new AttackMove(Moves.CRUSH_CLAW, "Crush Claw", Type.NORMAL, MoveCategory.PHYSICAL, 75, 95, 10, -1, "May lower opponent's Defense.", 50, 0, 3) @@ -3011,9 +3097,11 @@ export function initMoves() { new AttackMove(Moves.SILVER_WIND, "Silver Wind", Type.BUG, MoveCategory.SPECIAL, 60, 100, 5, -1, "May raise all stats of user at once.", 10, 0, 3) .attr(StatChangeAttr, [ BattleStat.ATK, BattleStat.DEF, BattleStat.SPATK, BattleStat.SPDEF, BattleStat.SPD ], 1, true), new StatusMove(Moves.METAL_SOUND, "Metal Sound", Type.STEEL, 85, 40, -1, "Sharply lowers opponent's Special Defense.", -1, 0, 3) - .attr(StatChangeAttr, BattleStat.SPDEF, -2), + .attr(StatChangeAttr, BattleStat.SPDEF, -2) + .soundBased(), new StatusMove(Moves.GRASS_WHISTLE, "Grass Whistle", Type.GRASS, 55, 15, -1, "Puts opponent to sleep.", -1, 0, 3) - .attr(StatusEffectAttr, StatusEffect.SLEEP), + .attr(StatusEffectAttr, StatusEffect.SLEEP) + .soundBased(), new StatusMove(Moves.TICKLE, "Tickle", Type.NORMAL, 100, 20, -1, "Lowers opponent's Attack and Defense.", -1, 0, 3) .attr(StatChangeAttr, BattleStat.ATK, -1) .attr(StatChangeAttr, BattleStat.DEF, -1), @@ -3051,7 +3139,8 @@ export function initMoves() { .attr(AddBattlerTagAttr, BattlerTagType.TRAPPED, false, 1, true), new SelfStatusMove(Moves.HOWL, "Howl", Type.NORMAL, -1, 40, -1, "Raises Attack of allies.", -1, 0, 3) .attr(StatChangeAttr, BattleStat.ATK, 1, true) - .target(MoveTarget.USER_AND_ALLIES), // TODO + .target(MoveTarget.USER_AND_ALLIES) + .soundBased(), // TODO new AttackMove(Moves.DRAGON_CLAW, "Dragon Claw", Type.DRAGON, MoveCategory.PHYSICAL, 80, 100, 15, 78, "", -1, 0, 3), new AttackMove(Moves.FRENZY_PLANT, "Frenzy Plant", Type.GRASS, MoveCategory.SPECIAL, 150, 90, 5, 155, "User must recharge next turn.", -1, 0, 3) .attr(AddBattlerTagAttr, BattlerTagType.RECHARGING, true), @@ -3191,7 +3280,8 @@ export function initMoves() { .target(MoveTarget.OTHER), new AttackMove(Moves.X_SCISSOR, "X-Scissor", Type.BUG, MoveCategory.PHYSICAL, 80, 100, 15, 105, "", -1, 0, 4), new AttackMove(Moves.BUG_BUZZ, "Bug Buzz", Type.BUG, MoveCategory.SPECIAL, 90, 100, 10, 162, "May lower opponent's Special Defense.", 10, 0, 4) - .attr(StatChangeAttr, BattleStat.SPDEF, -1), + .attr(StatChangeAttr, BattleStat.SPDEF, -1) + .soundBased(), new AttackMove(Moves.DRAGON_PULSE, "Dragon Pulse", Type.DRAGON, MoveCategory.SPECIAL, 85, 100, 10, 115, "", -1, 0, 4) .target(MoveTarget.OTHER), new AttackMove(Moves.DRAGON_RUSH, "Dragon Rush", Type.DRAGON, MoveCategory.PHYSICAL, 100, 75, 10, -1, "May cause flinching.", 20, 0, 4) @@ -3289,7 +3379,8 @@ export function initMoves() { .makesContact(), new AttackMove(Moves.CHATTER, "Chatter", Type.FLYING, MoveCategory.SPECIAL, 65, 100, 20, -1, "Confuses opponent.", 100, 0, 4) .attr(ConfuseAttr) - .target(MoveTarget.OTHER), + .target(MoveTarget.OTHER) + .soundBased(), new AttackMove(Moves.JUDGMENT, "Judgment (N)", Type.NORMAL, MoveCategory.SPECIAL, 100, 100, 10, -1, "Type depends on the Arceus Plate being held.", -1, 0, 4), new AttackMove(Moves.BUG_BITE, "Bug Bite (N)", Type.BUG, MoveCategory.PHYSICAL, 60, 100, 20, -1, "Receives the effect from the opponent's held berry.", -1, 0, 4), new AttackMove(Moves.CHARGE_BEAM, "Charge Beam", Type.ELECTRIC, MoveCategory.SPECIAL, 50, 90, 10, 23, "May raise user's Special Attack.", 70, 0, 4) @@ -3377,9 +3468,11 @@ export function initMoves() { new StatusMove(Moves.ENTRAINMENT, "Entrainment (N)", Type.NORMAL, 100, 15, -1, "Makes target's ability same as user's.", -1, 0, 5), new StatusMove(Moves.AFTER_YOU, "After You (N)", Type.NORMAL, -1, 15, -1, "Gives target priority in the next turn.", -1, 0, 5) .ignoresProtect(), - new AttackMove(Moves.ROUND, "Round", Type.NORMAL, MoveCategory.SPECIAL, 60, 100, 15, -1, "Power increases if teammates use it in the same turn.", -1, 0, 5), // TODO + new AttackMove(Moves.ROUND, "Round", Type.NORMAL, MoveCategory.SPECIAL, 60, 100, 15, -1, "Power increases if teammates use it in the same turn.", -1, 0, 5) + .soundBased(), // TODO new AttackMove(Moves.ECHOED_VOICE, "Echoed Voice", Type.NORMAL, MoveCategory.SPECIAL, 40, 100, 15, -1, "Power increases each turn.", -1, 0, 5) - .attr(ConsecutiveUseMultiBasePowerAttr, 5, false), + .attr(ConsecutiveUseMultiBasePowerAttr, 5, false) + .soundBased(), new AttackMove(Moves.CHIP_AWAY, "Chip Away (N)", Type.NORMAL, MoveCategory.PHYSICAL, 70, 100, 20, -1, "Ignores opponent's stat changes.", -1, 0, 5), new AttackMove(Moves.CLEAR_SMOG, "Clear Smog (N)", Type.POISON, MoveCategory.SPECIAL, 50, -1, 15, -1, "Removes all of the target's stat changes.", -1, 0, 5), new AttackMove(Moves.STORED_POWER, "Stored Power (N)", Type.PSYCHIC, MoveCategory.SPECIAL, 20, 100, 10, 41, "Power increases when user's stats have been raised.", -1, 0, 5), @@ -3405,7 +3498,8 @@ export function initMoves() { new SelfStatusMove(Moves.SHIFT_GEAR, "Shift Gear", Type.STEEL, -1, 10, -1, "Raises user's Attack and sharply raises Speed.", -1, 0, 5) .attr(StatChangeAttr, BattleStat.ATK, 1, true) .attr(StatChangeAttr, BattleStat.SPD, 2, true), - new AttackMove(Moves.CIRCLE_THROW, "Circle Throw (N)", Type.FIGHTING, MoveCategory.PHYSICAL, 60, 90, 10, -1, "In battles, the opponent switches. In the wild, the Pokémon runs.", -1, -6, 5), + new AttackMove(Moves.CIRCLE_THROW, "Circle Throw", Type.FIGHTING, MoveCategory.PHYSICAL, 60, 90, 10, -1, "In battles, the opponent switches. In the wild, the Pokémon runs.", -1, -6, 5) + .attr(ForceSwitchOutAttr), new AttackMove(Moves.INCINERATE, "Incinerate (N)", Type.FIRE, MoveCategory.SPECIAL, 60, 100, 15, -1, "Destroys the target's held berry.", -1, 0, 5) .target(MoveTarget.ALL_NEAR_ENEMIES), new StatusMove(Moves.QUASH, "Quash (N)", Type.DARK, 100, 15, -1, "Makes the target act last this turn.", -1, 0, 5), @@ -3434,7 +3528,8 @@ export function initMoves() { .target(MoveTarget.ALL_NEAR_OTHERS), new AttackMove(Moves.FROST_BREATH, "Frost Breath", Type.ICE, MoveCategory.SPECIAL, 60, 90, 10, -1, "Always results in a critical hit.", 100, 0, 5) .attr(CritOnlyAttr), - new AttackMove(Moves.DRAGON_TAIL, "Dragon Tail (N)", Type.DRAGON, MoveCategory.PHYSICAL, 60, 90, 10, 44, "In battles, the opponent switches. In the wild, the Pokémon runs.", -1, -6, 5), + new AttackMove(Moves.DRAGON_TAIL, "Dragon Tail", Type.DRAGON, MoveCategory.PHYSICAL, 60, 90, 10, 44, "In battles, the opponent switches. In the wild, the Pokémon runs.", -1, -6, 5) + .attr(ForceSwitchOutAttr), new SelfStatusMove(Moves.WORK_UP, "Work Up", Type.NORMAL, -1, 30, -1, "Raises user's Attack and Special Attack.", -1, 0, 5) .attr(StatChangeAttr, [ BattleStat.ATK, BattleStat.SPATK ], 1, true), new AttackMove(Moves.ELECTROWEB, "Electroweb", Type.ELECTRIC, MoveCategory.SPECIAL, 55, 95, 15, -1, "Lowers opponent's Speed.", 100, 0, 5) @@ -3479,7 +3574,8 @@ export function initMoves() { new AttackMove(Moves.TECHNO_BLAST, "Techno Blast (N)", Type.NORMAL, MoveCategory.SPECIAL, 120, 100, 5, -1, "Type depends on the Drive being held.", -1, 0, 5), new AttackMove(Moves.RELIC_SONG, "Relic Song", Type.NORMAL, MoveCategory.SPECIAL, 75, 100, 10, -1, "May put the target to sleep.", 10, 0, 5) .attr(StatusEffectAttr, StatusEffect.SLEEP) - .target(MoveTarget.ALL_NEAR_ENEMIES), + .target(MoveTarget.ALL_NEAR_ENEMIES) + .soundBased(), new AttackMove(Moves.SECRET_SWORD, "Secret Sword (N)", Type.FIGHTING, MoveCategory.SPECIAL, 85, 100, 10, -1, "Inflicts damage based on the target's Defense, not Special Defense.", -1, 0, 5), new AttackMove(Moves.GLACIATE, "Glaciate", Type.ICE, MoveCategory.SPECIAL, 65, 95, 10, -1, "Lowers opponent's Speed.", 100, 0, 5) .attr(StatChangeAttr, BattleStat.SPD, -1) @@ -3499,7 +3595,8 @@ export function initMoves() { .ignoresVirtual(), new AttackMove(Moves.SNARL, "Snarl", Type.DARK, MoveCategory.SPECIAL, 55, 95, 15, 30, "Lowers opponent's Special Attack.", 100, 0, 5) .attr(StatChangeAttr, BattleStat.SPATK, -1) - .target(MoveTarget.ALL_NEAR_ENEMIES), + .target(MoveTarget.ALL_NEAR_ENEMIES) + .soundBased(), new AttackMove(Moves.ICICLE_CRASH, "Icicle Crash", Type.ICE, MoveCategory.PHYSICAL, 85, 90, 10, -1, "May cause flinching.", 30, 0, 5) .attr(FlinchAttr) .makesContact(false), diff --git a/src/pokemon.ts b/src/pokemon.ts index a1842f8c7e9..ecdebf3b560 100644 --- a/src/pokemon.ts +++ b/src/pokemon.ts @@ -15,7 +15,7 @@ import { initMoveAnim, loadMoveAnimAssets } from './data/battle-anims'; import { Status, StatusEffect } from './data/status-effect'; import { tmSpecies } from './data/tms'; import { pokemonEvolutions, pokemonPrevolutions, SpeciesEvolution, SpeciesEvolutionCondition } from './data/pokemon-evolutions'; -import { DamagePhase, FaintPhase } from './battle-phases'; +import { DamagePhase, FaintPhase, SwitchSummonPhase } from './battle-phases'; import { BattleStat } from './data/battle-stat'; import { BattlerTag, BattlerTagLapseType, BattlerTagType, TypeBoostTag, getBattlerTag } from './data/battler-tag'; import { Species } from './data/species'; @@ -26,6 +26,8 @@ import { Biome } from './data/biome'; import { Abilities, Ability, BattleStatMultiplierAbAttr, BlockCritAbAttr, NonSuperEffectiveImmunityAbAttr, PreApplyBattlerTagAbAttr, StabBoostAbAttr, StatusEffectImmunityAbAttr, TypeImmunityAbAttr, VariableMovePowerAbAttr, abilities, applyAbAttrs, applyBattleStatMultiplierAbAttrs, applyPostDefendAbAttrs, applyPreApplyBattlerTagAbAttrs, applyPreAttackAbAttrs, applyPreDefendAbAttrs, applyPreSetStatusAbAttrs } from './data/ability'; import PokemonData from './system/pokemon-data'; import { BattlerIndex } from './battle'; +import { Mode } from './ui/ui'; +import PartyUiHandler, { PartyOption, PartyUiMode } from './ui/party-ui-handler'; export enum FieldPosition { CENTER, @@ -184,6 +186,8 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container { } isOnField(): boolean { + if (!this.scene) + return false; return this.scene.field.getIndex(this) > -1; } @@ -192,6 +196,8 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container { } isActive(onField?: boolean): boolean { + if (!this.scene) + return false; return !this.isFainted() && !!this.scene && (!onField || this.isOnField()); } @@ -1161,6 +1167,21 @@ export class PlayerPokemon extends Pokemon { } } + switchOut(batonPass: boolean): Promise { + return new Promise(resolve => { + this.resetTurnData(); + this.resetSummonData(); + this.hideInfo(); + this.setVisible(false); + + this.scene.ui.setMode(Mode.PARTY, PartyUiMode.FAINT_SWITCH, this.getFieldIndex(), (slotIndex: integer, option: PartyOption) => { + if (slotIndex >= this.scene.currentBattle.getBattlerCount() && slotIndex < 6) + this.scene.unshiftPhase(new SwitchSummonPhase(this.scene, this.getFieldIndex(), slotIndex, false, batonPass)); + this.scene.ui.setMode(Mode.MESSAGE).then(() => resolve()); + }, PartyUiHandler.FilterNonFainted); + }); + } + evolve(evolution: SpeciesEvolution): Promise { return new Promise(resolve => { this.handleSpecialEvolutions(evolution); diff --git a/src/utils.ts b/src/utils.ts index 3fbb16218b7..2888a7cfd0c 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -114,6 +114,10 @@ export function getEnumValues(enumType): integer[] { return Object.values(enumType).filter(v => !isNaN(parseInt(v.toString()))).map(v => parseInt(v.toString())); } +export function executeIf(condition: boolean, promiseFunc: () => Promise): Promise { + return condition ? promiseFunc() : new Promise(resolve => resolve(null)); +} + export class BooleanHolder { public value: boolean;