mirror of
https://github.com/pagefaultgames/pokerogue.git
synced 2025-06-21 00:52:47 +02:00
* Added `MoveUseType` and refactored MEP * Fixed Wimp out tests & ME code finally i think all the booleans are gone i hope * Added version migration for last resort and co. buh gumbug * Fixed various bugs and added tests for previous bugfixes * Reverted a couple doc changes * WIP * Update pokemon-species.ts Co-authored-by: NightKev <34855794+DayKev@users.noreply.github.com> * Update pokemon-phase.ts Co-authored-by: NightKev <34855794+DayKev@users.noreply.github.com> * Fixed remaining tests (I think) * Reverted rollout test changes * Fixed command phase bug causing metronome test timeout * Revert early_bird.test.ts * Fix biome.jsonc * Made `MoveUseType` start at 1 As per @DayKev's request * Fixed a thing * Fixed bolt beak condition to be marginally less jank * Applied some review suggestions * Reverted move phase operations * Added helper functions complete with markdown tables * Fixed things * Update battler-tags.ts * Fixed random issues * Fixed code * Fixed comment * Fixed import issues * Fix disable.test.ts conflicts * Update instruct.test.ts * Update `biome.jsonc` * Renamed `MoveUseType` to `MoveUseMode`; applied review comments * Fixed space * Fixed phasemanager bugs * Fixed instruct test to not bork * Fixed gorilla tactics bug * Battler Tags doc fixes * Fixed formatting and suttff * Minor comment updates and remove unused imports in `move.ts` * Re-add `public`, remove unnecessary default value in `battler-tags.ts` * Restore `{}` in `turn-start-phase.ts` Fixes `lint/correctness/noSwitchDeclarations` * Remove extra space in TSDoc in `move-phase.ts` * Use `game.field` instead of `game.scene` in `instruct.test.ts` Also `game.toEndOfTurn()` instead of `game.phaseInterceptor.to("BerryPhase")` * Use `game.field` instead of `game.scene` in `metronome.test.ts` * Use `toEndOfTurn()` instead of `to("BerryPhase")` in `powder.test.ts` * Convert `MoveUseMode` enum to `const` object * Update move-phase.ts Co-authored-by: NightKev <34855794+DayKev@users.noreply.github.com> * Add `enumValueToKey` utility function * Apply Biome --------- Co-authored-by: NightKev <34855794+DayKev@users.noreply.github.com>
254 lines
10 KiB
TypeScript
254 lines
10 KiB
TypeScript
import { applyAbAttrs } from "#app/data/abilities/apply-ab-attrs";
|
|
import { allMoves } from "#app/data/data-lists";
|
|
import { AbilityId } from "#enums/ability-id";
|
|
import { Stat } from "#app/enums/stat";
|
|
import type Pokemon from "#app/field/pokemon";
|
|
import { PokemonMove } from "#app/data/moves/pokemon-move";
|
|
import { BypassSpeedChanceModifier } from "#app/modifier/modifier";
|
|
import { Command } from "#enums/command";
|
|
import { randSeedShuffle, BooleanHolder } from "#app/utils/common";
|
|
import { FieldPhase } from "./field-phase";
|
|
import { BattlerIndex } from "#enums/battler-index";
|
|
import { TrickRoomTag } from "#app/data/arena-tag";
|
|
import { SwitchType } from "#enums/switch-type";
|
|
import { globalScene } from "#app/global-scene";
|
|
|
|
export class TurnStartPhase extends FieldPhase {
|
|
public readonly phaseName = "TurnStartPhase";
|
|
/**
|
|
* This orders the active Pokemon on the field by speed into an BattlerIndex array and returns that array.
|
|
* It also checks for Trick Room and reverses the array if it is present.
|
|
* @returns {@linkcode BattlerIndex[]} the battle indices of all pokemon on the field ordered by speed
|
|
*/
|
|
getSpeedOrder(): BattlerIndex[] {
|
|
const playerField = globalScene.getPlayerField().filter(p => p.isActive()) as Pokemon[];
|
|
const enemyField = globalScene.getEnemyField().filter(p => p.isActive()) as Pokemon[];
|
|
|
|
// We shuffle the list before sorting so speed ties produce random results
|
|
let orderedTargets: Pokemon[] = playerField.concat(enemyField);
|
|
// We seed it with the current turn to prevent an inconsistency where it
|
|
// was varying based on how long since you last reloaded
|
|
globalScene.executeWithSeedOffset(
|
|
() => {
|
|
orderedTargets = randSeedShuffle(orderedTargets);
|
|
},
|
|
globalScene.currentBattle.turn,
|
|
globalScene.waveSeed,
|
|
);
|
|
|
|
// Next, a check for Trick Room is applied to determine sort order.
|
|
const speedReversed = new BooleanHolder(false);
|
|
globalScene.arena.applyTags(TrickRoomTag, false, speedReversed);
|
|
|
|
// Adjust the sort function based on whether Trick Room is active.
|
|
orderedTargets.sort((a: Pokemon, b: Pokemon) => {
|
|
const aSpeed = a?.getEffectiveStat(Stat.SPD) ?? 0;
|
|
const bSpeed = b?.getEffectiveStat(Stat.SPD) ?? 0;
|
|
|
|
return speedReversed.value ? aSpeed - bSpeed : bSpeed - aSpeed;
|
|
});
|
|
|
|
return orderedTargets.map(t => t.getFieldIndex() + (!t.isPlayer() ? BattlerIndex.ENEMY : BattlerIndex.PLAYER));
|
|
}
|
|
|
|
/**
|
|
* This takes the result of getSpeedOrder and applies priority / bypass speed attributes to it.
|
|
* This also considers the priority levels of various commands and changes the result of getSpeedOrder based on such.
|
|
* @returns {@linkcode BattlerIndex[]} the final sequence of commands for this turn
|
|
*/
|
|
getCommandOrder(): BattlerIndex[] {
|
|
let moveOrder = this.getSpeedOrder();
|
|
// The creation of the battlerBypassSpeed object contains checks for the ability Quick Draw and the held item Quick Claw
|
|
// The ability Mycelium Might disables Quick Claw's activation when using a status move
|
|
// This occurs before the main loop because of battles with more than two Pokemon
|
|
const battlerBypassSpeed = {};
|
|
|
|
globalScene.getField(true).forEach(p => {
|
|
const bypassSpeed = new BooleanHolder(false);
|
|
const canCheckHeldItems = new BooleanHolder(true);
|
|
applyAbAttrs("BypassSpeedChanceAbAttr", p, null, false, bypassSpeed);
|
|
applyAbAttrs("PreventBypassSpeedChanceAbAttr", p, null, false, bypassSpeed, canCheckHeldItems);
|
|
if (canCheckHeldItems.value) {
|
|
globalScene.applyModifiers(BypassSpeedChanceModifier, p.isPlayer(), p, bypassSpeed);
|
|
}
|
|
battlerBypassSpeed[p.getBattlerIndex()] = bypassSpeed;
|
|
});
|
|
|
|
// The function begins sorting orderedTargets based on command priority, move priority, and possible speed bypasses.
|
|
// Non-FIGHT commands (SWITCH, BALL, RUN) have a higher command priority and will always occur before any FIGHT commands.
|
|
moveOrder = moveOrder.slice(0);
|
|
moveOrder.sort((a, b) => {
|
|
const aCommand = globalScene.currentBattle.turnCommands[a];
|
|
const bCommand = globalScene.currentBattle.turnCommands[b];
|
|
|
|
if (aCommand?.command !== bCommand?.command) {
|
|
if (aCommand?.command === Command.FIGHT) {
|
|
return 1;
|
|
}
|
|
if (bCommand?.command === Command.FIGHT) {
|
|
return -1;
|
|
}
|
|
} else if (aCommand?.command === Command.FIGHT) {
|
|
const aMove = allMoves[aCommand.move!.move];
|
|
const bMove = allMoves[bCommand!.move!.move];
|
|
|
|
const aUser = globalScene.getField(true).find(p => p.getBattlerIndex() === a)!;
|
|
const bUser = globalScene.getField(true).find(p => p.getBattlerIndex() === b)!;
|
|
|
|
const aPriority = aMove.getPriority(aUser, false);
|
|
const bPriority = bMove.getPriority(bUser, false);
|
|
|
|
// The game now checks for differences in priority levels.
|
|
// If the moves share the same original priority bracket, it can check for differences in battlerBypassSpeed and return the result.
|
|
// This conditional is used to ensure that Quick Claw can still activate with abilities like Stall and Mycelium Might (attack moves only)
|
|
// Otherwise, the game returns the user of the move with the highest priority.
|
|
const isSameBracket = Math.ceil(aPriority) - Math.ceil(bPriority) === 0;
|
|
if (aPriority !== bPriority) {
|
|
if (isSameBracket && battlerBypassSpeed[a].value !== battlerBypassSpeed[b].value) {
|
|
return battlerBypassSpeed[a].value ? -1 : 1;
|
|
}
|
|
return aPriority < bPriority ? 1 : -1;
|
|
}
|
|
}
|
|
|
|
// If there is no difference between the move's calculated priorities, the game checks for differences in battlerBypassSpeed and returns the result.
|
|
if (battlerBypassSpeed[a].value !== battlerBypassSpeed[b].value) {
|
|
return battlerBypassSpeed[a].value ? -1 : 1;
|
|
}
|
|
|
|
const aIndex = moveOrder.indexOf(a);
|
|
const bIndex = moveOrder.indexOf(b);
|
|
|
|
return aIndex < bIndex ? -1 : aIndex > bIndex ? 1 : 0;
|
|
});
|
|
return moveOrder;
|
|
}
|
|
|
|
// TODO: Refactor this alongside `CommandPhase.handleCommand` to use SEPARATE METHODS
|
|
// Also need a clearer distinction between "turn command" and queued moves
|
|
start() {
|
|
super.start();
|
|
|
|
const field = globalScene.getField();
|
|
const moveOrder = this.getCommandOrder();
|
|
|
|
let orderIndex = 0;
|
|
|
|
for (const o of this.getSpeedOrder()) {
|
|
const pokemon = field[o];
|
|
const preTurnCommand = globalScene.currentBattle.preTurnCommands[o];
|
|
|
|
if (preTurnCommand?.skip) {
|
|
continue;
|
|
}
|
|
|
|
switch (preTurnCommand?.command) {
|
|
case Command.TERA:
|
|
globalScene.phaseManager.pushNew("TeraPhase", pokemon);
|
|
}
|
|
}
|
|
|
|
const phaseManager = globalScene.phaseManager;
|
|
|
|
for (const o of moveOrder) {
|
|
const pokemon = field[o];
|
|
const turnCommand = globalScene.currentBattle.turnCommands[o];
|
|
|
|
if (turnCommand?.skip) {
|
|
continue;
|
|
}
|
|
|
|
switch (turnCommand?.command) {
|
|
case Command.FIGHT: {
|
|
const queuedMove = turnCommand.move;
|
|
pokemon.turnData.order = orderIndex++;
|
|
if (!queuedMove) {
|
|
continue;
|
|
}
|
|
const move =
|
|
pokemon.getMoveset().find(m => m.moveId === queuedMove.move && m.ppUsed < m.getMovePp()) ??
|
|
new PokemonMove(queuedMove.move);
|
|
if (move.getMove().hasAttr("MoveHeaderAttr")) {
|
|
phaseManager.unshiftNew("MoveHeaderPhase", pokemon, move);
|
|
}
|
|
|
|
if (pokemon.isPlayer() && turnCommand.cursor === -1) {
|
|
phaseManager.pushNew(
|
|
"MovePhase",
|
|
pokemon,
|
|
turnCommand.targets || turnCommand.move!.targets,
|
|
move,
|
|
turnCommand.move!.useMode,
|
|
); //TODO: is the bang correct here?
|
|
} else {
|
|
phaseManager.pushNew(
|
|
"MovePhase",
|
|
pokemon,
|
|
turnCommand.targets || turnCommand.move!.targets,
|
|
move,
|
|
queuedMove.useMode,
|
|
); // TODO: is the bang correct here?
|
|
}
|
|
break;
|
|
}
|
|
case Command.BALL:
|
|
phaseManager.unshiftNew("AttemptCapturePhase", turnCommand.targets![0] % 2, turnCommand.cursor!); //TODO: is the bang correct here?
|
|
break;
|
|
case Command.POKEMON:
|
|
{
|
|
const switchType = turnCommand.args?.[0] ? SwitchType.BATON_PASS : SwitchType.SWITCH;
|
|
phaseManager.unshiftNew(
|
|
"SwitchSummonPhase",
|
|
switchType,
|
|
pokemon.getFieldIndex(),
|
|
turnCommand.cursor!,
|
|
true,
|
|
pokemon.isPlayer(),
|
|
);
|
|
}
|
|
break;
|
|
case Command.RUN:
|
|
{
|
|
let runningPokemon = pokemon;
|
|
if (globalScene.currentBattle.double) {
|
|
const playerActivePokemon = field.filter(pokemon => {
|
|
if (pokemon) {
|
|
return pokemon.isPlayer() && pokemon.isActive();
|
|
}
|
|
return;
|
|
});
|
|
// if only one pokemon is alive, use that one
|
|
if (playerActivePokemon.length > 1) {
|
|
// find which active pokemon has faster speed
|
|
const fasterPokemon =
|
|
playerActivePokemon[0].getStat(Stat.SPD) > playerActivePokemon[1].getStat(Stat.SPD)
|
|
? playerActivePokemon[0]
|
|
: playerActivePokemon[1];
|
|
// check if either active pokemon has the ability "Run Away"
|
|
const hasRunAway = playerActivePokemon.find(p => p.hasAbility(AbilityId.RUN_AWAY));
|
|
runningPokemon = hasRunAway !== undefined ? hasRunAway : fasterPokemon;
|
|
}
|
|
}
|
|
phaseManager.unshiftNew("AttemptRunPhase", runningPokemon.getFieldIndex());
|
|
}
|
|
break;
|
|
}
|
|
}
|
|
|
|
phaseManager.pushNew("WeatherEffectPhase");
|
|
phaseManager.pushNew("BerryPhase");
|
|
|
|
/** Add a new phase to check who should be taking status damage */
|
|
phaseManager.pushNew("CheckStatusEffectPhase", moveOrder);
|
|
|
|
phaseManager.pushNew("TurnEndPhase");
|
|
|
|
/**
|
|
* this.end() will call shiftPhase(), which dumps everything from PrependQueue (aka everything that is unshifted()) to the front
|
|
* of the queue and dequeues to start the next phase
|
|
* this is important since stuff like SwitchSummon, AttemptRun, AttemptCapture Phases break the "flow" and should take precedence
|
|
*/
|
|
this.end();
|
|
}
|
|
}
|