diff --git a/public/images/ui/party_bg_double.png b/public/images/ui/party_bg_double.png new file mode 100644 index 000000000..89beff1ed Binary files /dev/null and b/public/images/ui/party_bg_double.png differ diff --git a/public/images/ui/pbinfo_enemy.png b/public/images/ui/pbinfo_enemy_mini.png similarity index 100% rename from public/images/ui/pbinfo_enemy.png rename to public/images/ui/pbinfo_enemy_mini.png diff --git a/public/images/ui/pbinfo_player_mini.png b/public/images/ui/pbinfo_player_mini.png new file mode 100644 index 000000000..1ccaf4efd Binary files /dev/null and b/public/images/ui/pbinfo_player_mini.png differ diff --git a/src/arena.ts b/src/arena.ts index 6e4488fb9..66a04e1d5 100644 --- a/src/arena.ts +++ b/src/arena.ts @@ -161,7 +161,7 @@ export class Arena { this.weather = weather ? new Weather(weather, viaMove ? 5 : 0) : null; if (this.weather) { - this.scene.unshiftPhase(new CommonAnimPhase(this.scene, true, CommonAnim.SUNNY + (weather - 1))); + this.scene.unshiftPhase(new CommonAnimPhase(this.scene, undefined, undefined, CommonAnim.SUNNY + (weather - 1))); this.scene.queueMessage(getWeatherStartMessage(weather)); } else this.scene.queueMessage(getWeatherClearMessage(oldWeatherType)); diff --git a/src/battle-phases.ts b/src/battle-phases.ts index 65edb62ee..ef5e9695c 100644 --- a/src/battle-phases.ts +++ b/src/battle-phases.ts @@ -1,15 +1,15 @@ import BattleScene, { maxExpLevel, startingLevel, startingWave } from "./battle-scene"; -import { default as Pokemon, PlayerPokemon, EnemyPokemon, PokemonMove, MoveResult, DamageResult } 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, MoveTargetSet } 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"; -import { StatusEffect, getStatusEffectActivationText, getStatusEffectCatchRateMultiplier, getStatusEffectHealText, getStatusEffectObtainText, getStatusEffectOverlapText } from "./data/status-effect"; +import { Status, StatusEffect, getStatusEffectActivationText, getStatusEffectCatchRateMultiplier, getStatusEffectHealText, getStatusEffectObtainText, getStatusEffectOverlapText } from "./data/status-effect"; import { SummaryUiMode } from "./ui/summary-ui-handler"; import EvolutionSceneHandler from "./ui/evolution-scene-handler"; import { EvolutionPhase } from "./evolution-phase"; @@ -28,6 +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 { BattlerIndex, TurnCommand } from "./battle"; export class CheckLoadPhase extends BattlePhase { private loaded: boolean; @@ -73,7 +74,9 @@ export class CheckLoadPhase extends BattlePhase { this.scene.arena.playBgm(); this.scene.pushPhase(new EncounterPhase(this.scene, this.loaded)); - this.scene.pushPhase(new SummonPhase(this.scene)); + this.scene.pushPhase(new SummonPhase(this.scene, 0)); + if (this.scene.currentBattle.double && this.scene.getParty().filter(p => !p.isFainted()).length > 1) + this.scene.pushPhase(new SummonPhase(this.scene, 1)); super.end(); } @@ -113,6 +116,74 @@ export class SelectStarterPhase extends BattlePhase { } } +type PokemonFunc = (pokemon: Pokemon) => void; + +export abstract class FieldPhase extends BattlePhase { + getOrder(): BattlerIndex[] { + const playerField = this.scene.getPlayerField().filter(p => p.isActive()) as Pokemon[]; + const enemyField = this.scene.getEnemyField().filter(p => p.isActive()) as Pokemon[]; + + let orderedTargets: Pokemon[] = playerField.concat(enemyField).sort((a: Pokemon, b: Pokemon) => { + const aSpeed = a?.getBattleStat(Stat.SPD) || 0; + const bSpeed = b?.getBattleStat(Stat.SPD) || 0; + + return aSpeed < bSpeed ? 1 : aSpeed > bSpeed ? -1 : !Utils.randInt(2) ? -1 : 1; + }); + + const speedReversed = new Utils.BooleanHolder(false); + this.scene.arena.applyTags(TrickRoomTag, speedReversed); + + if (speedReversed.value) + orderedTargets = orderedTargets.reverse(); + + return orderedTargets.map(t => t.getFieldIndex() + (!t.isPlayer() ? BattlerIndex.ENEMY : 0)); + } + + executeForAll(func: PokemonFunc): void { + const field = this.scene.getField().filter(p => p?.isActive()); + field.forEach(pokemon => func(pokemon)); + } +} + +export abstract class PokemonPhase extends FieldPhase { + protected battlerIndex: BattlerIndex; + protected player: boolean; + protected fieldIndex: integer; + + constructor(scene: BattleScene, battlerIndex: BattlerIndex) { + super(scene); + + if (battlerIndex === undefined) + battlerIndex = scene.getField().find(p => p?.isActive()).getBattlerIndex(); + + this.battlerIndex = battlerIndex; + this.player = battlerIndex < 2; + this.fieldIndex = battlerIndex % 2; + } + + getPokemon() { + return this.scene.getField()[this.battlerIndex]; + } +} + +export abstract class PartyMemberPokemonPhase extends FieldPhase { + protected partyMemberIndex: integer; + protected fieldIndex: integer; + + constructor(scene: BattleScene, partyMemberIndex: integer) { + super(scene); + + this.partyMemberIndex = partyMemberIndex; + this.fieldIndex = partyMemberIndex < this.scene.currentBattle.getBattlerCount() + ? partyMemberIndex + : -1; + } + + getPokemon() { + return this.scene.getParty()[this.partyMemberIndex]; + } +} + export class EncounterPhase extends BattlePhase { private loaded: boolean; @@ -127,25 +198,37 @@ export class EncounterPhase extends BattlePhase { this.scene.updateWaveCountText(); + const loadEnemyAssets = []; + const battle = this.scene.currentBattle; - const enemySpecies = this.scene.randomSpecies(battle.waveIndex, battle.enemyLevel, true); - if (!this.loaded) - battle.enemyPokemon = new EnemyPokemon(this.scene, enemySpecies, battle.enemyLevel); - const enemyPokemon = this.scene.getEnemyPokemon(); - enemyPokemon.resetSummonData(); - this.scene.gameData.setPokemonSeen(enemyPokemon); + battle.enemyLevels.forEach((level, e) => { + const enemySpecies = this.scene.randomSpecies(battle.waveIndex, level, true); + if (!this.loaded) + battle.enemyField[e] = new EnemyPokemon(this.scene, enemySpecies, level); + const enemyPokemon = this.scene.getEnemyField()[e]; + enemyPokemon.resetSummonData(); + + this.scene.gameData.setPokemonSeen(enemyPokemon); - console.log(enemyPokemon.species.name, enemyPokemon.species.speciesId, enemyPokemon.stats); + loadEnemyAssets.push(enemyPokemon.loadAssets()); + + console.log(enemyPokemon.species.name, enemyPokemon.species.speciesId, enemyPokemon.stats); + }); - enemyPokemon.loadAssets().then(() => { - this.scene.field.add(enemyPokemon); - if (this.scene.getPlayerPokemon().visible) - this.scene.field.moveBelow(enemyPokemon, this.scene.getPlayerPokemon()); - enemyPokemon.tint(0, 0.5); + Promise.all(loadEnemyAssets).then(() => { + battle.enemyField.forEach((enemyPokemon, e) => { + this.scene.field.add(enemyPokemon); + const playerPokemon = this.scene.getPlayerPokemon(); + if (playerPokemon.visible) + this.scene.field.moveBelow(enemyPokemon, playerPokemon); + enemyPokemon.tint(0, 0.5); + if (battle.enemyField.length > 1) + enemyPokemon.setFieldPosition(e ? FieldPosition.RIGHT : FieldPosition.LEFT); + }); if (!this.loaded) { - regenerateModifierPoolThresholds(this.scene.getEnemyParty(), false); + regenerateModifierPoolThresholds(this.scene.getEnemyField(), false); this.scene.generateEnemyModifiers(); } @@ -165,27 +248,36 @@ export class EncounterPhase extends BattlePhase { this.scene.arena.trySetWeather(getRandomWeatherType(this.scene.arena.biomeType), false); - const enemyPokemon = this.scene.getEnemyPokemon(); + const enemyField = this.scene.getEnemyField(); this.scene.tweens.add({ - targets: [ this.scene.arenaEnemy, enemyPokemon, this.scene.arenaPlayer, this.scene.trainer ], - x: (_target, _key, value, targetIndex: integer) => targetIndex < 2 ? value + 300 : value - 300, + targets: [ this.scene.arenaEnemy, enemyField, this.scene.arenaPlayer, this.scene.trainer ].flat(), + x: (_target, _key, value, fieldIndex: integer) => fieldIndex < 1 + (enemyField.length) ? value + 300 : value - 300, duration: 2000, onComplete: () => { - enemyPokemon.untint(100, 'Sine.easeOut'); - enemyPokemon.cry(); - enemyPokemon.showInfo(); - this.scene.ui.showText(`A wild ${enemyPokemon.name} appeared!`, null, () => this.end(), 1500); + enemyField.forEach(enemyPokemon => { + enemyPokemon.untint(100, 'Sine.easeOut'); + enemyPokemon.cry(); + enemyPokemon.showInfo(); + }); + const text = enemyField.length === 1 + ? `A wild ${enemyField[0].name} appeared!` + : `A wild ${enemyField[0].name}\nand ${enemyField[1].name} appeared!`; + this.scene.ui.showText(text, null, () => this.end(), 1500); } }); } end() { - if (this.scene.getEnemyPokemon().shiny) - this.scene.unshiftPhase(new ShinySparklePhase(this.scene, false)); + const enemyField = this.scene.getEnemyField(); - this.scene.arena.applyTags(ArenaTrapTag, this.scene.getEnemyPokemon()); + enemyField.forEach((enemyPokemon, e) => { + if (enemyPokemon.shiny) + this.scene.unshiftPhase(new ShinySparklePhase(this.scene, BattlerIndex.ENEMY + e)); + }); - applyPostSummonAbAttrs(PostSummonAbAttr, this.scene.getEnemyPokemon()); + enemyField.forEach(enemyPokemon => this.scene.arena.applyTags(ArenaTrapTag, enemyPokemon)); + + enemyField.forEach(enemyPokemon => applyPostSummonAbAttrs(PostSummonAbAttr, enemyPokemon)); // TODO: Remove //this.scene.unshiftPhase(new SelectModifierPhase(this.scene)); @@ -200,28 +292,33 @@ export class NextEncounterPhase extends EncounterPhase { } doEncounter(): void { - const enemyPokemon = this.scene.getEnemyPokemon(); + const enemyField = this.scene.getEnemyField(); this.scene.tweens.add({ - targets: [ this.scene.arenaEnemy, this.scene.arenaNextEnemy, enemyPokemon ], + targets: [ this.scene.arenaEnemy, this.scene.arenaNextEnemy, enemyField ].flat(), x: '+=300', duration: 2000, onComplete: () => { this.scene.arenaEnemy.setX(this.scene.arenaNextEnemy.x); this.scene.arenaEnemy.setAlpha(1); this.scene.arenaNextEnemy.setX(this.scene.arenaNextEnemy.x - 300); - enemyPokemon.untint(100, 'Sine.easeOut'); - enemyPokemon.cry(); - enemyPokemon.showInfo(); - this.scene.ui.showText(`A wild ${enemyPokemon.name} appeared!`, null, () => this.end(), 1500); + enemyField.forEach(enemyPokemon => { + enemyPokemon.untint(100, 'Sine.easeOut'); + enemyPokemon.cry(); + enemyPokemon.showInfo(); + }); + const text = enemyField.length === 1 + ? `A wild ${enemyField[0].name} appeared!` + : `A wild ${enemyField[0].name}\nand ${enemyField[1].name} appeared!`; + this.scene.ui.showText(text, null, () => this.end(), 1500); } }); } end() { - if (this.scene.getEnemyPokemon().shiny) - this.scene.unshiftPhase(new ShinySparklePhase(this.scene, false)); - - this.scene.unshiftPhase(new CheckSwitchPhase(this.scene)); + this.scene.getEnemyField().forEach((enemyPokemon, e) => { + if (enemyPokemon.shiny) + this.scene.unshiftPhase(new ShinySparklePhase(this.scene, BattlerIndex.ENEMY + e)); + }); super.end(); } @@ -235,16 +332,21 @@ export class NewBiomeEncounterPhase extends NextEncounterPhase { doEncounter(): void { this.scene.arena.trySetWeather(getRandomWeatherType(this.scene.arena.biomeType), false); - const enemyPokemon = this.scene.getEnemyPokemon(); + const enemyField = this.scene.getEnemyField(); this.scene.tweens.add({ - targets: [ this.scene.arenaEnemy, enemyPokemon ], - x: (_target, _key, value, targetIndex: integer) => targetIndex < 2 ? value + 300 : value - 300, + targets: [ this.scene.arenaEnemy, enemyField ].flat(), + x: (_target, _key, value, fieldIndex: integer) => fieldIndex < 2 ? value + 300 : value - 300, duration: 2000, onComplete: () => { - enemyPokemon.untint(100, 'Sine.easeOut'); - enemyPokemon.cry(); - enemyPokemon.showInfo(); - this.scene.ui.showText(`A wild ${enemyPokemon.name} appeared!`, null, () => this.end(), 1500); + enemyField.forEach(enemyPokemon => { + enemyPokemon.untint(100, 'Sine.easeOut'); + enemyPokemon.cry(); + enemyPokemon.showInfo(); + }); + const text = enemyField.length === 1 + ? `A wild ${enemyField[0].name} appeared!` + : `A wild ${enemyField[0].name}\nand ${enemyField[1].name} appeared!`; + this.scene.ui.showText(text, null, () => this.end(), 1500); } }); } @@ -339,9 +441,9 @@ export class SwitchBiomePhase extends BattlePhase { } } -export class SummonPhase extends BattlePhase { - constructor(scene: BattleScene) { - super(scene); +export class SummonPhase extends PartyMemberPokemonPhase { + constructor(scene: BattleScene, fieldIndex: integer) { + super(scene, fieldIndex); } start() { @@ -351,7 +453,7 @@ export class SummonPhase extends BattlePhase { } preSummon(): void { - this.scene.ui.showText(`Go! ${this.scene.getPlayerPokemon().name}!`); + this.scene.ui.showText(`Go! ${this.getPokemon().name}!`); this.scene.trainer.play('trainer_m_pb'); this.scene.tweens.add({ targets: this.scene.trainer, @@ -367,23 +469,37 @@ export class SummonPhase extends BattlePhase { pokeball.setOrigin(0.5, 0.625); this.scene.field.add(pokeball); - const playerPokemon = this.scene.getPlayerPokemon(); + const playerPokemon = this.getPokemon(); + + if (this.fieldIndex === 1) + playerPokemon.setFieldPosition(FieldPosition.RIGHT, 0); + else + playerPokemon.setFieldPosition(!this.scene.currentBattle.double ? FieldPosition.CENTER : FieldPosition.LEFT); + + const fpOffset = playerPokemon.getFieldPositionOffset(); + + console.log(fpOffset); pokeball.setVisible(true); + + this.scene.tweens.add({ + targets: pokeball, + duration: 650, + x: 100 + fpOffset[0] + }); + this.scene.tweens.add({ targets: pokeball, - ease: 'Cubic.easeOut', duration: 150, - x: 54, - y: 70, + ease: 'Cubic.easeOut', + y: 70 + fpOffset[1], onComplete: () => { this.scene.tweens.add({ targets: pokeball, duration: 500, - angle: 1440, - x: 100, - y: 132, ease: 'Cubic.easeIn', + angle: 1440, + y: 132 + fpOffset[1], onComplete: () => { this.scene.sound.play('pb_rel'); pokeball.destroy(); @@ -414,16 +530,18 @@ export class SummonPhase extends BattlePhase { } end() { - const pokemon = this.scene.getPlayerPokemon(); + const playerField = this.scene.getPlayerField(); - if (pokemon.shiny) - this.scene.unshiftPhase(new ShinySparklePhase(this.scene, true)); + playerField.forEach((pokemon, p) => { + if (pokemon.shiny) + this.scene.unshiftPhase(new ShinySparklePhase(this.scene, p)); + }); - pokemon.resetTurnData(); + playerField.forEach(pokemon => pokemon.resetTurnData()); - this.scene.arena.applyTags(ArenaTrapTag, pokemon); + playerField.forEach(pokemon => this.scene.arena.applyTags(ArenaTrapTag, pokemon)); - applyPostSummonAbAttrs(PostSummonAbAttr, pokemon); + playerField.forEach(pokemon => applyPostSummonAbAttrs(PostSummonAbAttr, pokemon)); super.end(); } @@ -436,30 +554,26 @@ export class SwitchSummonPhase extends SummonPhase { private lastPokemon: PlayerPokemon; - constructor(scene: BattleScene, slotIndex: integer, doReturn: boolean, batonPass: boolean) { - super(scene); + constructor(scene: BattleScene, fieldIndex: integer, slotIndex: integer, doReturn: boolean, batonPass: boolean) { + super(scene, fieldIndex); this.slotIndex = slotIndex; this.doReturn = doReturn; this.batonPass = batonPass; } - start() { - super.start(); - } - preSummon(): void { if (!this.doReturn) { this.switchAndSummon(); return; } - const playerPokemon = this.scene.getPlayerPokemon(); + const playerPokemon = this.getPokemon(); if (!this.batonPass) - this.scene.getEnemyPokemon()?.removeTagsBySourceId(playerPokemon.id); + this.scene.getEnemyField().forEach(enemyPokemon => enemyPokemon.removeTagsBySourceId(playerPokemon.id)); - this.scene.ui.showText(`Come back, ${this.scene.getPlayerPokemon().name}!`); + this.scene.ui.showText(`Come back, ${playerPokemon.name}!`); this.scene.sound.play('pb_rel'); playerPokemon.hideInfo(); playerPokemon.tint(getPokeballTintColor(playerPokemon.pokeball), 1, 250, 'Sine.easeIn'); @@ -479,9 +593,9 @@ export class SwitchSummonPhase extends SummonPhase { switchAndSummon() { const party = this.scene.getParty(); const switchedPokemon = party[this.slotIndex]; - this.lastPokemon = this.scene.getPlayerPokemon(); + this.lastPokemon = this.getPokemon(); if (this.batonPass) { - this.scene.getEnemyPokemon()?.transferTagsBySourceId(this.lastPokemon.id, switchedPokemon.id); + this.scene.getEnemyField().forEach(enemyPokemon => enemyPokemon.transferTagsBySourceId(this.lastPokemon.id, switchedPokemon.id)); 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; @@ -489,49 +603,93 @@ export class SwitchSummonPhase extends SummonPhase { } } party[this.slotIndex] = this.lastPokemon; - party[0] = switchedPokemon; + party[this.fieldIndex] = switchedPokemon; this.scene.ui.showText(`Go! ${switchedPokemon.name}!`); this.summon(); } end() { if (this.batonPass) - this.scene.getPlayerPokemon().transferSummon(this.lastPokemon); + this.getPokemon().transferSummon(this.lastPokemon); - this.lastPokemon.resetSummonData(); + this.lastPokemon?.resetSummonData(); super.end(); } } -export class CheckSwitchPhase extends BattlePhase { - constructor(scene: BattleScene) { - super(scene) +export class ReturnPhase extends SwitchSummonPhase { + constructor(scene: BattleScene, fieldIndex: integer) { + super(scene, fieldIndex, -1, true, false); + } + + switchAndSummon(): void { + this.end(); + } + + summon(): void { } +} + +export class ToggleDoublePositionPhase extends BattlePhase { + private double: boolean; + + constructor(scene: BattleScene, double: boolean) { + super(scene); + + this.double = double; } start() { super.start(); - if (this.scene.field.getAll().indexOf(this.scene.getPlayerPokemon()) === -1) { - this.scene.unshiftPhase(new SummonMissingPhase(this.scene)); + const playerPokemon = this.scene.getPlayerPokemon(); + playerPokemon.setFieldPosition(this.double ? FieldPosition.LEFT : FieldPosition.CENTER, 500).then(() => { + if (!this.double && playerPokemon.getFieldIndex() === 1) { + const party = this.scene.getParty(); + party[1] = party[0]; + party[0] = playerPokemon; + } + this.end(); + }); + } +} + +export class CheckSwitchPhase extends BattlePhase { + protected fieldIndex: integer; + protected useName: boolean; + + constructor(scene: BattleScene, fieldIndex: integer, useName: boolean) { + super(scene); + + this.fieldIndex = fieldIndex; + this.useName = useName; + } + + start() { + super.start(); + + const pokemon = this.scene.getPlayerField()[this.fieldIndex]; + + if (this.scene.field.getAll().indexOf(pokemon) === -1) { + this.scene.unshiftPhase(new SummonMissingPhase(this.scene, this.fieldIndex)); super.end(); return; } - if (!this.scene.getParty().slice(1).filter(p => p.hp).length) { + if (!this.scene.getParty().slice(1).filter(p => p.isActive()).length) { super.end(); return; } - if (this.scene.getPlayerPokemon().getTag(BattlerTagType.FRENZY)) { + if (pokemon.getTag(BattlerTagType.FRENZY)) { super.end(); return; } - this.scene.ui.showText('Will you switch\nPOKéMON?', null, () => { + this.scene.ui.showText(`Will you switch\n${this.useName ? pokemon.name : 'POKéMON'}?`, null, () => { this.scene.ui.setMode(Mode.CONFIRM, () => { this.scene.ui.setMode(Mode.MESSAGE); - this.scene.unshiftPhase(new SwitchPhase(this.scene, false, true)); + this.scene.unshiftPhase(new SwitchPhase(this.scene, this.fieldIndex, false, true)); this.end(); }, () => { this.scene.ui.setMode(Mode.MESSAGE); @@ -542,59 +700,63 @@ export class CheckSwitchPhase extends BattlePhase { } export class SummonMissingPhase extends SummonPhase { - constructor(scene: BattleScene) { - super(scene); + constructor(scene: BattleScene, fieldIndex: integer) { + super(scene, fieldIndex); } preSummon(): void { - this.scene.ui.showText(`Go! ${this.scene.getPlayerPokemon().name}!`); + this.scene.ui.showText(`Go! ${this.getPokemon().name}!`); this.scene.time.delayedCall(250, () => this.summon()); } } -type PokemonFunc = (pokemon: Pokemon) => void; - -export abstract class FieldPhase extends BattlePhase { - isPlayerDelayed(): boolean { - const playerPokemon = this.scene.getPlayerPokemon(); - const enemyPokemon = this.scene.getEnemyPokemon(); - - const playerSpeed = playerPokemon?.getBattleStat(Stat.SPD) || 0; - const enemySpeed = enemyPokemon?.getBattleStat(Stat.SPD) || 0; - - const speedDelayed = new Utils.BooleanHolder(playerSpeed < enemySpeed); - this.scene.arena.applyTags(TrickRoomTag, speedDelayed); - - return speedDelayed.value || (playerSpeed === enemySpeed && Utils.randInt(2) === 1); - } - - executeForBoth(func: PokemonFunc): void { - const playerPokemon = this.scene.getPlayerPokemon(); - const enemyPokemon = this.scene.getEnemyPokemon(); - const delayed = this.isPlayerDelayed(); - if (!delayed && playerPokemon) - func(playerPokemon); - if (enemyPokemon) - func(enemyPokemon); - if (delayed && playerPokemon) - func(playerPokemon); - } -} - -export class CommandPhase extends FieldPhase { +export class TurnInitPhase extends FieldPhase { constructor(scene: BattleScene) { - super(scene) + super(scene); } start() { super.start(); - const playerPokemon = this.scene.getPlayerPokemon(); + this.scene.getPlayerField().forEach(playerPokemon => { + if (playerPokemon.isActive()) + this.scene.currentBattle.addParticipant(playerPokemon); - this.scene.currentBattle.addParticipant(playerPokemon); + playerPokemon.resetTurnData(); + }); - playerPokemon.resetTurnData(); - this.scene.getEnemyPokemon().resetTurnData(); + this.scene.getEnemyField().forEach(enemyPokemon => { + if (enemyPokemon.isActive()) + enemyPokemon.resetTurnData() + }); + + const order = this.getOrder(); + for (let o of order) { + if (o < BattlerIndex.ENEMY) + this.scene.pushPhase(new CommandPhase(this.scene, o)); + else + this.scene.pushPhase(new EnemyCommandPhase(this.scene, o - BattlerIndex.ENEMY)); + } + + this.scene.pushPhase(new TurnStartPhase(this.scene)); + + this.end(); + } +} + +export class CommandPhase extends FieldPhase { + protected fieldIndex: integer; + + constructor(scene: BattleScene, fieldIndex: integer) { + super(scene); + + this.fieldIndex = fieldIndex; + } + + start() { + super.start(); + + const playerPokemon = this.scene.getPlayerField()[this.fieldIndex]; const moveQueue = playerPokemon.getMoveQueue(); @@ -609,51 +771,32 @@ export class CommandPhase extends FieldPhase { this.handleCommand(Command.FIGHT, -1, false); else { const moveIndex = playerPokemon.getMoveset().findIndex(m => m.moveId === queuedMove.move); - if (moveIndex > -1 && playerPokemon.getMoveset()[moveIndex].isUsable(queuedMove.ignorePP)) - this.handleCommand(Command.FIGHT, moveIndex, queuedMove.ignorePP); + if (moveIndex > -1 && playerPokemon.getMoveset()[moveIndex].isUsable(queuedMove.ignorePP)) { + this.handleCommand(Command.FIGHT, moveIndex, queuedMove.ignorePP, { targets: queuedMove.targets, multiple: queuedMove.targets.length > 1 }); + } else + this.scene.ui.setMode(Mode.COMMAND); } } else this.scene.ui.setMode(Mode.COMMAND); } handleCommand(command: Command, cursor: integer, ...args: any[]): boolean { - const playerPokemon = this.scene.getPlayerPokemon(); - const enemyPokemon = this.scene.getEnemyPokemon(); + const playerPokemon = this.scene.getPlayerField()[this.fieldIndex]; + const enemyField = this.scene.getEnemyField(); let success: boolean; - let isDelayed = (command: Command, playerMove: PokemonMove, enemyMove: PokemonMove) => { - switch (command) { - case Command.FIGHT: - if (playerMove || enemyMove) { - const playerMovePriority = playerMove?.getMove()?.priority || 0; - const enemyMovePriority = enemyMove?.getMove()?.priority || 0; - if (playerMovePriority !== enemyMovePriority) - return playerMovePriority < enemyMovePriority; - } - break; - case Command.BALL: - case Command.POKEMON: - case Command.RUN: - return false; - } - - return this.isPlayerDelayed(); - }; - - let playerMove: PokemonMove; - switch (command) { case Command.FIGHT: - if (cursor == -1) { - this.scene.pushPhase(new PlayerMovePhase(this.scene, playerPokemon, new PokemonMove(Moves.NONE))); - success = true; - break; - } - - if (playerPokemon.trySelectMove(cursor, args[0] as boolean)) { - playerMove = playerPokemon.getMoveset()[cursor]; - const playerPhase = new PlayerMovePhase(this.scene, playerPokemon, playerMove, false, args[0] as boolean); - this.scene.pushPhase(playerPhase); + if (cursor === -1 || playerPokemon.trySelectMove(cursor, args[0] as boolean)) { + const turnCommand: TurnCommand = { command: Command.FIGHT, cursor: cursor, + move: cursor > -1 ? { move: playerPokemon.moveset[cursor].moveId, targets: [] } : null, args: args }; // TODO: Struggle logic + const moveTargets: MoveTargetSet = args.length < 3 ? getMoveTargets(playerPokemon, cursor > -1 ? playerPokemon.moveset[cursor].moveId : Moves.NONE) : args[2]; + console.log(moveTargets, playerPokemon.name); + if (moveTargets.targets.length <= 1 || moveTargets.multiple) + turnCommand.move.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]; @@ -676,7 +819,13 @@ export class CommandPhase extends FieldPhase { this.scene.ui.setMode(Mode.COMMAND); }, null, true); } else if (cursor < 4) { - this.scene.unshiftPhase(new AttemptCapturePhase(this.scene, cursor as PokeballType)); + const targets = this.scene.getEnemyField().filter(p => p.isActive()).map(p => p.getBattlerIndex()); + this.scene.currentBattle.turnCommands[this.fieldIndex] = { command: Command.BALL, cursor: cursor }; + this.scene.currentBattle.turnPokeballCounts[cursor as PokeballType]--; + if (targets.length > 1) + this.scene.unshiftPhase(new SelectTargetPhase(this.scene, this.fieldIndex)); + else + this.scene.currentBattle.turnCommands[this.fieldIndex].targets = targets; success = true; } break; @@ -687,12 +836,11 @@ export class CommandPhase extends FieldPhase { const trapped = new Utils.BooleanHolder(false); const batonPass = isSwitch && args[0] as boolean; if (!batonPass) - applyCheckTrappedAbAttrs(CheckTrappedAbAttr, enemyPokemon, trapped); + enemyField.forEach(enemyPokemon => applyCheckTrappedAbAttrs(CheckTrappedAbAttr, enemyPokemon, trapped)); if (batonPass || (!trapTag && !trapped.value)) { - if (isSwitch) - this.scene.unshiftPhase(new SwitchSummonPhase(this.scene, cursor, true, args[0] as boolean)); - else - this.scene.unshiftPhase(new AttemptRunPhase(this.scene)); + this.scene.currentBattle.turnCommands[this.fieldIndex] = isSwitch + ? { command: Command.POKEMON, cursor: cursor, args: args } + : { command: Command.RUN }; success = true; } else if (trapTag) this.scene.ui.showText(`${this.scene.getPokemonById(trapTag.sourceId).name}'s ${trapTag.getMoveName()}\nprevents ${isSwitch ? 'switching' : 'fleeing'}!`, null, () => { @@ -701,47 +849,153 @@ export class CommandPhase extends FieldPhase { break; } - if (success) { - if (this.scene.arena.weather) - this.scene.unshiftPhase(new WeatherEffectPhase(this.scene, this.scene.arena.weather)); - - const enemyNextMove = enemyPokemon.getNextMove(); - let enemyMove: PokemonMove; - if (enemyNextMove.move) { - enemyMove = enemyPokemon.getMoveset().find(m => m.moveId === enemyNextMove.move) || new PokemonMove(enemyNextMove.move, 0, 0); - const enemyPhase = new EnemyMovePhase(this.scene, enemyPokemon, enemyMove, false, enemyNextMove.ignorePP); - if (isDelayed(command, playerMove, enemyMove)) - this.scene.unshiftPhase(enemyPhase); - else - this.scene.pushPhase(enemyPhase); - } - - const statusEffectPhases: PostTurnStatusEffectPhase[] = []; - if (playerPokemon.status && playerPokemon.status.isPostTurn()) - statusEffectPhases.push(new PostTurnStatusEffectPhase(this.scene, true)); - if (enemyPokemon.status && enemyPokemon.status.isPostTurn()) { - const enemyStatusEffectPhase = new PostTurnStatusEffectPhase(this.scene, false); - if (this.isPlayerDelayed()) - statusEffectPhases.unshift(enemyStatusEffectPhase); - else - statusEffectPhases.push(enemyStatusEffectPhase); - } - for (let sef of statusEffectPhases) - this.scene.pushPhase(sef); - - this.scene.pushPhase(new TurnEndPhase(this.scene)); - + if (success) this.end(); - } return success; } + getPokemon(): PlayerPokemon { + return this.scene.getPlayerField()[this.fieldIndex]; + } + end() { this.scene.ui.setMode(Mode.MESSAGE).then(() => super.end()); } } +export class EnemyCommandPhase extends FieldPhase { + protected fieldIndex: integer; + + constructor(scene: BattleScene, fieldIndex: integer) { + super(scene); + + this.fieldIndex = fieldIndex; + } + + start() { + super.start(); + + const enemyPokemon = this.scene.getEnemyField()[this.fieldIndex]; + + const nextMove = enemyPokemon.getNextMove(); + + this.scene.currentBattle.turnCommands[this.fieldIndex + BattlerIndex.ENEMY] = + { command: Command.FIGHT, move: nextMove }; + + this.end(); + } +} + +export class SelectTargetPhase extends PokemonPhase { + constructor(scene: BattleScene, fieldIndex: integer) { + super(scene, fieldIndex); + } + + start() { + super.start(); + + const turnCommand = this.scene.currentBattle.turnCommands[this.fieldIndex]; + const move = turnCommand.move?.move; + this.scene.ui.setMode(Mode.TARGET_SELECT, this.fieldIndex, move, (cursor: integer) => { + this.scene.ui.setMode(Mode.MESSAGE); + if (cursor === -1) { + if (turnCommand.command === Command.BALL) + this.scene.currentBattle.turnPokeballCounts[turnCommand.cursor]++; + this.scene.currentBattle.turnCommands[this.fieldIndex] = null; + this.scene.unshiftPhase(new CommandPhase(this.scene, this.fieldIndex)); + } else + turnCommand.targets = [ cursor ]; + this.end(); + }); + } +} + +export class TurnStartPhase extends FieldPhase { + constructor(scene: BattleScene) { + super(scene); + } + + start() { + super.start(); + + if (this.scene.arena.weather) + this.scene.unshiftPhase(new WeatherEffectPhase(this.scene, this.scene.arena.weather)); + + const field = this.scene.getField(); + const order = this.getOrder(); + + const moveOrder = order.slice(0); + + moveOrder.sort((a, b) => { + const aCommand = this.scene.currentBattle.turnCommands[a]; + const bCommand = this.scene.currentBattle.turnCommands[b]; + + if (aCommand.command !== bCommand.command) { + if (aCommand.command === Command.FIGHT) + return 1; + else if (bCommand.command === Command.FIGHT) + return -1; + } else if (aCommand.command === Command.FIGHT) { + const aPriority = allMoves[aCommand.move.move].priority; + const bPriority = allMoves[bCommand.move.move].priority; + + if (aPriority !== bPriority) + return aPriority < bPriority ? 1 : -1; + } + + const aIndex = order.indexOf(a); + const bIndex = order.indexOf(b); + + return aIndex < bIndex ? -1 : aIndex > bIndex ? 1 : 0; + }); + + for (let o of moveOrder) { + + const pokemon = field[o]; + const turnCommand = this.scene.currentBattle.turnCommands[o]; + + switch (turnCommand.command) { + case Command.FIGHT: + const queuedMove = turnCommand.move; + if (!queuedMove) + continue; + 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 MovePhase(this.scene, pokemon, turnCommand.targets || turnCommand.move.targets, move)); + else { + const playerPhase = new MovePhase(this.scene, pokemon, turnCommand.targets || turnCommand.move.targets, move, false, queuedMove.ignorePP); + this.scene.pushPhase(playerPhase); + } + } else + this.scene.pushPhase(new MovePhase(this.scene, pokemon, turnCommand.targets || turnCommand.move.targets, move, false, queuedMove.ignorePP)); + break; + case Command.BALL: + this.scene.unshiftPhase(new AttemptCapturePhase(this.scene, turnCommand.targets[0] % 2, turnCommand.cursor)); + break; + case Command.POKEMON: + case Command.RUN: + const isSwitch = turnCommand.command === Command.POKEMON; + if (isSwitch) + this.scene.unshiftPhase(new SwitchSummonPhase(this.scene, pokemon.getFieldIndex(), turnCommand.cursor, true, turnCommand.args[0] as boolean)); + else + this.scene.unshiftPhase(new AttemptRunPhase(this.scene, pokemon.getFieldIndex())); + break; + } + } + + for (let o of order) { + if (field[o].status && field[o].status.isPostTurn()) + this.scene.pushPhase(new PostTurnStatusEffectPhase(this.scene, o)); + } + + this.scene.pushPhase(new TurnEndPhase(this.scene)); + + this.end(); + } +} + export class TurnEndPhase extends FieldPhase { constructor(scene: BattleScene) { super(scene); @@ -750,12 +1004,9 @@ export class TurnEndPhase extends FieldPhase { start() { super.start(); - this.scene.currentBattle.incrementTurn(); + this.scene.currentBattle.incrementTurn(this.scene); const handlePokemon = (pokemon: Pokemon) => { - if (!pokemon || !pokemon.hp) - return; - pokemon.lapseTags(BattlerTagLapseType.TURN_END); const disabledMoves = pokemon.getMoveset().filter(m => m.isDisabled()); @@ -766,18 +1017,18 @@ export class TurnEndPhase extends FieldPhase { const hasUsableBerry = !!this.scene.findModifier(m => m instanceof BerryModifier && m.shouldApply([ pokemon ]), pokemon.isPlayer()); if (hasUsableBerry) - this.scene.pushPhase(new BerryPhase(this.scene, pokemon.isPlayer())); + this.scene.pushPhase(new BerryPhase(this.scene, pokemon.getBattlerIndex())); this.scene.applyModifiers(TurnHealModifier, pokemon.isPlayer(), pokemon); - this.executeForBoth((pokemon: Pokemon) => applyPostTurnAbAttrs(PostTurnAbAttr, pokemon)); + this.executeForAll((pokemon: Pokemon) => applyPostTurnAbAttrs(PostTurnAbAttr, pokemon)); this.scene.applyModifiers(TurnHeldItemTransferModifier, pokemon.isPlayer(), pokemon); pokemon.battleSummonData.turnCount++; }; - this.executeForBoth(handlePokemon); + this.executeForAll(handlePokemon); this.scene.arena.lapseTags(); @@ -789,13 +1040,14 @@ export class TurnEndPhase extends FieldPhase { } export class BattleEndPhase extends BattlePhase { - constructor(scene: BattleScene) { - super(scene); - } - start() { super.start(); + for (let pokemon of this.scene.getField()) { + if (pokemon) + pokemon.resetBattleSummonData(); + } + this.scene.clearEnemyModifiers(); const tempBattleStatBoosterModifiers = this.scene.getModifiers(TempBattleStatBoosterModifier) as TempBattleStatBoosterModifier[]; @@ -808,71 +1060,58 @@ export class BattleEndPhase extends BattlePhase { } } -export abstract class PokemonPhase extends FieldPhase { - protected player: boolean; +export class NewBattlePhase extends BattlePhase { + start() { + super.start(); - constructor(scene: BattleScene, player: boolean) { - super(scene); + this.scene.newBattle(); - this.player = player; - } - - getPokemon() { - return this.player ? this.scene.getPlayerPokemon() : this.scene.getEnemyPokemon(); - } -} - -export abstract class PartyMemberPokemonPhase extends PokemonPhase { - protected partyMemberIndex: integer; - - constructor(scene: BattleScene, partyMemberIndex: integer) { - super(scene, true); - - this.partyMemberIndex = partyMemberIndex; - } - - getPokemon() { - return this.scene.getParty()[this.partyMemberIndex]; + this.end(); } } export class CommonAnimPhase extends PokemonPhase { private anim: CommonAnim; + private targetIndex: integer; - constructor(scene: BattleScene, player: boolean, anim: CommonAnim) { - super(scene, player); + constructor(scene: BattleScene, battlerIndex: BattlerIndex, targetIndex: BattlerIndex, anim: CommonAnim) { + super(scene, battlerIndex); + + if (targetIndex === undefined) + targetIndex = this.battlerIndex; this.anim = anim; + this.targetIndex = targetIndex; } start() { - new CommonBattleAnim(this.anim, this.getPokemon(), this.player ? this.scene.getEnemyPokemon() : this.scene.getPlayerPokemon()).play(this.scene, () => { + new CommonBattleAnim(this.anim, this.getPokemon(), (this.player ? this.scene.getEnemyField() : this.scene.getPlayerField())[this.targetIndex]).play(this.scene, () => { this.end(); }); } } -export abstract class MovePhase extends BattlePhase { +export class MovePhase extends BattlePhase { protected pokemon: Pokemon; + protected targets: BattlerIndex[]; protected move: PokemonMove; protected followUp: boolean; protected ignorePp: boolean; protected cancelled: boolean; - constructor(scene: BattleScene, pokemon: Pokemon, move: PokemonMove, followUp?: boolean, ignorePp?: boolean) { + constructor(scene: BattleScene, pokemon: Pokemon, targets: BattlerIndex[], move: PokemonMove, followUp?: boolean, ignorePp?: boolean) { super(scene); this.pokemon = pokemon; + 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 { @@ -891,7 +1130,17 @@ export abstract class MovePhase extends BattlePhase { return; } - const target = this.pokemon.getOpponent(); + console.log(this.targets); + + const targets = this.scene.getField().filter(p => { + if (p?.isActive() && this.targets.indexOf(p.getBattlerIndex()) > -1) { + const hiddenTag = p.getTag(HiddenTag); + if (hiddenTag && !this.move.getMove().getAttrs(HitsTagAttr).filter(hta => (hta as HitsTagAttr).tagType === hiddenTag.tagType).length) + return false; + return true; + } + return false; + }); if (!this.followUp && this.canMove()) this.pokemon.lapseTags(BattlerTagLapseType.MOVE); @@ -899,13 +1148,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; } @@ -914,13 +1163,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!'); } @@ -940,7 +1190,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; @@ -953,7 +1203,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(), CommonAnim.POISON + (this.pokemon.status.effect - 1))); + this.scene.unshiftPhase(new CommonAnimPhase(this.scene, this.pokemon.getBattlerIndex(), undefined, CommonAnim.POISON + (this.pokemon.status.effect - 1))); doMove(); } else { if (healed) { @@ -967,52 +1217,39 @@ export abstract class MovePhase extends BattlePhase { doMove(); } + getEffectPhase(): MoveEffectPhase { + return new MoveEffectPhase(this.scene, this.pokemon.getBattlerIndex(), this.targets, this.move); + } + end() { if (!this.followUp && this.canMove()) - this.scene.unshiftPhase(new MoveEndPhase(this.scene, this.pokemon.isPlayer())); + this.scene.unshiftPhase(new MoveEndPhase(this.scene, this.pokemon.getBattlerIndex())); super.end(); } } -export class PlayerMovePhase extends MovePhase { - constructor(scene: BattleScene, pokemon: PlayerPokemon, move: PokemonMove, followUp?: boolean, ignorePp?: boolean) { - super(scene, pokemon, move, followUp, ignorePp); - } - - getEffectPhase(): MoveEffectPhase { - return new PlayerMoveEffectPhase(this.scene, this.move); - } -} - -export class EnemyMovePhase extends MovePhase { - constructor(scene: BattleScene, pokemon: EnemyPokemon, move: PokemonMove, followUp?: boolean, ignorePp?: boolean) { - super(scene, pokemon, move, followUp, ignorePp); - } - - getEffectPhase(): MoveEffectPhase { - return new EnemyMoveEffectPhase(this.scene, this.move); - } -} - -abstract class MoveEffectPhase extends PokemonPhase { +class MoveEffectPhase extends PokemonPhase { protected move: PokemonMove; + protected targets: BattlerIndex[]; - constructor(scene: BattleScene, player: boolean, move: PokemonMove) { - super(scene, player); + constructor(scene: BattleScene, battlerIndex: BattlerIndex, targets: BattlerIndex[], move: PokemonMove) { + super(scene, battlerIndex); this.move = move; + 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(); @@ -1023,39 +1260,59 @@ 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.getBattlerIndex(), 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).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()?.getBattlerIndex()).play(this.scene, () => { + for (let target of targets) { + if (!targetHitChecks[target.getBattlerIndex()]) { + 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); + + 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) { + 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) + 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); + } + if (!isProtected && !chargeEffect) { + applyFilteredMoveAttrs((attr: MoveAttr) => attr instanceof MoveHitEffectAttr && (!!target.hp || (attr as MoveHitEffectAttr).selfTarget), user, target, this.move.getMove()); + if (!target.isFainted()) + 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(); @@ -1065,7 +1322,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().isFainted()) this.scene.unshiftPhase(this.getNewHitPhase()); else { if (user.turnData.hitCount > 1) @@ -1076,15 +1333,13 @@ 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); - if (hiddenTag) { - if (!this.move.getMove().getAttrs(HitsTagAttr).filter(hta => (hta as HitsTagAttr).tagType === hiddenTag.tagType).length) - return false; - } + const hiddenTag = target.getTag(HiddenTag); + if (hiddenTag && !this.move.getMove().getAttrs(HitsTagAttr).filter(hta => (hta as HitsTagAttr).tagType === hiddenTag.tagType).length) + return false; if (this.getUserPokemon().getTag(BattlerTagType.IGNORE_ACCURACY)) return true; @@ -1094,14 +1349,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; @@ -1116,56 +1371,26 @@ abstract class MoveEffectPhase extends PokemonPhase { return true; } - abstract getUserPokemon(): Pokemon; - - abstract getTargetPokemon(): Pokemon; - - abstract getNewHitPhase(): MoveEffectPhase; -} - -export class PlayerMoveEffectPhase extends MoveEffectPhase { - constructor(scene: BattleScene, move: PokemonMove) { - super(scene, true, move); - } - getUserPokemon(): Pokemon { - return this.scene.getPlayerPokemon(); + return (this.player ? this.scene.getPlayerField() : this.scene.getEnemyField())[this.fieldIndex]; } - getTargetPokemon(): Pokemon { - /*if (this.move.getMove().category === MoveCategory.STATUS) - return this.getUserPokemon();*/ - return this.scene.getEnemyPokemon(); + getTargets(): Pokemon[] { + return this.scene.getField().filter(p => p?.isActive() && this.targets.indexOf(p.getBattlerIndex()) > -1); + } + + getTarget(): Pokemon { + return this.getTargets().find(() => true); } getNewHitPhase() { - return new PlayerMoveEffectPhase(this.scene, this.move); - } -} - -export class EnemyMoveEffectPhase extends MoveEffectPhase { - constructor(scene: BattleScene, move: PokemonMove) { - super(scene, false, move); - } - - getUserPokemon(): Pokemon { - return this.scene.getEnemyPokemon(); - } - - getTargetPokemon(): Pokemon { - /*if (this.move.getMove().category === MoveCategory.STATUS) - return this.getUserPokemon();*/ - return this.scene.getPlayerPokemon(); - } - - getNewHitPhase() { - return new EnemyMoveEffectPhase(this.scene, this.move); + return new MoveEffectPhase(this.scene, this.battlerIndex, this.targets, this.move); } } export class MoveEndPhase extends PokemonPhase { - constructor(scene: BattleScene, player: boolean) { - super(scene, player); + constructor(scene: BattleScene, battlerIndex: BattlerIndex) { + super(scene, battlerIndex); } start() { @@ -1202,7 +1427,7 @@ export class MoveAnimTestPhase extends BattlePhase { initMoveAnim(moveId).then(() => { loadMoveAnimAssets(this.scene, [ moveId ], true) .then(() => { - new MoveAnim(moveId, player ? this.scene.getPlayerPokemon() : this.scene.getEnemyPokemon()).play(this.scene, () => { + new MoveAnim(moveId, player ? this.scene.getPlayerPokemon() : this.scene.getEnemyPokemon(), 0).play(this.scene, () => { if (player) this.playMoveAnim(moveQueue, false); else @@ -1214,8 +1439,8 @@ export class MoveAnimTestPhase extends BattlePhase { } export class ShowAbilityPhase extends PokemonPhase { - constructor(scene: BattleScene, player: boolean) { - super(scene, player); + constructor(scene: BattleScene, battlerIndex: BattlerIndex) { + super(scene, battlerIndex); } start() { @@ -1230,8 +1455,8 @@ export class StatChangePhase extends PokemonPhase { private selfTarget: boolean; private levels: integer; - constructor(scene: BattleScene, player: boolean, selfTarget: boolean, stats: BattleStat[], levels: integer) { - super(scene, player); + constructor(scene: BattleScene, battlerIndex: BattlerIndex, selfTarget: boolean, stats: BattleStat[], levels: integer) { + super(scene, battlerIndex); const allStats = Utils.getEnumValues(BattleStat); this.selfTarget = selfTarget; @@ -1319,7 +1544,7 @@ export class WeatherEffectPhase extends CommonAnimPhase { private weather: Weather; constructor(scene: BattleScene, weather: Weather) { - super(scene, true, CommonAnim.SUNNY + (weather.weatherType - 1)); + super(scene, undefined, undefined, CommonAnim.SUNNY + (weather.weatherType - 1)); this.weather = weather; } @@ -1328,7 +1553,7 @@ export class WeatherEffectPhase extends CommonAnimPhase { const cancelled = new Utils.BooleanHolder(false); - this.executeForBoth((pokemon: Pokemon) => applyPreWeatherEffectAbAttrs(SuppressWeatherEffectAbAttr, pokemon, this.weather, cancelled)); + this.executeForAll((pokemon: Pokemon) => applyPreWeatherEffectAbAttrs(SuppressWeatherEffectAbAttr, pokemon, this.weather, cancelled)); if (!cancelled.value) { const inflictDamage = (pokemon: Pokemon) => { @@ -1340,11 +1565,11 @@ export class WeatherEffectPhase extends CommonAnimPhase { return; this.scene.queueMessage(getWeatherDamageMessage(this.weather.weatherType, pokemon)); - this.scene.unshiftPhase(new DamagePhase(this.scene, pokemon.isPlayer())); + this.scene.unshiftPhase(new DamagePhase(this.scene, pokemon.getBattlerIndex())); pokemon.damage(Math.ceil(pokemon.getMaxHp() / 16)); }; - this.executeForBoth((pokemon: Pokemon) => { + this.executeForAll((pokemon: Pokemon) => { const immune = !pokemon || !!pokemon.getTypes().filter(t => this.weather.isTypeDamageImmune(t)).length; if (!immune) inflictDamage(pokemon); @@ -1353,7 +1578,7 @@ export class WeatherEffectPhase extends CommonAnimPhase { } this.scene.ui.showText(getWeatherLapseMessage(this.weather.weatherType), null, () => { - this.executeForBoth((pokemon: Pokemon) => applyPostWeatherLapseAbAttrs(PostWeatherLapseAbAttr, pokemon, this.weather)); + this.executeForAll((pokemon: Pokemon) => applyPostWeatherLapseAbAttrs(PostWeatherLapseAbAttr, pokemon, this.weather)); super.start(); }); @@ -1365,8 +1590,8 @@ export class ObtainStatusEffectPhase extends PokemonPhase { private cureTurn: integer; private sourceText: string; - constructor(scene: BattleScene, player: boolean, statusEffect: StatusEffect, cureTurn?: integer, sourceText?: string) { - super(scene, player); + constructor(scene: BattleScene, battlerIndex: BattlerIndex, statusEffect: StatusEffect, cureTurn?: integer, sourceText?: string) { + super(scene, battlerIndex); this.statusEffect = statusEffect; this.cureTurn = cureTurn; @@ -1383,7 +1608,7 @@ export class ObtainStatusEffectPhase extends PokemonPhase { new CommonBattleAnim(CommonAnim.POISON + (this.statusEffect - 1), pokemon).play(this.scene, () => { this.scene.queueMessage(getPokemonMessage(pokemon, getStatusEffectObtainText(this.statusEffect, this.sourceText))); if (pokemon.status.isPostTurn()) - this.scene.pushPhase(new PostTurnStatusEffectPhase(this.scene, this.player)); + this.scene.pushPhase(new PostTurnStatusEffectPhase(this.scene, this.battlerIndex)); this.end(); }); return; @@ -1395,13 +1620,13 @@ export class ObtainStatusEffectPhase extends PokemonPhase { } export class PostTurnStatusEffectPhase extends PokemonPhase { - constructor(scene: BattleScene, player: boolean) { - super(scene, player); + constructor(scene: BattleScene, battlerIndex: BattlerIndex) { + super(scene, battlerIndex); } start() { const pokemon = this.getPokemon(); - if (pokemon?.hp && pokemon.status && pokemon.status.isPostTurn()) { + if (pokemon?.isActive() && pokemon.status && pokemon.status.isPostTurn()) { pokemon.status.incrementTurn(); new CommonBattleAnim(CommonAnim.POISON + (pokemon.status.effect - 1), pokemon).play(this.scene, () => { this.scene.queueMessage(getPokemonMessage(pokemon, getStatusEffectActivationText(pokemon.status.effect))); @@ -1453,28 +1678,28 @@ export class MessagePhase extends BattlePhase { export class DamagePhase extends PokemonPhase { private damageResult: DamageResult; - constructor(scene: BattleScene, player: boolean, damageResult?: DamageResult) { - super(scene, player); + constructor(scene: BattleScene, battlerIndex: BattlerIndex, damageResult?: DamageResult) { + super(scene, battlerIndex); - 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, @@ -1491,8 +1716,8 @@ export class DamagePhase extends PokemonPhase { } export class FaintPhase extends PokemonPhase { - constructor(scene: BattleScene, player: boolean) { - super(scene, player); + constructor(scene: BattleScene, battlerIndex: BattlerIndex) { + super(scene, battlerIndex); } start() { @@ -1501,17 +1726,18 @@ export class FaintPhase extends PokemonPhase { this.scene.queueMessage(getPokemonMessage(this.getPokemon(), ' fainted!'), null, true); if (this.player) { - this.scene.unshiftPhase(this.scene.getParty().filter(p => p.hp).length ? new SwitchPhase(this.scene, true, false) : new GameOverPhase(this.scene)); + const nonFaintedPartyMemberCount = this.scene.getParty().filter(p => !p.isFainted()).length; + if (!nonFaintedPartyMemberCount) + this.scene.unshiftPhase(new GameOverPhase(this.scene)); + else if (nonFaintedPartyMemberCount >= this.scene.currentBattle.getBattlerCount()) + this.scene.unshiftPhase(new SwitchPhase(this.scene, this.fieldIndex, true, false)); } else - this.scene.unshiftPhase(new VictoryPhase(this.scene)); + this.scene.unshiftPhase(new VictoryPhase(this.scene, this.fieldIndex)); const pokemon = this.getPokemon(); pokemon.lapseTags(BattlerTagLapseType.FAINT); - if (pokemon.isPlayer()) - this.scene.getEnemyPokemon()?.removeTagsBySourceId(pokemon.id); - else - this.scene.getPlayerPokemon()?.removeTagsBySourceId(pokemon.id); + this.scene.getField().filter(p => p && p !== pokemon).forEach(p => p.removeTagsBySourceId(pokemon.id)); pokemon.faintCry(() => { pokemon.hideInfo(); @@ -1536,8 +1762,8 @@ export class FaintPhase extends PokemonPhase { } export class VictoryPhase extends PokemonPhase { - constructor(scene: BattleScene) { - super(scene, true); + constructor(scene: BattleScene, targetIndex: integer) { + super(scene, targetIndex); } start() { @@ -1548,7 +1774,7 @@ export class VictoryPhase extends PokemonPhase { const expShareModifier = this.scene.findModifier(m => m instanceof ExpShareModifier) as ExpShareModifier; const expBalanceModifier = this.scene.findModifier(m => m instanceof ExpBalanceModifier) as ExpBalanceModifier; const multipleParticipantExpBonusModifier = this.scene.findModifier(m => m instanceof MultipleParticipantExpBonusModifier) as MultipleParticipantExpBonusModifier; - const expValue = this.scene.getEnemyPokemon().getExpValue(); + const expValue = this.getPokemon().getExpValue(); const expPartyMembers = party.filter(p => p.hp && p.level < maxExpLevel); const partyMemberExp = []; for (let partyMember of expPartyMembers) { @@ -1603,13 +1829,15 @@ export class VictoryPhase extends PokemonPhase { this.scene.unshiftPhase(new ExpPhase(this.scene, partyMemberIndex, exp)); } } - - this.scene.pushPhase(new BattleEndPhase(this.scene)); - if (this.scene.currentBattle.waveIndex < this.scene.finalWave) { - this.scene.pushPhase(new SelectModifierPhase(this.scene)); - this.scene.newBattle(); - } else - this.scene.pushPhase(new GameOverPhase(this.scene, true)); + + if (!this.scene.getEnemyField().filter(p => !p?.isFainted(true)).length) { + this.scene.pushPhase(new BattleEndPhase(this.scene)); + if (this.scene.currentBattle.waveIndex < this.scene.finalWave) { + this.scene.pushPhase(new SelectModifierPhase(this.scene)); + this.scene.pushPhase(new NewBattlePhase(this.scene)); + } else + this.scene.pushPhase(new GameOverPhase(this.scene, true)); + } this.end(); } @@ -1677,12 +1905,14 @@ export class UnlockPhase extends BattlePhase { } export class SwitchPhase extends BattlePhase { + protected fieldIndex: integer; private isModal: boolean; private doReturn: boolean; - constructor(scene: BattleScene, isModal: boolean, doReturn: boolean) { + constructor(scene: BattleScene, fieldIndex: integer, isModal: boolean, doReturn: boolean) { super(scene); + this.fieldIndex = fieldIndex; this.isModal = isModal; this.doReturn = doReturn; } @@ -1690,9 +1920,9 @@ export class SwitchPhase extends BattlePhase { start() { super.start(); - this.scene.ui.setMode(Mode.PARTY, this.isModal ? PartyUiMode.FAINT_SWITCH : PartyUiMode.POST_BATTLE_SWITCH, (slotIndex: integer, option: PartyOption) => { - if (slotIndex && slotIndex < 6) - this.scene.unshiftPhase(new SwitchSummonPhase(this.scene, slotIndex, this.doReturn, option === PartyOption.PASS_BATON)); + this.scene.ui.setMode(Mode.PARTY, this.isModal ? PartyUiMode.FAINT_SWITCH : PartyUiMode.POST_BATTLE_SWITCH, this.fieldIndex, (slotIndex: integer, option: PartyOption) => { + if (slotIndex >= this.scene.currentBattle.getBattlerCount() && slotIndex < 6) + this.scene.unshiftPhase(new SwitchSummonPhase(this.scene, this.fieldIndex, slotIndex, this.doReturn, option === PartyOption.PASS_BATON)); this.scene.ui.setMode(Mode.MESSAGE).then(() => super.end()); }, PartyUiHandler.FilterNonFainted); } @@ -1850,8 +2080,8 @@ export class LearnMovePhase extends PartyMemberPokemonPhase { } export class BerryPhase extends CommonAnimPhase { - constructor(scene: BattleScene, player: boolean) { - super(scene, player, CommonAnim.USE_ITEM); + constructor(scene: BattleScene, battlerIndex: BattlerIndex) { + super(scene, battlerIndex, undefined, CommonAnim.USE_ITEM); } start() { @@ -1879,8 +2109,8 @@ export class PokemonHealPhase extends CommonAnimPhase { private showFullHpMessage: boolean; private skipAnim: boolean; - constructor(scene: BattleScene, player: boolean, hpHealed: integer, message: string, showFullHpMessage: boolean, skipAnim?: boolean) { - super(scene, player, CommonAnim.HEALTH_UP); + constructor(scene: BattleScene, battlerIndex: BattlerIndex, hpHealed: integer, message: string, showFullHpMessage: boolean, skipAnim?: boolean) { + super(scene, battlerIndex, undefined, CommonAnim.HEALTH_UP); this.hpHealed = hpHealed; this.message = message; @@ -1898,7 +2128,7 @@ export class PokemonHealPhase extends CommonAnimPhase { end() { const pokemon = this.getPokemon(); - if (!this.getPokemon().hp) { + if (!this.getPokemon().isActive()) { super.end(); return; } @@ -1921,13 +2151,13 @@ export class PokemonHealPhase extends CommonAnimPhase { } } -export class AttemptCapturePhase extends BattlePhase { +export class AttemptCapturePhase extends PokemonPhase { private pokeballType: PokeballType; private pokeball: Phaser.GameObjects.Sprite; private originalY: number; - constructor(scene: BattleScene, pokeballType: PokeballType) { - super(scene); + constructor(scene: BattleScene, targetIndex: integer, pokeballType: PokeballType) { + super(scene, BattlerIndex.ENEMY + targetIndex); this.pokeballType = pokeballType; } @@ -1935,9 +2165,15 @@ export class AttemptCapturePhase extends BattlePhase { start() { super.start(); + const pokemon = this.getPokemon(); + + if (!pokemon?.hp) { + this.end(); + return; + } + this.scene.pokeballCounts[this.pokeballType]--; - const pokemon = this.scene.getEnemyPokemon(); this.originalY = pokemon.y; const _3m = 3 * pokemon.getMaxHp(); @@ -1947,6 +2183,7 @@ export class AttemptCapturePhase extends BattlePhase { const statusMultiplier = pokemon.status ? getStatusEffectCatchRateMultiplier(pokemon.status.effect) : 1; const x = Math.round((((_3m - _2h) * catchRate * pokeballMultiplier) / _3m) * statusMultiplier); const y = Math.round(65536 / Math.sqrt(Math.sqrt(255 / x))); + const fpOffset = pokemon.getFieldPositionOffset(); const pokeballAtlasKey = getPokeballAtlasKey(this.pokeballType); this.pokeball = this.scene.add.sprite(16, 80, 'pb', pokeballAtlasKey); @@ -1959,8 +2196,8 @@ export class AttemptCapturePhase extends BattlePhase { }); this.scene.tweens.add({ targets: this.pokeball, - x: { value: 236, ease: 'Linear' }, - y: { value: 16, ease: 'Cubic.easeOut' }, + x: { value: 236 + fpOffset[0], ease: 'Linear' }, + y: { value: 16 + fpOffset[1], ease: 'Cubic.easeOut' }, duration: 500, onComplete: () => { this.pokeball.setTexture('pb', `${pokeballAtlasKey}_opening`); @@ -2024,7 +2261,7 @@ export class AttemptCapturePhase extends BattlePhase { } failCatch(shakeCount: integer) { - const pokemon = this.scene.getEnemyPokemon(); + const pokemon = this.getPokemon(); this.scene.sound.play('pb_rel'); pokemon.setY(this.originalY); @@ -2049,8 +2286,8 @@ export class AttemptCapturePhase extends BattlePhase { } catch() { - const pokemon = this.scene.getEnemyPokemon(); - this.scene.unshiftPhase(new VictoryPhase(this.scene)); + const pokemon = this.getPokemon() as EnemyPokemon; + this.scene.unshiftPhase(new VictoryPhase(this.scene, this.fieldIndex)); this.scene.ui.showText(`${pokemon.name} was caught!`, null, () => { const end = () => { this.removePb(); @@ -2060,7 +2297,7 @@ export class AttemptCapturePhase extends BattlePhase { const newPokemon = pokemon.addToParty(); const modifiers = this.scene.findModifiers(m => m instanceof PokemonHeldItemModifier, false); Promise.all(modifiers.map(m => this.scene.addModifier(m))).then(() => { - this.scene.getPlayerPokemon().removeTagsBySourceId(pokemon.id); + this.scene.getPlayerField().forEach(playerPokemon => playerPokemon.removeTagsBySourceId(pokemon.id)); pokemon.hp = 0; this.scene.clearEnemyModifiers(); this.scene.field.remove(pokemon, true); @@ -2075,7 +2312,7 @@ export class AttemptCapturePhase extends BattlePhase { const promptRelease = () => { this.scene.ui.showText(`Your party is full.\nRelease a POKéMON to make room for ${pokemon.name}?`, null, () => { this.scene.ui.setMode(Mode.CONFIRM, () => { - this.scene.ui.setMode(Mode.PARTY, PartyUiMode.RELEASE, (slotIndex: integer, _option: PartyOption) => { + this.scene.ui.setMode(Mode.PARTY, PartyUiMode.RELEASE, this.fieldIndex, (slotIndex: integer, _option: PartyOption) => { this.scene.ui.setMode(Mode.MESSAGE).then(() => { if (slotIndex < 6) addToParty(); @@ -2109,34 +2346,36 @@ export class AttemptCapturePhase extends BattlePhase { } } -export class AttemptRunPhase extends BattlePhase { - constructor(scene: BattleScene) { - super(scene); +export class AttemptRunPhase extends PokemonPhase { + constructor(scene: BattleScene, fieldIndex: integer) { + super(scene, fieldIndex); } start() { super.start(); - const playerPokemon = this.scene.getPlayerPokemon(); - const enemyPokemon = this.scene.getEnemyPokemon(); + const playerPokemon = this.getPokemon(); + const enemyField = this.scene.getEnemyField(); - const escapeChance = (((playerPokemon.stats[Stat.SPD] * 128) / enemyPokemon.stats[Stat.SPD]) + (30 * this.scene.currentBattle.escapeAttempts++)) % 256; + const enemySpeed = enemyField.reduce((total: integer, enemyPokemon: Pokemon) => total + enemyPokemon.stats[Stat.SPD], 0) / enemyField.length; + + const escapeChance = (((playerPokemon.stats[Stat.SPD] * 128) / enemySpeed) + (30 * this.scene.currentBattle.escapeAttempts++)) % 256; if (Utils.randInt(256) < escapeChance) { this.scene.sound.play('flee'); this.scene.queueMessage('You got away safely!', null, true, 500); this.scene.tweens.add({ - targets: [ this.scene.arenaEnemy, enemyPokemon ], + targets: [ this.scene.arenaEnemy, enemyField ].flat(), alpha: 0, duration: 250, ease: 'Sine.easeIn' }); - enemyPokemon.hp = 0; + enemyField.forEach(enemyPokemon => enemyPokemon.hp = 0); this.scene.pushPhase(new BattleEndPhase(this.scene)); - this.scene.newBattle(); + this.scene.pushPhase(new NewBattlePhase(this.scene)); } else this.scene.queueMessage('You can\'t escape!', null, true); @@ -2156,7 +2395,7 @@ export class SelectModifierPhase extends BattlePhase { regenerateModifierPoolThresholds(party); const modifierCount = new Utils.IntegerHolder(3); this.scene.applyModifiers(ExtraModifierModifier, true, modifierCount); - const typeOptions: Array = getPlayerModifierTypeOptionsForWave(this.scene.currentBattle.waveIndex - 1, modifierCount.value, party); + const typeOptions: Array = getPlayerModifierTypeOptionsForWave(this.scene.currentBattle.waveIndex, modifierCount.value, party); const modifierSelectCallback = (cursor: integer) => { if (cursor < 0) { @@ -2164,7 +2403,7 @@ export class SelectModifierPhase extends BattlePhase { super.end(); return; } else if (cursor >= typeOptions.length) { - this.scene.ui.setModeWithoutClear(Mode.PARTY, PartyUiMode.MODIFIER_TRANSFER, (fromSlotIndex: integer, itemIndex: integer, toSlotIndex: integer) => { + this.scene.ui.setModeWithoutClear(Mode.PARTY, PartyUiMode.MODIFIER_TRANSFER, -1, (fromSlotIndex: integer, itemIndex: integer, toSlotIndex: integer) => { if (toSlotIndex !== undefined && fromSlotIndex < 6 && toSlotIndex < 6 && fromSlotIndex !== toSlotIndex && itemIndex > -1) { this.scene.ui.setMode(Mode.MODIFIER_SELECT).then(() => { const itemModifiers = this.scene.findModifiers(m => m instanceof PokemonHeldItemModifier @@ -2189,7 +2428,7 @@ export class SelectModifierPhase extends BattlePhase { if (modifierType instanceof PokemonModifierType) { const pokemonModifierType = modifierType as PokemonModifierType; const isMoveModifier = modifierType instanceof PokemonMoveModifierType; - this.scene.ui.setModeWithoutClear(Mode.PARTY, !isMoveModifier ? PartyUiMode.MODIFIER : PartyUiMode.MOVE_MODIFIER, (slotIndex: integer, option: PartyOption) => { + this.scene.ui.setModeWithoutClear(Mode.PARTY, !isMoveModifier ? PartyUiMode.MODIFIER : PartyUiMode.MOVE_MODIFIER, -1, (slotIndex: integer, option: PartyOption) => { if (slotIndex < 6) { this.scene.ui.setMode(Mode.MODIFIER_SELECT).then(() => { const modifierType = typeOptions[cursor].type; @@ -2214,8 +2453,8 @@ export class SelectModifierPhase extends BattlePhase { } export class ShinySparklePhase extends PokemonPhase { - constructor(scene: BattleScene, player: boolean) { - super(scene, player); + constructor(scene: BattleScene, battlerIndex: BattlerIndex) { + super(scene, battlerIndex); } start() { diff --git a/src/battle-scene.ts b/src/battle-scene.ts index 8a8557a47..9c2d7a625 100644 --- a/src/battle-scene.ts +++ b/src/battle-scene.ts @@ -1,15 +1,14 @@ import Phaser from 'phaser'; import { Biome } from './data/biome'; import UI from './ui/ui'; -import { EncounterPhase, SummonPhase, CommandPhase, NextEncounterPhase, NewBiomeEncounterPhase, SelectBiomePhase, MessagePhase, CheckLoadPhase } from './battle-phases'; -import Pokemon, { PlayerPokemon, EnemyPokemon } from './pokemon'; +import { EncounterPhase, SummonPhase, NextEncounterPhase, NewBiomeEncounterPhase, SelectBiomePhase, MessagePhase, CheckLoadPhase, TurnInitPhase, ReturnPhase, ToggleDoublePositionPhase, CheckSwitchPhase } from './battle-phases'; +import Pokemon, { PlayerPokemon, EnemyPokemon, FieldPosition } from './pokemon'; import PokemonSpecies, { allSpecies, getPokemonSpecies, initSpecies } from './data/pokemon-species'; import * as Utils from './utils'; import { Modifier, ModifierBar, ConsumablePokemonModifier, ConsumableModifier, PokemonHpRestoreModifier, HealingBoosterModifier, PersistentModifier, PokemonHeldItemModifier, ModifierPredicate } from './modifier/modifier'; import { PokeballType } from './data/pokeball'; import { Species } from './data/species'; import { initAutoPlay } from './system/auto-play'; -import { Battle } from './battle'; import { initCommonAnims, initMoveAnim, loadCommonAnimAssets, loadMoveAnimAssets, populateAnims } from './data/battle-anims'; import { BattlePhase } from './battle-phase'; import { initGameSpeed } from './system/game-speed'; @@ -20,7 +19,8 @@ import { TextStyle, addTextObject } from './ui/text'; import { Moves, initMoves } from './data/move'; import { getDefaultModifierTypeForTier, getEnemyModifierTypesForWave } from './modifier/modifier-type'; import AbilityBar from './ui/ability-bar'; -import { BlockItemTheftAbAttr, applyAbAttrs, initAbilities } from './data/ability'; +import { BlockItemTheftAbAttr, DoubleBattleChanceAbAttr, applyAbAttrs, initAbilities } from './data/ability'; +import Battle from './battle'; const enableAuto = true; const quickStart = false; @@ -156,7 +156,8 @@ export default class BattleScene extends Phaser.Scene { this.loadAtlas('prompt', 'ui'); this.loadImage('cursor', 'ui'); this.loadImage('pbinfo_player', 'ui'); - this.loadImage('pbinfo_enemy', 'ui'); + this.loadImage('pbinfo_player_mini', 'ui'); + this.loadImage('pbinfo_enemy_mini', 'ui'); this.loadImage('overlay_lv', 'ui'); this.loadAtlas('numbers', 'ui'); this.loadAtlas('overlay_hp', 'ui'); @@ -168,6 +169,7 @@ export default class BattleScene extends Phaser.Scene { this.loadImage('boolean_window', 'ui'); this.loadImage('party_bg', 'ui'); + this.loadImage('party_bg_double', 'ui'); this.loadAtlas('party_slot_main', 'ui'); this.loadAtlas('party_slot', 'ui'); this.loadImage('party_slot_overlay_lv', 'ui'); @@ -209,6 +211,8 @@ export default class BattleScene extends Phaser.Scene { this.loadImage('starter_select_gen_cursor', 'ui'); this.loadImage('starter_select_gen_cursor_highlight', 'ui'); + this.loadImage('default_bg', 'arenas'); + // Load arena images Utils.getEnumValues(Biome).map(bt => { const btKey = Biome[bt].toLowerCase(); @@ -446,21 +450,35 @@ export default class BattleScene extends Phaser.Scene { return this.party; } - getEnemyParty(): EnemyPokemon[] { - return this.getEnemyPokemon() ? [ this.getEnemyPokemon() ] : []; + getPlayerPokemon(): PlayerPokemon { + return this.getPlayerField().find(p => p.isActive()); } - getPlayerPokemon(): PlayerPokemon { - return this.getParty()[0]; + getPlayerField(): PlayerPokemon[] { + const party = this.getParty(); + return party.slice(0, Math.min(party.length, this.currentBattle?.double ? 2 : 1)); } getEnemyPokemon(): EnemyPokemon { - return this.currentBattle?.enemyPokemon; + return this.getEnemyField().find(p => p.isActive()); + } + + getEnemyField(): EnemyPokemon[] { + return this.currentBattle?.enemyField || []; + } + + getField(): Pokemon[] { + const ret = new Array(4).fill(null); + const playerField = this.getPlayerField(); + const enemyField = this.getEnemyField(); + ret.splice(0, playerField.length, ...playerField); + ret.splice(2, enemyField.length, ...enemyField); + return ret; } getPokemonById(pokemonId: integer): Pokemon { const findInParty = (party: Pokemon[]) => party.find(p => p.id === pokemonId); - return findInParty(this.getParty()) || findInParty(this.getEnemyParty()); + return findInParty(this.getParty()) || findInParty(this.getEnemyField()); } reset(): void { @@ -475,7 +493,7 @@ export default class BattleScene extends Phaser.Scene { for (let p of this.getParty()) p.destroy(); this.party = []; - for (let p of this.getEnemyParty()) + for (let p of this.getEnemyField()) p.destroy(); this.currentBattle = null; @@ -493,11 +511,26 @@ export default class BattleScene extends Phaser.Scene { this.trainer.setPosition(406, 132); } - newBattle(waveIndex?: integer): Battle { + newBattle(waveIndex?: integer, double?: boolean): Battle { + let newWaveIndex = waveIndex || ((this.currentBattle?.waveIndex || (startingWave - 1)) + 1); + let newDouble: boolean; + + if (double === undefined) { + const doubleChance = new Utils.IntegerHolder(newWaveIndex % 10 === 0 ? 32 : 8); + this.getPlayerField().forEach(p => applyAbAttrs(DoubleBattleChanceAbAttr, p, null, doubleChance)); + newDouble = !Utils.randInt(doubleChance.value); + } else + newDouble = double; + + const lastBattle = this.currentBattle; + + this.currentBattle = new Battle(newWaveIndex, newDouble); + this.currentBattle.incrementTurn(this); + if (!waveIndex) { - if (this.currentBattle) { - this.getEnemyPokemon().destroy(); - if (this.currentBattle.waveIndex % 10) + if (lastBattle) { + this.getEnemyField().forEach(enemyPokemon => enemyPokemon.destroy()); + if (lastBattle.waveIndex % 10) this.pushPhase(new NextEncounterPhase(this)); else { this.pushPhase(new SelectBiomePhase(this)); @@ -509,12 +542,29 @@ export default class BattleScene extends Phaser.Scene { else { this.arena.playBgm(); this.pushPhase(new EncounterPhase(this)); - this.pushPhase(new SummonPhase(this)); + this.pushPhase(new SummonPhase(this, 0)); } } - } - this.currentBattle = new Battle(waveIndex || ((this.currentBattle?.waveIndex || (startingWave - 1)) + 1)); + if ((lastBattle?.double || false) !== newDouble) { + const availablePartyMemberCount = this.getParty().filter(p => !p.isFainted()).length; + if (newDouble) { + this.pushPhase(new ToggleDoublePositionPhase(this, true)); + if (availablePartyMemberCount > 1) + this.pushPhase(new SummonPhase(this, 1)); + } else { + if (availablePartyMemberCount > 1) + this.pushPhase(new ReturnPhase(this, 1)); + this.pushPhase(new ToggleDoublePositionPhase(this, false)); + } + } + + if (lastBattle) { + this.pushPhase(new CheckSwitchPhase(this, 0, newDouble)); + if (newDouble) + this.pushPhase(new CheckSwitchPhase(this, 1, newDouble)); + } + } return this.currentBattle; } @@ -699,7 +749,7 @@ export default class BattleScene extends Phaser.Scene { } populatePhaseQueue(): void { - this.phaseQueue.push(new CommandPhase(this)); + this.phaseQueue.push(new TurnInitPhase(this)); } addModifier(modifier: Modifier, playSound?: boolean, virtual?: boolean): Promise { @@ -828,7 +878,9 @@ export default class BattleScene extends Phaser.Scene { } if (isBoss) count = Math.max(count, Math.floor(chances / 2)); - getEnemyModifierTypesForWave(waveIndex, count, this.getEnemyParty()).map(mt => mt.newModifier(this.getEnemyPokemon()).add(this.enemyModifiers, false)); + const enemyField = this.getEnemyField(); + getEnemyModifierTypesForWave(waveIndex, count, this.getEnemyField()) + .map(mt => mt.newModifier(enemyField[Utils.randInt(enemyField.length)]).add(this.enemyModifiers, false)); this.updateModifiers(false).then(() => resolve()); }); @@ -855,7 +907,7 @@ export default class BattleScene extends Phaser.Scene { modifiers.splice(modifiers.indexOf(modifier), 1); } - this.updatePartyForModifiers(player ? this.getParty() : this.getEnemyParty()).then(() => { + this.updatePartyForModifiers(player ? this.getParty() : this.getEnemyField().filter(p => p.isActive())).then(() => { (player ? this.modifierBar : this.enemyModifierBar).updateModifiers(modifiers); if (!player) this.updateWaveCountPosition(); diff --git a/src/battle.ts b/src/battle.ts index 474b7a249..ae086b6e7 100644 --- a/src/battle.ts +++ b/src/battle.ts @@ -1,18 +1,44 @@ -import { EnemyPokemon, PlayerPokemon } from "./pokemon"; +import BattleScene, { PokeballCounts } from "./battle-scene"; +import { EnemyPokemon, PlayerPokemon, QueuedMove } from "./pokemon"; +import { Command } from "./ui/command-ui-handler"; import * as Utils from "./utils"; -export class Battle { +export enum BattlerIndex { + PLAYER, + PLAYER_2, + ENEMY, + ENEMY_2 +} + +export interface TurnCommand { + command: Command; + cursor?: integer; + move?: QueuedMove; + targets?: BattlerIndex[]; + args?: any[]; +}; + +interface TurnCommands { + [key: integer]: TurnCommand +} + +export default class Battle { public waveIndex: integer; - public enemyLevel: integer; - public enemyPokemon: EnemyPokemon; + public enemyLevels: integer[]; + public enemyField: EnemyPokemon[]; + public double: boolean; public turn: integer; + public turnCommands: TurnCommands; + public turnPokeballCounts: PokeballCounts; public playerParticipantIds: Set = new Set(); public escapeAttempts: integer = 0; - constructor(waveIndex: integer) { + constructor(waveIndex: integer, double: boolean) { this.waveIndex = waveIndex; - this.enemyLevel = this.getLevelForWave(); - this.turn = 1; + this.enemyLevels = new Array(double ? 2 : 1).fill(null).map(() => this.getLevelForWave()); + this.enemyField = []; + this.double = double; + this.turn = 0; } private getLevelForWave(): number { @@ -29,8 +55,14 @@ export class Battle { return Math.max(Math.round(baseLevel + Math.abs(Utils.randGauss(deviation))), 1); } - incrementTurn() { + getBattlerCount(): integer { + return this.double ? 2 : 1; + } + + incrementTurn(scene: BattleScene): void { this.turn++; + this.turnCommands = Object.fromEntries(Utils.getEnumValues(BattlerIndex).map(bt => [ bt, null ])); + this.turnPokeballCounts = Object.assign({}, scene.pokeballCounts); } addParticipant(playerPokemon: PlayerPokemon): void { diff --git a/src/data/ability.ts b/src/data/ability.ts index 3421a3d2a..2f20204e2 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"; @@ -83,6 +83,18 @@ export class BlockRecoilDamageAttr extends AbAttr { } } +export class DoubleBattleChanceAbAttr extends AbAttr { + constructor() { + super(false); + } + + apply(pokemon: Pokemon, cancelled: Utils.BooleanHolder, args: any[]): boolean { + const doubleChance = (args[0] as Utils.IntegerHolder); + doubleChance.value = Math.max(doubleChance.value / 2, 1); + return true; + } +} + export class PreDefendAbAttr extends AbAttr { applyPreDefend(pokemon: Pokemon, attacker: Pokemon, move: PokemonMove, cancelled: Utils.BooleanHolder, args: any[]): boolean { return false; @@ -155,9 +167,10 @@ export class TypeImmunityHealAbAttr extends TypeImmunityAbAttr { applyPreDefend(pokemon: Pokemon, attacker: Pokemon, move: PokemonMove, cancelled: Utils.BooleanHolder, args: any[]): boolean { const ret = super.applyPreDefend(pokemon, attacker, move, cancelled, args); - if (ret && pokemon.getHpRatio() < 1) { - const scene = pokemon.scene; - scene.unshiftPhase(new PokemonHealPhase(scene, pokemon.isPlayer(), Math.max(Math.floor(pokemon.getMaxHp() / 4), 1), getPokemonMessage(pokemon, `'s ${pokemon.getAbility().name}\nrestored its HP a little!`), true)); + if (ret) { + if (pokemon.getHpRatio() < 1) + pokemon.scene.unshiftPhase(new PokemonHealPhase(pokemon.scene, pokemon.getBattlerIndex(), + Math.max(Math.floor(pokemon.getMaxHp() / 4), 1), getPokemonMessage(pokemon, `'s ${pokemon.getAbility().name}\nrestored its HP a little!`), true)); return true; } @@ -181,7 +194,7 @@ class TypeImmunityStatChangeAbAttr extends TypeImmunityAbAttr { if (ret) { cancelled.value = true; - pokemon.scene.unshiftPhase(new StatChangePhase(pokemon.scene, pokemon.isPlayer(), true, [ this.stat ], this.levels)); + pokemon.scene.unshiftPhase(new StatChangePhase(pokemon.scene, pokemon.getBattlerIndex(), true, [ this.stat ], this.levels)); } return ret; @@ -232,14 +245,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 +280,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 +303,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); @@ -406,13 +419,21 @@ export class PostSummonStatChangeAbAttr extends PostSummonAbAttr { } applyPostSummon(pokemon: Pokemon, args: any[]): boolean { - const statChangePhase = new StatChangePhase(pokemon.scene, pokemon.isPlayer() === this.selfTarget, this.selfTarget, this.stats, this.levels); + const statChangePhases: StatChangePhase[] = []; - if (!this.selfTarget && !pokemon.getOpponent()?.summonData) - pokemon.scene.pushPhase(statChangePhase); // TODO: This causes the ability bar to be shown at the wrong time - else - pokemon.scene.unshiftPhase(statChangePhase); + if (this.selfTarget) + statChangePhases.push(new StatChangePhase(pokemon.scene, pokemon.getBattlerIndex(), true, this.stats, this.levels)); + else { + for (let opponent of pokemon.getOpponents()) + statChangePhases.push(new StatChangePhase(pokemon.scene, opponent.getBattlerIndex(), false, this.stats, this.levels)); + } + for (let statChangePhase of statChangePhases) { + if (!this.selfTarget && !statChangePhase.getPokemon().summonData) + pokemon.scene.pushPhase(statChangePhase); // TODO: This causes the ability bar to be shown at the wrong time + else + pokemon.scene.unshiftPhase(statChangePhase); + } return true; } @@ -576,7 +597,7 @@ export class PostTurnAbAttr extends AbAttr { export class PostTurnSpeedBoostAbAttr extends PostTurnAbAttr { applyPostTurn(pokemon: Pokemon, args: any[]): boolean { - pokemon.scene.unshiftPhase(new StatChangePhase(pokemon.scene, pokemon.isPlayer(), true, [ BattleStat.SPD ], 1)); + pokemon.scene.unshiftPhase(new StatChangePhase(pokemon.scene, pokemon.getBattlerIndex(), true, [ BattleStat.SPD ], 1)); return true; } } @@ -594,7 +615,8 @@ export class PostTurnHealAbAttr extends PostTurnAbAttr { applyPostTurn(pokemon: Pokemon, args: any[]): boolean { if (pokemon.getHpRatio() < 1) { const scene = pokemon.scene; - scene.unshiftPhase(new PokemonHealPhase(scene, pokemon.isPlayer(), Math.max(Math.floor(pokemon.getMaxHp() / 16), 1), getPokemonMessage(pokemon, `'s ${pokemon.getAbility().name}\nrestored its HP a little!`), true)); + scene.unshiftPhase(new PokemonHealPhase(scene, pokemon.getBattlerIndex(), + Math.max(Math.floor(pokemon.getMaxHp() / 16), 1), getPokemonMessage(pokemon, `'s ${pokemon.getAbility().name}\nrestored its HP a little!`), true)); return true; } @@ -632,7 +654,8 @@ export class PostWeatherLapseHealAbAttr extends PostWeatherLapseAbAttr { applyPostWeatherLapse(pokemon: Pokemon, weather: Weather, args: any[]): boolean { if (pokemon.getHpRatio() < 1) { const scene = pokemon.scene; - scene.unshiftPhase(new PokemonHealPhase(scene, pokemon.isPlayer(), Math.max(Math.floor(pokemon.getMaxHp() / (16 / this.healFactor)), 1), getPokemonMessage(pokemon, `'s ${pokemon.getAbility().name}\nrestored its HP a little!`), true)); + scene.unshiftPhase(new PokemonHealPhase(scene, pokemon.getBattlerIndex(), + Math.max(Math.floor(pokemon.getMaxHp() / (16 / this.healFactor)), 1), getPokemonMessage(pokemon, `'s ${pokemon.getAbility().name}\nrestored its HP a little!`), true)); return true; } @@ -653,7 +676,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(), MoveResult.OTHER)); + scene.unshiftPhase(new DamagePhase(pokemon.scene, pokemon.getBattlerIndex(), HitResult.OTHER)); pokemon.damage(Math.ceil(pokemon.getMaxHp() * (16 / this.damageFactor))); return true; } @@ -685,7 +708,6 @@ export function applyAbAttrs(attrType: { new(...args: any[]): AbAttr }, pokemon: const ability = pokemon.getAbility(); const attrs = ability.getAttrs(attrType) as AbAttr[]; - console.log(attrs, ability); for (let attr of attrs) { if (!canApplyAttr(pokemon, attr)) continue; @@ -726,7 +748,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; @@ -736,7 +758,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); @@ -988,7 +1010,7 @@ function canApplyAttr(pokemon: Pokemon, attr: AbAttr): boolean { } function queueShowAbility(pokemon: Pokemon): void { - pokemon.scene.unshiftPhase(new ShowAbilityPhase(pokemon.scene, pokemon.isPlayer())); + pokemon.scene.unshiftPhase(new ShowAbilityPhase(pokemon.scene, pokemon.getBattlerIndex())); pokemon.scene.clearPhaseQueueSplice(); } @@ -1210,7 +1232,8 @@ export function initAbilities() { new Ability(Abilities.HUSTLE, "Hustle (N)", "Boosts the ATTACK stat, but lowers accuracy.", 3), new Ability(Abilities.HYPER_CUTTER, "Hyper Cutter", "Prevents other POKéMON from lowering ATTACK stat.", 3) .attr(ProtectStatAbAttr, BattleStat.ATK), - new Ability(Abilities.ILLUMINATE, "Illuminate (N)", "Raises the likelihood of meeting wild POKéMON.", 3), + new Ability(Abilities.ILLUMINATE, "Illuminate", "Raises the likelihood of an encounter being a double battle.", 3) + .attr(DoubleBattleChanceAbAttr), new Ability(Abilities.IMMUNITY, "Immunity", "Prevents the POKéMON from getting poisoned.", 3) .attr(StatusEffectImmunityAbAttr, StatusEffect.POISON), new Ability(Abilities.INNER_FOCUS, "Inner Focus", "The POKéMON is protected from flinching.", 3) @@ -1232,8 +1255,8 @@ export function initAbilities() { new Ability(Abilities.MAGMA_ARMOR, "Magma Armor", "Prevents the POKéMON from becoming frozen.", 3) .attr(StatusEffectImmunityAbAttr, StatusEffect.FREEZE), new Ability(Abilities.MAGNET_PULL, "Magnet Pull", "Prevents STEEL-type POKéMON from escaping.", 3) - .attr(ArenaTrapAbAttr) - .condition((pokemon: Pokemon) => pokemon.getOpponent()?.isOfType(Type.STEEL)), + /*.attr(ArenaTrapAbAttr) + .condition((pokemon: Pokemon) => pokemon.getOpponent()?.isOfType(Type.STEEL))*/, // TODO: Rework new Ability(Abilities.MARVEL_SCALE, "Marvel Scale (N)", "Ups DEFENSE if there is a status problem.", 3), new Ability(Abilities.MINUS, "Minus (N)", "Ups SP. ATK if another POKéMON has PLUS or MINUS.", 3), new Ability(Abilities.NATURAL_CURE, "Natural Cure (N)", "All status problems heal when it switches out.", 3), diff --git a/src/data/arena-tag.ts b/src/data/arena-tag.ts index de0fa347a..bf6383f75 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"; @@ -143,8 +143,7 @@ class SpikesTag extends ArenaTrapTag { super.onAdd(arena); const source = arena.scene.getPokemonById(this.sourceId); - const target = source.getOpponent(); - arena.scene.queueMessage(`${this.getMoveName()} were scattered\nall around ${target.name}'s feet!`); + arena.scene.queueMessage(`${this.getMoveName()} were scattered\nall around ${source.getOpponentDescriptor()}'s feet!`); } activateTrap(pokemon: Pokemon): boolean { @@ -152,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(), MoveResult.OTHER)); + pokemon.scene.unshiftPhase(new DamagePhase(pokemon.scene, pokemon.getBattlerIndex(), HitResult.OTHER)); pokemon.damage(Math.ceil(pokemon.getMaxHp() * damageHpRatio)); return true; } @@ -170,15 +169,14 @@ class ToxicSpikesTag extends ArenaTrapTag { super.onAdd(arena); const source = arena.scene.getPokemonById(this.sourceId); - const target = source.getOpponent(); - arena.scene.queueMessage(`${this.getMoveName()} were scattered\nall around ${target.name}'s feet!`); + arena.scene.queueMessage(`${this.getMoveName()} were scattered\nall around ${source.getOpponentDescriptor()}'s feet!`); } activateTrap(pokemon: Pokemon): boolean { if (!pokemon.status && (!pokemon.isOfType(Type.FLYING) || pokemon.getTag(BattlerTagType.IGNORE_FLYING) || pokemon.scene.arena.getTag(ArenaTagType.GRAVITY))) { const toxic = this.layers > 1; - pokemon.scene.unshiftPhase(new ObtainStatusEffectPhase(pokemon.scene, pokemon.isPlayer(), + pokemon.scene.unshiftPhase(new ObtainStatusEffectPhase(pokemon.scene, pokemon.getBattlerIndex(), !toxic ? StatusEffect.POISON : StatusEffect.TOXIC, null, `the ${this.getMoveName()}`)); return true; } @@ -196,8 +194,7 @@ class StealthRockTag extends ArenaTrapTag { super.onAdd(arena); const source = arena.scene.getPokemonById(this.sourceId); - const target = source.getOpponent(); - arena.scene.queueMessage(`Pointed stones float in the air\naround ${target.name}!`); + arena.scene.queueMessage(`Pointed stones float in the air\naround ${source.getOpponentDescriptor()}!`); } activateTrap(pokemon: Pokemon): boolean { @@ -228,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(), MoveResult.OTHER)); + pokemon.scene.unshiftPhase(new DamagePhase(pokemon.scene, pokemon.getBattlerIndex(), HitResult.OTHER)); pokemon.damage(Math.ceil(pokemon.getMaxHp() * damageHpRatio)); } @@ -242,8 +239,8 @@ export class TrickRoomTag extends ArenaTag { } apply(args: any[]): boolean { - const speedDelayed = args[0] as Utils.BooleanHolder; - speedDelayed.value = !speedDelayed.value; + const speedReversed = args[0] as Utils.BooleanHolder; + speedReversed.value = !speedReversed.value; return true; } diff --git a/src/data/battle-anims.ts b/src/data/battle-anims.ts index 6275fec7c..516db0492 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 { BattlerIndex } 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) { - super(user, getMoveTarget(user, move)); + constructor(move: Moves, user: Pokemon, target: BattlerIndex) { + super(user, user.scene.getField()[target]); this.move = move; } @@ -852,7 +853,7 @@ export class MoveChargeAnim extends MoveAnim { private chargeAnim: ChargeAnim; constructor(chargeAnim: ChargeAnim, move: Moves, user: Pokemon) { - super(move, user); + super(move, user, 0); this.chargeAnim = chargeAnim; } diff --git a/src/data/battler-tag.ts b/src/data/battler-tag.ts index 595125d97..27572f710 100644 --- a/src/data/battler-tag.ts +++ b/src/data/battler-tag.ts @@ -103,7 +103,7 @@ export class RechargingTag extends BattlerTag { onAdd(pokemon: Pokemon): void { super.onAdd(pokemon); - pokemon.getMoveQueue().push({ move: Moves.NONE }) + pokemon.getMoveQueue().push({ move: Moves.NONE, targets: [] }) } lapse(pokemon: Pokemon, lapseType: BattlerTagLapseType): boolean { @@ -177,7 +177,7 @@ export class ConfusedTag extends BattlerTag { onAdd(pokemon: Pokemon): void { super.onAdd(pokemon); - pokemon.scene.unshiftPhase(new CommonAnimPhase(pokemon.scene, pokemon.isPlayer(), CommonAnim.CONFUSION)); + pokemon.scene.unshiftPhase(new CommonAnimPhase(pokemon.scene, pokemon.getBattlerIndex(), undefined, CommonAnim.CONFUSION)); pokemon.scene.queueMessage(getPokemonMessage(pokemon, ' became\nconfused!')); } @@ -198,14 +198,14 @@ export class ConfusedTag extends BattlerTag { if (ret) { pokemon.scene.queueMessage(getPokemonMessage(pokemon, ' is\nconfused!')); - pokemon.scene.unshiftPhase(new CommonAnimPhase(pokemon.scene, pokemon.isPlayer(), CommonAnim.CONFUSION)); + pokemon.scene.unshiftPhase(new CommonAnimPhase(pokemon.scene, pokemon.getBattlerIndex(), undefined, CommonAnim.CONFUSION)); if (Utils.randInt(2)) { const atk = pokemon.getBattleStat(Stat.ATK); const def = pokemon.getBattleStat(Stat.DEF); const damage = Math.ceil(((((2 * pokemon.level / 5 + 2) * 40 * atk / def) / 50) + 2) * ((Utils.randInt(15) + 85) / 100)); pokemon.scene.queueMessage('It hurt itself in its\nconfusion!'); - pokemon.scene.unshiftPhase(new DamagePhase(pokemon.scene, pokemon.isPlayer())); + pokemon.scene.unshiftPhase(new DamagePhase(pokemon.scene, pokemon.getBattlerIndex())); pokemon.damage(damage); (pokemon.scene.getCurrentPhase() as MovePhase).cancel(); } @@ -245,7 +245,7 @@ export class InfatuatedTag extends BattlerTag { if (ret) { pokemon.scene.queueMessage(getPokemonMessage(pokemon, ` is in love\nwith ${pokemon.scene.getPokemonById(this.sourceId).name}!`)); - pokemon.scene.unshiftPhase(new CommonAnimPhase(pokemon.scene, pokemon.isPlayer(), CommonAnim.ATTRACT)); + pokemon.scene.unshiftPhase(new CommonAnimPhase(pokemon.scene, pokemon.getBattlerIndex(), undefined, CommonAnim.ATTRACT)); if (Utils.randInt(2)) { pokemon.scene.queueMessage(getPokemonMessage(pokemon, ' is\nimmobilized by love!')); @@ -282,12 +282,13 @@ export class SeedTag extends BattlerTag { const ret = lapseType !== BattlerTagLapseType.CUSTOM || super.lapse(pokemon, lapseType); if (ret) { - pokemon.scene.unshiftPhase(new CommonAnimPhase(pokemon.scene, !pokemon.isPlayer(), CommonAnim.LEECH_SEED)); + const source = pokemon.scene.getPokemonById(this.sourceId); + pokemon.scene.unshiftPhase(new CommonAnimPhase(pokemon.scene, source.getBattlerIndex(), pokemon.getBattlerIndex(), CommonAnim.LEECH_SEED)); const damage = Math.max(Math.floor(pokemon.getMaxHp() / 8), 1); - pokemon.scene.unshiftPhase(new DamagePhase(pokemon.scene, pokemon.isPlayer())); + pokemon.scene.unshiftPhase(new DamagePhase(pokemon.scene, pokemon.getBattlerIndex())); pokemon.damage(damage); - pokemon.scene.unshiftPhase(new PokemonHealPhase(pokemon.scene, !pokemon.isPlayer(), damage, getPokemonMessage(pokemon, '\'s health is\nsapped by LEECH SEED!'), false, true)); + pokemon.scene.unshiftPhase(new PokemonHealPhase(pokemon.scene, source.getBattlerIndex(), damage, getPokemonMessage(pokemon, '\'s health is\nsapped by LEECH SEED!'), false, true)); } return ret; @@ -320,10 +321,10 @@ export class NightmareTag extends BattlerTag { if (ret) { pokemon.scene.queueMessage(getPokemonMessage(pokemon, ' is locked\nin a NIGHTMARE!')); - pokemon.scene.unshiftPhase(new CommonAnimPhase(pokemon.scene, pokemon.isPlayer(), CommonAnim.CURSE)); // TODO: Update animation type + pokemon.scene.unshiftPhase(new CommonAnimPhase(pokemon.scene, pokemon.getBattlerIndex(), undefined, CommonAnim.CURSE)); // TODO: Update animation type const damage = Math.ceil(pokemon.getMaxHp() / 4); - pokemon.scene.unshiftPhase(new DamagePhase(pokemon.scene, pokemon.isPlayer())); + pokemon.scene.unshiftPhase(new DamagePhase(pokemon.scene, pokemon.getBattlerIndex())); pokemon.damage(damage); } @@ -344,7 +345,7 @@ export class IngrainTag extends TrappedTag { const ret = lapseType !== BattlerTagLapseType.CUSTOM || super.lapse(pokemon, lapseType); if (ret) - pokemon.scene.unshiftPhase(new PokemonHealPhase(pokemon.scene, pokemon.isPlayer(), Math.floor(pokemon.getMaxHp() / 16), + pokemon.scene.unshiftPhase(new PokemonHealPhase(pokemon.scene, pokemon.getBattlerIndex(), Math.floor(pokemon.getMaxHp() / 16), getPokemonMessage(pokemon, ` absorbed\nnutrients with its roots!`), true)); return ret; @@ -374,7 +375,8 @@ export class AquaRingTag extends BattlerTag { const ret = lapseType !== BattlerTagLapseType.CUSTOM || super.lapse(pokemon, lapseType); if (ret) - pokemon.scene.unshiftPhase(new PokemonHealPhase(pokemon.scene, pokemon.isPlayer(), Math.floor(pokemon.getMaxHp() / 16), `${this.getMoveName()} restored\n${pokemon.name}\'s HP!`, true)); + pokemon.scene.unshiftPhase(new PokemonHealPhase(pokemon.scene, pokemon.getBattlerIndex(), + Math.floor(pokemon.getMaxHp() / 16), `${this.getMoveName()} restored\n${pokemon.name}\'s HP!`, true)); return ret; } @@ -393,7 +395,7 @@ export class DrowsyTag extends BattlerTag { lapse(pokemon: Pokemon, lapseType: BattlerTagLapseType): boolean { if (!super.lapse(pokemon, lapseType)) { - pokemon.scene.unshiftPhase(new ObtainStatusEffectPhase(pokemon.scene, pokemon.isPlayer(), StatusEffect.SLEEP)); + pokemon.scene.unshiftPhase(new ObtainStatusEffectPhase(pokemon.scene, pokemon.getBattlerIndex(), StatusEffect.SLEEP)); return false; } @@ -423,10 +425,10 @@ export abstract class DamagingTrapTag extends TrappedTag { if (ret) { pokemon.scene.queueMessage(getPokemonMessage(pokemon, ` is hurt\nby ${this.getMoveName()}!`)); - pokemon.scene.unshiftPhase(new CommonAnimPhase(pokemon.scene, pokemon.isPlayer(), this.commonAnim)); + pokemon.scene.unshiftPhase(new CommonAnimPhase(pokemon.scene, pokemon.getBattlerIndex(), undefined, this.commonAnim)); const damage = Math.ceil(pokemon.getMaxHp() / 16); - pokemon.scene.unshiftPhase(new DamagePhase(pokemon.scene, pokemon.isPlayer())); + pokemon.scene.unshiftPhase(new DamagePhase(pokemon.scene, pokemon.getBattlerIndex())); pokemon.damage(damage); } @@ -541,7 +543,7 @@ export class TruantTag extends BattlerTag { if (lastMove && lastMove.move !== Moves.NONE) { (pokemon.scene.getCurrentPhase() as MovePhase).cancel(); - pokemon.scene.unshiftPhase(new ShowAbilityPhase(pokemon.scene, pokemon.isPlayer())); + pokemon.scene.unshiftPhase(new ShowAbilityPhase(pokemon.scene, pokemon.getBattlerIndex())); pokemon.scene.queueMessage(getPokemonMessage(pokemon, ' is\nloafing around!')); } diff --git a/src/data/berry.ts b/src/data/berry.ts index b5f2fc30a..4cb9fcdf9 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,12 +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) => { - const opponent = pokemon.getOpponent(); - const opponentLastMove = opponent ? opponent.getLastXMoves(1).find(() => true) : null; // TODO: Update so this works even if opponent has fainted - - return opponentLastMove && opponentLastMove.turn === pokemon.scene.currentBattle?.turn - 1 && opponentLastMove.result === MoveResult.SUPER_EFFECTIVE; - }; + return (pokemon: Pokemon) => !!pokemon.turnData.attacksReceived.filter(a => a.result === HitResult.SUPER_EFFECTIVE).length; case BerryType.LIECHI: case BerryType.GANLON: case BerryType.SALAC: @@ -83,7 +78,8 @@ export function getBerryEffectFunc(berryType: BerryType): BerryEffectFunc { case BerryType.SITRUS: case BerryType.ENIGMA: return (pokemon: Pokemon) => { - pokemon.scene.unshiftPhase(new PokemonHealPhase(pokemon.scene, pokemon.isPlayer(), Math.floor(pokemon.getMaxHp() / 4), getPokemonMessage(pokemon, `'s ${getBerryName(berryType)}\nrestored its HP!`), true)); + pokemon.scene.unshiftPhase(new PokemonHealPhase(pokemon.scene, pokemon.getBattlerIndex(), + Math.floor(pokemon.getMaxHp() / 4), getPokemonMessage(pokemon, `'s ${getBerryName(berryType)}\nrestored its HP!`), true)); }; case BerryType.LUM: return (pokemon: Pokemon) => { @@ -101,13 +97,13 @@ export function getBerryEffectFunc(berryType: BerryType): BerryEffectFunc { case BerryType.APICOT: return (pokemon: Pokemon) => { const battleStat = (berryType - BerryType.LIECHI) as BattleStat; - pokemon.scene.unshiftPhase(new StatChangePhase(pokemon.scene, pokemon.isPlayer(), true, [ battleStat ], 1)); + pokemon.scene.unshiftPhase(new StatChangePhase(pokemon.scene, pokemon.getBattlerIndex(), true, [ battleStat ], 1)); }; case BerryType.LANSAT: return (pokemon: Pokemon) => { pokemon.addTag(BattlerTagType.CRIT_BOOST); }; case BerryType.STARF: - return (pokemon: Pokemon) => pokemon.scene.unshiftPhase(new StatChangePhase(pokemon.scene, pokemon.isPlayer(), true, [ BattleStat.RAND ], 2)); + return (pokemon: Pokemon) => pokemon.scene.unshiftPhase(new StatChangePhase(pokemon.scene, pokemon.getBattlerIndex(), true, [ BattleStat.RAND ], 2)); } } \ No newline at end of file diff --git a/src/data/move.ts b/src/data/move.ts index 67fe9335c..351396ab4 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,8 @@ import { WeatherType } from "./weather"; import { ArenaTagType, ArenaTrapTag } from "./arena-tag"; import { BlockRecoilDamageAttr, applyAbAttrs } from "./ability"; import { PokemonHeldItemModifier } from "../modifier/modifier"; +import { BattlerIndex } from "../battle"; +import { Stat } from "./pokemon-stat"; export enum MoveCategory { PHYSICAL, @@ -24,14 +26,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 +48,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; @@ -159,12 +161,67 @@ export default class Move { return true; } + + getUserBenefitScore(user: Pokemon, target: Pokemon, move: Move): integer { + let score = 0; + + for (let attr of this.attrs) + score += attr.getUserBenefitScore(user, target, move); + + return score; + } + + getTargetBenefitScore(user: Pokemon, target: Pokemon, move: Move): integer { + let score = 0; + + for (let attr of this.attrs) + score += attr.getTargetBenefitScore(user, target, move); + + return score; + } } export class AttackMove extends Move { constructor(id: Moves, name: string, type: Type, category: MoveCategory, power: integer, accuracy: integer, pp: integer, tm: integer, effect: string, chance: integer, priority: integer, generation: integer) { super(id, name, type, category, MoveTarget.NEAR_OTHER, power, accuracy, pp, tm, effect, chance, priority, generation); } + + getTargetBenefitScore(user: Pokemon, target: Pokemon, move: Move): integer { + let ret = super.getTargetBenefitScore(user, target, move); + + let attackScore = 0; + + const effectiveness = target.getAttackMoveEffectiveness(this.type); + attackScore = Math.pow(effectiveness - 1, 2) * effectiveness < 1 ? -2 : 2; + if (attackScore) { + if (this.category === MoveCategory.PHYSICAL) { + if (user.getBattleStat(Stat.ATK) > user.getBattleStat(Stat.SPATK)) { + const statRatio = user.getBattleStat(Stat.SPATK) / user.getBattleStat(Stat.ATK); + if (statRatio <= 0.75) + attackScore *= 2; + else if (statRatio <= 0.875) + attackScore *= 1.5; + } + } else { + if (user.getBattleStat(Stat.SPATK) > user.getBattleStat(Stat.ATK)) { + const statRatio = user.getBattleStat(Stat.ATK) / user.getBattleStat(Stat.SPATK); + if (statRatio <= 0.75) + attackScore *= 2; + else if (statRatio <= 0.875) + attackScore *= 1.5; + } + } + + const power = new Utils.NumberHolder(this.power); + applyMoveAttrs(VariablePowerAttr, user, target, move, power); + + attackScore += Math.floor(power.value / 5); + } + + ret -= attackScore; + + return ret; + } } export class StatusMove extends Move { @@ -755,6 +812,14 @@ export abstract class MoveAttr { getCondition(): MoveCondition { return null; } + + getUserBenefitScore(user: Pokemon, target: Pokemon, move: Move): integer { + return 0; + } + + getTargetBenefitScore(user: Pokemon, target: Pokemon, move: Move): integer { + return 0; + } } export class MoveEffectAttr extends MoveAttr { @@ -792,6 +857,10 @@ export class HighCritAttr extends MoveAttr { return true; } + + getUserBenefitScore(user: Pokemon, target: Pokemon, move: Move): integer { + return 3; + } } export class CritOnlyAttr extends MoveAttr { @@ -800,6 +869,10 @@ export class CritOnlyAttr extends MoveAttr { return true; } + + getUserBenefitScore(user: Pokemon, target: Pokemon, move: Move): integer { + return 5; + } } export class FixedDamageAttr extends MoveAttr { @@ -889,12 +962,16 @@ 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(), MoveResult.OTHER)); + user.scene.unshiftPhase(new DamagePhase(user.scene, user.getBattlerIndex(), HitResult.OTHER)); user.scene.queueMessage(getPokemonMessage(user, ' is hit\nwith recoil!')); user.damage(recoilDamage); return true; } + + getUserBenefitScore(user: Pokemon, target: Pokemon, move: Move): integer { + return Math.floor((move.power / 5) / -4); + } } export class SacrificialAttr extends MoveEffectAttr { @@ -906,11 +983,15 @@ export class SacrificialAttr extends MoveEffectAttr { if (!super.apply(user, target, move, args)) return false; - user.scene.unshiftPhase(new DamagePhase(user.scene, user.isPlayer(), MoveResult.OTHER)); + user.scene.unshiftPhase(new DamagePhase(user.scene, user.getBattlerIndex(), HitResult.OTHER)); user.damage(user.getMaxHp()); return true; } + + getUserBenefitScore(user: Pokemon, target: Pokemon, move: Move): integer { + return Math.ceil((1 - user.getHpRatio()) * 10) - 10; + } } export enum MultiHitType { @@ -936,7 +1017,12 @@ export class HealAttr extends MoveEffectAttr { } addHealPhase(user: Pokemon, healRatio: number) { - user.scene.unshiftPhase(new PokemonHealPhase(user.scene, user.isPlayer(), Math.max(Math.floor(user.getMaxHp() * healRatio), 1), getPokemonMessage(user, ' regained\nhealth!'), true, !this.showAnim)); + user.scene.unshiftPhase(new PokemonHealPhase(user.scene, user.getBattlerIndex(), + Math.max(Math.floor(user.getMaxHp() * healRatio), 1), getPokemonMessage(user, ' regained\nhealth!'), true, !this.showAnim)); + } + + getTargetBenefitScore(user: Pokemon, target: Pokemon, move: Move): integer { + return (1 - (this.selfTarget ? user : target).getHpRatio()) * 20; } } @@ -977,9 +1063,14 @@ export class HitHealAttr extends MoveHitEffectAttr { } apply(user: Pokemon, target: Pokemon, move: Move, args: any[]): boolean { - user.scene.unshiftPhase(new PokemonHealPhase(user.scene, user.isPlayer(), Math.max(Math.floor(user.turnData.damageDealt * this.healRatio), 1), getPokemonMessage(target, ` had its\nenergy drained!`), false, true)); + user.scene.unshiftPhase(new PokemonHealPhase(user.scene, user.getBattlerIndex(), + Math.max(Math.floor(user.turnData.damageDealt * this.healRatio), 1), getPokemonMessage(target, ` had its\nenergy drained!`), false, true)); return true; } + + getUserBenefitScore(user: Pokemon, target: Pokemon, move: Move): integer { + return Math.floor(Math.max((1 - user.getHpRatio()) - 0.33, 0) * ((move.power / 5) / 4)); + } } export class MultiHitAttr extends MoveAttr { @@ -1016,6 +1107,10 @@ export class MultiHitAttr extends MoveAttr { (args[0] as Utils.IntegerHolder).value = hitTimes; return true; } + + getUserBenefitScore(user: Pokemon, target: Pokemon, move: Move): number { + return 5; + } } export class StatusEffectAttr extends MoveHitEffectAttr { @@ -1034,12 +1129,16 @@ export class StatusEffectAttr extends MoveHitEffectAttr { if (statusCheck) { const pokemon = this.selfTarget ? user : target; if (!pokemon.status || (pokemon.status.effect === this.effect && move.chance < 0)) { - user.scene.unshiftPhase(new ObtainStatusEffectPhase(user.scene, pokemon.isPlayer(), this.effect, this.cureTurn)); + user.scene.unshiftPhase(new ObtainStatusEffectPhase(user.scene, pokemon.getBattlerIndex(), this.effect, this.cureTurn)); return true; } } return false; } + + getTargetBenefitScore(user: Pokemon, target: Pokemon, move: Move): number { + return !(this.selfTarget ? user : target).status ? Math.floor(move.chance * -0.1) : 0; + } } export class StealHeldItemAttr extends MoveHitEffectAttr { @@ -1048,8 +1147,7 @@ export class StealHeldItemAttr extends MoveHitEffectAttr { } apply(user: Pokemon, target: Pokemon, move: Move, args: any[]): boolean { - const heldItems = user.scene.findModifiers(m => m instanceof PokemonHeldItemModifier - && (m as PokemonHeldItemModifier).pokemonId === target.id, target.isPlayer()) as PokemonHeldItemModifier[]; + const heldItems = this.getTargetHeldItems(target); if (heldItems.length) { const stolenItem = heldItems[Utils.randInt(heldItems.length)]; user.scene.tryTransferHeldItemModifier(stolenItem, user, false, false); @@ -1060,6 +1158,21 @@ export class StealHeldItemAttr extends MoveHitEffectAttr { return false; } + + getTargetHeldItems(target: Pokemon): PokemonHeldItemModifier[] { + return target.scene.findModifiers(m => m instanceof PokemonHeldItemModifier + && (m as PokemonHeldItemModifier).pokemonId === target.id, target.isPlayer()) as PokemonHeldItemModifier[]; + } + + getUserBenefitScore(user: Pokemon, target: Pokemon, move: Move): number { + const heldItems = this.getTargetHeldItems(target); + return heldItems.length ? 5 : 0; + } + + getTargetBenefitScore(user: Pokemon, target: Pokemon, move: Move): number { + const heldItems = this.getTargetHeldItems(target); + return heldItems.length ? -5 : 0; + } } export class HealStatusEffectAttr extends MoveEffectAttr { @@ -1086,6 +1199,10 @@ export class HealStatusEffectAttr extends MoveEffectAttr { return false; } + + getUserBenefitScore(user: Pokemon, target: Pokemon, move: Move): integer { + return user.status ? 10 : 0; + } } export class BypassSleepAttr extends MoveAttr { @@ -1170,8 +1287,8 @@ 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.getMoveQueue().push({ move: move.id, ignorePP: true }); + user.pushMoveHistory({ move: move.id, targets: [ target.getBattlerIndex() ], result: MoveResult.OTHER }); + user.getMoveQueue().push({ move: move.id, targets: [ target.getBattlerIndex() ], ignorePP: true }); resolve(true); }); } else @@ -1214,7 +1331,7 @@ export class StatChangeAttr extends MoveEffectAttr { if (move.chance < 0 || move.chance === 100 || Utils.randInt(100) < move.chance) { const levels = this.getLevels(user); - user.scene.unshiftPhase(new StatChangePhase(user.scene, user.isPlayer() === this.selfTarget, this.selfTarget, this.stats, levels)); + user.scene.unshiftPhase(new StatChangePhase(user.scene, (this.selfTarget ? user : target).getBattlerIndex(), this.selfTarget, this.stats, levels)); return true; } @@ -1224,6 +1341,12 @@ export class StatChangeAttr extends MoveEffectAttr { getLevels(_user: Pokemon): integer { return this.levels; } + + getTargetBenefitScore(user: Pokemon, target: Pokemon, move: Move): integer { + // TODO: Add awareness of level limits + const levels = this.getLevels(user); + return (levels * 4) + (levels > 0 ? -2 : 2); + } } export class GrowthStatChangeAttr extends StatChangeAttr { @@ -1273,7 +1396,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) @@ -1458,16 +1581,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; } } @@ -1530,7 +1653,7 @@ export class FrenzyAttr extends MoveEffectAttr { } canApply(user: Pokemon, target: Pokemon, move: Move, args: any[]) { - return !!(this.selfTarget ? user.hp : target.hp); + return !(this.selfTarget ? user : target).isFainted(); } apply(user: Pokemon, target: Pokemon, move: Move, args: any[]): boolean { @@ -1540,7 +1663,7 @@ export class FrenzyAttr extends MoveEffectAttr { if (!user.getMoveQueue().length) { if (!user.getTag(BattlerTagType.FRENZY)) { const turnCount = Utils.randInt(2) + 1; - new Array(turnCount).fill(null).map(() => user.getMoveQueue().push({ move: move.id, ignorePP: true })); + new Array(turnCount).fill(null).map(() => user.getMoveQueue().push({ move: move.id, targets: [ target.getBattlerIndex() ], ignorePP: true })); user.addTag(BattlerTagType.FRENZY, 1, move.id, user.id); } else { applyMoveAttrs(AddBattlerTagAttr, user, target, move, args); @@ -1553,7 +1676,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); @@ -1596,6 +1719,48 @@ export class AddBattlerTagAttr extends MoveEffectAttr { ? (user: Pokemon, target: Pokemon, move: Move) => !(this.selfTarget ? user : target).getTag(this.tagType) : null; } + + getTargetBenefitScore(user: Pokemon, target: Pokemon, move: Move): integer { + switch (this.tagType) { + case BattlerTagType.FLINCHED: + return -5; + case BattlerTagType.CONFUSED: + return -5; + case BattlerTagType.INFATUATED: + return -5; + case BattlerTagType.SEEDED: + return -3; + case BattlerTagType.NIGHTMARE: + return -5; + case BattlerTagType.FRENZY: + return -2; + case BattlerTagType.INGRAIN: + return 3; + case BattlerTagType.AQUA_RING: + return 3; + case BattlerTagType.DROWSY: + return -5; + case BattlerTagType.TRAPPED: + case BattlerTagType.BIND: + case BattlerTagType.WRAP: + case BattlerTagType.FIRE_SPIN: + case BattlerTagType.WHIRLPOOL: + case BattlerTagType.CLAMP: + case BattlerTagType.SAND_TOMB: + case BattlerTagType.MAGMA_STORM: + return -3; + case BattlerTagType.PROTECTED: + return 10; + case BattlerTagType.FLYING: + return 5; + case BattlerTagType.CRIT_BOOST: + return 5; + case BattlerTagType.NO_CRIT: + return -5; + case BattlerTagType.IGNORE_ACCURACY: + return 3; + } + } } export class LapseBattlerTagAttr extends MoveEffectAttr { @@ -1646,7 +1811,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)); @@ -1680,6 +1845,10 @@ export class HitsTagAttr extends MoveAttr { this.tagType = tagType; this.doubleDamage = !!doubleDamage; } + + getTargetBenefitScore(user: Pokemon, target: Pokemon, move: Move): integer { + return target.getTag(this.tagType) ? this.doubleDamage ? 10 : 5 : 0; + } } export class AddArenaTagAttr extends MoveEffectAttr { @@ -1768,10 +1937,16 @@ export class RandomMovesetMoveAttr extends OverrideMoveEffectAttr { if (moves.length) { 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, moveset[moveIndex], true) - : new EnemyMovePhase(user.scene, user as EnemyPokemon, moveset[moveIndex], true)); + const moveTargets = getMoveTargets(user, move.moveId); + if (!moveTargets.targets.length) + return false; + const targets = moveTargets.multiple || moveTargets.targets.length === 1 + ? moveTargets.targets + : moveTargets.targets.indexOf(target.getBattlerIndex()) > -1 + ? [ target.getBattlerIndex() ] + : [ moveTargets.targets[Utils.randInt(moveTargets.targets.length)] ]; + user.getMoveQueue().push({ move: move.moveId, targets: targets, ignorePP: this.enemyMoveset }); + user.scene.unshiftPhase(new MovePhase(user.scene, user, targets, moveset[moveIndex], true)); return true; } @@ -1784,10 +1959,19 @@ export class RandomMoveAttr extends OverrideMoveEffectAttr { return new Promise(resolve => { 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, new PokemonMove(moveId, 0, 0, true), true) - : new EnemyMovePhase(user.scene, user as EnemyPokemon, 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.length === 1 + ? moveTargets.targets + : moveTargets.targets.indexOf(target.getBattlerIndex()) > -1 + ? [ target.getBattlerIndex() ] + : [ moveTargets.targets[Utils.randInt(moveTargets.targets.length)] ]; + user.getMoveQueue().push({ move: moveId, targets: targets, ignorePP: true }); + 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)); @@ -1822,10 +2006,18 @@ 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, new PokemonMove(copiedMove.move, 0, 0, true), true) - : new EnemyMovePhase(user.scene, user as EnemyPokemon, 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.length === 1 + ? moveTargets.targets + : moveTargets.targets.indexOf(target.getBattlerIndex()) > -1 + ? [ target.getBattlerIndex() ] + : [ moveTargets.targets[Utils.randInt(moveTargets.targets.length)] ]; + user.getMoveQueue().push({ move: copiedMove.move, targets: targets, ignorePP: true }); + + user.scene.unshiftPhase(new MovePhase(user.scene, user as PlayerPokemon, targets, new PokemonMove(copiedMove.move, 0, 0, true), true)); return true; } @@ -1932,20 +2124,60 @@ export function applyFilteredMoveAttrs(attrFilter: MoveAttrFilter, user: Pokemon return applyMoveAttrsInternal(attrFilter, user, target, move, args); } -export function getMoveTarget(user: Pokemon, move: Moves): Pokemon { - const moveTarget = allMoves[move].moveTarget; +export type MoveTargetSet = { + targets: BattlerIndex[]; + multiple: boolean; +} - const other = user.getOpponent(); +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 multiple = false; switch (moveTarget) { case MoveTarget.USER: + set = [ user.getBattlerIndex() ]; + 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()); + 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()); + multiple = moveTarget !== MoveTarget.NEAR_ENEMY; + break; + case MoveTarget.RANDOM_NEAR_ENEMY: + set = [ opponents[Utils.randInt(opponents.length)].getBattlerIndex() ]; + break; + case MoveTarget.ATTACKER: + set = [ user.scene.getPokemonById(user.turnData.attacksReceived[0].sourceId).getBattlerIndex() ]; + break; + case MoveTarget.NEAR_ALLY: + case MoveTarget.ALLY: + set = [ user.getAlly()?.getBattlerIndex() ]; + 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?.getBattlerIndex()); + 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()); + multiple = true; + break; } + + return { targets: set.filter(t => t !== undefined), multiple }; } export const allMoves = [ @@ -2002,7 +2234,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), @@ -2070,13 +2302,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), @@ -2253,7 +2487,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), @@ -2337,7 +2571,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/data/weather.ts b/src/data/weather.ts index ca64b62e5..b6a4a0139 100644 --- a/src/data/weather.ts +++ b/src/data/weather.ts @@ -100,17 +100,10 @@ export class Weather { } isEffectSuppressed(scene: BattleScene): boolean { - const playerPokemon = scene.getPlayerPokemon(); - const enemyPokemon = scene.getEnemyPokemon(); + const field = scene.getField().filter(p => p); - if (playerPokemon) { - const suppressWeatherEffectAbAttr = playerPokemon.getAbility().getAttrs(SuppressWeatherEffectAbAttr).find(() => true) as SuppressWeatherEffectAbAttr; - if (suppressWeatherEffectAbAttr && (!this.isImmutable() || suppressWeatherEffectAbAttr.affectsImmutable)) - return true; - } - - if (enemyPokemon) { - const suppressWeatherEffectAbAttr = enemyPokemon.getAbility().getAttrs(SuppressWeatherEffectAbAttr).find(() => true) as SuppressWeatherEffectAbAttr; + for (let pokemon of field) { + const suppressWeatherEffectAbAttr = pokemon.getAbility().getAttrs(SuppressWeatherEffectAbAttr).find(() => true) as SuppressWeatherEffectAbAttr; if (suppressWeatherEffectAbAttr && (!this.isImmutable() || suppressWeatherEffectAbAttr.affectsImmutable)) return true; } diff --git a/src/evolution-phase.ts b/src/evolution-phase.ts index a72223e51..3868640e9 100644 --- a/src/evolution-phase.ts +++ b/src/evolution-phase.ts @@ -44,7 +44,7 @@ export class EvolutionPhase extends BattlePhase { this.evolutionContainer = (this.scene.ui.getHandler() as EvolutionSceneHandler).evolutionContainer; - this.evolutionBaseBg = this.scene.add.image(0, 0, 'plains_bg'); + this.evolutionBaseBg = this.scene.add.image(0, 0, 'default_bg'); this.evolutionBaseBg.setOrigin(0, 0); this.evolutionContainer.add(this.evolutionBaseBg); @@ -97,7 +97,8 @@ export class EvolutionPhase extends BattlePhase { const levelMoves = pokemon.getLevelMoves(this.lastLevel + 1); for (let lm of levelMoves) - this.scene.unshiftPhase(new LearnMovePhase(this.scene, this.partyMemberIndex, lm)); + this.scene.unshiftPhase(new LearnMovePhase(this.scene, this.partyMemberIndex, lm)); + this.scene.unshiftPhase(new EndEvolutionPhase(this.scene)); this.scene.time.delayedCall(1000, () => { const evolutionBgm = this.scene.sound.add('evolution'); @@ -443,4 +444,12 @@ export class EvolutionPhase extends BattlePhase { updateParticle(); } +} + +export class EndEvolutionPhase extends BattlePhase { + start() { + super.start(); + + this.scene.ui.setModeForceTransition(Mode.MESSAGE).then(() => this.end()); + } } \ No newline at end of file diff --git a/src/modifier/modifier-type.ts b/src/modifier/modifier-type.ts index 71afc2470..bead50fc7 100644 --- a/src/modifier/modifier-type.ts +++ b/src/modifier/modifier-type.ts @@ -11,7 +11,6 @@ import * as Utils from '../utils'; import { TempBattleStat, getTempBattleStatBoosterItemName, getTempBattleStatName } from '../data/temp-battle-stat'; import { BerryType, getBerryEffectDescription, getBerryName } from '../data/berry'; import { Unlockables } from '../system/unlockables'; -import { maxExpLevel } from '../battle-scene'; type Modifier = Modifiers.Modifier; @@ -128,7 +127,7 @@ export class PokemonReviveModifierType extends PokemonHpRestoreModifierType { constructor(name: string, restorePercent: integer, iconImage?: string) { super(name, restorePercent, true, (_type, args) => new Modifiers.PokemonHpRestoreModifier(this, (args[0] as PlayerPokemon).id, this.restorePoints, true, true), ((pokemon: PlayerPokemon) => { - if (pokemon.hp) + if (!pokemon.isFainted()) return PartyUiHandler.NoEffectMessage; return null; }), iconImage, 'revive'); @@ -669,15 +668,15 @@ const modifierPool = { return statusEffectPartyMemberCount * 6; }), new WeightedModifierType(modifierTypes.REVIVE, (party: Pokemon[]) => { - const faintedPartyMemberCount = Math.min(party.filter(p => !p.hp).length, 3); + const faintedPartyMemberCount = Math.min(party.filter(p => p.isFainted()).length, 3); return faintedPartyMemberCount * 9; }), new WeightedModifierType(modifierTypes.MAX_REVIVE, (party: Pokemon[]) => { - const faintedPartyMemberCount = Math.min(party.filter(p => !p.hp).length, 3); + const faintedPartyMemberCount = Math.min(party.filter(p => p.isFainted()).length, 3); return faintedPartyMemberCount * 3; }), new WeightedModifierType(modifierTypes.SACRED_ASH, (party: Pokemon[]) => { - return party.filter(p => !p.hp).length >= Math.ceil(party.length / 2) ? 1 : 0; + return party.filter(p => p.isFainted()).length >= Math.ceil(party.length / 2) ? 1 : 0; }), new WeightedModifierType(modifierTypes.HYPER_POTION, (party: Pokemon[]) => { const thresholdPartyMemberCount = Math.min(party.filter(p => p.getInverseHp() >= 100 || p.getHpRatio() <= 0.625).length, 3); diff --git a/src/modifier/modifier.ts b/src/modifier/modifier.ts index 80bc4b4c6..c274b9aec 100644 --- a/src/modifier/modifier.ts +++ b/src/modifier/modifier.ts @@ -480,7 +480,8 @@ export class TurnHealModifier extends PokemonHeldItemModifier { if (pokemon.getHpRatio() < 1) { const scene = pokemon.scene; - scene.unshiftPhase(new PokemonHealPhase(scene, pokemon.isPlayer(), Math.max(Math.floor(pokemon.getMaxHp() / 16) * this.stackCount, 1), getPokemonMessage(pokemon, `'s ${this.type.name}\nrestored its HP a little!`), true)); + scene.unshiftPhase(new PokemonHealPhase(scene, pokemon.getBattlerIndex(), + Math.max(Math.floor(pokemon.getMaxHp() / 16) * this.stackCount, 1), getPokemonMessage(pokemon, `'s ${this.type.name}\nrestored its HP a little!`), true)); } return true; @@ -509,7 +510,8 @@ export class HitHealModifier extends PokemonHeldItemModifier { if (pokemon.turnData.damageDealt && pokemon.getHpRatio() < 1) { const scene = pokemon.scene; - scene.unshiftPhase(new PokemonHealPhase(scene, pokemon.isPlayer(), Math.max(Math.floor(pokemon.turnData.damageDealt / 8) * this.stackCount, 1), getPokemonMessage(pokemon, `'s ${this.type.name}\nrestored its HP a little!`), true)); + scene.unshiftPhase(new PokemonHealPhase(scene, pokemon.getBattlerIndex(), + Math.max(Math.floor(pokemon.turnData.damageDealt / 8) * this.stackCount, 1), getPokemonMessage(pokemon, `'s ${this.type.name}\nrestored its HP a little!`), true)); } return true; @@ -1001,7 +1003,7 @@ export abstract class HeldItemTransferModifier extends PokemonHeldItemModifier { apply(args: any[]): boolean { const pokemon = args[0] as Pokemon; - const targetPokemon = pokemon.getOpponent(); + const targetPokemon = pokemon.getOpponent(args.length > 1 ? args[1] as integer : !pokemon.scene.currentBattle.double ? 0 : Utils.randInt(2)); if (!targetPokemon) return false; diff --git a/src/pokemon.ts b/src/pokemon.ts index a7fd81a93..75109422c 100644 --- a/src/pokemon.ts +++ b/src/pokemon.ts @@ -1,7 +1,7 @@ import Phaser from 'phaser'; import BattleScene from './battle-scene'; import BattleInfo, { PlayerBattleInfo, EnemyBattleInfo } from './ui/battle-info'; -import Move, { StatChangeAttr, HighCritAttr, HitsTagAttr, applyMoveAttrs, FixedDamageAttr, VariablePowerAttr, Moves, allMoves, MoveCategory, TypelessAttr, CritOnlyAttr } from "./data/move"; +import Move, { StatChangeAttr, HighCritAttr, HitsTagAttr, applyMoveAttrs, FixedDamageAttr, VariablePowerAttr, Moves, allMoves, MoveCategory, TypelessAttr, CritOnlyAttr, getMoveTargets, AttackMove, AddBattlerTagAttr } from "./data/move"; import { pokemonLevelMoves } from './data/pokemon-level-moves'; import { default as PokemonSpecies, PokemonSpeciesForm, getPokemonSpecies } from './data/pokemon-species'; import * as Utils from './utils'; @@ -21,10 +21,17 @@ import { BattlerTag, BattlerTagLapseType, BattlerTagType, TypeBoostTag, getBattl import { Species } from './data/species'; import { WeatherType } from './data/weather'; import { TempBattleStat } from './data/temp-battle-stat'; -import { ArenaTagType, GravityTag, WeakenMoveTypeTag } from './data/arena-tag'; +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 { Abilities, Ability, BattleStatMultiplierAbAttr, BattlerTagImmunityAbAttr, BlockCritAbAttr, PreApplyBattlerTagAbAttr, StatusEffectImmunityAbAttr, TypeImmunityAbAttr, VariableMovePowerAbAttr, abilities, applyBattleStatMultiplierAbAttrs, applyPreApplyBattlerTagAbAttrs, applyPreAttackAbAttrs, applyPreDefendAbAttrs, applyPreSetStatusAbAttrs } from './data/ability'; import PokemonData from './system/pokemon-data'; +import { BattlerIndex } from './battle'; + +export enum FieldPosition { + CENTER, + LEFT, + RIGHT +} export default abstract class Pokemon extends Phaser.GameObjects.Container { public id: integer; @@ -50,6 +57,8 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container { public battleSummonData: PokemonBattleSummonData; public turnData: PokemonTurnData; + public fieldPosition: FieldPosition; + public maskEnabled: boolean; public maskSprite: Phaser.GameObjects.Sprite; @@ -139,7 +148,9 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container { this.calculateStats(); - scene.fieldUI.addAt(this.battleInfo, 0); + this.fieldPosition = FieldPosition.CENTER; + + scene.fieldUI.add(this.battleInfo); this.battleInfo.initInfo(this); @@ -177,8 +188,20 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container { } } + isFainted(checkStatus?: boolean): boolean { + return !this.hp && (!checkStatus || this.status?.effect === StatusEffect.FAINT); + } + + isActive(): boolean { + return !this.isFainted() && !!this.scene; + } + abstract isPlayer(): boolean; + abstract getFieldIndex(): integer; + + abstract getBattlerIndex(): BattlerIndex; + loadAssets(): Promise { return new Promise(resolve => { const moveIds = this.getMoveset().map(m => m.getMove().id); @@ -261,11 +284,57 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container { : this.maskSprite; } - playAnim(): void{ + playAnim(): void { this.getSprite().play(this.getBattleSpriteKey()); this.getTintSprite().play(this.getBattleSpriteKey()); } + getFieldPositionOffset(): [ number, number ] { + switch (this.fieldPosition) { + case FieldPosition.CENTER: + return [ 0, 0 ]; + case FieldPosition.LEFT: + return [ -32, -8 ]; + case FieldPosition.RIGHT: + return [ 32, 0 ]; + } + } + + setFieldPosition(fieldPosition: FieldPosition, duration?: integer): Promise { + return new Promise(resolve => { + if (fieldPosition === this.fieldPosition) { + resolve(); + return; + } + + const initialOffset = this.getFieldPositionOffset(); + + this.fieldPosition = fieldPosition; + + this.battleInfo.setMini(fieldPosition !== FieldPosition.CENTER); + this.battleInfo.setOffset(fieldPosition === FieldPosition.RIGHT); + + const newOffset = this.getFieldPositionOffset(); + + let relX = newOffset[0] - initialOffset[0]; + let relY = newOffset[1] - initialOffset[1]; + + if (duration) { + this.scene.tweens.add({ + targets: this, + x: (_target, _key, value: number) => value + relX, + y: (_target, _key, value: number) => value + relY, + duration: duration, + ease: 'Sine.easeOut', + onComplete: () => resolve() + }); + } else { + this.x += relX; + this.y += relY; + } + }); + } + getBattleStat(stat: Stat): integer { if (stat === Stat.HP) return this.stats[Stat.HP]; @@ -360,7 +429,7 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container { } canApplyAbility(): boolean { - return !this.getAbility().conditions.find(condition => !condition(this)); + return this.hp && !this.getAbility().conditions.find(condition => !condition(this)); } getAttackMoveEffectiveness(moveType: Type): TypeDamageMultiplier { @@ -405,7 +474,7 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container { setMove(moveIndex: integer, moveId: Moves): void { const move = moveId ? new PokemonMove(moveId) : null; this.moveset[moveIndex] = move; - if (this.summonData.moveset) + if (this.summonData?.moveset) this.summonData.moveset[moveIndex] = move; } @@ -496,15 +565,30 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container { this.levelExp = this.exp - getLevelTotalExp(this.level, this.getSpeciesForm().growthRate); } - getOpponent(): Pokemon { - const ret = this.isPlayer() ? this.scene.getEnemyPokemon() : this.scene.getPlayerPokemon(); + getOpponent(targetIndex: integer): Pokemon { + const ret = this.getOpponents()[targetIndex]; if (ret.summonData) return ret; return null; } - apply(source: Pokemon, battlerMove: PokemonMove): MoveResult { - let result: MoveResult; + getOpponents(): Pokemon[] { + return ((this.isPlayer() ? this.scene.getEnemyField() : this.scene.getPlayerField()) as Pokemon[]).filter(p => p.isActive()); + } + + getOpponentDescriptor(): string { + const opponents = this.getOpponents(); + if (opponents.length === 1) + return opponents[0].name; + return this.isPlayer() ? 'the opposing team' : 'your team'; + } + + 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; @@ -526,7 +610,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; @@ -567,46 +651,50 @@ 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) { - this.scene.unshiftPhase(new DamagePhase(this.scene, this.isPlayer(), result as DamageResult)); + this.scene.unshiftPhase(new DamagePhase(this.scene, this.getBattlerIndex(), result as DamageResult)); if (isCritical) this.scene.queueMessage('A critical hit!'); + this.scene.setPhaseQueueSplice(); this.damage(damage); source.turnData.damageDealt += damage; this.turnData.attacksReceived.unshift({ move: move.id, result: result as DamageResult, damage: damage, sourceId: source.id }); } 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; } + + if (damage) + this.scene.clearPhaseQueueSplice(); } break; case MoveCategory.STATUS: - result = MoveResult.STATUS; + result = HitResult.STATUS; break; } @@ -614,7 +702,7 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container { } damage(damage: integer, preventEndure?: boolean): void { - if (!this.hp) + if (this.isFainted()) return; if (this.hp > 1 && this.hp - damage <= 0 && !preventEndure) { @@ -625,10 +713,9 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container { } this.hp = Math.max(this.hp - damage, 0); - if (!this.hp) { - this.scene.pushPhase(new FaintPhase(this.scene, this.isPlayer())); + if (this.isFainted()) { + this.scene.unshiftPhase(new FaintPhase(this.scene, this.getBattlerIndex())); this.resetSummonData(); - this.getOpponent()?.resetBattleSummonData(); } } @@ -946,6 +1033,14 @@ export class PlayerPokemon extends Pokemon { return true; } + getFieldIndex(): integer { + return this.scene.getPlayerField().indexOf(this); + } + + getBattlerIndex(): BattlerIndex { + return this.getFieldIndex(); + } + generateCompatibleTms(): void { this.compatibleTms = []; @@ -1041,7 +1136,7 @@ export class EnemyPokemon extends Pokemon { : null; if (queuedMove) { if (queuedMove.isUsable(this.getMoveQueue()[0].ignorePP)) - return { move: queuedMove.moveId, ignorePP: this.getMoveQueue()[0].ignorePP }; + return { move: queuedMove.moveId, targets: this.getMoveQueue()[0].targets, ignorePP: this.getMoveQueue()[0].ignorePP }; else { this.getMoveQueue().shift(); return this.getNextMove(); @@ -1051,52 +1146,26 @@ export class EnemyPokemon extends Pokemon { const movePool = this.getMoveset().filter(m => m.isUsable()); if (movePool.length) { if (movePool.length === 1) - return { move: movePool[0].moveId }; + return { move: movePool[0].moveId, targets: this.getNextTargets(movePool[0].moveId) }; switch (this.aiType) { case AiType.RANDOM: - return { move: movePool[Utils.randInt(movePool.length)].moveId }; + const moveId = movePool[Utils.randInt(movePool.length)].moveId; + return { move: moveId, targets: this.getNextTargets(moveId) }; case AiType.SMART_RANDOM: case AiType.SMART: - const target = this.scene.getPlayerPokemon(); const moveScores = movePool.map(() => 0); + const moveTargets = Object.fromEntries(movePool.map(m => [ m.moveId, this.getNextTargets(m.moveId) ])); for (let m in movePool) { const pokemonMove = movePool[m]; const move = pokemonMove.getMove(); let moveScore = moveScores[m]; - if (move.category === MoveCategory.STATUS) - moveScore++; - else { - const effectiveness = this.getAttackMoveEffectiveness(move.type); - moveScore = Math.pow(effectiveness - 1, 2) * effectiveness < 1 ? -2 : 2; - if (moveScore) { - if (move.category === MoveCategory.PHYSICAL) { - if (this.getBattleStat(Stat.ATK) > this.getBattleStat(Stat.SPATK)) { - const statRatio = this.getBattleStat(Stat.SPATK) / this.getBattleStat(Stat.ATK); - if (statRatio <= 0.75) - moveScore *= 2; - else if (statRatio <= 0.875) - moveScore *= 1.5; - } - } else { - if (this.getBattleStat(Stat.SPATK) > this.getBattleStat(Stat.ATK)) { - const statRatio = this.getBattleStat(Stat.ATK) / this.getBattleStat(Stat.SPATK); - if (statRatio <= 0.75) - moveScore *= 2; - else if (statRatio <= 0.875) - moveScore *= 1.5; - } - } - moveScore += Math.floor(move.power / 5); - } + for (let mt of moveTargets[move.id]) { + const target = this.scene.getField()[mt]; + moveScore += move.getUserBenefitScore(this, target, move) + move.getTargetBenefitScore(this, target, move) * (mt < BattlerIndex.ENEMY === this.isPlayer() ? 1 : -1); } - const statChangeAttrs = move.getAttrs(StatChangeAttr) as StatChangeAttr[]; - - for (let sc of statChangeAttrs) { - moveScore += ((sc.levels >= 1) === sc.selfTarget ? -2 : 2) + sc.levels * (sc.selfTarget ? 4 : -4); - // TODO: Add awareness of current levels - } + moveScore /= moveTargets[move.id].length // could make smarter by checking opponent def/spdef moveScores[m] = moveScore; @@ -1116,17 +1185,48 @@ export class EnemyPokemon extends Pokemon { r++; } console.log(movePool.map(m => m.getName()), moveScores, r, sortedMovePool.map(m => m.getName())); - return { move: sortedMovePool[r].moveId }; + return { move: sortedMovePool[r].moveId, targets: moveTargets[sortedMovePool[r].moveId] }; } } - return { move: Moves.STRUGGLE }; + return { move: Moves.STRUGGLE, targets: this.getNextTargets(Moves.STRUGGLE) }; + } + + getNextTargets(moveId: Moves): BattlerIndex[] { + const moveTargets = getMoveTargets(this, moveId); + const targets = this.scene.getField().filter(p => p?.isActive() && moveTargets.targets.indexOf(p.getBattlerIndex()) > -1); + if (moveTargets.multiple) + return targets.map(p => p.getBattlerIndex()); + + const move = allMoves[moveId]; + + let benefitScores = targets + .map(p => [ p.getBattlerIndex(), move.getTargetBenefitScore(this, p, move) * (p.isPlayer() === this.isPlayer() ? 1 : -1) ]); + + const sortedBenefitScores = benefitScores.slice(0); + sortedBenefitScores.sort((a, b) => { + const scoreA = a[1]; + const scoreB = b[1]; + return scoreA < scoreB ? 1 : scoreA > scoreB ? -1 : 0; + }); + + // TODO: Add some randomness + + return [ sortedBenefitScores[0][0] ]; } isPlayer() { return false; } + getFieldIndex(): integer { + return this.scene.getEnemyField().indexOf(this); + } + + getBattlerIndex(): BattlerIndex { + return BattlerIndex.ENEMY + this.getFieldIndex(); + } + addToParty() { const party = this.scene.getParty(); let ret: PlayerPokemon = null; @@ -1143,6 +1243,7 @@ export class EnemyPokemon extends Pokemon { export interface TurnMove { move: Moves; + targets?: BattlerIndex[]; result: MoveResult; virtual?: boolean; turn?: integer; @@ -1150,6 +1251,7 @@ export interface TurnMove { export interface QueuedMove { move: Moves; + targets: BattlerIndex[]; ignorePP?: boolean; } @@ -1188,17 +1290,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 a483713b2..495424ba7 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)); }); } @@ -193,7 +193,7 @@ export function initAutoPlay() { const party = thisArg.getParty(); const modifierTypeOptions = modifierSelectUiHandler.options.map(o => o.modifierTypeOption); - const faintedPartyMemberIndex = party.findIndex(p => !p.hp); + const faintedPartyMemberIndex = party.findIndex(p => p.isFainted()); const lowHpPartyMemberIndex = party.findIndex(p => p.getHpRatio() <= 0.5); const criticalHpPartyMemberIndex = party.findIndex(p => p.getHpRatio() <= 0.25); diff --git a/src/system/game-data.ts b/src/system/game-data.ts index 68dddbe67..9d5e2270c 100644 --- a/src/system/game-data.ts +++ b/src/system/game-data.ts @@ -21,7 +21,7 @@ interface SystemSaveData { interface SessionSaveData { party: PokemonData[]; - enemyParty: PokemonData[]; + enemyField: PokemonData[]; modifiers: PersistentModifierData[]; enemyModifiers: PersistentModifierData[]; arena: ArenaData; @@ -126,9 +126,9 @@ export class GameData { saveSession(scene: BattleScene): boolean { const sessionData = { party: scene.getParty().map(p => new PokemonData(p)), - enemyParty: scene.getEnemyParty().map(p => new PokemonData(p)), - modifiers: scene.findModifiers(m => true).map(m => new PersistentModifierData(m, true)), - enemyModifiers: scene.findModifiers(m => true, false).map(m => new PersistentModifierData(m, false)), + enemyField: scene.getEnemyField().map(p => new PokemonData(p)), + modifiers: scene.findModifiers(() => true).map(m => new PersistentModifierData(m, true)), + enemyModifiers: scene.findModifiers(() => true, false).map(m => new PersistentModifierData(m, false)), arena: new ArenaData(scene.arena), pokeballCounts: scene.pokeballCounts, waveIndex: scene.currentBattle.waveIndex, @@ -153,7 +153,7 @@ export class GameData { try { const sessionData = JSON.parse(atob(localStorage.getItem('sessionData')), (k: string, v: any) => { - if (k === 'party' || k === 'enemyParty') { + if (k === 'party' || k === 'enemyField') { const ret: PokemonData[] = []; for (let pd of v) ret.push(new PokemonData(pd)); @@ -187,17 +187,20 @@ export class GameData { loadPokemonAssets.push(pokemon.loadAssets()); party.push(pokemon); } - - const enemyPokemon = sessionData.enemyParty[0].toPokemon(scene) as EnemyPokemon; Object.keys(scene.pokeballCounts).forEach((key: string) => { scene.pokeballCounts[key] = sessionData.pokeballCounts[key] || 0; }); - scene.newArena(sessionData.arena.biome, true); - scene.newBattle(sessionData.waveIndex).enemyPokemon = enemyPokemon; + scene.newArena(sessionData.arena.biome, sessionData.enemyField.length > 1); + const battle = scene.newBattle(sessionData.waveIndex, sessionData.enemyField.length > 1); - loadPokemonAssets.push(enemyPokemon.loadAssets()); + sessionData.enemyField.forEach((enemyData, e) => { + const enemyPokemon = enemyData.toPokemon(scene) as EnemyPokemon; + battle.enemyField[e] = enemyPokemon; + + loadPokemonAssets.push(enemyPokemon.loadAssets()); + }); scene.arena.weather = sessionData.arena.weather; // TODO diff --git a/src/system/modifier-data.ts b/src/system/modifier-data.ts index 602526057..84badd15e 100644 --- a/src/system/modifier-data.ts +++ b/src/system/modifier-data.ts @@ -37,7 +37,7 @@ export default class ModifierData { type.generatorId = this.typeGeneratorId; if (type instanceof ModifierTypeGenerator) - type = (type as ModifierTypeGenerator).generateType(this.player ? scene.getParty() : scene.getEnemyParty(), this.typePregenArgs); + type = (type as ModifierTypeGenerator).generateType(this.player ? scene.getParty() : scene.getEnemyField(), this.typePregenArgs); const ret = Reflect.construct(constructor, ([ type ] as any[]).concat(this.args).concat(this.stackCount)) as PersistentModifier diff --git a/src/ui/ability-bar.ts b/src/ui/ability-bar.ts index 0052bb51c..85f1a1f19 100644 --- a/src/ui/ability-bar.ts +++ b/src/ui/ability-bar.ts @@ -4,6 +4,7 @@ import { TextStyle, addTextObject } from "./text"; const hiddenX = -91; const shownX = 10; +const baseY = -116; export default class AbilityBar extends Phaser.GameObjects.Container { private bg: Phaser.GameObjects.Image; @@ -15,7 +16,7 @@ export default class AbilityBar extends Phaser.GameObjects.Container { public shown: boolean; constructor(scene: BattleScene) { - super(scene, hiddenX, (-scene.game.canvas.height / 6) + 64); + super(scene, hiddenX, baseY); } setup(): void { @@ -43,9 +44,12 @@ export default class AbilityBar extends Phaser.GameObjects.Container { if (this.shown) return; + (this.scene as BattleScene).fieldUI.bringToTop(this); + if (this.tween) this.tween.stop(); + this.y = baseY + ((this.scene as BattleScene).currentBattle.double ? 14 : 0); this.tween = this.scene.tweens.add({ targets: this, x: shownX, diff --git a/src/ui/ball-ui-handler.ts b/src/ui/ball-ui-handler.ts index 5ebf7553d..8cc8d9679 100644 --- a/src/ui/ball-ui-handler.ts +++ b/src/ui/ball-ui-handler.ts @@ -60,12 +60,12 @@ export default class BallUiHandler extends UiHandler { let success = false; - const pokeballTypeCount = Object.keys(this.scene.pokeballCounts).length; + const pokeballTypeCount = Object.keys(this.scene.currentBattle.turnPokeballCounts).length; if (button === Button.ACTION || button === Button.CANCEL) { success = true; if (button === Button.ACTION && this.cursor < pokeballTypeCount) { - if (this.scene.pokeballCounts[this.cursor]) { + if (this.scene.currentBattle.turnPokeballCounts[this.cursor]) { if ((this.scene.getCurrentPhase() as CommandPhase).handleCommand(Command.BALL, this.cursor)) { this.scene.ui.setMode(Mode.COMMAND); this.scene.ui.setMode(Mode.MESSAGE); @@ -93,7 +93,7 @@ export default class BallUiHandler extends UiHandler { } updateCounts() { - this.countsText.setText(Object.values(this.scene.pokeballCounts).map(c => `x${c}`).join('\n')); + this.countsText.setText(Object.values(this.scene.currentBattle.turnPokeballCounts).map(c => `x${c}`).join('\n')); } setCursor(cursor: integer): boolean { diff --git a/src/ui/battle-info.ts b/src/ui/battle-info.ts index 0e2b1bcfa..7b5f5fb5d 100644 --- a/src/ui/battle-info.ts +++ b/src/ui/battle-info.ts @@ -4,10 +4,12 @@ import * as Utils from '../utils'; import { addTextObject, TextStyle } from './text'; import { getGenderSymbol, getGenderColor } from '../data/gender'; import { StatusEffect } from '../data/status-effect'; -import BattleScene, { maxExpLevel } from '../battle-scene'; +import { maxExpLevel } from '../battle-scene'; export default class BattleInfo extends Phaser.GameObjects.Container { private player: boolean; + private mini: boolean; + private offset: boolean; private lastName: string; private lastStatus: StatusEffect; private lastHp: integer; @@ -17,9 +19,10 @@ export default class BattleInfo extends Phaser.GameObjects.Container { private lastLevelExp: integer; private lastLevel: integer; + private box: Phaser.GameObjects.Sprite; private nameText: Phaser.GameObjects.Text; private genderText: Phaser.GameObjects.Text; - private ownedIcon: Phaser.GameObjects.Image; + private ownedIcon: Phaser.GameObjects.Sprite; private statusIndicator: Phaser.GameObjects.Sprite; private levelContainer: Phaser.GameObjects.Container; private hpBar: Phaser.GameObjects.Image; @@ -30,6 +33,8 @@ export default class BattleInfo extends Phaser.GameObjects.Container { constructor(scene: Phaser.Scene, x: number, y: number, player: boolean) { super(scene, x, y); this.player = player; + this.mini = !player; + this.offset = false; this.lastName = null; this.lastStatus = StatusEffect.NONE; this.lastHp = -1; @@ -42,9 +47,9 @@ export default class BattleInfo extends Phaser.GameObjects.Container { // Initially invisible and shown via Pokemon.showInfo this.setVisible(false); - const box = this.scene.add.image(0, 0, `pbinfo_${player ? 'player' : 'enemy'}`); - box.setOrigin(1, 0.5); - this.add(box); + this.box = this.scene.add.sprite(0, 0, this.getTextureName()); + this.box.setOrigin(1, 0.5); + this.add(this.box); this.nameText = addTextObject(this.scene, player ? -115 : -124, player ? -15.2 : -11.2, '', TextStyle.BATTLE_INFO); this.nameText.setOrigin(0, 0); @@ -56,7 +61,7 @@ export default class BattleInfo extends Phaser.GameObjects.Container { this.add(this.genderText); if (!this.player) { - this.ownedIcon = this.scene.add.image(0, 0, 'icon_owned'); + this.ownedIcon = this.scene.add.sprite(0, 0, 'icon_owned'); this.ownedIcon.setVisible(false); this.ownedIcon.setOrigin(0, 0); this.ownedIcon.setPositionRelative(this.nameText, 0, 11.5); @@ -115,10 +120,11 @@ export default class BattleInfo extends Phaser.GameObjects.Container { } this.hpBar.setScale(pokemon.getHpRatio(), 1); + this.lastHpFrame = this.hpBar.scaleX > 0.5 ? 'high' : this.hpBar.scaleX > 0.25 ? 'medium' : 'low'; + this.hpBar.setFrame(this.lastHpFrame); if (this.player) this.setHpNumbers(pokemon.hp, pokemon.getMaxHp()); this.lastHp = pokemon.hp; - this.lastHpFrame = this.hpBar.scaleX > 0.5 ? 'high' : this.hpBar.scaleX > 0.25 ? 'medium' : 'low'; this.lastMaxHp = pokemon.getMaxHp(); this.setLevel(pokemon.level); @@ -131,6 +137,39 @@ export default class BattleInfo extends Phaser.GameObjects.Container { } } + getTextureName(): string { + return `pbinfo_${this.player ? 'player' : 'enemy'}${this.mini ? '_mini' : ''}`; + } + + setMini(mini: boolean): void { + if (this.mini === mini) + return; + + this.mini = mini; + + this.box.setTexture(this.getTextureName()); + + if (this.player) { + this.y -= 12 * (mini ? 1 : -1); + } + + const offsetElements = [ this.nameText, this.genderText, this.statusIndicator, this.levelContainer ]; + offsetElements.forEach(el => el.y += 1.5 * (mini ? -1 : 1)); + + const toggledElements = [ this.hpNumbersContainer, this.expBar ]; + toggledElements.forEach(el => el.setVisible(!mini)); + } + + setOffset(offset: boolean): void { + if (this.offset === offset) + return; + + this.offset = offset; + + this.x += 10 * (offset === this.player ? 1 : -1); + this.y += 27 * (offset ? 1 : -1); + } + updateInfo(pokemon: Pokemon, instant?: boolean): Promise { return new Promise(resolve => { if (!this.scene) { @@ -290,4 +329,6 @@ export class EnemyBattleInfo extends BattleInfo { constructor(scene: Phaser.Scene) { super(scene, 140, -141, false); } + + setMini(mini: boolean): void { } // Always mini } \ No newline at end of file diff --git a/src/ui/command-ui-handler.ts b/src/ui/command-ui-handler.ts index 5e85399bb..2882b495d 100644 --- a/src/ui/command-ui-handler.ts +++ b/src/ui/command-ui-handler.ts @@ -43,7 +43,7 @@ export default class CommandUiHandler extends UiHandler { const messageHandler = this.getUi().getMessageHandler(); messageHandler.bg.setTexture('bg_command'); messageHandler.message.setWordWrapWidth(1110); - messageHandler.showText(`What will\n${this.scene.getPlayerPokemon().name} do?`, 0); + messageHandler.showText(`What will\n${(this.scene.getCurrentPhase() as CommandPhase).getPokemon().name} do?`, 0); this.setCursor(this.cursor); } @@ -65,7 +65,7 @@ export default class CommandUiHandler extends UiHandler { success = true; break; case 2: - ui.setMode(Mode.PARTY, PartyUiMode.SWITCH); + ui.setMode(Mode.PARTY, PartyUiMode.SWITCH, (this.scene.getCurrentPhase() as CommandPhase).getPokemon().getFieldIndex()); success = true; break; case 3: diff --git a/src/ui/evolution-scene-handler.ts b/src/ui/evolution-scene-handler.ts index cba38f5ae..1abd817c3 100644 --- a/src/ui/evolution-scene-handler.ts +++ b/src/ui/evolution-scene-handler.ts @@ -13,6 +13,12 @@ export default class EvolutionSceneHandler extends UiHandler { this.evolutionContainer = this.scene.add.container(0, -this.scene.game.canvas.height / 6); this.scene.fieldUI.add(this.evolutionContainer); } + + show(_args: any[]): void { + super.show(_args); + + this.scene.fieldUI.bringToTop(this.evolutionContainer); + } processInput(button: Button) { this.scene.ui.getMessageHandler().processInput(button); diff --git a/src/ui/fight-ui-handler.ts b/src/ui/fight-ui-handler.ts index f856e380c..4a8efef99 100644 --- a/src/ui/fight-ui-handler.ts +++ b/src/ui/fight-ui-handler.ts @@ -91,7 +91,7 @@ export default class FightUiHandler extends UiHandler { ui.add(this.cursorObj); } - const moveset = this.scene.getPlayerPokemon().getMoveset(); + const moveset = (this.scene.getCurrentPhase() as CommandPhase).getPokemon().getMoveset(); const hasMove = cursor < moveset.length; @@ -114,7 +114,7 @@ export default class FightUiHandler extends UiHandler { } displayMoves() { - const moveset = this.scene.getPlayerPokemon().getMoveset(); + const moveset = (this.scene.getCurrentPhase() as CommandPhase).getPokemon().getMoveset(); for (let m = 0; m < 4; m++) { const moveText = addTextObject(this.scene, m % 2 === 0 ? 0 : 100, m < 2 ? 0 : 16, '-', TextStyle.WINDOW); if (m < moveset.length) diff --git a/src/ui/party-ui-handler.ts b/src/ui/party-ui-handler.ts index 8e196e800..acf737fe6 100644 --- a/src/ui/party-ui-handler.ts +++ b/src/ui/party-ui-handler.ts @@ -42,7 +42,9 @@ export type PokemonMoveSelectFilter = (pokemonMove: PokemonMove) => string; export default class PartyUiHandler extends MessageUiHandler { private partyUiMode: PartyUiMode; + private fieldIndex: integer; + private partyBg: Phaser.GameObjects.Image; private partyContainer: Phaser.GameObjects.Container; private partySlotsContainer: Phaser.GameObjects.Container; private partySlots: PartySlot[]; @@ -67,7 +69,7 @@ export default class PartyUiHandler extends MessageUiHandler { private static FilterAll = (_pokemon: PlayerPokemon) => null; public static FilterNonFainted = (pokemon: PlayerPokemon) => { - if (!pokemon.hp) + if (pokemon.isFainted()) return `${pokemon.name} has no energy\nleft to battle!`; return null; }; @@ -96,10 +98,10 @@ export default class PartyUiHandler extends MessageUiHandler { this.partyContainer = partyContainer; - const partyBg = this.scene.add.image(0, 0, 'party_bg'); - partyContainer.add(partyBg); + this.partyBg = this.scene.add.image(0, 0, 'party_bg'); + partyContainer.add(this.partyBg); - partyBg.setOrigin(0, 1); + this.partyBg.setOrigin(0, 1); const partySlotsContainer = this.scene.add.container(0, 0); partyContainer.add(partySlotsContainer); @@ -143,17 +145,20 @@ export default class PartyUiHandler extends MessageUiHandler { this.partyUiMode = args[0] as PartyUiMode; + this.fieldIndex = args.length > 1 ? args[1] as integer : -1; + this.partyContainer.setVisible(true); + this.partyBg.setTexture(`party_bg${this.scene.currentBattle.double ? '_double' : ''}`); this.populatePartySlots(); this.setCursor(this.cursor < 6 ? this.cursor : 0); - if (args.length > 1 && args[1] instanceof Function) - this.selectCallback = args[1]; - this.selectFilter = args.length > 2 && args[2] instanceof Function - ? args[2] as PokemonSelectFilter + if (args.length > 2 && args[2] instanceof Function) + this.selectCallback = args[2]; + this.selectFilter = args.length > 3 && args[3] instanceof Function + ? args[3] as PokemonSelectFilter : PartyUiHandler.FilterAll; - this.moveSelectFilter = args.length > 3 && args[3] instanceof Function - ? args[3] as PokemonMoveSelectFilter + this.moveSelectFilter = args.length > 4 && args[4] instanceof Function + ? args[4] as PokemonMoveSelectFilter : PartyUiHandler.FilterAllMoves; } @@ -226,7 +231,7 @@ export default class PartyUiHandler extends MessageUiHandler { } else if (option === PartyOption.RELEASE) { this.clearOptions(); ui.playSelect(); - if (this.cursor) { + if (this.cursor >= this.scene.currentBattle.getBattlerCount()) { this.showText(`Do you really want to release ${pokemon.name}?`, null, () => { ui.setModeWithoutClear(Mode.CONFIRM, () => { ui.setMode(Mode.PARTY); @@ -292,12 +297,13 @@ export default class PartyUiHandler extends MessageUiHandler { success = this.setCursor(this.cursor < 6 ? this.cursor < slotCount - 1 ? this.cursor + 1 : 6 : 0); break; case Button.LEFT: - if (this.cursor && this.cursor < 6) + if (this.cursor >= this.scene.currentBattle.getBattlerCount() && this.cursor < 6) success = this.setCursor(0); break; case Button.RIGHT: - if (!this.cursor) - success = this.setCursor(this.lastCursor < 6 ? this.lastCursor || 1 : 1); + const battlerCount = this.scene.currentBattle.getBattlerCount(); + if (this.cursor < battlerCount) + success = this.setCursor(this.lastCursor < 6 ? this.lastCursor || battlerCount : battlerCount); break; } } @@ -411,10 +417,11 @@ export default class PartyUiHandler extends MessageUiHandler { case PartyUiMode.SWITCH: case PartyUiMode.FAINT_SWITCH: case PartyUiMode.POST_BATTLE_SWITCH: - if (this.cursor) { + if (this.cursor >= this.scene.currentBattle.getBattlerCount()) { this.options.push(PartyOption.SEND_OUT); if (this.partyUiMode !== PartyUiMode.FAINT_SWITCH - && this.scene.findModifier(m => m instanceof SwitchEffectTransferModifier && (m as SwitchEffectTransferModifier).pokemonId === this.scene.getPlayerPokemon().id)) + && this.scene.findModifier(m => m instanceof SwitchEffectTransferModifier + && (m as SwitchEffectTransferModifier).pokemonId === this.scene.getPlayerField()[this.fieldIndex].id)) this.options.push(PartyOption.PASS_BATON); } break; @@ -578,7 +585,9 @@ class PartySlot extends Phaser.GameObjects.Container { private slotHpOverlay: Phaser.GameObjects.Sprite; constructor(scene: BattleScene, slotIndex: integer, pokemon: PlayerPokemon) { - super(scene, slotIndex ? 230.5 : 64, slotIndex ? -184 + 28 * slotIndex : -124); + super(scene, slotIndex >= scene.currentBattle.getBattlerCount() ? 230.5 : 64, + slotIndex >= scene.currentBattle.getBattlerCount() ? -184 + (scene.currentBattle.double ? -38 : 0) + + (28 + (scene.currentBattle.double ? 6 : 0)) * slotIndex : -124 + (scene.currentBattle.double ? -8 : 0) + slotIndex * 64); this.slotIndex = slotIndex; this.pokemon = pokemon; @@ -587,14 +596,16 @@ class PartySlot extends Phaser.GameObjects.Container { } setup() { - const slotKey = `party_slot${this.slotIndex ? '' : '_main'}`; + const battlerCount = (this.scene as BattleScene).currentBattle.getBattlerCount(); + + const slotKey = `party_slot${this.slotIndex >= battlerCount ? '' : '_main'}`; const slotBg = this.scene.add.sprite(0, 0, slotKey, `${slotKey}${this.pokemon.hp ? '' : '_fnt'}`); this.slotBg = slotBg; this.add(slotBg); - const slotPb = this.scene.add.sprite(this.slotIndex ? -85.5 : -51, this.slotIndex ? 0 : -20.5, 'party_pb'); + const slotPb = this.scene.add.sprite(this.slotIndex >= battlerCount ? -85.5 : -51, this.slotIndex >= battlerCount ? 0 : -20.5, 'party_pb'); this.slotPb = slotPb; this.add(slotPb); @@ -609,7 +620,7 @@ class PartySlot extends Phaser.GameObjects.Container { this.add(slotInfoContainer); const slotName = addTextObject(this.scene, 0, 0, this.pokemon.name, TextStyle.PARTY); - slotName.setPositionRelative(slotBg, this.slotIndex ? 21 : 24, this.slotIndex ? 3 : 10); + slotName.setPositionRelative(slotBg, this.slotIndex >= battlerCount ? 21 : 24, this.slotIndex >= battlerCount ? 3 : 10); slotName.setOrigin(0, 0); const slotLevelLabel = this.scene.add.image(0, 0, 'party_slot_overlay_lv'); @@ -621,7 +632,7 @@ class PartySlot extends Phaser.GameObjects.Container { slotLevelText.setOrigin(0, 0.25); const slotHpBar = this.scene.add.image(0, 0, 'party_slot_hp_bar'); - slotHpBar.setPositionRelative(slotBg, this.slotIndex ? 72 : 8, this.slotIndex ? 7 : 31); + slotHpBar.setPositionRelative(slotBg, this.slotIndex >= battlerCount ? 72 : 8, this.slotIndex >= battlerCount ? 7 : 31); slotHpBar.setOrigin(0, 0); const hpRatio = this.pokemon.getHpRatio(); @@ -669,8 +680,9 @@ class PartySlot extends Phaser.GameObjects.Container { } private updateSlotTexture(): void { - this.slotBg.setTexture(`party_slot${this.slotIndex ? '' : '_main'}`, - `party_slot${this.slotIndex ? '' : '_main'}${this.transfer ? '_swap' : this.pokemon.hp ? '' : '_fnt'}${this.selected ? '_sel' : ''}`); + const battlerCount = (this.scene as BattleScene).currentBattle.getBattlerCount(); + this.slotBg.setTexture(`party_slot${this.slotIndex >= battlerCount ? '' : '_main'}`, + `party_slot${this.slotIndex >= battlerCount ? '' : '_main'}${this.transfer ? '_swap' : this.pokemon.hp ? '' : '_fnt'}${this.selected ? '_sel' : ''}`); } } diff --git a/src/ui/target-select-ui-handler.ts b/src/ui/target-select-ui-handler.ts new file mode 100644 index 000000000..6195f7990 --- /dev/null +++ b/src/ui/target-select-ui-handler.ts @@ -0,0 +1,121 @@ +import { BattlerIndex } 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: BattlerIndex[]; + private targetFlashTween: Phaser.Tweens.Tween; + + constructor(scene: BattleScene) { + super(scene, Mode.TARGET_SELECT); + + this.cursor = -1; + } + + 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; + + 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 < BattlerIndex.ENEMY && this.targets.find(t => t >= BattlerIndex.ENEMY)) + success = this.setCursor(this.targets.find(t => t >= BattlerIndex.ENEMY)); + break; + case Button.DOWN: + if (this.cursor >= BattlerIndex.ENEMY && this.targets.find(t => t < BattlerIndex.ENEMY)) + success = this.setCursor(this.targets.find(t => t < BattlerIndex.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 2f4c707c1..13778608f 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 996c980d0..36b5fc023 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; }