[Move] Implement Shed Tail (#4382)

* Implement Shed Tail

* Fix leftover batonPass reference in docs

* Fix ChillyReceptionAttr

* oops

* Remove unneeded default arg in ReturnPhase

* Fix imports per Kev's suggestions

Co-authored-by: NightKev <34855794+DayKev@users.noreply.github.com>

* Docs and Shed Tail on-add message

* Remove mixin attribute

* Update battler-tags.json

* Update battler-tags.json

* Update battler-tags.json

* Update battler-tags.json

* Update battler-tags.json

* Update battler-tags.json

* Update battler-tags.json

* Update battler-tags.json

* Update battler-tags.json

* Update battler-tags.json

* Fix indents

* More nit fixes

* Make Switch[Summon]Phase params readonly

---------

Co-authored-by: NightKev <34855794+DayKev@users.noreply.github.com>
Co-authored-by: Lugiad <adrien.grivel@hotmail.fr>
Co-authored-by: flx-sta <50131232+flx-sta@users.noreply.github.com>
This commit is contained in:
innerthunder 2024-09-25 17:17:46 -07:00 committed by GitHub
parent 70c6edfaed
commit a6a61b2984
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
23 changed files with 216 additions and 108 deletions

View File

@ -2311,6 +2311,11 @@ export class AutotomizedTag extends BattlerTag {
} }
} }
/**
* Tag implementing the {@link https://bulbapedia.bulbagarden.net/wiki/Substitute_(doll)#Effect | Substitute Doll} effect,
* for use with the moves Substitute and Shed Tail. Pokemon with this tag deflect most forms of received attack damage
* onto the tag. This tag also grants immunity to most Status moves and several move effects.
*/
export class SubstituteTag extends BattlerTag { export class SubstituteTag extends BattlerTag {
/** The substitute's remaining HP. If HP is depleted, the Substitute fades. */ /** The substitute's remaining HP. If HP is depleted, the Substitute fades. */
public hp: number; public hp: number;
@ -2330,7 +2335,11 @@ export class SubstituteTag extends BattlerTag {
// Queue battle animation and message // Queue battle animation and message
pokemon.scene.triggerPokemonBattleAnim(pokemon, PokemonAnimType.SUBSTITUTE_ADD); pokemon.scene.triggerPokemonBattleAnim(pokemon, PokemonAnimType.SUBSTITUTE_ADD);
pokemon.scene.queueMessage(i18next.t("battlerTags:substituteOnAdd", { pokemonNameWithAffix: getPokemonNameWithAffix(pokemon) }), 1500); if (this.sourceMove === Moves.SHED_TAIL) {
pokemon.scene.queueMessage(i18next.t("battlerTags:shedTailOnAdd", { pokemonNameWithAffix: getPokemonNameWithAffix(pokemon) }), 1500);
} else {
pokemon.scene.queueMessage(i18next.t("battlerTags:substituteOnAdd", { pokemonNameWithAffix: getPokemonNameWithAffix(pokemon) }), 1500);
}
// Remove any binding effects from the user // Remove any binding effects from the user
pokemon.findAndRemoveTags(tag => tag instanceof DamagingTrapTag); pokemon.findAndRemoveTags(tag => tag instanceof DamagingTrapTag);

View File

@ -37,6 +37,7 @@ import { SwitchSummonPhase } from "#app/phases/switch-summon-phase";
import { SpeciesFormChangeRevertWeatherFormTrigger } from "./pokemon-forms"; import { SpeciesFormChangeRevertWeatherFormTrigger } from "./pokemon-forms";
import { GameMode } from "#app/game-mode"; import { GameMode } from "#app/game-mode";
import { applyChallenges, ChallengeType } from "./challenge"; import { applyChallenges, ChallengeType } from "./challenge";
import { SwitchType } from "#enums/switch-type";
export enum MoveCategory { export enum MoveCategory {
PHYSICAL, PHYSICAL,
@ -1476,8 +1477,13 @@ export class HalfSacrificialAttr extends MoveEffectAttr {
* @see {@linkcode apply} * @see {@linkcode apply}
*/ */
export class AddSubstituteAttr extends MoveEffectAttr { export class AddSubstituteAttr extends MoveEffectAttr {
constructor() { /** The ratio of the user's max HP that is required to apply this effect */
private hpCost: number;
constructor(hpCost: number = 0.25) {
super(true); super(true);
this.hpCost = hpCost;
} }
/** /**
@ -1493,8 +1499,7 @@ export class AddSubstituteAttr extends MoveEffectAttr {
return false; return false;
} }
const hpCost = Math.floor(user.getMaxHp() / 4); user.damageAndUpdate(Math.floor(user.getMaxHp() * this.hpCost), HitResult.OTHER, false, true, true);
user.damageAndUpdate(hpCost, HitResult.OTHER, false, true, true);
user.addTag(BattlerTagType.SUBSTITUTE, 0, move.id, user.id); user.addTag(BattlerTagType.SUBSTITUTE, 0, move.id, user.id);
return true; return true;
} }
@ -1507,7 +1512,7 @@ export class AddSubstituteAttr extends MoveEffectAttr {
} }
getCondition(): MoveConditionFunc { getCondition(): MoveConditionFunc {
return (user, target, move) => !user.getTag(SubstituteTag) && user.hp > Math.floor(user.getMaxHp() / 4) && user.getMaxHp() > 1; return (user, target, move) => !user.getTag(SubstituteTag) && user.hp > Math.floor(user.getMaxHp() * this.hpCost) && user.getMaxHp() > 1;
} }
getFailedText(user: Pokemon, target: Pokemon, move: Move, cancelled: Utils.BooleanHolder): string | null { getFailedText(user: Pokemon, target: Pokemon, move: Move, cancelled: Utils.BooleanHolder): string | null {
@ -5151,9 +5156,9 @@ export class RevivalBlessingAttr extends MoveEffectAttr {
if (user.scene.currentBattle.double && user.scene.getEnemyParty().length > 1) { if (user.scene.currentBattle.double && user.scene.getEnemyParty().length > 1) {
const allyPokemon = user.getAlly(); const allyPokemon = user.getAlly();
if (slotIndex<=1) { if (slotIndex<=1) {
user.scene.unshiftPhase(new SwitchSummonPhase(user.scene, pokemon.getFieldIndex(), slotIndex, false, false, false)); user.scene.unshiftPhase(new SwitchSummonPhase(user.scene, SwitchType.SWITCH, pokemon.getFieldIndex(), slotIndex, false, false));
} else if (allyPokemon.isFainted()) { } else if (allyPokemon.isFainted()) {
user.scene.unshiftPhase(new SwitchSummonPhase(user.scene, allyPokemon.getFieldIndex(), slotIndex, false, false, false)); user.scene.unshiftPhase(new SwitchSummonPhase(user.scene, SwitchType.SWITCH, allyPokemon.getFieldIndex(), slotIndex, false, false));
} }
} }
resolve(true); resolve(true);
@ -5176,72 +5181,69 @@ export class RevivalBlessingAttr extends MoveEffectAttr {
export class ForceSwitchOutAttr extends MoveEffectAttr { export class ForceSwitchOutAttr extends MoveEffectAttr {
constructor( constructor(
private selfSwitch: boolean = false, private selfSwitch: boolean = false,
private batonPass: boolean = false private switchType: SwitchType = SwitchType.SWITCH
) { ) {
super(false, MoveEffectTrigger.POST_APPLY, false, true); super(false, MoveEffectTrigger.POST_APPLY, false, true);
} }
isBatonPass() { isBatonPass() {
return this.batonPass; return this.switchType === SwitchType.BATON_PASS;
} }
// TODO: Why is this a Promise? apply(user: Pokemon, target: Pokemon, move: Move, args: any[]): boolean {
apply(user: Pokemon, target: Pokemon, move: Move, args: any[]): Promise<boolean> { // Check if the move category is not STATUS or if the switch out condition is not met
return new Promise(resolve => { if (!this.getSwitchOutCondition()(user, target, move)) {
return false;
}
if (!this.getSwitchOutCondition()(user, target, move)) { /**
return resolve(false); * Move the switch out logic inside the conditional block
* This ensures that the switch out only happens when the conditions are met
*/
const switchOutTarget = this.selfSwitch ? user : target;
if (switchOutTarget instanceof PlayerPokemon) {
switchOutTarget.leaveField(this.switchType === SwitchType.SWITCH);
if (switchOutTarget.hp > 0) {
user.scene.prependToPhase(new SwitchPhase(user.scene, this.switchType, switchOutTarget.getFieldIndex(), true, true), MoveEndPhase);
return true;
}
return false;
} else if (user.scene.currentBattle.battleType !== BattleType.WILD) {
// Switch out logic for trainer battles
switchOutTarget.leaveField(this.switchType === SwitchType.SWITCH);
if (switchOutTarget.hp > 0) {
// for opponent switching out
user.scene.prependToPhase(new SwitchSummonPhase(user.scene, this.switchType, switchOutTarget.getFieldIndex(),
(user.scene.currentBattle.trainer ? user.scene.currentBattle.trainer.getNextSummonIndex((switchOutTarget as EnemyPokemon).trainerSlot) : 0),
false, false), MoveEndPhase);
}
} else {
// Switch out logic for everything else (eg: WILD battles)
switchOutTarget.leaveField(false);
if (switchOutTarget.hp) {
user.scene.queueMessage(i18next.t("moveTriggers:fled", {pokemonName: getPokemonNameWithAffix(switchOutTarget)}), null, true, 500);
// in double battles redirect potential moves off fled pokemon
if (switchOutTarget.scene.currentBattle.double) {
const allyPokemon = switchOutTarget.getAlly();
switchOutTarget.scene.redirectPokemonMoves(switchOutTarget, allyPokemon);
}
} }
// Move the switch out logic inside the conditional block if (!switchOutTarget.getAlly()?.isActive(true)) {
// This ensures that the switch out only happens when the conditions are met user.scene.clearEnemyHeldItemModifiers();
const switchOutTarget = this.selfSwitch ? user : target;
if (switchOutTarget instanceof PlayerPokemon) {
switchOutTarget.leaveField(!this.batonPass);
if (switchOutTarget.hp > 0) {
user.scene.prependToPhase(new SwitchPhase(user.scene, switchOutTarget.getFieldIndex(), true, true), MoveEndPhase);
resolve(true);
} else {
resolve(false);
}
return;
} else if (user.scene.currentBattle.battleType !== BattleType.WILD) {
// Switch out logic for trainer battles
switchOutTarget.leaveField(!this.batonPass);
if (switchOutTarget.hp > 0) {
// for opponent switching out
user.scene.prependToPhase(new SwitchSummonPhase(user.scene, switchOutTarget.getFieldIndex(),
(user.scene.currentBattle.trainer ? user.scene.currentBattle.trainer.getNextSummonIndex((switchOutTarget as EnemyPokemon).trainerSlot) : 0),
false, this.batonPass, false), MoveEndPhase);
}
} else {
// Switch out logic for everything else (eg: WILD battles)
switchOutTarget.leaveField(false);
if (switchOutTarget.hp) { if (switchOutTarget.hp) {
user.scene.queueMessage(i18next.t("moveTriggers:fled", {pokemonName: getPokemonNameWithAffix(switchOutTarget)}), null, true, 500); user.scene.pushPhase(new BattleEndPhase(user.scene));
user.scene.pushPhase(new NewBattlePhase(user.scene));
// in double battles redirect potential moves off fled pokemon
if (switchOutTarget.scene.currentBattle.double) {
const allyPokemon = switchOutTarget.getAlly();
switchOutTarget.scene.redirectPokemonMoves(switchOutTarget, allyPokemon);
}
}
if (!switchOutTarget.getAlly()?.isActive(true)) {
user.scene.clearEnemyHeldItemModifiers();
if (switchOutTarget.hp) {
user.scene.pushPhase(new BattleEndPhase(user.scene));
user.scene.pushPhase(new NewBattlePhase(user.scene));
}
} }
} }
}
resolve(true); return true;
});
} }
getCondition(): MoveConditionFunc { getCondition(): MoveConditionFunc {
@ -5270,7 +5272,7 @@ export class ForceSwitchOutAttr extends MoveEffectAttr {
} }
if (!player && user.scene.currentBattle.battleType === BattleType.WILD) { if (!player && user.scene.currentBattle.battleType === BattleType.WILD) {
if (this.batonPass) { if (this.isBatonPass()) {
return false; return false;
} }
// Don't allow wild opponents to flee on the boss stage since it can ruin a run early on // Don't allow wild opponents to flee on the boss stage since it can ruin a run early on
@ -5291,7 +5293,7 @@ export class ForceSwitchOutAttr extends MoveEffectAttr {
return -20; return -20;
} }
let ret = this.selfSwitch ? Math.floor((1 - user.getHpRatio()) * 20) : super.getUserBenefitScore(user, target, move); let ret = this.selfSwitch ? Math.floor((1 - user.getHpRatio()) * 20) : super.getUserBenefitScore(user, target, move);
if (this.selfSwitch && this.batonPass) { if (this.selfSwitch && this.isBatonPass()) {
const statStageTotal = user.getStatStages().reduce((s: integer, total: integer) => total += s, 0); const statStageTotal = user.getStatStages().reduce((s: integer, total: integer) => total += s, 0);
ret = ret / 2 + (Phaser.Tweens.Builders.GetEaseFunction("Sine.easeOut")(Math.min(Math.abs(statStageTotal), 10) / 10) * (statStageTotal >= 0 ? 10 : -10)); ret = ret / 2 + (Phaser.Tweens.Builders.GetEaseFunction("Sine.easeOut")(Math.min(Math.abs(statStageTotal), 10) / 10) * (statStageTotal >= 0 ? 10 : -10));
} }
@ -5301,10 +5303,7 @@ export class ForceSwitchOutAttr extends MoveEffectAttr {
export class ChillyReceptionAttr extends ForceSwitchOutAttr { export class ChillyReceptionAttr extends ForceSwitchOutAttr {
apply(user: Pokemon, target: Pokemon, move: Move, args: any[]): boolean {
// using inherited constructor
apply(user: Pokemon, target: Pokemon, move: Move, args: any[]): Promise<boolean> {
user.scene.arena.trySetWeather(WeatherType.SNOW, true); user.scene.arena.trySetWeather(WeatherType.SNOW, true);
return super.apply(user, target, move, args); return super.apply(user, target, move, args);
} }
@ -7394,7 +7393,7 @@ export function initMoves() {
new AttackMove(Moves.DRAGON_BREATH, Type.DRAGON, MoveCategory.SPECIAL, 60, 100, 20, 30, 0, 2) new AttackMove(Moves.DRAGON_BREATH, Type.DRAGON, MoveCategory.SPECIAL, 60, 100, 20, 30, 0, 2)
.attr(StatusEffectAttr, StatusEffect.PARALYSIS), .attr(StatusEffectAttr, StatusEffect.PARALYSIS),
new SelfStatusMove(Moves.BATON_PASS, Type.NORMAL, -1, 40, -1, 0, 2) new SelfStatusMove(Moves.BATON_PASS, Type.NORMAL, -1, 40, -1, 0, 2)
.attr(ForceSwitchOutAttr, true, true) .attr(ForceSwitchOutAttr, true, SwitchType.BATON_PASS)
.hidesUser(), .hidesUser(),
new StatusMove(Moves.ENCORE, Type.NORMAL, 100, 5, -1, 0, 2) new StatusMove(Moves.ENCORE, Type.NORMAL, 100, 5, -1, 0, 2)
.attr(AddBattlerTagAttr, BattlerTagType.ENCORE, false, true) .attr(AddBattlerTagAttr, BattlerTagType.ENCORE, false, true)
@ -7820,7 +7819,7 @@ export function initMoves() {
.makesContact(false) .makesContact(false)
.target(MoveTarget.ATTACKER), .target(MoveTarget.ATTACKER),
new AttackMove(Moves.U_TURN, Type.BUG, MoveCategory.PHYSICAL, 70, 100, 20, -1, 0, 4) new AttackMove(Moves.U_TURN, Type.BUG, MoveCategory.PHYSICAL, 70, 100, 20, -1, 0, 4)
.attr(ForceSwitchOutAttr, true, false), .attr(ForceSwitchOutAttr, true),
new AttackMove(Moves.CLOSE_COMBAT, Type.FIGHTING, MoveCategory.PHYSICAL, 120, 100, 5, -1, 0, 4) new AttackMove(Moves.CLOSE_COMBAT, Type.FIGHTING, MoveCategory.PHYSICAL, 120, 100, 5, -1, 0, 4)
.attr(StatStageChangeAttr, [ Stat.DEF, Stat.SPDEF ], -1, true), .attr(StatStageChangeAttr, [ Stat.DEF, Stat.SPDEF ], -1, true),
new AttackMove(Moves.PAYBACK, Type.DARK, MoveCategory.PHYSICAL, 50, 100, 10, -1, 0, 4) new AttackMove(Moves.PAYBACK, Type.DARK, MoveCategory.PHYSICAL, 50, 100, 10, -1, 0, 4)
@ -8253,7 +8252,7 @@ export function initMoves() {
new AttackMove(Moves.GRASS_PLEDGE, Type.GRASS, MoveCategory.SPECIAL, 80, 100, 10, -1, 0, 5) new AttackMove(Moves.GRASS_PLEDGE, Type.GRASS, MoveCategory.SPECIAL, 80, 100, 10, -1, 0, 5)
.partial(), .partial(),
new AttackMove(Moves.VOLT_SWITCH, Type.ELECTRIC, MoveCategory.SPECIAL, 70, 100, 20, -1, 0, 5) new AttackMove(Moves.VOLT_SWITCH, Type.ELECTRIC, MoveCategory.SPECIAL, 70, 100, 20, -1, 0, 5)
.attr(ForceSwitchOutAttr, true, false), .attr(ForceSwitchOutAttr, true),
new AttackMove(Moves.STRUGGLE_BUG, Type.BUG, MoveCategory.SPECIAL, 50, 100, 20, 100, 0, 5) new AttackMove(Moves.STRUGGLE_BUG, Type.BUG, MoveCategory.SPECIAL, 50, 100, 20, 100, 0, 5)
.attr(StatStageChangeAttr, [ Stat.SPATK ], -1) .attr(StatStageChangeAttr, [ Stat.SPATK ], -1)
.target(MoveTarget.ALL_NEAR_ENEMIES), .target(MoveTarget.ALL_NEAR_ENEMIES),
@ -8421,7 +8420,7 @@ export function initMoves() {
.target(MoveTarget.ALL_NEAR_ENEMIES), .target(MoveTarget.ALL_NEAR_ENEMIES),
new StatusMove(Moves.PARTING_SHOT, Type.DARK, 100, 20, -1, 0, 6) new StatusMove(Moves.PARTING_SHOT, Type.DARK, 100, 20, -1, 0, 6)
.attr(StatStageChangeAttr, [ Stat.ATK, Stat.SPATK ], -1, false, null, true, true, MoveEffectTrigger.PRE_APPLY) .attr(StatStageChangeAttr, [ Stat.ATK, Stat.SPATK ], -1, false, null, true, true, MoveEffectTrigger.PRE_APPLY)
.attr(ForceSwitchOutAttr, true, false) .attr(ForceSwitchOutAttr, true)
.soundBased(), .soundBased(),
new StatusMove(Moves.TOPSY_TURVY, Type.DARK, -1, 20, -1, 0, 6) new StatusMove(Moves.TOPSY_TURVY, Type.DARK, -1, 20, -1, 0, 6)
.attr(InvertStatsAttr), .attr(InvertStatsAttr),
@ -9176,7 +9175,7 @@ export function initMoves() {
.attr(StatStageChangeAttr, [ Stat.ATK, Stat.DEF ], 1) .attr(StatStageChangeAttr, [ Stat.ATK, Stat.DEF ], 1)
.target(MoveTarget.NEAR_ALLY), .target(MoveTarget.NEAR_ALLY),
new AttackMove(Moves.FLIP_TURN, Type.WATER, MoveCategory.PHYSICAL, 60, 100, 20, -1, 0, 8) new AttackMove(Moves.FLIP_TURN, Type.WATER, MoveCategory.PHYSICAL, 60, 100, 20, -1, 0, 8)
.attr(ForceSwitchOutAttr, true, false), .attr(ForceSwitchOutAttr, true),
new AttackMove(Moves.TRIPLE_AXEL, Type.ICE, MoveCategory.PHYSICAL, 20, 90, 10, -1, 0, 8) new AttackMove(Moves.TRIPLE_AXEL, Type.ICE, MoveCategory.PHYSICAL, 20, 90, 10, -1, 0, 8)
.attr(MultiHitAttr, MultiHitType._3) .attr(MultiHitAttr, MultiHitType._3)
.attr(MultiHitPowerIncrementAttr, 3) .attr(MultiHitPowerIncrementAttr, 3)
@ -9503,10 +9502,11 @@ export function initMoves() {
.attr(MovePowerMultiplierAttr, (user, target, move) => target.getAttackTypeEffectiveness(move.type, user) >= 2 ? 5461/4096 : 1) .attr(MovePowerMultiplierAttr, (user, target, move) => target.getAttackTypeEffectiveness(move.type, user) >= 2 ? 5461/4096 : 1)
.makesContact(), .makesContact(),
new SelfStatusMove(Moves.SHED_TAIL, Type.NORMAL, -1, 10, -1, 0, 9) new SelfStatusMove(Moves.SHED_TAIL, Type.NORMAL, -1, 10, -1, 0, 9)
.unimplemented(), .attr(AddSubstituteAttr, 0.5)
.attr(ForceSwitchOutAttr, true, SwitchType.SHED_TAIL),
new SelfStatusMove(Moves.CHILLY_RECEPTION, Type.ICE, -1, 10, -1, 0, 9) new SelfStatusMove(Moves.CHILLY_RECEPTION, Type.ICE, -1, 10, -1, 0, 9)
.attr(PreMoveMessageAttr, (user, move) => i18next.t("moveTriggers:chillyReception", {pokemonName: getPokemonNameWithAffix(user)})) .attr(PreMoveMessageAttr, (user, move) => i18next.t("moveTriggers:chillyReception", {pokemonName: getPokemonNameWithAffix(user)}))
.attr(ChillyReceptionAttr, true, false), .attr(ChillyReceptionAttr, true),
new SelfStatusMove(Moves.TIDY_UP, Type.NORMAL, -1, 10, -1, 0, 9) new SelfStatusMove(Moves.TIDY_UP, Type.NORMAL, -1, 10, -1, 0, 9)
.attr(StatStageChangeAttr, [ Stat.ATK, Stat.SPD ], 1, true, null, true, true) .attr(StatStageChangeAttr, [ Stat.ATK, Stat.SPD ], 1, true, null, true, true)
.attr(RemoveArenaTrapAttr, true) .attr(RemoveArenaTrapAttr, true)

12
src/enums/switch-type.ts Normal file
View File

@ -0,0 +1,12 @@
/**
* Indicates the type of switch functionality that a {@linkcode SwitchPhase}
* or {@linkcode SwitchSummonPhase} will carry out.
*/
export enum SwitchType {
/** Basic switchout where the Pokemon to switch in is selected */
SWITCH,
/** Transfers stat stages and other effects from the returning Pokemon to the switched in Pokemon */
BATON_PASS,
/** Transfers the returning Pokemon's Substitute to the switched in Pokemon */
SHED_TAIL
}

View File

@ -61,6 +61,7 @@ import { Challenges } from "#enums/challenges";
import { PokemonAnimType } from "#app/enums/pokemon-anim-type"; import { PokemonAnimType } from "#app/enums/pokemon-anim-type";
import { PLAYER_PARTY_MAX_SIZE } from "#app/constants"; import { PLAYER_PARTY_MAX_SIZE } from "#app/constants";
import { MysteryEncounterPokemonData } from "#app/data/mystery-encounters/mystery-encounter-pokemon-data"; import { MysteryEncounterPokemonData } from "#app/data/mystery-encounters/mystery-encounter-pokemon-data";
import { SwitchType } from "#enums/switch-type";
export enum FieldPosition { export enum FieldPosition {
CENTER, CENTER,
@ -4003,16 +4004,17 @@ export class PlayerPokemon extends Pokemon {
/** /**
* Causes this mon to leave the field (via {@linkcode leaveField}) and then * Causes this mon to leave the field (via {@linkcode leaveField}) and then
* opens the party switcher UI to switch a new mon in * opens the party switcher UI to switch a new mon in
* @param batonPass Indicates if this switch was caused by a baton pass (and * @param switchType the {@linkcode SwitchType} for this switch-out. If this is
* thus should maintain active mon effects) * `BATON_PASS` or `SHED_TAIL`, this Pokemon's effects are not cleared upon leaving
* the field.
*/ */
switchOut(batonPass: boolean): Promise<void> { switchOut(switchType: SwitchType = SwitchType.SWITCH): Promise<void> {
return new Promise(resolve => { return new Promise(resolve => {
this.leaveField(!batonPass); this.leaveField(switchType === SwitchType.SWITCH);
this.scene.ui.setMode(Mode.PARTY, PartyUiMode.FAINT_SWITCH, this.getFieldIndex(), (slotIndex: integer, option: PartyOption) => { this.scene.ui.setMode(Mode.PARTY, PartyUiMode.FAINT_SWITCH, this.getFieldIndex(), (slotIndex: integer, option: PartyOption) => {
if (slotIndex >= this.scene.currentBattle.getBattlerCount() && slotIndex < 6) { if (slotIndex >= this.scene.currentBattle.getBattlerCount() && slotIndex < 6) {
this.scene.prependToPhase(new SwitchSummonPhase(this.scene, this.getFieldIndex(), slotIndex, false, batonPass), MoveEndPhase); this.scene.prependToPhase(new SwitchSummonPhase(this.scene, switchType, this.getFieldIndex(), slotIndex, false), MoveEndPhase);
} }
this.scene.ui.setMode(Mode.MESSAGE).then(resolve); this.scene.ui.setMode(Mode.MESSAGE).then(resolve);
}, PartyUiHandler.FilterNonFainted); }, PartyUiHandler.FilterNonFainted);
@ -4074,11 +4076,11 @@ export class PlayerPokemon extends Pokemon {
const allyPokemon = this.getAlly(); const allyPokemon = this.getAlly();
if (slotIndex<=1) { if (slotIndex<=1) {
// Revived ally pokemon // Revived ally pokemon
this.scene.unshiftPhase(new SwitchSummonPhase(this.scene, pokemon.getFieldIndex(), slotIndex, false, false, true)); this.scene.unshiftPhase(new SwitchSummonPhase(this.scene, SwitchType.SWITCH, pokemon.getFieldIndex(), slotIndex, false, true));
this.scene.unshiftPhase(new ToggleDoublePositionPhase(this.scene, true)); this.scene.unshiftPhase(new ToggleDoublePositionPhase(this.scene, true));
} else if (allyPokemon.isFainted()) { } else if (allyPokemon.isFainted()) {
// Revived party pokemon, and ally pokemon is fainted // Revived party pokemon, and ally pokemon is fainted
this.scene.unshiftPhase(new SwitchSummonPhase(this.scene, allyPokemon.getFieldIndex(), slotIndex, false, false, true)); this.scene.unshiftPhase(new SwitchSummonPhase(this.scene, SwitchType.SWITCH, allyPokemon.getFieldIndex(), slotIndex, false, true));
this.scene.unshiftPhase(new ToggleDoublePositionPhase(this.scene, true)); this.scene.unshiftPhase(new ToggleDoublePositionPhase(this.scene, true));
} }
} }

View File

@ -71,6 +71,7 @@
"disabledOnAdd": " {{moveName}} von {{pokemonNameWithAffix}} wurde blockiert!", "disabledOnAdd": " {{moveName}} von {{pokemonNameWithAffix}} wurde blockiert!",
"disabledLapse": "{{moveName}} von {{pokemonNameWithAffix}} ist nicht länger blockiert!", "disabledLapse": "{{moveName}} von {{pokemonNameWithAffix}} ist nicht länger blockiert!",
"tarShotOnAdd": "{{pokemonNameWithAffix}} ist nun schwach gegenüber Feuer-Attacken!", "tarShotOnAdd": "{{pokemonNameWithAffix}} ist nun schwach gegenüber Feuer-Attacken!",
"shedTailOnAdd": "{{pokemonNameWithAffix}} wirft seinen Schwanz ab, um eine Ablenkung zu schaffen!",
"substituteOnAdd": "Ein Delegator von {{pokemonNameWithAffix}} ist erschienen!", "substituteOnAdd": "Ein Delegator von {{pokemonNameWithAffix}} ist erschienen!",
"substituteOnHit": "Der Delegator steckt den Schlag für {{pokemonNameWithAffix}} ein!", "substituteOnHit": "Der Delegator steckt den Schlag für {{pokemonNameWithAffix}} ein!",
"substituteOnRemove": "Der Delegator von {{pokemonNameWithAffix}} hört auf zu wirken!" "substituteOnRemove": "Der Delegator von {{pokemonNameWithAffix}} hört auf zu wirken!"

View File

@ -71,6 +71,7 @@
"disabledOnAdd": "{{pokemonNameWithAffix}}'s {{moveName}}\nwas disabled!", "disabledOnAdd": "{{pokemonNameWithAffix}}'s {{moveName}}\nwas disabled!",
"disabledLapse": "{{pokemonNameWithAffix}}'s {{moveName}}\nis no longer disabled.", "disabledLapse": "{{pokemonNameWithAffix}}'s {{moveName}}\nis no longer disabled.",
"tarShotOnAdd": "{{pokemonNameWithAffix}} became weaker to fire!", "tarShotOnAdd": "{{pokemonNameWithAffix}} became weaker to fire!",
"shedTailOnAdd": "{{pokemonNameWithAffix}} shed its tail to create a decoy!",
"substituteOnAdd": "{{pokemonNameWithAffix}} put in a substitute!", "substituteOnAdd": "{{pokemonNameWithAffix}} put in a substitute!",
"substituteOnHit": "The substitute took damage for {{pokemonNameWithAffix}}!", "substituteOnHit": "The substitute took damage for {{pokemonNameWithAffix}}!",
"substituteOnRemove": "{{pokemonNameWithAffix}}'s substitute faded!", "substituteOnRemove": "{{pokemonNameWithAffix}}'s substitute faded!",

View File

@ -71,6 +71,7 @@
"disabledOnAdd": "¡Se ha anulado el movimiento {{moveName}}\nde {{pokemonNameWithAffix}}!", "disabledOnAdd": "¡Se ha anulado el movimiento {{moveName}}\nde {{pokemonNameWithAffix}}!",
"disabledLapse": "¡El movimiento {{moveName}} de {{pokemonNameWithAffix}} ya no está anulado!", "disabledLapse": "¡El movimiento {{moveName}} de {{pokemonNameWithAffix}} ya no está anulado!",
"tarShotOnAdd": "¡{{pokemonNameWithAffix}} se ha vuelto débil ante el fuego!", "tarShotOnAdd": "¡{{pokemonNameWithAffix}} se ha vuelto débil ante el fuego!",
"shedTailOnAdd": "{{pokemonNameWithAffix}} se desprende\nde un segmento de su cuerpo y lo usa comoseñuelo!",
"substituteOnAdd": "¡{{pokemonNameWithAffix}} creó un sustituto!", "substituteOnAdd": "¡{{pokemonNameWithAffix}} creó un sustituto!",
"substituteOnHit": "¡El sustituto recibe daño en lugar del {{pokemonNameWithAffix}}!", "substituteOnHit": "¡El sustituto recibe daño en lugar del {{pokemonNameWithAffix}}!",
"substituteOnRemove": "¡El sustituto del {{pokemonNameWithAffix}} se debilitó!" "substituteOnRemove": "¡El sustituto del {{pokemonNameWithAffix}} se debilitó!"

View File

@ -71,6 +71,7 @@
"disabledOnAdd": "La capacité {{moveName}}\nde {{pokemonNameWithAffix}} est mise sous entrave !", "disabledOnAdd": "La capacité {{moveName}}\nde {{pokemonNameWithAffix}} est mise sous entrave !",
"disabledLapse": "La capacité {{moveName}}\nde {{pokemonNameWithAffix}} nest plus sous entrave !", "disabledLapse": "La capacité {{moveName}}\nde {{pokemonNameWithAffix}} nest plus sous entrave !",
"tarShotOnAdd": "{{pokemonNameWithAffix}} est maintenant\nvulnérable au feu !", "tarShotOnAdd": "{{pokemonNameWithAffix}} est maintenant\nvulnérable au feu !",
"shedTailOnAdd": "{{pokemonNameWithAffix}} détache\nsa queue pour créer un leurre !",
"substituteOnAdd": "{{pokemonNameWithAffix}}\ncrée un clone !", "substituteOnAdd": "{{pokemonNameWithAffix}}\ncrée un clone !",
"substituteOnHit": "Le clone subit les dégâts à la place\nde {{pokemonNameWithAffix}} !", "substituteOnHit": "Le clone subit les dégâts à la place\nde {{pokemonNameWithAffix}} !",
"substituteOnRemove": "Le clone de {{pokemonNameWithAffix}}\ndisparait…" "substituteOnRemove": "Le clone de {{pokemonNameWithAffix}}\ndisparait…"

View File

@ -71,6 +71,7 @@
"disabledOnAdd": "La mossa {{moveName}} di\n{{pokemonNameWithAffix}} è stata bloccata!", "disabledOnAdd": "La mossa {{moveName}} di\n{{pokemonNameWithAffix}} è stata bloccata!",
"disabledLapse": "La mossa {{moveName}} di\n{{pokemonNameWithAffix}} non è più bloccata!", "disabledLapse": "La mossa {{moveName}} di\n{{pokemonNameWithAffix}} non è più bloccata!",
"tarShotOnAdd": "{{pokemonNameWithAffix}} è diventato vulnerabile\nal tipo Fuoco!", "tarShotOnAdd": "{{pokemonNameWithAffix}} è diventato vulnerabile\nal tipo Fuoco!",
"shedTailOnAdd": "{{pokemonNameWithAffix}} si taglia\nla coda e ne fa un sostituto!",
"substituteOnAdd": "Appare un sostituto di {{pokemonNameWithAffix}}!", "substituteOnAdd": "Appare un sostituto di {{pokemonNameWithAffix}}!",
"substituteOnHit": "Il sostituto viene colpito al posto di {{pokemonNameWithAffix}}!", "substituteOnHit": "Il sostituto viene colpito al posto di {{pokemonNameWithAffix}}!",
"substituteOnRemove": "Il sostituto di {{pokemonNameWithAffix}} svanisce!" "substituteOnRemove": "Il sostituto di {{pokemonNameWithAffix}} svanisce!"

View File

@ -70,5 +70,6 @@
"stockpilingOnAdd": "{{pokemonNameWithAffix}}は {{stockpiledCount}}つ たくわえた!", "stockpilingOnAdd": "{{pokemonNameWithAffix}}は {{stockpiledCount}}つ たくわえた!",
"disabledOnAdd": "{{pokemonNameWithAffix}}の\n{{moveName}}\nを 封じこめた", "disabledOnAdd": "{{pokemonNameWithAffix}}の\n{{moveName}}\nを 封じこめた",
"disabledLapse": "{{pokemonNameWithAffix}}の\nかなしばりが 解けた", "disabledLapse": "{{pokemonNameWithAffix}}の\nかなしばりが 解けた",
"tarShotOnAdd": "{{pokemonNameWithAffix}}は ほのおに 弱くなった!" "tarShotOnAdd": "{{pokemonNameWithAffix}}は ほのおに 弱くなった!",
"shedTailOnAdd": "{{pokemonNameWithAffix}}は\nしっぽを 切って みがわりにした"
} }

View File

@ -71,6 +71,7 @@
"disabledOnAdd": "{{pokemonNameWithAffix}}의 {{moveName}}[[는]]\n사용할 수 없다!", "disabledOnAdd": "{{pokemonNameWithAffix}}의 {{moveName}}[[는]]\n사용할 수 없다!",
"disabledLapse": "{{pokemonNameWithAffix}}의 {{moveName}}[[는]]\n이제 사용할 수 있다.", "disabledLapse": "{{pokemonNameWithAffix}}의 {{moveName}}[[는]]\n이제 사용할 수 있다.",
"tarShotOnAdd": "{{pokemonNameWithAffix}}[[는]] 불꽃에 약해졌다!", "tarShotOnAdd": "{{pokemonNameWithAffix}}[[는]] 불꽃에 약해졌다!",
"shedTailOnAdd": "{{pokemonNameWithAffix}}[[는]] 꼬리를 잘라 대타로 삼았다!",
"substituteOnAdd": "{{pokemonNameWithAffix}}의\n대타가 나타났다!", "substituteOnAdd": "{{pokemonNameWithAffix}}의\n대타가 나타났다!",
"substituteOnHit": "{{pokemonNameWithAffix}}[[를]] 대신하여\n대타가 공격을 받았다!", "substituteOnHit": "{{pokemonNameWithAffix}}[[를]] 대신하여\n대타가 공격을 받았다!",
"substituteOnRemove": "{{pokemonNameWithAffix}}의\n대타는 사라져 버렸다..." "substituteOnRemove": "{{pokemonNameWithAffix}}의\n대타는 사라져 버렸다..."

View File

@ -71,6 +71,7 @@
"disabledOnAdd": "封住了{{pokemonNameWithAffix}}的\n{{moveName}}", "disabledOnAdd": "封住了{{pokemonNameWithAffix}}的\n{{moveName}}",
"disabledLapse": "{{pokemonNameWithAffix}}的\n定身法解除了", "disabledLapse": "{{pokemonNameWithAffix}}的\n定身法解除了",
"tarShotOnAdd": "{{pokemonNameWithAffix}}\n变得怕火了", "tarShotOnAdd": "{{pokemonNameWithAffix}}\n变得怕火了",
"shedTailOnAdd": "{{pokemonNameWithAffix}}\n断掉尾巴并将其作为替身了",
"substituteOnAdd": "{{pokemonNameWithAffix}}的\n替身出现了", "substituteOnAdd": "{{pokemonNameWithAffix}}的\n替身出现了",
"substituteOnHit": "替身代替{{pokemonNameWithAffix}}\n承受了攻击", "substituteOnHit": "替身代替{{pokemonNameWithAffix}}\n承受了攻击",
"substituteOnRemove": "{{pokemonNameWithAffix}}的\n替身消失了……" "substituteOnRemove": "{{pokemonNameWithAffix}}的\n替身消失了……"

View File

@ -71,6 +71,7 @@
"disabledOnAdd": "封住了{{pokemonNameWithAffix}}的\n{moveName}}", "disabledOnAdd": "封住了{{pokemonNameWithAffix}}的\n{moveName}}",
"disabledLapse": "{{pokemonNameWithAffix}}的\n定身法解除了", "disabledLapse": "{{pokemonNameWithAffix}}的\n定身法解除了",
"tarShotOnAdd": "{{pokemonNameWithAffix}}\n變得怕火了", "tarShotOnAdd": "{{pokemonNameWithAffix}}\n變得怕火了",
"shedTailOnAdd": "{{pokemonNameWithAffix}}\n截斷尾巴把它做成了替身",
"substituteOnAdd": "{{pokemonNameWithAffix}}的\n替身出現了", "substituteOnAdd": "{{pokemonNameWithAffix}}的\n替身出現了",
"substituteOnHit": "替身代替{{pokemonNameWithAffix}}承受了攻擊!", "substituteOnHit": "替身代替{{pokemonNameWithAffix}}承受了攻擊!",
"substituteOnRemove": "{{pokemonNameWithAffix}}的\n替身消失了……" "substituteOnRemove": "{{pokemonNameWithAffix}}的\n替身消失了……"

View File

@ -8,6 +8,7 @@ import { BattlePhase } from "./battle-phase";
import { PostSummonPhase } from "./post-summon-phase"; import { PostSummonPhase } from "./post-summon-phase";
import { SummonMissingPhase } from "./summon-missing-phase"; import { SummonMissingPhase } from "./summon-missing-phase";
import { SwitchPhase } from "./switch-phase"; import { SwitchPhase } from "./switch-phase";
import { SwitchType } from "#enums/switch-type";
export class CheckSwitchPhase extends BattlePhase { export class CheckSwitchPhase extends BattlePhase {
protected fieldIndex: integer; protected fieldIndex: integer;
@ -50,7 +51,7 @@ export class CheckSwitchPhase extends BattlePhase {
this.scene.ui.setMode(Mode.CONFIRM, () => { this.scene.ui.setMode(Mode.CONFIRM, () => {
this.scene.ui.setMode(Mode.MESSAGE); this.scene.ui.setMode(Mode.MESSAGE);
this.scene.tryRemovePhase(p => p instanceof PostSummonPhase && p.player && p.fieldIndex === this.fieldIndex); this.scene.tryRemovePhase(p => p instanceof PostSummonPhase && p.player && p.fieldIndex === this.fieldIndex);
this.scene.unshiftPhase(new SwitchPhase(this.scene, this.fieldIndex, false, true)); this.scene.unshiftPhase(new SwitchPhase(this.scene, SwitchType.SWITCH, this.fieldIndex, false, true));
this.end(); this.end();
}, () => { }, () => {
this.scene.ui.setMode(Mode.MESSAGE); this.scene.ui.setMode(Mode.MESSAGE);

View File

@ -18,6 +18,7 @@ import { GameOverPhase } from "./game-over-phase";
import { SwitchPhase } from "./switch-phase"; import { SwitchPhase } from "./switch-phase";
import { VictoryPhase } from "./victory-phase"; import { VictoryPhase } from "./victory-phase";
import { SpeciesFormChangeActiveTrigger } from "#app/data/pokemon-forms"; import { SpeciesFormChangeActiveTrigger } from "#app/data/pokemon-forms";
import { SwitchType } from "#enums/switch-type";
export class FaintPhase extends PokemonPhase { export class FaintPhase extends PokemonPhase {
private preventEndure: boolean; private preventEndure: boolean;
@ -106,14 +107,14 @@ export class FaintPhase extends PokemonPhase {
* If previous conditions weren't met, and the player has at least 1 legal Pokemon off the field, * If previous conditions weren't met, and the player has at least 1 legal Pokemon off the field,
* push a phase that prompts the player to summon a Pokemon from their party. * push a phase that prompts the player to summon a Pokemon from their party.
*/ */
this.scene.pushPhase(new SwitchPhase(this.scene, this.fieldIndex, true, false)); this.scene.pushPhase(new SwitchPhase(this.scene, SwitchType.SWITCH, this.fieldIndex, true, false));
} }
} else { } else {
this.scene.unshiftPhase(new VictoryPhase(this.scene, this.battlerIndex)); this.scene.unshiftPhase(new VictoryPhase(this.scene, this.battlerIndex));
if ([BattleType.TRAINER, BattleType.MYSTERY_ENCOUNTER].includes(this.scene.currentBattle.battleType)) { if ([BattleType.TRAINER, BattleType.MYSTERY_ENCOUNTER].includes(this.scene.currentBattle.battleType)) {
const hasReservePartyMember = !!this.scene.getEnemyParty().filter(p => p.isActive() && !p.isOnField() && p.trainerSlot === (pokemon as EnemyPokemon).trainerSlot).length; const hasReservePartyMember = !!this.scene.getEnemyParty().filter(p => p.isActive() && !p.isOnField() && p.trainerSlot === (pokemon as EnemyPokemon).trainerSlot).length;
if (hasReservePartyMember) { if (hasReservePartyMember) {
this.scene.pushPhase(new SwitchSummonPhase(this.scene, this.fieldIndex, -1, false, false, false)); this.scene.pushPhase(new SwitchSummonPhase(this.scene, SwitchType.SWITCH, this.fieldIndex, -1, false, false));
} }
} }
} }

View File

@ -24,6 +24,7 @@ import { NewBattlePhase } from "#app/phases/new-battle-phase";
import { GameOverPhase } from "#app/phases/game-over-phase"; import { GameOverPhase } from "#app/phases/game-over-phase";
import { SwitchPhase } from "#app/phases/switch-phase"; import { SwitchPhase } from "#app/phases/switch-phase";
import { SeenEncounterData } from "#app/data/mystery-encounters/mystery-encounter-save-data"; import { SeenEncounterData } from "#app/data/mystery-encounters/mystery-encounter-save-data";
import { SwitchType } from "#enums/switch-type";
/** /**
* Will handle (in order): * Will handle (in order):
@ -241,7 +242,7 @@ export class MysteryEncounterBattleStartCleanupPhase extends Phase {
const playerField = this.scene.getPlayerField(); const playerField = this.scene.getPlayerField();
playerField.forEach((pokemon, i) => { playerField.forEach((pokemon, i) => {
if (!pokemon.isAllowedInBattle() && legalPlayerPartyPokemon.length > i) { if (!pokemon.isAllowedInBattle() && legalPlayerPartyPokemon.length > i) {
this.scene.unshiftPhase(new SwitchPhase(this.scene, i, true, false)); this.scene.unshiftPhase(new SwitchPhase(this.scene, SwitchType.SWITCH, i, true, false));
} }
}); });

View File

@ -1,10 +1,11 @@
import BattleScene from "#app/battle-scene"; import BattleScene from "#app/battle-scene";
import { SpeciesFormChangeActiveTrigger } from "#app/data/pokemon-forms"; import { SpeciesFormChangeActiveTrigger } from "#app/data/pokemon-forms";
import { SwitchType } from "#enums/switch-type";
import { SwitchSummonPhase } from "./switch-summon-phase"; import { SwitchSummonPhase } from "./switch-summon-phase";
export class ReturnPhase extends SwitchSummonPhase { export class ReturnPhase extends SwitchSummonPhase {
constructor(scene: BattleScene, fieldIndex: integer) { constructor(scene: BattleScene, fieldIndex: integer) {
super(scene, fieldIndex, -1, true, false); super(scene, SwitchType.SWITCH, fieldIndex, -1, true);
} }
switchAndSummon(): void { switchAndSummon(): void {

View File

@ -1,4 +1,5 @@
import BattleScene from "#app/battle-scene"; import BattleScene from "#app/battle-scene";
import { SwitchType } from "#enums/switch-type";
import PartyUiHandler, { PartyUiMode, PartyOption } from "#app/ui/party-ui-handler"; import PartyUiHandler, { PartyUiMode, PartyOption } from "#app/ui/party-ui-handler";
import { Mode } from "#app/ui/ui"; import { Mode } from "#app/ui/ui";
import { BattlePhase } from "./battle-phase"; import { BattlePhase } from "./battle-phase";
@ -9,22 +10,25 @@ import { SwitchSummonPhase } from "./switch-summon-phase";
* for the player (if a switch would be valid for the current battle state). * for the player (if a switch would be valid for the current battle state).
*/ */
export class SwitchPhase extends BattlePhase { export class SwitchPhase extends BattlePhase {
protected fieldIndex: integer; protected readonly fieldIndex: integer;
private isModal: boolean; private readonly switchType: SwitchType;
private doReturn: boolean; private readonly isModal: boolean;
private readonly doReturn: boolean;
/** /**
* Creates a new SwitchPhase * Creates a new SwitchPhase
* @param scene {@linkcode BattleScene} Current battle scene * @param scene {@linkcode BattleScene} Current battle scene
* @param switchType {@linkcode SwitchType} The type of switch logic this phase implements
* @param fieldIndex Field index to switch out * @param fieldIndex Field index to switch out
* @param isModal Indicates if the switch should be forced (true) or is * @param isModal Indicates if the switch should be forced (true) or is
* optional (false). * optional (false).
* @param doReturn Indicates if the party member on the field should be * @param doReturn Indicates if the party member on the field should be
* recalled to ball or has already left the field. Passed to {@linkcode SwitchSummonPhase}. * recalled to ball or has already left the field. Passed to {@linkcode SwitchSummonPhase}.
*/ */
constructor(scene: BattleScene, fieldIndex: integer, isModal: boolean, doReturn: boolean) { constructor(scene: BattleScene, switchType: SwitchType, fieldIndex: integer, isModal: boolean, doReturn: boolean) {
super(scene); super(scene);
this.switchType = switchType;
this.fieldIndex = fieldIndex; this.fieldIndex = fieldIndex;
this.isModal = isModal; this.isModal = isModal;
this.doReturn = doReturn; this.doReturn = doReturn;
@ -38,11 +42,13 @@ export class SwitchPhase extends BattlePhase {
return super.end(); return super.end();
} }
// Skip if the fainted party member has been revived already. doReturn is /**
// only passed as `false` from FaintPhase (as opposed to other usages such * Skip if the fainted party member has been revived already. doReturn is
// as ForceSwitchOutAttr or CheckSwitchPhase), so we only want to check this * only passed as `false` from FaintPhase (as opposed to other usages such
// if the mon should have already been returned but is still alive and well * as ForceSwitchOutAttr or CheckSwitchPhase), so we only want to check this
// on the field. see also; battle.test.ts * if the mon should have already been returned but is still alive and well
* on the field. see also; battle.test.ts
*/
if (this.isModal && !this.doReturn && !this.scene.getParty()[this.fieldIndex].isFainted()) { if (this.isModal && !this.doReturn && !this.scene.getParty()[this.fieldIndex].isFainted()) {
return super.end(); return super.end();
} }
@ -57,7 +63,8 @@ export class SwitchPhase extends BattlePhase {
this.scene.ui.setMode(Mode.PARTY, this.isModal ? PartyUiMode.FAINT_SWITCH : PartyUiMode.POST_BATTLE_SWITCH, fieldIndex, (slotIndex: integer, option: PartyOption) => { this.scene.ui.setMode(Mode.PARTY, this.isModal ? PartyUiMode.FAINT_SWITCH : PartyUiMode.POST_BATTLE_SWITCH, fieldIndex, (slotIndex: integer, option: PartyOption) => {
if (slotIndex >= this.scene.currentBattle.getBattlerCount() && slotIndex < 6) { if (slotIndex >= this.scene.currentBattle.getBattlerCount() && slotIndex < 6) {
this.scene.unshiftPhase(new SwitchSummonPhase(this.scene, fieldIndex, slotIndex, this.doReturn, option === PartyOption.PASS_BATON)); const switchType = (option === PartyOption.PASS_BATON) ? SwitchType.BATON_PASS : this.switchType;
this.scene.unshiftPhase(new SwitchSummonPhase(this.scene, switchType, fieldIndex, slotIndex, this.doReturn));
} }
this.scene.ui.setMode(Mode.MESSAGE).then(() => super.end()); this.scene.ui.setMode(Mode.MESSAGE).then(() => super.end());
}, PartyUiHandler.FilterNonFainted); }, PartyUiHandler.FilterNonFainted);

View File

@ -12,29 +12,30 @@ import i18next from "i18next";
import { PostSummonPhase } from "./post-summon-phase"; import { PostSummonPhase } from "./post-summon-phase";
import { SummonPhase } from "./summon-phase"; import { SummonPhase } from "./summon-phase";
import { SubstituteTag } from "#app/data/battler-tags"; import { SubstituteTag } from "#app/data/battler-tags";
import { SwitchType } from "#enums/switch-type";
export class SwitchSummonPhase extends SummonPhase { export class SwitchSummonPhase extends SummonPhase {
private slotIndex: integer; private readonly switchType: SwitchType;
private doReturn: boolean; private readonly slotIndex: integer;
private batonPass: boolean; private readonly doReturn: boolean;
private lastPokemon: Pokemon; private lastPokemon: Pokemon;
/** /**
* Constructor for creating a new SwitchSummonPhase * Constructor for creating a new SwitchSummonPhase
* @param scene {@linkcode BattleScene} the scene the phase is associated with * @param scene {@linkcode BattleScene} the scene the phase is associated with
* @param switchType the type of switch behavior
* @param fieldIndex integer representing position on the battle field * @param fieldIndex integer representing position on the battle field
* @param slotIndex integer for the index of pokemon (in party of 6) to switch into * @param slotIndex integer for the index of pokemon (in party of 6) to switch into
* @param doReturn boolean whether to render "comeback" dialogue * @param doReturn boolean whether to render "comeback" dialogue
* @param batonPass boolean if the switch is from baton pass
* @param player boolean if the switch is from the player * @param player boolean if the switch is from the player
*/ */
constructor(scene: BattleScene, fieldIndex: integer, slotIndex: integer, doReturn: boolean, batonPass: boolean, player?: boolean) { constructor(scene: BattleScene, switchType: SwitchType, fieldIndex: integer, slotIndex: integer, doReturn: boolean, player?: boolean) {
super(scene, fieldIndex, player !== undefined ? player : true); super(scene, fieldIndex, player !== undefined ? player : true);
this.switchType = switchType;
this.slotIndex = slotIndex; this.slotIndex = slotIndex;
this.doReturn = doReturn; this.doReturn = doReturn;
this.batonPass = batonPass;
} }
start(): void { start(): void {
@ -64,7 +65,7 @@ export class SwitchSummonPhase extends SummonPhase {
const pokemon = this.getPokemon(); const pokemon = this.getPokemon();
if (!this.batonPass) { if (this.switchType === SwitchType.SWITCH) {
(this.player ? this.scene.getEnemyField() : this.scene.getPlayerField()).forEach(enemyPokemon => enemyPokemon.removeTagsBySourceId(pokemon.id)); (this.player ? this.scene.getEnemyField() : this.scene.getPlayerField()).forEach(enemyPokemon => enemyPokemon.removeTagsBySourceId(pokemon.id));
const substitute = pokemon.getTag(SubstituteTag); const substitute = pokemon.getTag(SubstituteTag);
if (substitute) { if (substitute) {
@ -94,7 +95,7 @@ export class SwitchSummonPhase extends SummonPhase {
ease: "Sine.easeIn", ease: "Sine.easeIn",
scale: 0.5, scale: 0.5,
onComplete: () => { onComplete: () => {
pokemon.leaveField(!this.batonPass, false); pokemon.leaveField(this.switchType === SwitchType.SWITCH, false);
this.scene.time.delayedCall(750, () => this.switchAndSummon()); this.scene.time.delayedCall(750, () => this.switchAndSummon());
} }
}); });
@ -105,7 +106,7 @@ export class SwitchSummonPhase extends SummonPhase {
const switchedInPokemon = party[this.slotIndex]; const switchedInPokemon = party[this.slotIndex];
this.lastPokemon = this.getPokemon(); this.lastPokemon = this.getPokemon();
applyPreSwitchOutAbAttrs(PreSwitchOutAbAttr, this.lastPokemon); applyPreSwitchOutAbAttrs(PreSwitchOutAbAttr, this.lastPokemon);
if (this.batonPass && switchedInPokemon) { if (this.switchType === SwitchType.BATON_PASS && switchedInPokemon) {
(this.player ? this.scene.getEnemyField() : this.scene.getPlayerField()).forEach(enemyPokemon => enemyPokemon.transferTagsBySourceId(this.lastPokemon.id, switchedInPokemon.id)); (this.player ? this.scene.getEnemyField() : this.scene.getPlayerField()).forEach(enemyPokemon => enemyPokemon.transferTagsBySourceId(this.lastPokemon.id, switchedInPokemon.id));
if (!this.scene.findModifier(m => m instanceof SwitchEffectTransferModifier && (m as SwitchEffectTransferModifier).pokemonId === switchedInPokemon.id)) { if (!this.scene.findModifier(m => m instanceof SwitchEffectTransferModifier && (m as SwitchEffectTransferModifier).pokemonId === switchedInPokemon.id)) {
const batonPassModifier = this.scene.findModifier(m => m instanceof SwitchEffectTransferModifier const batonPassModifier = this.scene.findModifier(m => m instanceof SwitchEffectTransferModifier
@ -130,7 +131,7 @@ export class SwitchSummonPhase extends SummonPhase {
* If this switch is passing a Substitute, make the switched Pokemon match the returned Pokemon's state as it left. * If this switch is passing a Substitute, make the switched Pokemon match the returned Pokemon's state as it left.
* Otherwise, clear any persisting tags on the returned Pokemon. * Otherwise, clear any persisting tags on the returned Pokemon.
*/ */
if (this.batonPass) { if (this.switchType === SwitchType.BATON_PASS || this.switchType === SwitchType.SHED_TAIL) {
const substitute = this.lastPokemon.getTag(SubstituteTag); const substitute = this.lastPokemon.getTag(SubstituteTag);
if (substitute) { if (substitute) {
switchedInPokemon.x += this.lastPokemon.getSubstituteOffset()[0]; switchedInPokemon.x += this.lastPokemon.getSubstituteOffset()[0];
@ -174,8 +175,13 @@ export class SwitchSummonPhase extends SummonPhase {
pokemon.battleSummonData.turnCount--; pokemon.battleSummonData.turnCount--;
} }
if (this.batonPass && pokemon) { if (this.switchType === SwitchType.BATON_PASS && pokemon) {
pokemon.transferSummon(this.lastPokemon); pokemon.transferSummon(this.lastPokemon);
} else if (this.switchType === SwitchType.SHED_TAIL && pokemon) {
const subTag = this.lastPokemon.getTag(SubstituteTag);
if (subTag) {
pokemon.summonData.tags.push(subTag);
}
} }
this.lastPokemon?.resetSummonData(); this.lastPokemon?.resetSummonData();

View File

@ -32,7 +32,7 @@ export class TurnInitPhase extends FieldPhase {
this.scene.unshiftPhase(new GameOverPhase(this.scene)); this.scene.unshiftPhase(new GameOverPhase(this.scene));
} else if (allowedPokemon.length >= this.scene.currentBattle.getBattlerCount() || (this.scene.currentBattle.double && !allowedPokemon[0].isActive(true))) { } else if (allowedPokemon.length >= this.scene.currentBattle.getBattlerCount() || (this.scene.currentBattle.double && !allowedPokemon[0].isActive(true))) {
// If there is at least one pokemon in the back that is legal to switch in, force a switch. // If there is at least one pokemon in the back that is legal to switch in, force a switch.
p.switchOut(false); p.switchOut();
} else { } else {
// If there are no pokemon in the back but we're not game overing, just hide the pokemon. // If there are no pokemon in the back but we're not game overing, just hide the pokemon.
// This should only happen in double battles. // This should only happen in double battles.

View File

@ -19,6 +19,7 @@ import { TurnEndPhase } from "./turn-end-phase";
import { WeatherEffectPhase } from "./weather-effect-phase"; import { WeatherEffectPhase } from "./weather-effect-phase";
import { BattlerIndex } from "#app/battle"; import { BattlerIndex } from "#app/battle";
import { TrickRoomTag } from "#app/data/arena-tag"; import { TrickRoomTag } from "#app/data/arena-tag";
import { SwitchType } from "#enums/switch-type";
export class TurnStartPhase extends FieldPhase { export class TurnStartPhase extends FieldPhase {
constructor(scene: BattleScene) { constructor(scene: BattleScene) {
@ -179,7 +180,8 @@ export class TurnStartPhase extends FieldPhase {
this.scene.unshiftPhase(new AttemptCapturePhase(this.scene, turnCommand.targets![0] % 2, turnCommand.cursor!));//TODO: is the bang correct here? this.scene.unshiftPhase(new AttemptCapturePhase(this.scene, turnCommand.targets![0] % 2, turnCommand.cursor!));//TODO: is the bang correct here?
break; break;
case Command.POKEMON: case Command.POKEMON:
this.scene.unshiftPhase(new SwitchSummonPhase(this.scene, pokemon.getFieldIndex(), turnCommand.cursor!, true, turnCommand.args![0] as boolean, pokemon.isPlayer()));//TODO: is the bang correct here? const switchType = turnCommand.args?.[0] ? SwitchType.BATON_PASS : SwitchType.SWITCH;
this.scene.unshiftPhase(new SwitchSummonPhase(this.scene, switchType, pokemon.getFieldIndex(), turnCommand.cursor!, true, pokemon.isPlayer()));
break; break;
case Command.RUN: case Command.RUN:
let runningPokemon = pokemon; let runningPokemon = pokemon;

View File

@ -18,6 +18,7 @@ import GameManager from "#test/utils/gameManager";
import Phaser from "phaser"; import Phaser from "phaser";
import { afterEach, beforeAll, beforeEach, describe, expect, test } from "vitest"; import { afterEach, beforeAll, beforeEach, describe, expect, test } from "vitest";
import { Status, StatusEffect } from "#app/data/status-effect"; import { Status, StatusEffect } from "#app/data/status-effect";
import { SwitchType } from "#enums/switch-type";
@ -113,7 +114,7 @@ describe("Abilities - ZEN MODE", () => {
await game.phaseInterceptor.run(EnemyCommandPhase); await game.phaseInterceptor.run(EnemyCommandPhase);
await game.phaseInterceptor.run(TurnStartPhase); await game.phaseInterceptor.run(TurnStartPhase);
game.onNextPrompt("SwitchPhase", Mode.PARTY, () => { game.onNextPrompt("SwitchPhase", Mode.PARTY, () => {
game.scene.unshiftPhase(new SwitchSummonPhase(game.scene, 0, 1, false, false)); game.scene.unshiftPhase(new SwitchSummonPhase(game.scene, SwitchType.SWITCH, 0, 1, false));
game.scene.ui.setMode(Mode.MESSAGE); game.scene.ui.setMode(Mode.MESSAGE);
}); });
game.onNextPrompt("SwitchPhase", Mode.MESSAGE, () => { game.onNextPrompt("SwitchPhase", Mode.MESSAGE, () => {

View File

@ -0,0 +1,56 @@
import { SubstituteTag } from "#app/data/battler-tags";
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, it, expect } from "vitest";
describe("Moves - Shed Tail", () => {
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.SHED_TAIL])
.battleType("single")
.enemySpecies(Species.SNORLAX)
.enemyAbility(Abilities.BALL_FETCH)
.enemyMoveset(Moves.SPLASH);
});
it("transfers a Substitute doll to the switched in Pokemon", async () => {
await game.classicMode.startBattle([Species.MAGIKARP, Species.FEEBAS]);
const magikarp = game.scene.getPlayerPokemon()!;
game.move.select(Moves.SHED_TAIL);
game.doSelectPartyPokemon(1);
await game.phaseInterceptor.to("TurnEndPhase", false);
const feebas = game.scene.getPlayerPokemon()!;
const substituteTag = feebas.getTag(SubstituteTag);
expect(feebas).not.toBe(magikarp);
expect(feebas.hp).toBe(feebas.getMaxHp());
// Note: Shed Tail's HP cost is currently not accurate to mainline, as it
// should cost ceil(maxHP / 2) instead of max(floor(maxHp / 2), 1). The current
// implementation is consistent with Substitute's HP cost logic, but that's not
// the case in mainline for some reason :regiDespair:.
expect(magikarp.hp).toBe(Math.ceil(magikarp.getMaxHp() / 2));
expect(substituteTag).toBeDefined();
expect(substituteTag?.hp).toBe(Math.floor(magikarp.getMaxHp() / 4));
});
});