diff --git a/src/battle-phases.ts b/src/battle-phases.ts index de754cf96ce..0c4bd6d611f 100644 --- a/src/battle-phases.ts +++ b/src/battle-phases.ts @@ -1,11 +1,11 @@ import BattleScene, { maxExpLevel, startingLevel, startingWave } from "./battle-scene"; -import { default as Pokemon, PlayerPokemon, EnemyPokemon, PokemonMove, MoveResult, DamageResult, FieldPosition } from "./pokemon"; +import { default as Pokemon, PlayerPokemon, EnemyPokemon, PokemonMove, MoveResult, DamageResult, FieldPosition, HitResult } from "./pokemon"; import * as Utils from './utils'; -import { allMoves, applyMoveAttrs, BypassSleepAttr, ChargeAttr, applyFilteredMoveAttrs, HitsTagAttr, MissEffectAttr, MoveAttr, MoveCategory, MoveEffectAttr, MoveFlags, MoveHitEffectAttr, Moves, MultiHitAttr, OverrideMoveEffectAttr, VariableAccuracyAttr, MoveTarget, OneHitKOAttr } from "./data/move"; +import { allMoves, applyMoveAttrs, BypassSleepAttr, ChargeAttr, applyFilteredMoveAttrs, HitsTagAttr, MissEffectAttr, MoveAttr, MoveCategory, MoveEffectAttr, MoveFlags, MoveHitEffectAttr, Moves, MultiHitAttr, OverrideMoveEffectAttr, VariableAccuracyAttr, MoveTarget, OneHitKOAttr, getMoveTargets } from "./data/move"; import { Mode } from './ui/ui'; import { Command } from "./ui/command-ui-handler"; import { Stat } from "./data/pokemon-stat"; -import { BerryModifier, ContactHeldItemTransferChanceModifier, ExpBalanceModifier, ExpBoosterModifier, ExpShareModifier, ExtraModifierModifier, FlinchChanceModifier, HealingBoosterModifier, HeldItemTransferModifier, HitHealModifier, MapModifier, MultipleParticipantExpBonusModifier, PokemonExpBoosterModifier, PokemonHeldItemModifier, SwitchEffectTransferModifier, TempBattleStatBoosterModifier, TurnHealModifier, TurnHeldItemTransferModifier } from "./modifier/modifier"; +import { BerryModifier, ContactHeldItemTransferChanceModifier, ExpBalanceModifier, ExpBoosterModifier, ExpShareModifier, ExtraModifierModifier, FlinchChanceModifier, HealingBoosterModifier, HitHealModifier, MapModifier, MultipleParticipantExpBonusModifier, PokemonExpBoosterModifier, PokemonHeldItemModifier, SwitchEffectTransferModifier, TempBattleStatBoosterModifier, TurnHealModifier, TurnHeldItemTransferModifier } from "./modifier/modifier"; import PartyUiHandler, { PartyOption, PartyUiMode } from "./ui/party-ui-handler"; import { doPokeballBounceAnim, getPokeballAtlasKey, getPokeballCatchMultiplier, getPokeballName, getPokeballTintColor, PokeballType } from "./data/pokeball"; import { CommonAnim, CommonBattleAnim, MoveAnim, initMoveAnim, loadMoveAnimAssets } from "./data/battle-anims"; @@ -28,7 +28,7 @@ import { ArenaTagType, ArenaTrapTag, TrickRoomTag } from "./data/arena-tag"; import { CheckTrappedAbAttr, PostDefendAbAttr, PostSummonAbAttr, PostTurnAbAttr, PostWeatherLapseAbAttr, PreWeatherDamageAbAttr, ProtectStatAbAttr, SuppressWeatherEffectAbAttr, applyCheckTrappedAbAttrs, applyPostDefendAbAttrs, applyPostSummonAbAttrs, applyPostTurnAbAttrs, applyPostWeatherLapseAbAttrs, applyPreStatChangeAbAttrs, applyPreWeatherEffectAbAttrs } from "./data/ability"; import { Unlockables, getUnlockableName } from "./system/unlockables"; import { getBiomeKey } from "./arena"; -import { BattleTarget } from "./battle"; +import { BattleTarget, TurnCommand } from "./battle"; export class CheckLoadPhase extends BattlePhase { private loaded: boolean; @@ -472,21 +472,28 @@ export class SummonPhase extends PartyMemberPokemonPhase { } else playerPokemon.setFieldPosition(!this.scene.currentBattle.double ? FieldPosition.CENTER : FieldPosition.LEFT); + const xOffset = playerPokemon.getFieldPositionOffset()[0]; + pokeball.setVisible(true); + + this.scene.tweens.add({ + targets: pokeball, + duration: 650, + x: 100 + xOffset + }); + this.scene.tweens.add({ targets: pokeball, - ease: 'Cubic.easeOut', duration: 150, - x: 54, + ease: 'Cubic.easeOut', y: 70, onComplete: () => { this.scene.tweens.add({ targets: pokeball, duration: 500, - angle: 1440, - x: 100, - y: 132, ease: 'Cubic.easeIn', + angle: 1440, + y: 132, onComplete: () => { this.scene.sound.play('pb_rel'); pokeball.destroy(); @@ -734,11 +741,15 @@ export class CommandPhase extends FieldPhase { switch (command) { case Command.FIGHT: - const targetIndex = Utils.randInt(enemyField.length); // TODO: Let user select this - if (cursor === -1 || playerPokemon.trySelectMove(cursor, args[0] as boolean)) { - this.scene.currentBattle.turnCommands[this.fieldIndex] = { command: Command.FIGHT, cursor: cursor, - move: cursor > -1 ? { move: playerPokemon.moveset[cursor].moveId } : null, targetIndex: targetIndex, args: args }; // TODO: Struggle logic + const turnCommand: TurnCommand = { command: Command.FIGHT, cursor: cursor, + move: cursor > -1 ? { move: playerPokemon.moveset[cursor].moveId } : null, args: args }; // TODO: Struggle logic + const moveTargets = getMoveTargets(playerPokemon, playerPokemon.moveset[cursor].moveId); + if (moveTargets.targets.length <= 1 || moveTargets.multiple) + turnCommand.targets = moveTargets.targets; + else + this.scene.unshiftPhase(new SelectTargetPhase(this.scene, this.fieldIndex)); + this.scene.currentBattle.turnCommands[this.fieldIndex] = turnCommand; success = true; } else if (cursor < playerPokemon.getMoveset().length) { const move = playerPokemon.getMoveset()[cursor]; @@ -761,8 +772,8 @@ export class CommandPhase extends FieldPhase { this.scene.ui.setMode(Mode.COMMAND); }, null, true); } else if (cursor < 4) { - const targetIndex = Utils.randInt(enemyField.length); // TODO: Let user select this - this.scene.currentBattle.turnCommands[this.fieldIndex] = { command: Command.BALL, cursor: cursor, targetIndex: targetIndex }; + this.scene.currentBattle.turnCommands[this.fieldIndex] = { command: Command.BALL, cursor: cursor }; + this.scene.unshiftPhase(new SelectTargetPhase(this.scene, this.fieldIndex)) success = true; } break; @@ -814,14 +825,39 @@ export class EnemyCommandPhase extends FieldPhase { super.start(); const enemyPokemon = this.scene.getEnemyField()[this.fieldIndex]; - const playerField = this.scene.getPlayerField(); - this.scene.currentBattle.turnCommands[this.fieldIndex + BattleTarget.ENEMY] = { command: Command.FIGHT, move: enemyPokemon.getNextMove(), targetIndex: Utils.randInt(playerField.length) } + const nextMove = enemyPokemon.getNextMove(); + const moveTargets = getMoveTargets(enemyPokemon, nextMove.move); + + this.scene.currentBattle.turnCommands[this.fieldIndex + BattleTarget.ENEMY] = + { command: Command.FIGHT, move: nextMove, targets: !moveTargets.multiple ? [ moveTargets.targets[Utils.randInt(moveTargets.targets.length)] ] : moveTargets.targets }; this.end(); } } +export class SelectTargetPhase extends PokemonPhase { + constructor(scene: BattleScene, fieldIndex: integer) { + super(scene, true, fieldIndex); + } + + start() { + super.start(); + + const move = this.scene.currentBattle.turnCommands[this.fieldIndex].move?.move || Moves.NONE; + this.scene.ui.setMode(Mode.TARGET_SELECT, this.fieldIndex, move, (cursor: integer) => { + this.scene.ui.setMode(Mode.MESSAGE); + if (cursor === -1) { + this.scene.currentBattle.turnCommands[this.fieldIndex] = null; + this.scene.unshiftPhase(new CommandPhase(this.scene, this.fieldIndex)); + } else + this.scene.currentBattle.turnCommands[this.fieldIndex].targets = [ cursor ]; + console.log(cursor, this.fieldIndex, this.scene.currentBattle.turnCommands[this.fieldIndex].targets) + this.end(); + }); + } +} + export class TurnStartPhase extends FieldPhase { constructor(scene: BattleScene) { super(scene); @@ -874,16 +910,16 @@ export class TurnStartPhase extends FieldPhase { const move = pokemon.getMoveset().find(m => m.moveId === queuedMove.move) || new PokemonMove(queuedMove.move); if (pokemon.isPlayer()) { if (turnCommand.cursor === -1) - this.scene.pushPhase(new PlayerMovePhase(this.scene, pokemon as PlayerPokemon, turnCommand.targetIndex, move)); + this.scene.pushPhase(new MovePhase(this.scene, pokemon, turnCommand.targets, move)); else { - const playerPhase = new PlayerMovePhase(this.scene, pokemon as PlayerPokemon, turnCommand.targetIndex, move, false, queuedMove.ignorePP); + const playerPhase = new MovePhase(this.scene, pokemon, turnCommand.targets, move, false, queuedMove.ignorePP); this.scene.pushPhase(playerPhase); } } else - this.scene.pushPhase(new EnemyMovePhase(this.scene, pokemon as EnemyPokemon, turnCommand.targetIndex, move, false, queuedMove.ignorePP)); + this.scene.pushPhase(new MovePhase(this.scene, pokemon, turnCommand.targets, move, false, queuedMove.ignorePP)); break; case Command.BALL: - this.scene.unshiftPhase(new AttemptCapturePhase(this.scene, turnCommand.targetIndex, turnCommand.cursor)); + this.scene.unshiftPhase(new AttemptCapturePhase(this.scene, turnCommand.targets[0] % 2, turnCommand.cursor)); break; case Command.POKEMON: case Command.RUN: @@ -996,29 +1032,27 @@ export class CommonAnimPhase extends PokemonPhase { } } -export abstract class MovePhase extends BattlePhase { +export class MovePhase extends BattlePhase { protected pokemon: Pokemon; - protected targetIndex: integer; + protected targets: BattleTarget[]; protected move: PokemonMove; protected followUp: boolean; protected ignorePp: boolean; protected cancelled: boolean; - constructor(scene: BattleScene, pokemon: Pokemon, targetIndex: integer, move: PokemonMove, followUp?: boolean, ignorePp?: boolean) { + constructor(scene: BattleScene, pokemon: Pokemon, targets: BattleTarget[], move: PokemonMove, followUp?: boolean, ignorePp?: boolean) { super(scene); this.pokemon = pokemon; - this.targetIndex = targetIndex; + this.targets = targets; this.move = move; this.followUp = !!followUp; this.ignorePp = !!ignorePp; this.cancelled = false; } - abstract getEffectPhase(): MoveEffectPhase; - canMove(): boolean { - return !!this.pokemon.hp && this.move.isUsable(this.ignorePp); + return !!this.pokemon.hp && this.move.isUsable(this.ignorePp) && !!this.targets.length; } cancel(): void { @@ -1030,6 +1064,8 @@ export abstract class MovePhase extends BattlePhase { console.log(Moves[this.move.moveId]); + console.log(this.scene.currentBattle.turnCommands); + if (!this.canMove()) { if (this.move.isDisabled()) this.scene.queueMessage(`${this.move.getName()} is disabled!`); @@ -1037,7 +1073,9 @@ export abstract class MovePhase extends BattlePhase { return; } - const target = this.pokemon.getOpponent(this.targetIndex); + console.log(this.targets); + + const targets = this.scene.getField().filter(p => p && p.hp && this.targets.indexOf(p.getBattleTarget()) > -1); if (!this.followUp && this.canMove()) this.pokemon.lapseTags(BattlerTagLapseType.MOVE); @@ -1045,13 +1083,13 @@ export abstract class MovePhase extends BattlePhase { const doMove = () => { const moveQueue = this.pokemon.getMoveQueue(); - if (moveQueue.length && moveQueue[0].move === Moves.NONE) { + if ((moveQueue.length && moveQueue[0].move === Moves.NONE) || !targets.length) { moveQueue.shift(); this.cancel(); } if (this.cancelled) { - this.pokemon.pushMoveHistory({ move: Moves.NONE, result: MoveResult.FAILED }); + this.pokemon.pushMoveHistory({ move: Moves.NONE, result: MoveResult.FAIL }); this.end(); return; } @@ -1060,13 +1098,14 @@ export abstract class MovePhase extends BattlePhase { if (!moveQueue.length || !moveQueue.shift().ignorePP) this.move.ppUsed++; - let success = this.move.getMove().applyConditions(this.pokemon, target, this.move.getMove()); + // Assume conditions affecting targets only apply to moves with a single target + let success = this.move.getMove().applyConditions(this.pokemon, targets[0], this.move.getMove()); if (success && this.scene.arena.isMoveWeatherCancelled(this.move.getMove())) success = false; if (success) this.scene.unshiftPhase(this.getEffectPhase()); else { - this.pokemon.pushMoveHistory({ move: this.move.moveId, result: MoveResult.FAILED, virtual: this.move.virtual }); + this.pokemon.pushMoveHistory({ move: this.move.moveId, targets: this.targets, result: MoveResult.FAIL, virtual: this.move.virtual }); this.scene.queueMessage('But it failed!'); } @@ -1086,7 +1125,7 @@ export abstract class MovePhase extends BattlePhase { } break; case StatusEffect.SLEEP: - applyMoveAttrs(BypassSleepAttr, this.pokemon, target, this.move.getMove()); + applyMoveAttrs(BypassSleepAttr, this.pokemon, null, this.move.getMove()); healed = this.pokemon.status.turnCount === this.pokemon.status.cureTurn; activated = !healed && !this.pokemon.getTag(BattlerTagType.BYPASS_SLEEP); this.cancelled = activated; @@ -1099,7 +1138,7 @@ export abstract class MovePhase extends BattlePhase { } if (activated) { this.scene.queueMessage(getPokemonMessage(this.pokemon, getStatusEffectActivationText(this.pokemon.status.effect))); - this.scene.unshiftPhase(new CommonAnimPhase(this.scene, this.pokemon.isPlayer(), this.pokemon.getFieldIndex(), this.targetIndex, CommonAnim.POISON + (this.pokemon.status.effect - 1))); + this.scene.unshiftPhase(new CommonAnimPhase(this.scene, this.pokemon.isPlayer(), this.pokemon.getFieldIndex(), this.pokemon.getBattleTarget(), CommonAnim.POISON + (this.pokemon.status.effect - 1))); doMove(); } else { if (healed) { @@ -1113,6 +1152,10 @@ export abstract class MovePhase extends BattlePhase { doMove(); } + getEffectPhase(): MoveEffectPhase { + return new MoveEffectPhase(this.scene, this.pokemon.isPlayer(), this.pokemon.getFieldIndex(), this.targets, this.move); + } + end() { if (!this.followUp && this.canMove()) this.scene.unshiftPhase(new MoveEndPhase(this.scene, this.pokemon.isPlayer(), this.pokemon.getFieldIndex())); @@ -1121,46 +1164,27 @@ export abstract class MovePhase extends BattlePhase { } } -export class PlayerMovePhase extends MovePhase { - constructor(scene: BattleScene, pokemon: PlayerPokemon, targetIndex: integer, move: PokemonMove, followUp?: boolean, ignorePp?: boolean) { - super(scene, pokemon, targetIndex, move, followUp, ignorePp); - } - - getEffectPhase(): MoveEffectPhase { - return new PlayerMoveEffectPhase(this.scene, this.pokemon.getFieldIndex(), this.targetIndex, this.move); - } -} - -export class EnemyMovePhase extends MovePhase { - constructor(scene: BattleScene, pokemon: EnemyPokemon, targetIndex: integer, move: PokemonMove, followUp?: boolean, ignorePp?: boolean) { - super(scene, pokemon, targetIndex, move, followUp, ignorePp); - } - - getEffectPhase(): MoveEffectPhase { - return new EnemyMoveEffectPhase(this.scene, this.pokemon.getFieldIndex(), this.targetIndex, this.move); - } -} - -abstract class MoveEffectPhase extends PokemonPhase { +class MoveEffectPhase extends PokemonPhase { protected move: PokemonMove; - protected targetIndex: integer; + protected targets: BattleTarget[]; - constructor(scene: BattleScene, player: boolean, fieldIndex: integer, targetIndex: integer, move: PokemonMove) { + constructor(scene: BattleScene, player: boolean, fieldIndex: integer, targets: BattleTarget[], move: PokemonMove) { super(scene, player, fieldIndex); this.move = move; - this.targetIndex = targetIndex; + this.targets = targets; } start() { super.start(); const user = this.getUserPokemon(); - const target = this.getTargetPokemon(); + const targets = this.getTargets(); const overridden = new Utils.BooleanHolder(false); - applyMoveAttrs(OverrideMoveEffectAttr, user, target, this.move.getMove(), overridden).then(() => { + // Assume single target for override + applyMoveAttrs(OverrideMoveEffectAttr, user, this.getTarget(), this.move.getMove(), overridden).then(() => { if (overridden.value) { this.end(); @@ -1171,39 +1195,57 @@ abstract class MoveEffectPhase extends PokemonPhase { if (user.turnData.hitsLeft === undefined) { const hitCount = new Utils.IntegerHolder(1); - applyMoveAttrs(MultiHitAttr, user, target, this.move.getMove(), hitCount); + // Assume single target for multi hit + applyMoveAttrs(MultiHitAttr, user, this.getTarget(), this.move.getMove(), hitCount); user.turnData.hitCount = 0; user.turnData.hitsLeft = user.turnData.hitCount = hitCount.value; } - if (!this.hitCheck()) { + const moveHistoryEntry = { move: this.move.moveId, targets: this.targets, result: MoveResult.PENDING, virtual: this.move.virtual }; + user.pushMoveHistory(moveHistoryEntry); + + const targetHitChecks = Object.fromEntries(targets.map(p => [ p.getBattleTarget(), this.hitCheck(p) ])); + if (targets.length === 1 && !targetHitChecks[this.targets[0]]) { this.scene.queueMessage(getPokemonMessage(user, '\'s\nattack missed!')); - user.pushMoveHistory({ move: this.move.moveId, result: MoveResult.MISSED, virtual: this.move.virtual }); - applyMoveAttrs(MissEffectAttr, user, target, this.move.getMove()); + moveHistoryEntry.result = MoveResult.MISS; + applyMoveAttrs(MissEffectAttr, user, null, this.move.getMove()); this.end(); return; } - const isProtected = !this.move.getMove().hasFlag(MoveFlags.IGNORE_PROTECT) && target.lapseTag(BattlerTagType.PROTECTED); - - new MoveAnim(this.move.getMove().id as Moves, user, this.targetIndex).play(this.scene, () => { - const result = !isProtected ? target.apply(user, this.move) : MoveResult.NO_EFFECT; - user.pushMoveHistory({ move: this.move.moveId, result: result, virtual: this.move.virtual }); - if (result !== MoveResult.NO_EFFECT && result !== MoveResult.FAILED) { - applyMoveAttrs(MoveEffectAttr, user, target, this.move.getMove()); - if (result < MoveResult.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); + // Move animation only needs one target + new MoveAnim(this.move.getMove().id as Moves, user, this.getTarget()?.getBattleTarget()).play(this.scene, () => { + for (let target of targets) { + if (!targetHitChecks[target.getBattleTarget()]) { + this.scene.queueMessage(getPokemonMessage(user, '\'s\nattack missed!')); + if (moveHistoryEntry.result === MoveResult.PENDING) + moveHistoryEntry.result = MoveResult.MISS; + applyMoveAttrs(MissEffectAttr, user, null, this.move.getMove()); + continue; } - // Charge attribute with charge effect takes all effect attributes and applies them to charge stage, so ignore them if this is present - if (!isProtected && !this.move.getMove().getAttrs(ChargeAttr).filter(ca => (ca as ChargeAttr).chargeEffect).length) { - applyFilteredMoveAttrs((attr: MoveAttr) => attr instanceof MoveHitEffectAttr && (!!target.hp || (attr as MoveHitEffectAttr).selfTarget), user, target, this.move.getMove()); - if (target.hp) - applyPostDefendAbAttrs(PostDefendAbAttr, target, user, this.move, result); - if (this.move.getMove().hasFlag(MoveFlags.MAKES_CONTACT)) - this.scene.applyModifiers(ContactHeldItemTransferChanceModifier, this.player, user, target.getFieldIndex()); + + const isProtected = !this.move.getMove().hasFlag(MoveFlags.IGNORE_PROTECT) && target.lapseTag(BattlerTagType.PROTECTED); + + moveHistoryEntry.result = MoveResult.SUCCESS; + + const hitResult = !isProtected ? target.apply(user, this.move) : HitResult.NO_EFFECT; + + if (hitResult !== HitResult.NO_EFFECT && hitResult !== HitResult.FAIL) { + applyMoveAttrs(MoveEffectAttr, 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); + } + // Charge attribute with charge effect takes all effect attributes and applies them to charge stage, so ignore them if this is present + if (!isProtected && !this.move.getMove().getAttrs(ChargeAttr).filter(ca => (ca as ChargeAttr).chargeEffect).length) { + applyFilteredMoveAttrs((attr: MoveAttr) => attr instanceof MoveHitEffectAttr && (!!target.hp || (attr as MoveHitEffectAttr).selfTarget), user, target, this.move.getMove()); + if (target.hp) + applyPostDefendAbAttrs(PostDefendAbAttr, target, user, this.move, hitResult); + if (this.move.getMove().hasFlag(MoveFlags.MAKES_CONTACT)) + this.scene.applyModifiers(ContactHeldItemTransferChanceModifier, this.player, user, target.getFieldIndex()); + } } } this.end(); @@ -1213,7 +1255,7 @@ abstract class MoveEffectPhase extends PokemonPhase { end() { const user = this.getUserPokemon(); - if (--user.turnData.hitsLeft >= 1 && this.getTargetPokemon().hp) + if (--user.turnData.hitsLeft >= 1 && this.getTarget()?.hp) this.scene.unshiftPhase(this.getNewHitPhase()); else { if (user.turnData.hitCount > 1) @@ -1224,11 +1266,11 @@ abstract class MoveEffectPhase extends PokemonPhase { super.end(); } - hitCheck(): boolean { + hitCheck(target: Pokemon): boolean { if (this.move.getMove().moveTarget === MoveTarget.USER) return true; - const hiddenTag = this.getTargetPokemon().getTag(HiddenTag); + const hiddenTag = target.getTag(HiddenTag); if (hiddenTag) { if (!this.move.getMove().getAttrs(HitsTagAttr).filter(hta => (hta as HitsTagAttr).tagType === hiddenTag.tagType).length) return false; @@ -1242,14 +1284,14 @@ abstract class MoveEffectPhase extends PokemonPhase { if (moveAccuracy.value === -1) return true; - applyMoveAttrs(VariableAccuracyAttr, this.getUserPokemon(), this.getTargetPokemon(), this.move.getMove(), moveAccuracy); + applyMoveAttrs(VariableAccuracyAttr, this.getUserPokemon(), target, this.move.getMove(), moveAccuracy); if (!this.move.getMove().getAttrs(OneHitKOAttr).length && this.scene.arena.getTag(ArenaTagType.GRAVITY)) moveAccuracy.value = Math.floor(moveAccuracy.value * 1.67); if (this.move.getMove().category !== MoveCategory.STATUS) { const userAccuracyLevel = new Utils.IntegerHolder(this.getUserPokemon().summonData.battleStats[BattleStat.ACC]); - const targetEvasionLevel = new Utils.IntegerHolder(this.getTargetPokemon().summonData.battleStats[BattleStat.EVA]); + const targetEvasionLevel = new Utils.IntegerHolder(target.summonData.battleStats[BattleStat.EVA]); this.scene.applyModifiers(TempBattleStatBoosterModifier, this.player, TempBattleStat.ACC, userAccuracyLevel); const rand = Utils.randInt(100, 1); let accuracyMultiplier = 1; @@ -1264,40 +1306,20 @@ abstract class MoveEffectPhase extends PokemonPhase { return true; } - abstract getUserPokemon(): Pokemon; - - getTargetPokemon(): Pokemon { - return this.getUserPokemon().getOpponent(this.targetIndex); - } - - abstract getNewHitPhase(): MoveEffectPhase; -} - -export class PlayerMoveEffectPhase extends MoveEffectPhase { - constructor(scene: BattleScene, fieldIndex: integer, targetIndex: integer, move: PokemonMove) { - super(scene, true, fieldIndex, targetIndex, move); - } - getUserPokemon(): Pokemon { - return this.scene.getPlayerField()[this.fieldIndex]; + return (this.player ? this.scene.getPlayerField() : this.scene.getEnemyField())[this.fieldIndex]; + } + + getTargets(): Pokemon[] { + return this.scene.getField().filter(p => this.targets.indexOf(p.getBattleTarget()) > -1); + } + + getTarget(): Pokemon { + return this.getTargets().find(() => true); } getNewHitPhase() { - return new PlayerMoveEffectPhase(this.scene, this.fieldIndex, this.targetIndex, this.move); - } -} - -export class EnemyMoveEffectPhase extends MoveEffectPhase { - constructor(scene: BattleScene, fieldIndex: integer, targetIndex: integer, move: PokemonMove) { - super(scene, false, fieldIndex, targetIndex, move); - } - - getUserPokemon(): Pokemon { - return this.scene.getEnemyField()[this.fieldIndex]; - } - - getNewHitPhase() { - return new EnemyMoveEffectPhase(this.scene, this.fieldIndex, this.targetIndex, this.move); + return new MoveEffectPhase(this.scene, this.player, this.fieldIndex, this.targets, this.move); } } @@ -1371,6 +1393,8 @@ export class StatChangePhase extends PokemonPhase { constructor(scene: BattleScene, player: boolean, fieldIndex: integer, selfTarget: boolean, stats: BattleStat[], levels: integer) { super(scene, player, fieldIndex); + console.log(this.player, this.fieldIndex); + const allStats = Utils.getEnumValues(BattleStat); this.selfTarget = selfTarget; this.stats = stats.map(s => s !== BattleStat.RAND ? s : allStats[Utils.randInt(BattleStat.SPD + 1)]); @@ -1594,25 +1618,25 @@ export class DamagePhase extends PokemonPhase { constructor(scene: BattleScene, player: boolean, fieldIndex: integer, damageResult?: DamageResult) { super(scene, player, fieldIndex); - this.damageResult = damageResult || MoveResult.EFFECTIVE; + this.damageResult = damageResult || HitResult.EFFECTIVE; } start() { super.start(); switch (this.damageResult) { - case MoveResult.EFFECTIVE: + case HitResult.EFFECTIVE: this.scene.sound.play('hit'); break; - case MoveResult.SUPER_EFFECTIVE: + case HitResult.SUPER_EFFECTIVE: this.scene.sound.play('hit_strong'); break; - case MoveResult.NOT_VERY_EFFECTIVE: + case HitResult.NOT_VERY_EFFECTIVE: this.scene.sound.play('hit_weak'); break; } - if (this.damageResult !== MoveResult.OTHER) { + if (this.damageResult !== HitResult.OTHER) { const flashTimer = this.scene.time.addEvent({ delay: 100, repeat: 5, diff --git a/src/battle-scene.ts b/src/battle-scene.ts index e4c9f9447cf..045eaf2b3fe 100644 --- a/src/battle-scene.ts +++ b/src/battle-scene.ts @@ -847,7 +847,7 @@ export default class BattleScene extends Phaser.Scene { count = Math.max(count, Math.floor(chances / 2)); const enemyField = this.getEnemyField(); getEnemyModifierTypesForWave(waveIndex, count, this.getEnemyField()) - .map(mt => mt.newModifier(enemyField[enemyField.length === 1 ? 0 : Utils.randInt(enemyField.length)]).add(this.enemyModifiers, false)); + .map(mt => mt.newModifier(enemyField[Utils.randInt(enemyField.length)]).add(this.enemyModifiers, false)); this.updateModifiers(false).then(() => resolve()); }); diff --git a/src/battle.ts b/src/battle.ts index c1ebc4f7ac0..80805a6956a 100644 --- a/src/battle.ts +++ b/src/battle.ts @@ -9,11 +9,11 @@ export enum BattleTarget { ENEMY_2 } -interface TurnCommand { +export interface TurnCommand { command: Command; cursor?: integer; move?: QueuedMove; - targetIndex?: integer; + targets?: BattleTarget[]; args?: any[]; }; diff --git a/src/data/ability.ts b/src/data/ability.ts index 49f2bae8793..1027c0121ac 100644 --- a/src/data/ability.ts +++ b/src/data/ability.ts @@ -1,4 +1,4 @@ -import Pokemon, { MoveResult, PokemonMove } from "../pokemon"; +import Pokemon, { HitResult, MoveResult, PokemonMove } from "../pokemon"; import { Type } from "./type"; import * as Utils from "../utils"; import { BattleStat, getBattleStatName } from "./battle-stat"; @@ -232,14 +232,14 @@ export class NonSuperEffectiveImmunityAbAttr extends TypeImmunityAbAttr { } export class PostDefendAbAttr extends AbAttr { - applyPostDefend(pokemon: Pokemon, attacker: Pokemon, move: PokemonMove, moveResult: MoveResult, args: any[]): boolean { + applyPostDefend(pokemon: Pokemon, attacker: Pokemon, move: PokemonMove, hitResult: HitResult, args: any[]): boolean { return false; } } export class PostDefendTypeChangeAbAttr extends PostDefendAbAttr { - applyPostDefend(pokemon: Pokemon, attacker: Pokemon, move: PokemonMove, moveResult: MoveResult, args: any[]): boolean { - if (moveResult < MoveResult.NO_EFFECT) { + applyPostDefend(pokemon: Pokemon, attacker: Pokemon, move: PokemonMove, hitResult: HitResult, args: any[]): boolean { + if (hitResult < HitResult.NO_EFFECT) { const type = move.getMove().type; const pokemonTypes = pokemon.getTypes(); if (pokemonTypes.length !== 1 || pokemonTypes[0] !== type) { @@ -267,7 +267,7 @@ export class PostDefendContactApplyStatusEffectAbAttr extends PostDefendAbAttr { this.effects = effects; } - applyPostDefend(pokemon: Pokemon, attacker: Pokemon, move: PokemonMove, moveResult: MoveResult, args: any[]): boolean { + applyPostDefend(pokemon: Pokemon, attacker: Pokemon, move: PokemonMove, hitResult: HitResult, args: any[]): boolean { if (move.getMove().hasFlag(MoveFlags.MAKES_CONTACT) && Utils.randInt(100) < this.chance) { const effect = this.effects.length === 1 ? this.effects[0] : this.effects[Utils.randInt(this.effects.length)]; return attacker.trySetStatus(effect); @@ -290,7 +290,7 @@ export class PostDefendContactApplyTagChanceAbAttr extends PostDefendAbAttr { this.turnCount = turnCount; } - applyPostDefend(pokemon: Pokemon, attacker: Pokemon, move: PokemonMove, moveResult: MoveResult, args: any[]): boolean { + applyPostDefend(pokemon: Pokemon, attacker: Pokemon, move: PokemonMove, hitResult: HitResult, args: any[]): boolean { if (move.getMove().hasFlag(MoveFlags.MAKES_CONTACT) && Utils.randInt(100) < this.chance) return attacker.addTag(this.tagType, this.turnCount, move.moveId, pokemon.id); @@ -654,7 +654,7 @@ export class PostWeatherLapseDamageAbAttr extends PostWeatherLapseAbAttr { if (pokemon.getHpRatio() < 1) { const scene = pokemon.scene; scene.queueMessage(getPokemonMessage(pokemon, ` is hurt\nby its ${pokemon.getAbility()}!`)); - scene.unshiftPhase(new DamagePhase(pokemon.scene, pokemon.isPlayer(), pokemon.getFieldIndex(), MoveResult.OTHER)); + scene.unshiftPhase(new DamagePhase(pokemon.scene, pokemon.isPlayer(), pokemon.getFieldIndex(), HitResult.OTHER)); pokemon.damage(Math.ceil(pokemon.getMaxHp() * (16 / this.damageFactor))); return true; } @@ -727,7 +727,7 @@ export function applyPreDefendAbAttrs(attrType: { new(...args: any[]): PreDefend } export function applyPostDefendAbAttrs(attrType: { new(...args: any[]): PostDefendAbAttr }, - pokemon: Pokemon, attacker: Pokemon, move: PokemonMove, moveResult: MoveResult, ...args: any[]): void { + pokemon: Pokemon, attacker: Pokemon, move: PokemonMove, hitResult: HitResult, ...args: any[]): void { if (!pokemon.canApplyAbility()) return; @@ -737,7 +737,7 @@ export function applyPostDefendAbAttrs(attrType: { new(...args: any[]): PostDefe if (!canApplyAttr(pokemon, attr)) continue; pokemon.scene.setPhaseQueueSplice(); - if (attr.applyPostDefend(pokemon, attacker, move, moveResult, args)) { + if (attr.applyPostDefend(pokemon, attacker, move, hitResult, args)) { if (attr.showAbility) queueShowAbility(pokemon); const message = attr.getTriggerMessage(pokemon, attacker, move); diff --git a/src/data/arena-tag.ts b/src/data/arena-tag.ts index 26ffe451a73..44e600b35d5 100644 --- a/src/data/arena-tag.ts +++ b/src/data/arena-tag.ts @@ -3,7 +3,7 @@ import { Type } from "./type"; import * as Utils from "../utils"; import { Moves, allMoves } from "./move"; import { getPokemonMessage } from "../messages"; -import Pokemon, { DamageResult, MoveResult } from "../pokemon"; +import Pokemon, { DamageResult, HitResult, MoveResult } from "../pokemon"; import { DamagePhase, ObtainStatusEffectPhase } from "../battle-phases"; import { StatusEffect } from "./status-effect"; import { BattlerTagType } from "./battler-tag"; @@ -151,7 +151,7 @@ class SpikesTag extends ArenaTrapTag { const damageHpRatio = 1 / (10 - 2 * this.layers); pokemon.scene.queueMessage(getPokemonMessage(pokemon, ' is hurt\nby the spikes!')); - pokemon.scene.unshiftPhase(new DamagePhase(pokemon.scene, pokemon.isPlayer(), pokemon.getFieldIndex(), MoveResult.OTHER)); + pokemon.scene.unshiftPhase(new DamagePhase(pokemon.scene, pokemon.isPlayer(), pokemon.getFieldIndex(), HitResult.OTHER)); pokemon.damage(Math.ceil(pokemon.getMaxHp() * damageHpRatio)); return true; } @@ -225,7 +225,7 @@ class StealthRockTag extends ArenaTrapTag { if (damageHpRatio) { pokemon.scene.queueMessage(`Pointed stones dug into\n${pokemon.name}!`); - pokemon.scene.unshiftPhase(new DamagePhase(pokemon.scene, pokemon.isPlayer(), pokemon.getFieldIndex(), MoveResult.OTHER)); + pokemon.scene.unshiftPhase(new DamagePhase(pokemon.scene, pokemon.isPlayer(), pokemon.getFieldIndex(), HitResult.OTHER)); pokemon.damage(Math.ceil(pokemon.getMaxHp() * damageHpRatio)); } diff --git a/src/data/battle-anims.ts b/src/data/battle-anims.ts index 5e245f7cb2f..d93cb8b0c76 100644 --- a/src/data/battle-anims.ts +++ b/src/data/battle-anims.ts @@ -1,8 +1,9 @@ //import { battleAnimRawData } from "./battle-anim-raw-data"; import BattleScene from "../battle-scene"; -import { ChargeAttr, Moves, allMoves, getMoveTarget } from "./move"; +import { ChargeAttr, Moves, allMoves } from "./move"; import Pokemon from "../pokemon"; import * as Utils from "../utils"; +import { BattleTarget } from "../battle"; //import fs from 'vite-plugin-fs/browser'; export enum AnimFrameTarget { @@ -831,8 +832,8 @@ export class CommonBattleAnim extends BattleAnim { export class MoveAnim extends BattleAnim { public move: Moves; - constructor(move: Moves, user: Pokemon, targetIndex: integer) { - super(user, getMoveTarget(user, targetIndex, move)); + constructor(move: Moves, user: Pokemon, target: BattleTarget) { + super(user, user.scene.getField()[target]); this.move = move; } diff --git a/src/data/berry.ts b/src/data/berry.ts index c21e5bbbd0e..060f4ad6e86 100644 --- a/src/data/berry.ts +++ b/src/data/berry.ts @@ -1,6 +1,6 @@ import { PokemonHealPhase, StatChangePhase } from "../battle-phases"; import { getPokemonMessage } from "../messages"; -import Pokemon, { MoveResult } from "../pokemon"; +import Pokemon, { HitResult, MoveResult } from "../pokemon"; import { getBattleStatName } from "./battle-stat"; import { BattleStat } from "./battle-stat"; import { BattlerTagType } from "./battler-tag"; @@ -54,7 +54,7 @@ export function getBerryPredicate(berryType: BerryType): BerryPredicate { case BerryType.LUM: return (pokemon: Pokemon) => !!pokemon.status || !!pokemon.getTag(BattlerTagType.CONFUSED); case BerryType.ENIGMA: - return (pokemon: Pokemon) => !!pokemon.turnData.attacksReceived.filter(a => a.result === MoveResult.SUPER_EFFECTIVE).length; + return (pokemon: Pokemon) => !!pokemon.turnData.attacksReceived.filter(a => a.result === HitResult.SUPER_EFFECTIVE).length; case BerryType.LIECHI: case BerryType.GANLON: case BerryType.SALAC: diff --git a/src/data/move.ts b/src/data/move.ts index f053c02afb3..dad72ff31df 100644 --- a/src/data/move.ts +++ b/src/data/move.ts @@ -1,9 +1,9 @@ import { ChargeAnim, MoveChargeAnim, initMoveAnim, loadMoveAnimAssets } from "./battle-anims"; -import { DamagePhase, EnemyMovePhase, ObtainStatusEffectPhase, PlayerMovePhase, PokemonHealPhase, StatChangePhase } from "../battle-phases"; +import { DamagePhase, MovePhase, ObtainStatusEffectPhase, PokemonHealPhase, StatChangePhase } from "../battle-phases"; import { BattleStat } from "./battle-stat"; import { BattlerTagType } from "./battler-tag"; import { getPokemonMessage } from "../messages"; -import Pokemon, { AttackMoveResult, EnemyPokemon, MoveResult, PlayerPokemon, PokemonMove, TurnMove } from "../pokemon"; +import Pokemon, { AttackMoveResult, EnemyPokemon, HitResult, MoveResult, PlayerPokemon, PokemonMove, TurnMove } from "../pokemon"; import { StatusEffect, getStatusEffectDescriptor } from "./status-effect"; import { Type } from "./type"; import * as Utils from "../utils"; @@ -11,6 +11,7 @@ import { WeatherType } from "./weather"; import { ArenaTagType, ArenaTrapTag } from "./arena-tag"; import { BlockRecoilDamageAttr, applyAbAttrs } from "./ability"; import { PokemonHeldItemModifier } from "../modifier/modifier"; +import { BattleTarget } from "../battle"; export enum MoveCategory { PHYSICAL, @@ -24,14 +25,13 @@ export enum MoveTarget { ALL_OTHERS, NEAR_OTHER, ALL_NEAR_OTHERS, - ENEMY, NEAR_ENEMY, ALL_NEAR_ENEMIES, RANDOM_NEAR_ENEMY, ALL_ENEMIES, ATTACKER, - ALLY, NEAR_ALLY, + ALLY, USER_OR_NEAR_ALLY, USER_AND_ALLIES, ALL, @@ -47,6 +47,7 @@ export enum MoveFlags { } type MoveCondition = (user: Pokemon, target: Pokemon, move: Move) => boolean; +type UserMoveCondition = (user: Pokemon, move: Move) => boolean; export default class Move { public id: Moves; @@ -889,7 +890,7 @@ export class RecoilAttr extends MoveEffectAttr { return false; const recoilDamage = Math.max(Math.floor((!this.useHp ? user.turnData.damageDealt : user.getMaxHp()) / 4), 1); - user.scene.unshiftPhase(new DamagePhase(user.scene, user.isPlayer(), user.getFieldIndex(), MoveResult.OTHER)); + user.scene.unshiftPhase(new DamagePhase(user.scene, user.isPlayer(), user.getFieldIndex(), HitResult.OTHER)); user.scene.queueMessage(getPokemonMessage(user, ' is hit\nwith recoil!')); user.damage(recoilDamage); @@ -906,7 +907,7 @@ export class SacrificialAttr extends MoveEffectAttr { if (!super.apply(user, target, move, args)) return false; - user.scene.unshiftPhase(new DamagePhase(user.scene, user.isPlayer(), user.getFieldIndex(), MoveResult.OTHER)); + user.scene.unshiftPhase(new DamagePhase(user.scene, user.isPlayer(), user.getFieldIndex(), HitResult.OTHER)); user.damage(user.getMaxHp()); return true; @@ -1172,7 +1173,7 @@ export class ChargeAttr extends OverrideMoveEffectAttr { user.addTag(this.tagType, 1, move.id, user.id); if (this.chargeEffect) applyMoveAttrs(MoveEffectAttr, user, target, move); - user.pushMoveHistory({ move: move.id, result: MoveResult.OTHER }); + user.pushMoveHistory({ move: move.id, targets: [ target.getBattleTarget() ], result: MoveResult.OTHER }); user.getMoveQueue().push({ move: move.id, ignorePP: true }); resolve(true); }); @@ -1275,7 +1276,7 @@ export abstract class ConsecutiveUsePowerMultiplierAttr extends MovePowerMultipl let count = 0; let turnMove: TurnMove; - while (((turnMove = moveHistory.shift())?.move === move.id || (comboMoves.length && comboMoves.indexOf(turnMove?.move) > -1)) && (!resetOnFail || turnMove.result < MoveResult.NO_EFFECT)) { + while (((turnMove = moveHistory.shift())?.move === move.id || (comboMoves.length && comboMoves.indexOf(turnMove?.move) > -1)) && (!resetOnFail || turnMove.result === MoveResult.SUCCESS)) { if (count < (limit - 1)) count++; else if (resetOnLimit) @@ -1460,16 +1461,16 @@ export class BlizzardAccuracyAttr extends VariableAccuracyAttr { } export class MissEffectAttr extends MoveAttr { - private missEffectFunc: MoveCondition; + private missEffectFunc: UserMoveCondition; - constructor(missEffectFunc: MoveCondition) { + constructor(missEffectFunc: UserMoveCondition) { super(); this.missEffectFunc = missEffectFunc; } apply(user: Pokemon, target: Pokemon, move: Move, args: any[]): boolean { - this.missEffectFunc(user, target, move); + this.missEffectFunc(user, move); return true; } } @@ -1555,7 +1556,7 @@ export class FrenzyAttr extends MoveEffectAttr { } } -export const frenzyMissFunc: MoveCondition = (user: Pokemon, target: Pokemon, move: Move) => { +export const frenzyMissFunc: UserMoveCondition = (user: Pokemon, move: Move) => { while (user.getMoveQueue().length && user.getMoveQueue()[0].move === move.id) user.getMoveQueue().shift(); user.lapseTag(BattlerTagType.FRENZY); @@ -1648,7 +1649,7 @@ export class ProtectAttr extends AddBattlerTagAttr { let timesUsed = 0; const moveHistory = user.getLastXMoves(-1); let turnMove: TurnMove; - while (moveHistory.length && (turnMove = moveHistory.shift()).move === move.id && turnMove.result === MoveResult.STATUS) + while (moveHistory.length && (turnMove = moveHistory.shift()).move === move.id && turnMove.result === MoveResult.SUCCESS) timesUsed++; if (timesUsed) return !Utils.randInt(Math.pow(2, timesUsed)); @@ -1771,9 +1772,11 @@ export class RandomMovesetMoveAttr extends OverrideMoveEffectAttr { const move = moves[Utils.randInt(moves.length)]; const moveIndex = moveset.findIndex(m => m.moveId === move.moveId); user.getMoveQueue().push({ move: move.moveId, ignorePP: this.enemyMoveset }); - user.scene.unshiftPhase(user.isPlayer() - ? new PlayerMovePhase(user.scene, user as PlayerPokemon, target.getFieldIndex(), moveset[moveIndex], true) - : new EnemyMovePhase(user.scene, user as EnemyPokemon, target.getFieldIndex(), moveset[moveIndex], true)); + const moveTargets = getMoveTargets(user, move.moveId); + if (!moveTargets.targets.length) + return false; + const targets = moveTargets.multiple ? moveTargets.targets : [ moveTargets.targets[Utils.randInt(moveTargets.targets.length)] ]; + user.scene.unshiftPhase(new MovePhase(user.scene, user, targets, moveset[moveIndex], true)); return true; } @@ -1787,9 +1790,13 @@ export class RandomMoveAttr extends OverrideMoveEffectAttr { const moveIds = Utils.getEnumValues(Moves).filter(m => !allMoves[m].hasFlag(MoveFlags.IGNORE_VIRTUAL)); const moveId = moveIds[Utils.randInt(moveIds.length)]; user.getMoveQueue().push({ move: moveId, ignorePP: true }); - user.scene.unshiftPhase(user.isPlayer() - ? new PlayerMovePhase(user.scene, user as PlayerPokemon, target.getFieldIndex(), new PokemonMove(moveId, 0, 0, true), true) - : new EnemyMovePhase(user.scene, user as EnemyPokemon, target.getFieldIndex(), new PokemonMove(moveId, 0, 0, true), true)); + const moveTargets = getMoveTargets(user, moveId); + if (!moveTargets.targets.length) { + resolve(false); + return; + } + const targets = moveTargets.multiple ? moveTargets.targets : [ moveTargets.targets[Utils.randInt(moveTargets.targets.length)] ]; + user.scene.unshiftPhase(new MovePhase(user.scene, user, targets, new PokemonMove(moveId, 0, 0, true), true)); initMoveAnim(moveId).then(() => { loadMoveAnimAssets(user.scene, [ moveId ], true) .then(() => resolve(true)); @@ -1825,9 +1832,14 @@ export class CopyMoveAttr extends OverrideMoveEffectAttr { const copiedMove = targetMoves[0]; user.getMoveQueue().push({ move: copiedMove.move, ignorePP: true }); - user.scene.unshiftPhase(user.isPlayer() - ? new PlayerMovePhase(user.scene, user as PlayerPokemon, target.getFieldIndex(), new PokemonMove(copiedMove.move, 0, 0, true), true) - : new EnemyMovePhase(user.scene, user as EnemyPokemon, target.getFieldIndex(), new PokemonMove(copiedMove.move, 0, 0, true), true)); + + const moveTargets = getMoveTargets(user, copiedMove.move); + if (!moveTargets.targets.length) + return false; + + const targets = moveTargets.multiple ? moveTargets.targets : [ moveTargets.targets[Utils.randInt(moveTargets.targets.length)] ]; + + user.scene.unshiftPhase(new MovePhase(user.scene, user as PlayerPokemon, targets, new PokemonMove(copiedMove.move, 0, 0, true), true)); return true; } @@ -1934,20 +1946,64 @@ export function applyFilteredMoveAttrs(attrFilter: MoveAttrFilter, user: Pokemon return applyMoveAttrsInternal(attrFilter, user, target, move, args); } -export function getMoveTarget(user: Pokemon, targetIndex: integer, move: Moves): Pokemon { - const moveTarget = allMoves[move].moveTarget; +export type MoveTargetSet = { + targets: BattleTarget[]; + multiple: boolean; +} - const other = user.getOpponent(targetIndex); +export function getMoveTargets(user: Pokemon, move: Moves): MoveTargetSet { + const moveTarget = move ? allMoves[move].moveTarget : MoveTarget.NEAR_ENEMY; + const opponents = user.getOpponents(); + + let set: BattleTarget[]; + let multiple = false; switch (moveTarget) { case MoveTarget.USER: + set = [ user.getBattleTarget() ]; + break; + case MoveTarget.OTHER: + set = user.getLastXMoves().find(() => true)?.targets || []; + break; + case MoveTarget.NEAR_OTHER: + case MoveTarget.ALL_NEAR_OTHERS: + case MoveTarget.ALL_OTHERS: + set = (opponents.concat([ user.getAlly() ])).map(p => p?.getBattleTarget()); + multiple = moveTarget !== MoveTarget.NEAR_OTHER; + break; + case MoveTarget.NEAR_ENEMY: + case MoveTarget.ALL_NEAR_ENEMIES: + case MoveTarget.ALL_ENEMIES: + case MoveTarget.ENEMY_SIDE: + set = opponents.map(p => p.getBattleTarget()); + multiple = moveTarget !== MoveTarget.NEAR_ENEMY; + break; + case MoveTarget.RANDOM_NEAR_ENEMY: + set = [ opponents[Utils.randInt(opponents.length)].getBattleTarget() ]; + break; + case MoveTarget.ATTACKER: + set = [ user.scene.getPokemonById(user.turnData.attacksReceived[0].sourceId).getBattleTarget() ]; + break; + case MoveTarget.NEAR_ALLY: + case MoveTarget.ALLY: + set = [ user.getAlly()?.getBattleTarget() ]; + break; case MoveTarget.USER_OR_NEAR_ALLY: case MoveTarget.USER_AND_ALLIES: case MoveTarget.USER_SIDE: - return user; - default: - return other; + set = [ user, user.getAlly() ].map(p => p?.getBattleTarget()); + 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?.getBattleTarget()); + multiple = true; + break; } + + console.log(set, multiple, MoveTarget[moveTarget]); + + return { targets: set.filter(t => t !== undefined), multiple }; } export const allMoves = [ @@ -2004,7 +2060,7 @@ export function initMoves() { .attr(MultiHitAttr, MultiHitType._2), new AttackMove(Moves.MEGA_KICK, "Mega Kick", Type.NORMAL, MoveCategory.PHYSICAL, 120, 75, 5, -1, "", -1, 0, 1), new AttackMove(Moves.JUMP_KICK, "Jump Kick", Type.FIGHTING, MoveCategory.PHYSICAL, 100, 95, 10, -1, "If it misses, the user loses half their HP.", -1, 0, 1) - .attr(MissEffectAttr, (user: Pokemon, target: Pokemon, move: Move) => { user.damage(Math.floor(user.getMaxHp() / 2)); return true; }) + .attr(MissEffectAttr, (user: Pokemon, move: Move) => { user.damage(Math.floor(user.getMaxHp() / 2)); return true; }) .condition(failOnGravityCondition), new AttackMove(Moves.ROLLING_KICK, "Rolling Kick", Type.FIGHTING, MoveCategory.PHYSICAL, 60, 85, 15, -1, "May cause flinching.", 30, 0, 1) .attr(FlinchAttr), @@ -2072,13 +2128,15 @@ export function initMoves() { .target(MoveTarget.USER_SIDE), new AttackMove(Moves.WATER_GUN, "Water Gun", Type.WATER, MoveCategory.SPECIAL, 40, 100, 25, -1, "", -1, 0, 1), new AttackMove(Moves.HYDRO_PUMP, "Hydro Pump", Type.WATER, MoveCategory.SPECIAL, 110, 80, 5, 142, "", -1, 0, 1), - new AttackMove(Moves.SURF, "Surf", Type.WATER, MoveCategory.SPECIAL, 90, 100, 15, 123, "Hits all adjacent Pokémon.", -1, 0, 1), // TODO + new AttackMove(Moves.SURF, "Surf", Type.WATER, MoveCategory.SPECIAL, 90, 100, 15, 123, "Hits all adjacent Pokémon.", -1, 0, 1) + .target(MoveTarget.ALL_NEAR_OTHERS), new AttackMove(Moves.ICE_BEAM, "Ice Beam", Type.ICE, MoveCategory.SPECIAL, 90, 100, 10, 135, "May freeze opponent.", 10, 0, 1) .attr(StatusEffectAttr, StatusEffect.FREEZE) .target(MoveTarget.ALL_NEAR_OTHERS), new AttackMove(Moves.BLIZZARD, "Blizzard", Type.ICE, MoveCategory.SPECIAL, 110, 70, 5, 143, "May freeze opponent.", 10, 0, 1) .attr(BlizzardAccuracyAttr) - .attr(StatusEffectAttr, StatusEffect.FREEZE), // TODO: 30% chance to hit protect/detect in hail + .attr(StatusEffectAttr, StatusEffect.FREEZE) // TODO: 30% chance to hit protect/detect in hail + .target(MoveTarget.ALL_NEAR_ENEMIES), new AttackMove(Moves.PSYBEAM, "Psybeam", Type.PSYCHIC, MoveCategory.SPECIAL, 65, 100, 20, 16, "May confuse opponent.", 10, 0, 1) .attr(ConfuseAttr) .target(MoveTarget.ALL_NEAR_ENEMIES), @@ -2255,7 +2313,7 @@ export function initMoves() { new SelfStatusMove(Moves.SOFT_BOILED, "Soft-Boiled", Type.NORMAL, -1, 5, -1, "User recovers half its max HP.", -1, 0, 1) .attr(HealAttr, 0.5), new AttackMove(Moves.HIGH_JUMP_KICK, "High Jump Kick", Type.FIGHTING, MoveCategory.PHYSICAL, 130, 90, 10, -1, "If it misses, the user loses half their HP.", -1, 0, 1) - .attr(MissEffectAttr, (user: Pokemon, target: Pokemon, move: Move) => { user.damage(Math.floor(user.getMaxHp() / 2)); return true; }) + .attr(MissEffectAttr, (user: Pokemon, move: Move) => { user.damage(Math.floor(user.getMaxHp() / 2)); return true; }) .condition(failOnGravityCondition), new StatusMove(Moves.GLARE, "Glare", Type.NORMAL, 100, 30, -1, "Paralyzes opponent.", -1, 0, 1) .attr(StatusEffectAttr, StatusEffect.PARALYSIS), @@ -2339,7 +2397,7 @@ export function initMoves() { .ignoresVirtual(), new AttackMove(Moves.TRIPLE_KICK, "Triple Kick", Type.FIGHTING, MoveCategory.PHYSICAL, 10, 90, 10, -1, "Hits thrice in one turn at increasing power.", -1, 0, 2) .attr(MultiHitAttr, MultiHitType._3_INCR) - .attr(MissEffectAttr, (user: Pokemon, target: Pokemon, move: Move) => { + .attr(MissEffectAttr, (user: Pokemon, move: Move) => { user.turnData.hitsLeft = 0; return true; }), diff --git a/src/pokemon.ts b/src/pokemon.ts index f49ca00d380..d75b6eeff27 100644 --- a/src/pokemon.ts +++ b/src/pokemon.ts @@ -25,6 +25,7 @@ import { ArenaTagType, WeakenMoveTypeTag } from './data/arena-tag'; import { Biome } from './data/biome'; import { Abilities, Ability, BattleStatMultiplierAbAttr, BlockCritAbAttr, PreApplyBattlerTagAbAttr, StatusEffectImmunityAbAttr, TypeImmunityAbAttr, VariableMovePowerAbAttr, abilities, applyBattleStatMultiplierAbAttrs, applyPreApplyBattlerTagAbAttrs, applyPreAttackAbAttrs, applyPreDefendAbAttrs, applyPreSetStatusAbAttrs } from './data/ability'; import PokemonData from './system/pokemon-data'; +import { BattleTarget } from './battle'; export enum FieldPosition { CENTER, @@ -191,6 +192,8 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container { abstract getFieldIndex(): integer; + abstract getBattleTarget(): BattleTarget; + loadAssets(): Promise { return new Promise(resolve => { const moveIds = this.getMoveset().map(m => m.getMove().id); @@ -572,8 +575,12 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container { return this.isPlayer() ? 'the opposing team' : 'your team'; } - apply(source: Pokemon, battlerMove: PokemonMove): MoveResult { - let result: MoveResult; + getAlly(): Pokemon { + return (this.isPlayer() ? this.scene.getPlayerField() : this.scene.getEnemyField())[this.getFieldIndex() ? 0 : 1]; + } + + apply(source: Pokemon, battlerMove: PokemonMove): HitResult { + let result: HitResult; const move = battlerMove.getMove(); const moveCategory = move.category; let damage = 0; @@ -595,7 +602,7 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container { applyPreDefendAbAttrs(TypeImmunityAbAttr, this, source, battlerMove, cancelled, typeMultiplier); if (cancelled.value) - result = MoveResult.NO_EFFECT; + result = HitResult.NO_EFFECT; else { if (source.findTag(t => t instanceof TypeBoostTag && (t as TypeBoostTag).boostedType === move.type)) power.value *= 1.5; @@ -636,20 +643,20 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container { if (damage && fixedDamage.value) { damage = fixedDamage.value; isCritical = false; - result = MoveResult.EFFECTIVE; + result = HitResult.EFFECTIVE; } console.log('damage', damage, move.name, move.power, sourceAtk, targetDef); if (!result) { if (typeMultiplier.value >= 2) - result = MoveResult.SUPER_EFFECTIVE; + result = HitResult.SUPER_EFFECTIVE; else if (typeMultiplier.value >= 1) - result = MoveResult.EFFECTIVE; + result = HitResult.EFFECTIVE; else if (typeMultiplier.value > 0) - result = MoveResult.NOT_VERY_EFFECTIVE; + result = HitResult.NOT_VERY_EFFECTIVE; else - result = MoveResult.NO_EFFECT; + result = HitResult.NO_EFFECT; } if (damage) { @@ -662,20 +669,20 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container { } switch (result) { - case MoveResult.SUPER_EFFECTIVE: + case HitResult.SUPER_EFFECTIVE: this.scene.queueMessage('It\'s super effective!'); break; - case MoveResult.NOT_VERY_EFFECTIVE: + case HitResult.NOT_VERY_EFFECTIVE: this.scene.queueMessage('It\'s not very effective!'); break; - case MoveResult.NO_EFFECT: + case HitResult.NO_EFFECT: this.scene.queueMessage(`It doesn\'t affect ${this.name}!`); break; } } break; case MoveCategory.STATUS: - result = MoveResult.STATUS; + result = HitResult.STATUS; break; } @@ -1018,6 +1025,10 @@ export class PlayerPokemon extends Pokemon { return this.scene.getPlayerField().indexOf(this); } + getBattleTarget(): BattleTarget { + return this.getFieldIndex(); + } + generateCompatibleTms(): void { this.compatibleTms = []; @@ -1203,6 +1214,10 @@ export class EnemyPokemon extends Pokemon { return this.scene.getEnemyField().indexOf(this); } + getBattleTarget(): BattleTarget { + return BattleTarget.ENEMY + this.getFieldIndex(); + } + addToParty() { const party = this.scene.getParty(); let ret: PlayerPokemon = null; @@ -1219,6 +1234,7 @@ export class EnemyPokemon extends Pokemon { export interface TurnMove { move: Moves; + targets?: BattleTarget[]; result: MoveResult; virtual?: boolean; turn?: integer; @@ -1264,17 +1280,25 @@ export enum AiType { } export enum MoveResult { + PENDING, + SUCCESS, + FAIL, + MISS, + OTHER +} + +export enum HitResult { EFFECTIVE = 1, SUPER_EFFECTIVE, NOT_VERY_EFFECTIVE, NO_EFFECT, STATUS, - FAILED, - MISSED, + FAIL, + MISS, OTHER } -export type DamageResult = MoveResult.EFFECTIVE | MoveResult.SUPER_EFFECTIVE | MoveResult.NOT_VERY_EFFECTIVE | MoveResult.OTHER; +export type DamageResult = HitResult.EFFECTIVE | HitResult.SUPER_EFFECTIVE | HitResult.NOT_VERY_EFFECTIVE | HitResult.OTHER; export class PokemonMove { public moveId: Moves; diff --git a/src/system/auto-play.ts b/src/system/auto-play.ts index a483713b2f0..76039ae1b6c 100644 --- a/src/system/auto-play.ts +++ b/src/system/auto-play.ts @@ -24,7 +24,7 @@ export function initAutoPlay() { const commandUiHandler = this.ui.handlers[Mode.COMMAND] as CommandUiHandler; const fightUiHandler = this.ui.handlers[Mode.FIGHT] as FightUiHandler; const partyUiHandler = this.ui.handlers[Mode.PARTY] as PartyUiHandler; - const switchCheckUiHandler = this.ui.handlers[Mode.CONFIRM] as ConfirmUiHandler; + const confirmUiHandler = this.ui.handlers[Mode.CONFIRM] as ConfirmUiHandler; const modifierSelectUiHandler = this.ui.handlers[Mode.MODIFIER_SELECT] as ModifierSelectUiHandler; const getBestPartyMemberIndex = () => { @@ -153,15 +153,15 @@ export function initAutoPlay() { } }; - const originalSwitchCheckUiHandlerShow = switchCheckUiHandler.show; - switchCheckUiHandler.show = function (args: any[]) { + const originalSwitchCheckUiHandlerShow = confirmUiHandler.show; + confirmUiHandler.show = function (args: any[]) { originalSwitchCheckUiHandlerShow.apply(this, [ args ]); if (thisArg.auto) { const bestPartyMemberIndex = getBestPartyMemberIndex(); thisArg.time.delayedCall(20, () => { if (bestPartyMemberIndex) nextPartyMemberIndex = bestPartyMemberIndex; - switchCheckUiHandler.setCursor(bestPartyMemberIndex ? 1 : 0); + confirmUiHandler.setCursor(bestPartyMemberIndex ? 1 : 0); thisArg.time.delayedCall(20, () => this.processInput(Button.ACTION)); }); } diff --git a/src/ui/target-select-ui-handler.ts b/src/ui/target-select-ui-handler.ts new file mode 100644 index 00000000000..57edc4001e6 --- /dev/null +++ b/src/ui/target-select-ui-handler.ts @@ -0,0 +1,121 @@ +import { BattleTarget } from "../battle"; +import BattleScene, { Button } from "../battle-scene"; +import { Moves, getMoveTargets } from "../data/move"; +import { Mode } from "./ui"; +import UiHandler from "./uiHandler"; +import * as Utils from "../utils"; + +export type TargetSelectCallback = (cursor: integer) => void; + +export default class TargetSelectUiHandler extends UiHandler { + private fieldIndex: integer; + private move: Moves; + private targetSelectCallback: TargetSelectCallback; + + private targets: BattleTarget[]; + private targetFlashTween: Phaser.Tweens.Tween; + + constructor(scene: BattleScene) { + super(scene, Mode.TARGET_SELECT); + } + + setup(): void { } + + show(args: any[]) { + if (args.length < 3) + return; + + super.show(args); + + this.fieldIndex = args[0] as integer; + this.move = args[1] as Moves; + this.targetSelectCallback = args[2] as TargetSelectCallback; + + this.targets = getMoveTargets(this.scene.getPlayerField()[this.fieldIndex], this.move).targets; + + if (!this.targets.length) + return; + + console.log(this.targets); + + this.setCursor(this.targets.indexOf(this.cursor) > -1 ? this.cursor : this.targets[0]); + } + + processInput(button: Button) { + const ui = this.getUi(); + + let success = false; + + if (button === Button.ACTION || button === Button.CANCEL) { + this.targetSelectCallback(button === Button.ACTION ? this.cursor : -1); + success = true; + } else { + switch (button) { + case Button.UP: + if (this.cursor < BattleTarget.ENEMY && this.targets.find(t => t >= BattleTarget.ENEMY)) + success = this.setCursor(this.targets.find(t => t >= BattleTarget.ENEMY)); + break; + case Button.DOWN: + if (this.cursor >= BattleTarget.ENEMY && this.targets.find(t => t < BattleTarget.ENEMY)) + success = this.setCursor(this.targets.find(t => t < BattleTarget.ENEMY)); + break; + case Button.LEFT: + if (this.cursor % 2 && this.targets.find(t => t === this.cursor - 1)) + success = this.setCursor(this.cursor - 1); + break; + case Button.RIGHT: + if (!(this.cursor % 2) && this.targets.find(t => t === this.cursor + 1)) + success = this.setCursor(this.cursor + 1); + break; + } + } + + if (success) + ui.playSelect(); + } + + setCursor(cursor: integer): boolean { + const lastCursor = this.cursor; + + const ret = super.setCursor(cursor); + + if (this.targetFlashTween) { + this.targetFlashTween.stop(); + const lastTarget = this.scene.getField()[lastCursor]; + if (lastTarget) + lastTarget.setAlpha(1); + } + + const target = this.scene.getField()[cursor]; + + this.targetFlashTween = this.scene.tweens.add({ + targets: [ target ], + alpha: 0, + loop: -1, + duration: new Utils.FixedInt(250) as unknown as integer, + ease: 'Sine.easeIn', + yoyo: true, + onUpdate: t => { + if (target) + target.setAlpha(t.getValue()); + } + }); + + return ret; + } + + eraseCursor() { + const target = this.scene.getField()[this.cursor]; + if (this.targetFlashTween) { + this.targetFlashTween.stop(); + this.targetFlashTween = null; + } + if (target) + target.setAlpha(1); + } + + clear() { + super.clear(); + this.eraseCursor(); + } +} \ No newline at end of file diff --git a/src/ui/ui.ts b/src/ui/ui.ts index 2f4c707c1d5..13778608ffe 100644 --- a/src/ui/ui.ts +++ b/src/ui/ui.ts @@ -12,12 +12,14 @@ import SummaryUiHandler from './summary-ui-handler'; import StarterSelectUiHandler from './starter-select-ui-handler'; import EvolutionSceneHandler from './evolution-scene-handler'; import BiomeSelectUiHandler from './biome-select-ui-handler'; +import TargetSelectUiHandler from './target-select-ui-handler'; export enum Mode { MESSAGE, COMMAND, FIGHT, BALL, + TARGET_SELECT, MODIFIER_SELECT, PARTY, SUMMARY, @@ -54,6 +56,7 @@ export default class UI extends Phaser.GameObjects.Container { new CommandUiHandler(scene), new FightUiHandler(scene), new BallUiHandler(scene), + new TargetSelectUiHandler(scene), new ModifierSelectUiHandler(scene), new PartyUiHandler(scene), new SummaryUiHandler(scene), diff --git a/src/utils.ts b/src/utils.ts index 996c980d08c..36b5fc0230d 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -25,6 +25,8 @@ export function padInt(value: integer, length: integer, padWith?: string): strin export function randInt(range: integer, min?: integer): integer { if (!min) min = 0; + if (range === 1) + return min; return Math.floor(Math.random() * range) + min; }