mirror of
https://github.com/pagefaultgames/pokerogue.git
synced 2025-03-13 05:15:17 +00:00
315 lines
13 KiB
TypeScript
315 lines
13 KiB
TypeScript
import { BattlerIndex } from "#app/battle";
|
|
import { PLAYER_PARTY_MAX_SIZE } from "#app/constants";
|
|
import { SubstituteTag } from "#app/data/battler-tags";
|
|
import { doPokeballBounceAnim, getPokeballAtlasKey, getPokeballCatchMultiplier, getPokeballTintColor, getCriticalCaptureChance } from "#app/data/pokeball";
|
|
import { getStatusEffectCatchRateMultiplier } from "#app/data/status-effect";
|
|
import { addPokeballCaptureStars, addPokeballOpenParticles } from "#app/field/anims";
|
|
import type { EnemyPokemon } from "#app/field/pokemon";
|
|
import { getPokemonNameWithAffix } from "#app/messages";
|
|
import { PokemonHeldItemModifier } from "#app/modifier/modifier";
|
|
import { PokemonPhase } from "#app/phases/pokemon-phase";
|
|
import { VictoryPhase } from "#app/phases/victory-phase";
|
|
import { achvs } from "#app/system/achv";
|
|
import type { PartyOption } from "#app/ui/party-ui-handler";
|
|
import { PartyUiMode } from "#app/ui/party-ui-handler";
|
|
import { SummaryUiMode } from "#app/ui/summary-ui-handler";
|
|
import { Mode } from "#app/ui/ui";
|
|
import type { PokeballType } from "#enums/pokeball";
|
|
import { StatusEffect } from "#enums/status-effect";
|
|
import i18next from "i18next";
|
|
import { globalScene } from "#app/global-scene";
|
|
|
|
export class AttemptCapturePhase extends PokemonPhase {
|
|
private pokeballType: PokeballType;
|
|
private pokeball: Phaser.GameObjects.Sprite;
|
|
private originalY: number;
|
|
|
|
constructor(targetIndex: integer, pokeballType: PokeballType) {
|
|
super(BattlerIndex.ENEMY + targetIndex);
|
|
|
|
this.pokeballType = pokeballType;
|
|
}
|
|
|
|
start() {
|
|
super.start();
|
|
|
|
const pokemon = this.getPokemon() as EnemyPokemon;
|
|
|
|
if (!pokemon?.hp) {
|
|
return this.end();
|
|
}
|
|
|
|
const substitute = pokemon.getTag(SubstituteTag);
|
|
if (substitute) {
|
|
substitute.sprite.setVisible(false);
|
|
}
|
|
|
|
globalScene.pokeballCounts[this.pokeballType]--;
|
|
|
|
this.originalY = pokemon.y;
|
|
|
|
const _3m = 3 * pokemon.getMaxHp();
|
|
const _2h = 2 * pokemon.hp;
|
|
const catchRate = pokemon.species.catchRate;
|
|
const pokeballMultiplier = getPokeballCatchMultiplier(this.pokeballType);
|
|
const statusMultiplier = pokemon.status ? getStatusEffectCatchRateMultiplier(pokemon.status.effect) : 1;
|
|
const modifiedCatchRate = Math.round((((_3m - _2h) * catchRate * pokeballMultiplier) / _3m) * statusMultiplier);
|
|
const shakeProbability = Math.round(65536 / Math.pow((255 / modifiedCatchRate), 0.1875)); // Formula taken from gen 6
|
|
const criticalCaptureChance = getCriticalCaptureChance(modifiedCatchRate);
|
|
const isCritical = pokemon.randSeedInt(256) < criticalCaptureChance;
|
|
const fpOffset = pokemon.getFieldPositionOffset();
|
|
|
|
const pokeballAtlasKey = getPokeballAtlasKey(this.pokeballType);
|
|
this.pokeball = globalScene.addFieldSprite(16, 80, "pb", pokeballAtlasKey);
|
|
this.pokeball.setOrigin(0.5, 0.625);
|
|
globalScene.field.add(this.pokeball);
|
|
|
|
globalScene.playSound(isCritical ? "se/crit_throw" : "se/pb_throw");
|
|
globalScene.time.delayedCall(300, () => {
|
|
globalScene.field.moveBelow(this.pokeball as Phaser.GameObjects.GameObject, pokemon);
|
|
});
|
|
|
|
globalScene.tweens.add({
|
|
// Throw animation
|
|
targets: this.pokeball,
|
|
x: { value: 236 + fpOffset[0], ease: "Linear" },
|
|
y: { value: 16 + fpOffset[1], ease: "Cubic.easeOut" },
|
|
duration: 500,
|
|
onComplete: () => {
|
|
// Ball opens
|
|
this.pokeball.setTexture("pb", `${pokeballAtlasKey}_opening`);
|
|
globalScene.time.delayedCall(17, () => this.pokeball.setTexture("pb", `${pokeballAtlasKey}_open`));
|
|
globalScene.playSound("se/pb_rel");
|
|
pokemon.tint(getPokeballTintColor(this.pokeballType));
|
|
|
|
addPokeballOpenParticles(this.pokeball.x, this.pokeball.y, this.pokeballType);
|
|
|
|
globalScene.tweens.add({
|
|
// Mon enters ball
|
|
targets: pokemon,
|
|
duration: 500,
|
|
ease: "Sine.easeIn",
|
|
scale: 0.25,
|
|
y: 20,
|
|
onComplete: () => {
|
|
// Ball closes
|
|
this.pokeball.setTexture("pb", `${pokeballAtlasKey}_opening`);
|
|
pokemon.setVisible(false);
|
|
globalScene.playSound("se/pb_catch");
|
|
globalScene.time.delayedCall(17, () => this.pokeball.setTexture("pb", `${pokeballAtlasKey}`));
|
|
|
|
const doShake = () => {
|
|
// After the overall catch rate check, the game does 3 shake checks before confirming the catch.
|
|
let shakeCount = 0;
|
|
const pbX = this.pokeball.x;
|
|
const shakeCounter = globalScene.tweens.addCounter({
|
|
from: 0,
|
|
to: 1,
|
|
repeat: isCritical ? 2 : 4, // Critical captures only perform 1 shake check
|
|
yoyo: true,
|
|
ease: "Cubic.easeOut",
|
|
duration: 250,
|
|
repeatDelay: 500,
|
|
onUpdate: t => {
|
|
if (shakeCount && shakeCount < (isCritical ? 2 : 4)) {
|
|
const value = t.getValue();
|
|
const directionMultiplier = shakeCount % 2 === 1 ? 1 : -1;
|
|
this.pokeball.setX(pbX + value * 4 * directionMultiplier);
|
|
this.pokeball.setAngle(value * 27.5 * directionMultiplier);
|
|
}
|
|
},
|
|
onRepeat: () => {
|
|
if (!pokemon.species.isObtainable()) {
|
|
shakeCounter.stop();
|
|
this.failCatch(shakeCount);
|
|
} else if (shakeCount++ < (isCritical ? 1 : 3)) {
|
|
// Shake check (skip check for critical or guaranteed captures, but still play the sound)
|
|
if (pokeballMultiplier === -1 || isCritical || modifiedCatchRate >= 255 || pokemon.randSeedInt(65536) < shakeProbability) {
|
|
globalScene.playSound("se/pb_move");
|
|
} else {
|
|
shakeCounter.stop();
|
|
this.failCatch(shakeCount);
|
|
}
|
|
} else if (isCritical && pokemon.randSeedInt(65536) >= shakeProbability) {
|
|
// Above, perform the one shake check for critical captures after the ball shakes once
|
|
shakeCounter.stop();
|
|
this.failCatch(shakeCount);
|
|
} else {
|
|
globalScene.playSound("se/pb_lock");
|
|
addPokeballCaptureStars(this.pokeball);
|
|
|
|
const pbTint = globalScene.add.sprite(this.pokeball.x, this.pokeball.y, "pb", "pb");
|
|
pbTint.setOrigin(this.pokeball.originX, this.pokeball.originY);
|
|
pbTint.setTintFill(0);
|
|
pbTint.setAlpha(0);
|
|
globalScene.field.add(pbTint);
|
|
globalScene.tweens.add({
|
|
targets: pbTint,
|
|
alpha: 0.375,
|
|
duration: 200,
|
|
easing: "Sine.easeOut",
|
|
onComplete: () => {
|
|
globalScene.tweens.add({
|
|
targets: pbTint,
|
|
alpha: 0,
|
|
duration: 200,
|
|
easing: "Sine.easeIn",
|
|
onComplete: () => pbTint.destroy()
|
|
});
|
|
}
|
|
});
|
|
}
|
|
},
|
|
onComplete: () => {
|
|
this.catch();
|
|
}
|
|
});
|
|
};
|
|
|
|
// Ball bounces (handled in pokemon.ts)
|
|
globalScene.time.delayedCall(250, () => doPokeballBounceAnim(this.pokeball, 16, 72, 350, doShake, isCritical));
|
|
}
|
|
});
|
|
}
|
|
});
|
|
}
|
|
|
|
failCatch(shakeCount: integer) {
|
|
const pokemon = this.getPokemon();
|
|
|
|
globalScene.playSound("se/pb_rel");
|
|
pokemon.setY(this.originalY);
|
|
if (pokemon.status?.effect !== StatusEffect.SLEEP) {
|
|
pokemon.cry(pokemon.getHpRatio() > 0.25 ? undefined : { rate: 0.85 });
|
|
}
|
|
pokemon.tint(getPokeballTintColor(this.pokeballType));
|
|
pokemon.setVisible(true);
|
|
pokemon.untint(250, "Sine.easeOut");
|
|
|
|
const substitute = pokemon.getTag(SubstituteTag);
|
|
if (substitute) {
|
|
substitute.sprite.setVisible(true);
|
|
}
|
|
|
|
const pokeballAtlasKey = getPokeballAtlasKey(this.pokeballType);
|
|
this.pokeball.setTexture("pb", `${pokeballAtlasKey}_opening`);
|
|
globalScene.time.delayedCall(17, () => this.pokeball.setTexture("pb", `${pokeballAtlasKey}_open`));
|
|
|
|
globalScene.tweens.add({
|
|
targets: pokemon,
|
|
duration: 250,
|
|
ease: "Sine.easeOut",
|
|
scale: 1
|
|
});
|
|
|
|
globalScene.currentBattle.lastUsedPokeball = this.pokeballType;
|
|
this.removePb();
|
|
this.end();
|
|
}
|
|
|
|
catch() {
|
|
const pokemon = this.getPokemon() as EnemyPokemon;
|
|
|
|
const speciesForm = !pokemon.fusionSpecies ? pokemon.getSpeciesForm() : pokemon.getFusionSpeciesForm();
|
|
|
|
if (speciesForm.abilityHidden && (pokemon.fusionSpecies ? pokemon.fusionAbilityIndex : pokemon.abilityIndex) === speciesForm.getAbilityCount() - 1) {
|
|
globalScene.validateAchv(achvs.HIDDEN_ABILITY);
|
|
}
|
|
|
|
if (pokemon.species.subLegendary) {
|
|
globalScene.validateAchv(achvs.CATCH_SUB_LEGENDARY);
|
|
}
|
|
|
|
if (pokemon.species.legendary) {
|
|
globalScene.validateAchv(achvs.CATCH_LEGENDARY);
|
|
}
|
|
|
|
if (pokemon.species.mythical) {
|
|
globalScene.validateAchv(achvs.CATCH_MYTHICAL);
|
|
}
|
|
|
|
globalScene.pokemonInfoContainer.show(pokemon, true);
|
|
|
|
globalScene.gameData.updateSpeciesDexIvs(pokemon.species.getRootSpeciesId(true), pokemon.ivs);
|
|
|
|
globalScene.ui.showText(i18next.t("battle:pokemonCaught", { pokemonName: getPokemonNameWithAffix(pokemon) }), null, () => {
|
|
const end = () => {
|
|
globalScene.unshiftPhase(new VictoryPhase(this.battlerIndex));
|
|
globalScene.pokemonInfoContainer.hide();
|
|
this.removePb();
|
|
this.end();
|
|
};
|
|
const removePokemon = () => {
|
|
globalScene.addFaintedEnemyScore(pokemon);
|
|
globalScene.getPlayerField().filter(p => p.isActive(true)).forEach(playerPokemon => playerPokemon.removeTagsBySourceId(pokemon.id));
|
|
pokemon.hp = 0;
|
|
pokemon.trySetStatus(StatusEffect.FAINT);
|
|
globalScene.clearEnemyHeldItemModifiers();
|
|
globalScene.field.remove(pokemon, true);
|
|
};
|
|
const addToParty = (slotIndex?: number) => {
|
|
const newPokemon = pokemon.addToParty(this.pokeballType, slotIndex);
|
|
const modifiers = globalScene.findModifiers(m => m instanceof PokemonHeldItemModifier, false);
|
|
if (globalScene.getPlayerParty().filter(p => p.isShiny()).length === PLAYER_PARTY_MAX_SIZE) {
|
|
globalScene.validateAchv(achvs.SHINY_PARTY);
|
|
}
|
|
Promise.all(modifiers.map(m => globalScene.addModifier(m, true))).then(() => {
|
|
globalScene.updateModifiers(true);
|
|
removePokemon();
|
|
if (newPokemon) {
|
|
newPokemon.loadAssets().then(end);
|
|
} else {
|
|
end();
|
|
}
|
|
});
|
|
};
|
|
Promise.all([ pokemon.hideInfo(), globalScene.gameData.setPokemonCaught(pokemon) ]).then(() => {
|
|
if (globalScene.getPlayerParty().length === PLAYER_PARTY_MAX_SIZE) {
|
|
const promptRelease = () => {
|
|
globalScene.ui.showText(i18next.t("battle:partyFull", { pokemonName: pokemon.getNameToRender() }), null, () => {
|
|
globalScene.pokemonInfoContainer.makeRoomForConfirmUi(1, true);
|
|
globalScene.ui.setMode(Mode.CONFIRM, () => {
|
|
const newPokemon = globalScene.addPlayerPokemon(pokemon.species, pokemon.level, pokemon.abilityIndex, pokemon.formIndex, pokemon.gender, pokemon.shiny, pokemon.variant, pokemon.ivs, pokemon.nature, pokemon);
|
|
globalScene.ui.setMode(Mode.SUMMARY, newPokemon, 0, SummaryUiMode.DEFAULT, () => {
|
|
globalScene.ui.setMode(Mode.MESSAGE).then(() => {
|
|
promptRelease();
|
|
});
|
|
}, false);
|
|
}, () => {
|
|
globalScene.ui.setMode(Mode.PARTY, PartyUiMode.RELEASE, this.fieldIndex, (slotIndex: integer, _option: PartyOption) => {
|
|
globalScene.ui.setMode(Mode.MESSAGE).then(() => {
|
|
if (slotIndex < 6) {
|
|
addToParty(slotIndex);
|
|
} else {
|
|
promptRelease();
|
|
}
|
|
});
|
|
});
|
|
}, () => {
|
|
globalScene.ui.setMode(Mode.MESSAGE).then(() => {
|
|
removePokemon();
|
|
end();
|
|
});
|
|
}, "fullParty");
|
|
});
|
|
};
|
|
promptRelease();
|
|
} else {
|
|
addToParty();
|
|
}
|
|
});
|
|
}, 0, true);
|
|
}
|
|
|
|
removePb() {
|
|
globalScene.tweens.add({
|
|
targets: this.pokeball,
|
|
duration: 250,
|
|
delay: 250,
|
|
ease: "Sine.easeIn",
|
|
alpha: 0,
|
|
onComplete: () => this.pokeball.destroy()
|
|
});
|
|
}
|
|
}
|