Merge remote-tracking branch 'origin/beta' into move/spectralthief
This commit is contained in:
commit
ba178306d9
|
@ -38,7 +38,7 @@ import PokemonData from "#app/system/pokemon-data";
|
||||||
import { Nature } from "#app/data/nature";
|
import { Nature } from "#app/data/nature";
|
||||||
import { FormChangeItem, pokemonFormChanges, SpeciesFormChange, SpeciesFormChangeManualTrigger, SpeciesFormChangeTimeOfDayTrigger, SpeciesFormChangeTrigger } from "#app/data/pokemon-forms";
|
import { FormChangeItem, pokemonFormChanges, SpeciesFormChange, SpeciesFormChangeManualTrigger, SpeciesFormChangeTimeOfDayTrigger, SpeciesFormChangeTrigger } from "#app/data/pokemon-forms";
|
||||||
import { FormChangePhase } from "#app/phases/form-change-phase";
|
import { FormChangePhase } from "#app/phases/form-change-phase";
|
||||||
import { getTypeRgb } from "#app/data/type";
|
import { getTypeRgb, Type } from "#app/data/type";
|
||||||
import PokemonSpriteSparkleHandler from "#app/field/pokemon-sprite-sparkle-handler";
|
import PokemonSpriteSparkleHandler from "#app/field/pokemon-sprite-sparkle-handler";
|
||||||
import CharSprite from "#app/ui/char-sprite";
|
import CharSprite from "#app/ui/char-sprite";
|
||||||
import DamageNumberHandler from "#app/field/damage-number-handler";
|
import DamageNumberHandler from "#app/field/damage-number-handler";
|
||||||
|
@ -98,6 +98,7 @@ import { MysteryEncounterMode } from "#enums/mystery-encounter-mode";
|
||||||
import { ExpGainsSpeed } from "#enums/exp-gains-speed";
|
import { ExpGainsSpeed } from "#enums/exp-gains-speed";
|
||||||
import { BattlerTagType } from "#enums/battler-tag-type";
|
import { BattlerTagType } from "#enums/battler-tag-type";
|
||||||
import { FRIENDSHIP_GAIN_FROM_BATTLE } from "#app/data/balance/starters";
|
import { FRIENDSHIP_GAIN_FROM_BATTLE } from "#app/data/balance/starters";
|
||||||
|
import { StatusEffect } from "#enums/status-effect";
|
||||||
|
|
||||||
export const bypassLogin = import.meta.env.VITE_BYPASS_LOGIN === "1";
|
export const bypassLogin = import.meta.env.VITE_BYPASS_LOGIN === "1";
|
||||||
|
|
||||||
|
@ -2982,12 +2983,21 @@ export default class BattleScene extends SceneBase {
|
||||||
|
|
||||||
updateGameInfo(): void {
|
updateGameInfo(): void {
|
||||||
const gameInfo = {
|
const gameInfo = {
|
||||||
playTime: this.sessionPlayTime ? this.sessionPlayTime : 0,
|
playTime: this.sessionPlayTime ?? 0,
|
||||||
gameMode: this.currentBattle ? this.gameMode.getName() : "Title",
|
gameMode: this.currentBattle ? this.gameMode.getName() : "Title",
|
||||||
biome: this.currentBattle ? getBiomeName(this.arena.biomeType) : "",
|
biome: this.currentBattle ? getBiomeName(this.arena.biomeType) : "",
|
||||||
wave: this.currentBattle?.waveIndex || 0,
|
wave: this.currentBattle?.waveIndex ?? 0,
|
||||||
party: this.party ? this.party.map(p => {
|
party: this.party ? this.party.map((p) => {
|
||||||
return { name: p.name, level: p.level };
|
return {
|
||||||
|
name: p.name,
|
||||||
|
form: p.getFormKey(),
|
||||||
|
types: p.getTypes().map((type) => Type[type]),
|
||||||
|
teraType: p.getTeraType() !== Type.UNKNOWN ? Type[p.getTeraType()] : "",
|
||||||
|
level: p.level,
|
||||||
|
currentHP: p.hp,
|
||||||
|
maxHP: p.getMaxHp(),
|
||||||
|
status: p.status?.effect ? StatusEffect[p.status.effect] : ""
|
||||||
|
};
|
||||||
}) : [],
|
}) : [],
|
||||||
modeChain: this.ui?.getModeChain() ?? [],
|
modeChain: this.ui?.getModeChain() ?? [],
|
||||||
};
|
};
|
||||||
|
|
|
@ -5433,8 +5433,7 @@ export function initAbilities() {
|
||||||
.attr(EffectSporeAbAttr),
|
.attr(EffectSporeAbAttr),
|
||||||
new Ability(Abilities.SYNCHRONIZE, 3)
|
new Ability(Abilities.SYNCHRONIZE, 3)
|
||||||
.attr(SyncEncounterNatureAbAttr)
|
.attr(SyncEncounterNatureAbAttr)
|
||||||
.attr(SynchronizeStatusAbAttr)
|
.attr(SynchronizeStatusAbAttr),
|
||||||
.partial(), // interaction with psycho shift needs work, keeping to old Gen interaction for now
|
|
||||||
new Ability(Abilities.CLEAR_BODY, 3)
|
new Ability(Abilities.CLEAR_BODY, 3)
|
||||||
.attr(ProtectStatAbAttr)
|
.attr(ProtectStatAbAttr)
|
||||||
.ignorable(),
|
.ignorable(),
|
||||||
|
@ -6036,7 +6035,7 @@ export function initAbilities() {
|
||||||
.bypassFaint(),
|
.bypassFaint(),
|
||||||
new Ability(Abilities.CORROSION, 7)
|
new Ability(Abilities.CORROSION, 7)
|
||||||
.attr(IgnoreTypeStatusEffectImmunityAbAttr, [ StatusEffect.POISON, StatusEffect.TOXIC ], [ Type.STEEL, Type.POISON ])
|
.attr(IgnoreTypeStatusEffectImmunityAbAttr, [ StatusEffect.POISON, StatusEffect.TOXIC ], [ Type.STEEL, Type.POISON ])
|
||||||
.edgeCase(), // Should interact correctly with magic coat/bounce (not yet implemented), fling with toxic orb (not implemented yet), and synchronize (not fully implemented yet)
|
.edgeCase(), // Should interact correctly with magic coat/bounce (not yet implemented) + fling with toxic orb (not implemented yet)
|
||||||
new Ability(Abilities.COMATOSE, 7)
|
new Ability(Abilities.COMATOSE, 7)
|
||||||
.attr(UncopiableAbilityAbAttr)
|
.attr(UncopiableAbilityAbAttr)
|
||||||
.attr(UnswappableAbilityAbAttr)
|
.attr(UnswappableAbilityAbAttr)
|
||||||
|
|
|
@ -7666,7 +7666,7 @@ export function initBiomes() {
|
||||||
if (biome === Biome.END) {
|
if (biome === Biome.END) {
|
||||||
const biomeList = Object.keys(Biome).filter(key => !isNaN(Number(key)));
|
const biomeList = Object.keys(Biome).filter(key => !isNaN(Number(key)));
|
||||||
biomeList.pop(); // Removes Biome.END from the list
|
biomeList.pop(); // Removes Biome.END from the list
|
||||||
const randIndex = Utils.randInt(biomeList.length, 1); // Will never be Biome.TOWN
|
const randIndex = Utils.randSeedInt(biomeList.length, 1); // Will never be Biome.TOWN
|
||||||
biome = Biome[biomeList[randIndex]];
|
biome = Biome[biomeList[randIndex]];
|
||||||
}
|
}
|
||||||
const linkedBiomes: (Biome | [ Biome, integer ])[] = Array.isArray(biomeLinks[biome])
|
const linkedBiomes: (Biome | [ Biome, integer ])[] = Array.isArray(biomeLinks[biome])
|
||||||
|
|
|
@ -18,7 +18,7 @@ import Move, {
|
||||||
StatusCategoryOnAllyAttr
|
StatusCategoryOnAllyAttr
|
||||||
} from "#app/data/move";
|
} from "#app/data/move";
|
||||||
import { SpeciesFormChangeManualTrigger } from "#app/data/pokemon-forms";
|
import { SpeciesFormChangeManualTrigger } from "#app/data/pokemon-forms";
|
||||||
import { StatusEffect } from "#app/data/status-effect";
|
import { getStatusEffectHealText, StatusEffect } from "#app/data/status-effect";
|
||||||
import { TerrainType } from "#app/data/terrain";
|
import { TerrainType } from "#app/data/terrain";
|
||||||
import { Type } from "#app/data/type";
|
import { Type } from "#app/data/type";
|
||||||
import { WeatherType } from "#app/data/weather";
|
import { WeatherType } from "#app/data/weather";
|
||||||
|
@ -2866,6 +2866,28 @@ export class GrudgeTag extends BattlerTag {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Tag used to heal the user of Psycho Shift of its status effect if Psycho Shift succeeds in transferring its status effect to the target Pokemon
|
||||||
|
*/
|
||||||
|
export class PsychoShiftTag extends BattlerTag {
|
||||||
|
constructor() {
|
||||||
|
super(BattlerTagType.PSYCHO_SHIFT, BattlerTagLapseType.AFTER_MOVE, 1, Moves.PSYCHO_SHIFT);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Heals Psycho Shift's user of its status effect after it uses a move
|
||||||
|
* @returns `false` to expire the tag immediately
|
||||||
|
*/
|
||||||
|
override lapse(pokemon: Pokemon, _lapseType: BattlerTagLapseType): boolean {
|
||||||
|
if (pokemon.status && pokemon.isActive(true)) {
|
||||||
|
pokemon.scene.queueMessage(getStatusEffectHealText(pokemon.status.effect, getPokemonNameWithAffix(pokemon)));
|
||||||
|
pokemon.resetStatus();
|
||||||
|
pokemon.updateInfo();
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Retrieves a {@linkcode BattlerTag} based on the provided tag type, turn count, source move, and source ID.
|
* Retrieves a {@linkcode BattlerTag} based on the provided tag type, turn count, source move, and source ID.
|
||||||
* @param sourceId - The ID of the pokemon adding the tag
|
* @param sourceId - The ID of the pokemon adding the tag
|
||||||
|
@ -3049,6 +3071,8 @@ export function getBattlerTag(tagType: BattlerTagType, turnCount: number, source
|
||||||
return new PowerTrickTag(sourceMove, sourceId);
|
return new PowerTrickTag(sourceMove, sourceId);
|
||||||
case BattlerTagType.GRUDGE:
|
case BattlerTagType.GRUDGE:
|
||||||
return new GrudgeTag();
|
return new GrudgeTag();
|
||||||
|
case BattlerTagType.PSYCHO_SHIFT:
|
||||||
|
return new PsychoShiftTag();
|
||||||
case BattlerTagType.NONE:
|
case BattlerTagType.NONE:
|
||||||
default:
|
default:
|
||||||
return new BattlerTag(tagType, BattlerTagLapseType.CUSTOM, turnCount, sourceMove, sourceId);
|
return new BattlerTag(tagType, BattlerTagLapseType.CUSTOM, turnCount, sourceMove, sourceId);
|
||||||
|
|
|
@ -2270,24 +2270,26 @@ export class PsychoShiftEffectAttr extends MoveEffectAttr {
|
||||||
super(false, { trigger: MoveEffectTrigger.HIT });
|
super(false, { trigger: MoveEffectTrigger.HIT });
|
||||||
}
|
}
|
||||||
|
|
||||||
apply(user: Pokemon, target: Pokemon, move: Move, args: any[]): boolean {
|
/**
|
||||||
|
* Applies the effect of Psycho Shift to its target
|
||||||
|
* Psycho Shift takes the user's status effect and passes it onto the target. The user is then healed after the move has been successfully executed.
|
||||||
|
* @returns `true` if Psycho Shift's effect is able to be applied to the target
|
||||||
|
*/
|
||||||
|
apply(user: Pokemon, target: Pokemon, _move: Move, _args: any[]): boolean {
|
||||||
const statusToApply: StatusEffect | undefined = user.status?.effect ?? (user.hasAbility(Abilities.COMATOSE) ? StatusEffect.SLEEP : undefined);
|
const statusToApply: StatusEffect | undefined = user.status?.effect ?? (user.hasAbility(Abilities.COMATOSE) ? StatusEffect.SLEEP : undefined);
|
||||||
|
|
||||||
if (target.status) {
|
if (target.status) {
|
||||||
return false;
|
return false;
|
||||||
} else {
|
} else {
|
||||||
const canSetStatus = target.canSetStatus(statusToApply, true, false, user);
|
const canSetStatus = target.canSetStatus(statusToApply, true, false, user);
|
||||||
|
const trySetStatus = canSetStatus ? target.trySetStatus(statusToApply, true, user) : false;
|
||||||
|
|
||||||
if (canSetStatus) {
|
if (trySetStatus && user.status) {
|
||||||
if (user.status) {
|
// PsychoShiftTag is added to the user if move succeeds so that the user is healed of its status effect after its move
|
||||||
user.scene.queueMessage(getStatusEffectHealText(user.status.effect, getPokemonNameWithAffix(user)));
|
user.addTag(BattlerTagType.PSYCHO_SHIFT);
|
||||||
}
|
|
||||||
user.resetStatus();
|
|
||||||
user.updateInfo();
|
|
||||||
target.trySetStatus(statusToApply, true, user);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return canSetStatus;
|
return trySetStatus;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
|
import { NumberHolder } from "#app/utils";
|
||||||
import { PokeballType } from "#enums/pokeball";
|
import { PokeballType } from "#enums/pokeball";
|
||||||
import BattleScene from "../battle-scene";
|
import BattleScene from "../battle-scene";
|
||||||
import i18next from "i18next";
|
import i18next from "i18next";
|
||||||
|
@ -82,11 +83,38 @@ export function getPokeballTintColor(type: PokeballType): number {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function doPokeballBounceAnim(scene: BattleScene, pokeball: Phaser.GameObjects.Sprite, y1: number, y2: number, baseBounceDuration: integer, callback: Function) {
|
/**
|
||||||
|
* Gets the critical capture chance based on number of mons registered in Dex and modified {@link https://bulbapedia.bulbagarden.net/wiki/Catch_rate Catch rate}
|
||||||
|
* Formula from {@link https://www.dragonflycave.com/mechanics/gen-vi-vii-capturing Dragonfly Cave Gen 6 Capture Mechanics page}
|
||||||
|
* @param scene {@linkcode BattleScene} current BattleScene
|
||||||
|
* @param modifiedCatchRate the modified catch rate as calculated in {@linkcode AttemptCapturePhase}
|
||||||
|
* @returns the chance of getting a critical capture, out of 256
|
||||||
|
*/
|
||||||
|
export function getCriticalCaptureChance(scene: BattleScene, modifiedCatchRate: number): number {
|
||||||
|
if (scene.gameMode.isFreshStartChallenge()) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
const dexCount = scene.gameData.getSpeciesCount(d => !!d.caughtAttr);
|
||||||
|
const catchingCharmMultiplier = new NumberHolder(1);
|
||||||
|
//scene.findModifier(m => m instanceof CriticalCatchChanceBoosterModifier)?.apply(catchingCharmMultiplier);
|
||||||
|
const dexMultiplier = scene.gameMode.isDaily || dexCount > 800 ? 2.5
|
||||||
|
: dexCount > 600 ? 2
|
||||||
|
: dexCount > 400 ? 1.5
|
||||||
|
: dexCount > 200 ? 1
|
||||||
|
: dexCount > 100 ? 0.5
|
||||||
|
: 0;
|
||||||
|
return Math.floor(catchingCharmMultiplier.value * dexMultiplier * Math.min(255, modifiedCatchRate) / 6);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function doPokeballBounceAnim(scene: BattleScene, pokeball: Phaser.GameObjects.Sprite, y1: number, y2: number, baseBounceDuration: number, callback: Function, isCritical: boolean = false) {
|
||||||
let bouncePower = 1;
|
let bouncePower = 1;
|
||||||
let bounceYOffset = y1;
|
let bounceYOffset = y1;
|
||||||
let bounceY = y2;
|
let bounceY = y2;
|
||||||
const yd = y2 - y1;
|
const yd = y2 - y1;
|
||||||
|
const x0 = pokeball.x;
|
||||||
|
const x1 = x0 + 3;
|
||||||
|
const x2 = x0 - 3;
|
||||||
|
let critShakes = 4;
|
||||||
|
|
||||||
const doBounce = () => {
|
const doBounce = () => {
|
||||||
scene.tweens.add({
|
scene.tweens.add({
|
||||||
|
@ -117,5 +145,40 @@ export function doPokeballBounceAnim(scene: BattleScene, pokeball: Phaser.GameOb
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const doCritShake = () => {
|
||||||
|
scene.tweens.add({
|
||||||
|
targets: pokeball,
|
||||||
|
x: x2,
|
||||||
|
duration: 125,
|
||||||
|
ease: "Linear",
|
||||||
|
onComplete: () => {
|
||||||
|
scene.tweens.add({
|
||||||
|
targets: pokeball,
|
||||||
|
x: x1,
|
||||||
|
duration: 125,
|
||||||
|
ease: "Linear",
|
||||||
|
onComplete: () => {
|
||||||
|
critShakes--;
|
||||||
|
if (critShakes > 0) {
|
||||||
|
doCritShake();
|
||||||
|
} else {
|
||||||
|
scene.tweens.add({
|
||||||
|
targets: pokeball,
|
||||||
|
x: x0,
|
||||||
|
duration: 60,
|
||||||
|
ease: "Linear",
|
||||||
|
onComplete: () => scene.time.delayedCall(500, doBounce)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
if (isCritical) {
|
||||||
|
scene.time.delayedCall(500, doCritShake);
|
||||||
|
} else {
|
||||||
doBounce();
|
doBounce();
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -90,5 +90,6 @@ export enum BattlerTagType {
|
||||||
ELECTRIFIED = "ELECTRIFIED",
|
ELECTRIFIED = "ELECTRIFIED",
|
||||||
TELEKINESIS = "TELEKINESIS",
|
TELEKINESIS = "TELEKINESIS",
|
||||||
COMMANDED = "COMMANDED",
|
COMMANDED = "COMMANDED",
|
||||||
GRUDGE = "GRUDGE"
|
GRUDGE = "GRUDGE",
|
||||||
|
PSYCHO_SHIFT = "PSYCHO_SHIFT",
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,7 +2,7 @@ import { BattlerIndex } from "#app/battle";
|
||||||
import BattleScene from "#app/battle-scene";
|
import BattleScene from "#app/battle-scene";
|
||||||
import { PLAYER_PARTY_MAX_SIZE } from "#app/constants";
|
import { PLAYER_PARTY_MAX_SIZE } from "#app/constants";
|
||||||
import { SubstituteTag } from "#app/data/battler-tags";
|
import { SubstituteTag } from "#app/data/battler-tags";
|
||||||
import { doPokeballBounceAnim, getPokeballAtlasKey, getPokeballCatchMultiplier, getPokeballTintColor } from "#app/data/pokeball";
|
import { doPokeballBounceAnim, getPokeballAtlasKey, getPokeballCatchMultiplier, getPokeballTintColor, getCriticalCaptureChance } from "#app/data/pokeball";
|
||||||
import { getStatusEffectCatchRateMultiplier } from "#app/data/status-effect";
|
import { getStatusEffectCatchRateMultiplier } from "#app/data/status-effect";
|
||||||
import { addPokeballCaptureStars, addPokeballOpenParticles } from "#app/field/anims";
|
import { addPokeballCaptureStars, addPokeballOpenParticles } from "#app/field/anims";
|
||||||
import { EnemyPokemon } from "#app/field/pokemon";
|
import { EnemyPokemon } from "#app/field/pokemon";
|
||||||
|
@ -52,8 +52,10 @@ export class AttemptCapturePhase extends PokemonPhase {
|
||||||
const catchRate = pokemon.species.catchRate;
|
const catchRate = pokemon.species.catchRate;
|
||||||
const pokeballMultiplier = getPokeballCatchMultiplier(this.pokeballType);
|
const pokeballMultiplier = getPokeballCatchMultiplier(this.pokeballType);
|
||||||
const statusMultiplier = pokemon.status ? getStatusEffectCatchRateMultiplier(pokemon.status.effect) : 1;
|
const statusMultiplier = pokemon.status ? getStatusEffectCatchRateMultiplier(pokemon.status.effect) : 1;
|
||||||
const x = Math.round((((_3m - _2h) * catchRate * pokeballMultiplier) / _3m) * statusMultiplier);
|
const modifiedCatchRate = Math.round((((_3m - _2h) * catchRate * pokeballMultiplier) / _3m) * statusMultiplier);
|
||||||
const y = Math.round(65536 / Math.sqrt(Math.sqrt(255 / x)));
|
const shakeProbability = Math.round(65536 / Math.pow((255 / modifiedCatchRate), 0.1875)); // Formula taken from gen 6
|
||||||
|
const criticalCaptureChance = getCriticalCaptureChance(this.scene, modifiedCatchRate);
|
||||||
|
const isCritical = pokemon.randSeedInt(256) < criticalCaptureChance;
|
||||||
const fpOffset = pokemon.getFieldPositionOffset();
|
const fpOffset = pokemon.getFieldPositionOffset();
|
||||||
|
|
||||||
const pokeballAtlasKey = getPokeballAtlasKey(this.pokeballType);
|
const pokeballAtlasKey = getPokeballAtlasKey(this.pokeballType);
|
||||||
|
@ -61,17 +63,19 @@ export class AttemptCapturePhase extends PokemonPhase {
|
||||||
this.pokeball.setOrigin(0.5, 0.625);
|
this.pokeball.setOrigin(0.5, 0.625);
|
||||||
this.scene.field.add(this.pokeball);
|
this.scene.field.add(this.pokeball);
|
||||||
|
|
||||||
this.scene.playSound("se/pb_throw");
|
this.scene.playSound("se/pb_throw", isCritical ? { rate: 0.2 } : undefined); // Crit catch throws are higher pitched
|
||||||
this.scene.time.delayedCall(300, () => {
|
this.scene.time.delayedCall(300, () => {
|
||||||
this.scene.field.moveBelow(this.pokeball as Phaser.GameObjects.GameObject, pokemon);
|
this.scene.field.moveBelow(this.pokeball as Phaser.GameObjects.GameObject, pokemon);
|
||||||
});
|
});
|
||||||
|
|
||||||
this.scene.tweens.add({
|
this.scene.tweens.add({
|
||||||
|
// Throw animation
|
||||||
targets: this.pokeball,
|
targets: this.pokeball,
|
||||||
x: { value: 236 + fpOffset[0], ease: "Linear" },
|
x: { value: 236 + fpOffset[0], ease: "Linear" },
|
||||||
y: { value: 16 + fpOffset[1], ease: "Cubic.easeOut" },
|
y: { value: 16 + fpOffset[1], ease: "Cubic.easeOut" },
|
||||||
duration: 500,
|
duration: 500,
|
||||||
onComplete: () => {
|
onComplete: () => {
|
||||||
|
// Ball opens
|
||||||
this.pokeball.setTexture("pb", `${pokeballAtlasKey}_opening`);
|
this.pokeball.setTexture("pb", `${pokeballAtlasKey}_opening`);
|
||||||
this.scene.time.delayedCall(17, () => this.pokeball.setTexture("pb", `${pokeballAtlasKey}_open`));
|
this.scene.time.delayedCall(17, () => this.pokeball.setTexture("pb", `${pokeballAtlasKey}_open`));
|
||||||
this.scene.playSound("se/pb_rel");
|
this.scene.playSound("se/pb_rel");
|
||||||
|
@ -80,30 +84,33 @@ export class AttemptCapturePhase extends PokemonPhase {
|
||||||
addPokeballOpenParticles(this.scene, this.pokeball.x, this.pokeball.y, this.pokeballType);
|
addPokeballOpenParticles(this.scene, this.pokeball.x, this.pokeball.y, this.pokeballType);
|
||||||
|
|
||||||
this.scene.tweens.add({
|
this.scene.tweens.add({
|
||||||
|
// Mon enters ball
|
||||||
targets: pokemon,
|
targets: pokemon,
|
||||||
duration: 500,
|
duration: 500,
|
||||||
ease: "Sine.easeIn",
|
ease: "Sine.easeIn",
|
||||||
scale: 0.25,
|
scale: 0.25,
|
||||||
y: 20,
|
y: 20,
|
||||||
onComplete: () => {
|
onComplete: () => {
|
||||||
|
// Ball closes
|
||||||
this.pokeball.setTexture("pb", `${pokeballAtlasKey}_opening`);
|
this.pokeball.setTexture("pb", `${pokeballAtlasKey}_opening`);
|
||||||
pokemon.setVisible(false);
|
pokemon.setVisible(false);
|
||||||
this.scene.playSound("se/pb_catch");
|
this.scene.playSound("se/pb_catch");
|
||||||
this.scene.time.delayedCall(17, () => this.pokeball.setTexture("pb", `${pokeballAtlasKey}`));
|
this.scene.time.delayedCall(17, () => this.pokeball.setTexture("pb", `${pokeballAtlasKey}`));
|
||||||
|
|
||||||
const doShake = () => {
|
const doShake = () => {
|
||||||
|
// After the overall catch rate check, the game does 3 shake checks before confirming the catch.
|
||||||
let shakeCount = 0;
|
let shakeCount = 0;
|
||||||
const pbX = this.pokeball.x;
|
const pbX = this.pokeball.x;
|
||||||
const shakeCounter = this.scene.tweens.addCounter({
|
const shakeCounter = this.scene.tweens.addCounter({
|
||||||
from: 0,
|
from: 0,
|
||||||
to: 1,
|
to: 1,
|
||||||
repeat: 4,
|
repeat: isCritical ? 2 : 4, // Critical captures only perform 1 shake check
|
||||||
yoyo: true,
|
yoyo: true,
|
||||||
ease: "Cubic.easeOut",
|
ease: "Cubic.easeOut",
|
||||||
duration: 250,
|
duration: 250,
|
||||||
repeatDelay: 500,
|
repeatDelay: 500,
|
||||||
onUpdate: t => {
|
onUpdate: t => {
|
||||||
if (shakeCount && shakeCount < 4) {
|
if (shakeCount && shakeCount < (isCritical ? 2 : 4)) {
|
||||||
const value = t.getValue();
|
const value = t.getValue();
|
||||||
const directionMultiplier = shakeCount % 2 === 1 ? 1 : -1;
|
const directionMultiplier = shakeCount % 2 === 1 ? 1 : -1;
|
||||||
this.pokeball.setX(pbX + value * 4 * directionMultiplier);
|
this.pokeball.setX(pbX + value * 4 * directionMultiplier);
|
||||||
|
@ -114,13 +121,18 @@ export class AttemptCapturePhase extends PokemonPhase {
|
||||||
if (!pokemon.species.isObtainable()) {
|
if (!pokemon.species.isObtainable()) {
|
||||||
shakeCounter.stop();
|
shakeCounter.stop();
|
||||||
this.failCatch(shakeCount);
|
this.failCatch(shakeCount);
|
||||||
} else if (shakeCount++ < 3) {
|
} else if (shakeCount++ < (isCritical ? 1 : 3)) {
|
||||||
if (pokeballMultiplier === -1 || pokemon.randSeedInt(65536) < y) {
|
// Shake check (skip check for critical or guaranteed captures, but still play the sound)
|
||||||
|
if (pokeballMultiplier === -1 || isCritical || modifiedCatchRate >= 255 || pokemon.randSeedInt(65536) < shakeProbability) {
|
||||||
this.scene.playSound("se/pb_move");
|
this.scene.playSound("se/pb_move");
|
||||||
} else {
|
} else {
|
||||||
shakeCounter.stop();
|
shakeCounter.stop();
|
||||||
this.failCatch(shakeCount);
|
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 {
|
} else {
|
||||||
this.scene.playSound("se/pb_lock");
|
this.scene.playSound("se/pb_lock");
|
||||||
addPokeballCaptureStars(this.scene, this.pokeball);
|
addPokeballCaptureStars(this.scene, this.pokeball);
|
||||||
|
@ -153,7 +165,8 @@ export class AttemptCapturePhase extends PokemonPhase {
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
this.scene.time.delayedCall(250, () => doPokeballBounceAnim(this.scene, this.pokeball, 16, 72, 350, doShake));
|
// Ball bounces (handled in pokemon.ts)
|
||||||
|
this.scene.time.delayedCall(250, () => doPokeballBounceAnim(this.scene, this.pokeball, 16, 72, 350, doShake, isCritical));
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
|
@ -32,6 +32,8 @@ export class CommandPhase extends FieldPhase {
|
||||||
start() {
|
start() {
|
||||||
super.start();
|
super.start();
|
||||||
|
|
||||||
|
this.scene.updateGameInfo();
|
||||||
|
|
||||||
const commandUiHandler = this.scene.ui.handlers[Mode.COMMAND];
|
const commandUiHandler = this.scene.ui.handlers[Mode.COMMAND];
|
||||||
if (commandUiHandler) {
|
if (commandUiHandler) {
|
||||||
if (this.scene.currentBattle.turn === 1 || commandUiHandler.getCursor() === Command.POKEMON) {
|
if (this.scene.currentBattle.turn === 1 || commandUiHandler.getCursor() === Command.POKEMON) {
|
||||||
|
|
|
@ -0,0 +1,46 @@
|
||||||
|
import { Abilities } from "#enums/abilities";
|
||||||
|
import { Moves } from "#enums/moves";
|
||||||
|
import { Species } from "#enums/species";
|
||||||
|
import GameManager from "#test/utils/gameManager";
|
||||||
|
import Phaser from "phaser";
|
||||||
|
import { afterEach, beforeAll, beforeEach, describe, expect, it } from "vitest";
|
||||||
|
|
||||||
|
describe("Abilities - Corrosion", () => {
|
||||||
|
let phaserGame: Phaser.Game;
|
||||||
|
let game: GameManager;
|
||||||
|
|
||||||
|
beforeAll(() => {
|
||||||
|
phaserGame = new Phaser.Game({
|
||||||
|
type: Phaser.HEADLESS,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
game.phaseInterceptor.restoreOg();
|
||||||
|
});
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
game = new GameManager(phaserGame);
|
||||||
|
game.override
|
||||||
|
.moveset([ Moves.SPLASH ])
|
||||||
|
.battleType("single")
|
||||||
|
.disableCrits()
|
||||||
|
.enemySpecies(Species.GRIMER)
|
||||||
|
.enemyAbility(Abilities.CORROSION)
|
||||||
|
.enemyMoveset(Moves.TOXIC);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("If a Poison- or Steel-type Pokémon with this Ability poisons a target with Synchronize, Synchronize does not gain the ability to poison Poison- or Steel-type Pokémon.", async () => {
|
||||||
|
game.override.ability(Abilities.SYNCHRONIZE);
|
||||||
|
await game.classicMode.startBattle([ Species.FEEBAS ]);
|
||||||
|
|
||||||
|
const playerPokemon = game.scene.getPlayerPokemon();
|
||||||
|
const enemyPokemon = game.scene.getEnemyPokemon();
|
||||||
|
expect(playerPokemon!.status).toBeUndefined();
|
||||||
|
|
||||||
|
game.move.select(Moves.SPLASH);
|
||||||
|
await game.phaseInterceptor.to("BerryPhase");
|
||||||
|
expect(playerPokemon!.status).toBeDefined();
|
||||||
|
expect(enemyPokemon!.status).toBeUndefined();
|
||||||
|
});
|
||||||
|
});
|
|
@ -94,16 +94,4 @@ describe("Abilities - Synchronize", () => {
|
||||||
expect(game.scene.getEnemyPokemon()!.status?.effect).toBe(StatusEffect.PARALYSIS);
|
expect(game.scene.getEnemyPokemon()!.status?.effect).toBe(StatusEffect.PARALYSIS);
|
||||||
expect(game.phaseInterceptor.log).toContain("ShowAbilityPhase");
|
expect(game.phaseInterceptor.log).toContain("ShowAbilityPhase");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should activate with Psycho Shift after the move clears the status", async () => {
|
|
||||||
game.override.statusEffect(StatusEffect.PARALYSIS);
|
|
||||||
await game.classicMode.startBattle();
|
|
||||||
|
|
||||||
game.move.select(Moves.PSYCHO_SHIFT);
|
|
||||||
await game.phaseInterceptor.to("BerryPhase");
|
|
||||||
|
|
||||||
expect(game.scene.getPlayerPokemon()!.status?.effect).toBe(StatusEffect.PARALYSIS); // keeping old gen < V impl for now since it's buggy otherwise
|
|
||||||
expect(game.scene.getEnemyPokemon()!.status?.effect).toBe(StatusEffect.PARALYSIS);
|
|
||||||
expect(game.phaseInterceptor.log).toContain("ShowAbilityPhase");
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
|
@ -0,0 +1,49 @@
|
||||||
|
import { StatusEffect } from "#app/enums/status-effect";
|
||||||
|
import { Abilities } from "#enums/abilities";
|
||||||
|
import { Moves } from "#enums/moves";
|
||||||
|
import { Species } from "#enums/species";
|
||||||
|
import GameManager from "#test/utils/gameManager";
|
||||||
|
import Phaser from "phaser";
|
||||||
|
import { afterEach, beforeAll, beforeEach, describe, expect, it } from "vitest";
|
||||||
|
|
||||||
|
describe("Moves - Psycho Shift", () => {
|
||||||
|
let phaserGame: Phaser.Game;
|
||||||
|
let game: GameManager;
|
||||||
|
|
||||||
|
beforeAll(() => {
|
||||||
|
phaserGame = new Phaser.Game({
|
||||||
|
type: Phaser.HEADLESS,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
game.phaseInterceptor.restoreOg();
|
||||||
|
});
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
game = new GameManager(phaserGame);
|
||||||
|
game.override
|
||||||
|
.moveset([ Moves.PSYCHO_SHIFT ])
|
||||||
|
.ability(Abilities.BALL_FETCH)
|
||||||
|
.statusEffect(StatusEffect.POISON)
|
||||||
|
.battleType("single")
|
||||||
|
.disableCrits()
|
||||||
|
.enemySpecies(Species.MAGIKARP)
|
||||||
|
.enemyLevel(20)
|
||||||
|
.enemyAbility(Abilities.SYNCHRONIZE)
|
||||||
|
.enemyMoveset(Moves.SPLASH);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("If Psycho Shift is used on a Pokémon with Synchronize, the user of Psycho Shift will already be afflicted with a status condition when Synchronize activates", async () => {
|
||||||
|
await game.classicMode.startBattle([ Species.FEEBAS ]);
|
||||||
|
|
||||||
|
const playerPokemon = game.scene.getPlayerPokemon();
|
||||||
|
const enemyPokemon = game.scene.getEnemyPokemon();
|
||||||
|
expect(enemyPokemon?.status).toBeUndefined();
|
||||||
|
|
||||||
|
game.move.select(Moves.PSYCHO_SHIFT);
|
||||||
|
await game.phaseInterceptor.to("TurnEndPhase");
|
||||||
|
expect(playerPokemon?.status).toBeNull();
|
||||||
|
expect(enemyPokemon?.status).toBeDefined();
|
||||||
|
});
|
||||||
|
});
|
Loading…
Reference in New Issue