mirror of
https://github.com/pagefaultgames/pokerogue.git
synced 2025-08-20 06:19:29 +02:00
Merge branch 'beta' into mbhBug
This commit is contained in:
commit
6c858b322e
@ -516,7 +516,7 @@ export class NonSuperEffectiveImmunityAbAttr extends TypeImmunityAbAttr {
|
||||
applyPreDefend(pokemon: Pokemon, passive: boolean, simulated: boolean, attacker: Pokemon, move: Move, cancelled: Utils.BooleanHolder, args: any[]): boolean {
|
||||
const modifierValue = args.length > 0
|
||||
? (args[0] as Utils.NumberHolder).value
|
||||
: pokemon.getAttackTypeEffectiveness(attacker.getMoveType(move), attacker);
|
||||
: pokemon.getAttackTypeEffectiveness(attacker.getMoveType(move), attacker, undefined, undefined, move);
|
||||
|
||||
if (move instanceof AttackMove && modifierValue < 2) {
|
||||
cancelled.value = true; // Suppresses "No Effect" message
|
||||
@ -3180,7 +3180,7 @@ function getAnticipationCondition(): AbAttrCondition {
|
||||
continue;
|
||||
}
|
||||
// the move's base type (not accounting for variable type changes) is super effective
|
||||
if (move.getMove() instanceof AttackMove && pokemon.getAttackTypeEffectiveness(move.getMove().type, opponent, true) >= 2) {
|
||||
if (move.getMove() instanceof AttackMove && pokemon.getAttackTypeEffectiveness(move.getMove().type, opponent, true, undefined, move.getMove()) >= 2) {
|
||||
return true;
|
||||
}
|
||||
// move is a OHKO
|
||||
|
@ -1809,8 +1809,8 @@ export class TypeImmuneTag extends BattlerTag {
|
||||
* @see {@link https://bulbapedia.bulbagarden.net/wiki/Telekinesis_(move) | Moves.TELEKINESIS}
|
||||
*/
|
||||
export class FloatingTag extends TypeImmuneTag {
|
||||
constructor(tagType: BattlerTagType, sourceMove: Moves) {
|
||||
super(tagType, sourceMove, Type.GROUND, 5);
|
||||
constructor(tagType: BattlerTagType, sourceMove: Moves, turnCount: number) {
|
||||
super(tagType, sourceMove, Type.GROUND, turnCount);
|
||||
}
|
||||
|
||||
onAdd(pokemon: Pokemon): void {
|
||||
@ -2158,6 +2158,11 @@ export class CommandedTag extends BattlerTag {
|
||||
pokemon.scene.triggerPokemonBattleAnim(pokemon, PokemonAnimType.COMMANDER_REMOVE);
|
||||
}
|
||||
}
|
||||
|
||||
override loadTag(source: BattlerTag | any): void {
|
||||
super.loadTag(source);
|
||||
this._tatsugiriFormKey = source._tatsugiriFormKey;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@ -3053,7 +3058,7 @@ export function getBattlerTag(tagType: BattlerTagType, turnCount: number, source
|
||||
case BattlerTagType.CHARGED:
|
||||
return new TypeBoostTag(tagType, sourceMove, Type.ELECTRIC, 2, true);
|
||||
case BattlerTagType.FLOATING:
|
||||
return new FloatingTag(tagType, sourceMove);
|
||||
return new FloatingTag(tagType, sourceMove, turnCount);
|
||||
case BattlerTagType.MINIMIZED:
|
||||
return new MinimizeTag();
|
||||
case BattlerTagType.DESTINY_BOND:
|
||||
|
@ -653,7 +653,7 @@ export class FreshStartChallenge extends Challenge {
|
||||
pokemon.shiny = false; // Not shiny
|
||||
pokemon.variant = 0; // Not shiny
|
||||
pokemon.formIndex = 0; // Froakie should be base form
|
||||
pokemon.ivs = [ 10, 10, 10, 10, 10, 10 ]; // Default IVs of 10 for all stats
|
||||
pokemon.ivs = [ 15, 15, 15, 15, 15, 15 ]; // Default IVs of 15 for all stats (Updated to 15 from 10 in 1.2.0)
|
||||
return true;
|
||||
}
|
||||
|
||||
|
@ -897,7 +897,7 @@ export class AttackMove extends Move {
|
||||
|
||||
let attackScore = 0;
|
||||
|
||||
const effectiveness = target.getAttackTypeEffectiveness(this.type, user);
|
||||
const effectiveness = target.getAttackTypeEffectiveness(this.type, user, undefined, undefined, this);
|
||||
attackScore = Math.pow(effectiveness - 1, 2) * effectiveness < 1 ? -2 : 2;
|
||||
if (attackScore) {
|
||||
if (this.category === MoveCategory.PHYSICAL) {
|
||||
@ -2589,12 +2589,11 @@ export class HealStatusEffectAttr extends MoveEffectAttr {
|
||||
|
||||
/**
|
||||
* @param selfTarget - Whether this move targets the user
|
||||
* @param ...effects - List of status effects to cure
|
||||
* @param effects - status effect or list of status effects to cure
|
||||
*/
|
||||
constructor(selfTarget: boolean, ...effects: StatusEffect[]) {
|
||||
constructor(selfTarget: boolean, effects: StatusEffect | StatusEffect[]) {
|
||||
super(selfTarget, { lastHitOnly: true });
|
||||
|
||||
this.effects = effects;
|
||||
this.effects = [ effects ].flat(1);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -4971,22 +4970,6 @@ export class NeutralDamageAgainstFlyingTypeMultiplierAttr extends VariableMoveTy
|
||||
}
|
||||
}
|
||||
|
||||
export class WaterSuperEffectTypeMultiplierAttr extends VariableMoveTypeMultiplierAttr {
|
||||
apply(user: Pokemon, target: Pokemon, move: Move, args: any[]): boolean {
|
||||
const multiplier = args[0] as Utils.NumberHolder;
|
||||
if (target.isOfType(Type.WATER)) {
|
||||
const effectivenessAgainstWater = new Utils.NumberHolder(getTypeDamageMultiplier(move.type, Type.WATER));
|
||||
applyChallenges(user.scene.gameMode, ChallengeType.TYPE_EFFECTIVENESS, effectivenessAgainstWater);
|
||||
if (effectivenessAgainstWater.value !== 0) {
|
||||
multiplier.value *= 2 / effectivenessAgainstWater.value;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
export class IceNoEffectTypeAttr extends VariableMoveTypeMultiplierAttr {
|
||||
/**
|
||||
* Checks to see if the Target is Ice-Type or not. If so, the move will have no effect.
|
||||
@ -5014,6 +4997,41 @@ export class FlyingTypeMultiplierAttr extends VariableMoveTypeMultiplierAttr {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Attribute for moves which have a custom type chart interaction.
|
||||
*/
|
||||
export class VariableMoveTypeChartAttr extends MoveAttr {
|
||||
/**
|
||||
* @param user {@linkcode Pokemon} using the move
|
||||
* @param target {@linkcode Pokemon} target of the move
|
||||
* @param move {@linkcode Move} with this attribute
|
||||
* @param args [0] {@linkcode NumberHolder} holding the type effectiveness
|
||||
* @param args [1] A single defensive type of the target
|
||||
*
|
||||
* @returns true if application of the attribute succeeds
|
||||
*/
|
||||
apply(user: Pokemon, target: Pokemon, move: Move, args: any[]): boolean {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* This class forces Freeze-Dry to be super effective against Water Type.
|
||||
*/
|
||||
export class FreezeDryAttr extends VariableMoveTypeChartAttr {
|
||||
apply(user: Pokemon, target: Pokemon, move: Move, args: any[]): boolean {
|
||||
const multiplier = args[0] as Utils.NumberHolder;
|
||||
const defType = args[1] as Type;
|
||||
|
||||
if (defType === Type.WATER) {
|
||||
multiplier.value = 2;
|
||||
return true;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export class OneHitKOAccuracyAttr extends VariableAccuracyAttr {
|
||||
apply(user: Pokemon, target: Pokemon, move: Move, args: any[]): boolean {
|
||||
const accuracy = args[0] as Utils.NumberHolder;
|
||||
@ -8564,7 +8582,7 @@ export function initMoves() {
|
||||
.attr(AddArenaTagAttr, ArenaTagType.IMPRISON, 1, true, false)
|
||||
.target(MoveTarget.ENEMY_SIDE),
|
||||
new SelfStatusMove(Moves.REFRESH, Type.NORMAL, -1, 20, -1, 0, 3)
|
||||
.attr(HealStatusEffectAttr, true, StatusEffect.PARALYSIS, StatusEffect.POISON, StatusEffect.TOXIC, StatusEffect.BURN)
|
||||
.attr(HealStatusEffectAttr, true, [ StatusEffect.PARALYSIS, StatusEffect.POISON, StatusEffect.TOXIC, StatusEffect.BURN ])
|
||||
.condition((user, target, move) => !!user.status && (user.status.effect === StatusEffect.PARALYSIS || user.status.effect === StatusEffect.POISON || user.status.effect === StatusEffect.TOXIC || user.status.effect === StatusEffect.BURN)),
|
||||
new SelfStatusMove(Moves.GRUDGE, Type.GHOST, -1, 5, -1, 0, 3)
|
||||
.attr(AddBattlerTagAttr, BattlerTagType.GRUDGE, true, undefined, 1),
|
||||
@ -8871,7 +8889,7 @@ export function initMoves() {
|
||||
new SelfStatusMove(Moves.AQUA_RING, Type.WATER, -1, 20, -1, 0, 4)
|
||||
.attr(AddBattlerTagAttr, BattlerTagType.AQUA_RING, true, true),
|
||||
new SelfStatusMove(Moves.MAGNET_RISE, Type.ELECTRIC, -1, 10, -1, 0, 4)
|
||||
.attr(AddBattlerTagAttr, BattlerTagType.FLOATING, true, true)
|
||||
.attr(AddBattlerTagAttr, BattlerTagType.FLOATING, true, true, 5)
|
||||
.condition((user, target, move) => !user.scene.arena.getTag(ArenaTagType.GRAVITY) && [ BattlerTagType.FLOATING, BattlerTagType.IGNORE_FLYING, BattlerTagType.INGRAIN ].every((tag) => !user.getTag(tag))),
|
||||
new AttackMove(Moves.FLARE_BLITZ, Type.FIRE, MoveCategory.PHYSICAL, 120, 100, 15, 10, 0, 4)
|
||||
.attr(RecoilAttr, false, 0.33)
|
||||
@ -9422,8 +9440,7 @@ export function initMoves() {
|
||||
.target(MoveTarget.ALL_NEAR_OTHERS),
|
||||
new AttackMove(Moves.FREEZE_DRY, Type.ICE, MoveCategory.SPECIAL, 70, 100, 20, 10, 0, 6)
|
||||
.attr(StatusEffectAttr, StatusEffect.FREEZE)
|
||||
.attr(WaterSuperEffectTypeMultiplierAttr)
|
||||
.edgeCase(), // This currently just multiplies the move's power instead of changing its effectiveness. It also doesn't account for abilities that modify type effectiveness such as tera shell.
|
||||
.attr(FreezeDryAttr),
|
||||
new AttackMove(Moves.DISARMING_VOICE, Type.FAIRY, MoveCategory.SPECIAL, 40, -1, 15, -1, 0, 6)
|
||||
.soundBased()
|
||||
.target(MoveTarget.ALL_NEAR_ENEMIES),
|
||||
@ -9774,7 +9791,7 @@ export function initMoves() {
|
||||
.condition(
|
||||
(user: Pokemon, target: Pokemon, move: Move) => isNonVolatileStatusEffect(target.status?.effect!)) // TODO: is this bang correct?
|
||||
.attr(HealAttr, 0.5)
|
||||
.attr(HealStatusEffectAttr, false, ...getNonVolatileStatusEffects())
|
||||
.attr(HealStatusEffectAttr, false, getNonVolatileStatusEffects())
|
||||
.triageMove(),
|
||||
new AttackMove(Moves.REVELATION_DANCE, Type.NORMAL, MoveCategory.SPECIAL, 90, 100, 15, -1, 0, 7)
|
||||
.danceMove()
|
||||
@ -9858,11 +9875,9 @@ export function initMoves() {
|
||||
.ignoresSubstitute()
|
||||
.partial(), // Does not steal stats
|
||||
new AttackMove(Moves.SUNSTEEL_STRIKE, Type.STEEL, MoveCategory.PHYSICAL, 100, 100, 5, -1, 0, 7)
|
||||
.ignoresAbilities()
|
||||
.edgeCase(), // Should not ignore abilities when called virtually (metronome)
|
||||
.ignoresAbilities(),
|
||||
new AttackMove(Moves.MOONGEIST_BEAM, Type.GHOST, MoveCategory.SPECIAL, 100, 100, 5, -1, 0, 7)
|
||||
.ignoresAbilities()
|
||||
.edgeCase(), // Should not ignore abilities when called virtually (metronome)
|
||||
.ignoresAbilities(),
|
||||
new StatusMove(Moves.TEARFUL_LOOK, Type.NORMAL, -1, 20, -1, 0, 7)
|
||||
.attr(StatStageChangeAttr, [ Stat.ATK, Stat.SPATK ], -1),
|
||||
new AttackMove(Moves.ZING_ZAP, Type.ELECTRIC, MoveCategory.PHYSICAL, 80, 100, 10, 30, 0, 7)
|
||||
@ -9885,8 +9900,7 @@ export function initMoves() {
|
||||
.punchingMove(),
|
||||
new AttackMove(Moves.PHOTON_GEYSER, Type.PSYCHIC, MoveCategory.SPECIAL, 100, 100, 5, -1, 0, 7)
|
||||
.attr(PhotonGeyserCategoryAttr)
|
||||
.ignoresAbilities()
|
||||
.edgeCase(), // Should not ignore abilities when called virtually (metronome)
|
||||
.ignoresAbilities(),
|
||||
/* Unused */
|
||||
new AttackMove(Moves.LIGHT_THAT_BURNS_THE_SKY, Type.PSYCHIC, MoveCategory.SPECIAL, 200, -1, 1, -1, 0, 7)
|
||||
.attr(PhotonGeyserCategoryAttr)
|
||||
@ -10201,7 +10215,7 @@ export function initMoves() {
|
||||
.attr(StatusEffectAttr, StatusEffect.BURN),
|
||||
new StatusMove(Moves.JUNGLE_HEALING, Type.GRASS, -1, 10, -1, 0, 8)
|
||||
.attr(HealAttr, 0.25, true, false)
|
||||
.attr(HealStatusEffectAttr, false, StatusEffect.PARALYSIS, StatusEffect.POISON, StatusEffect.TOXIC, StatusEffect.BURN, StatusEffect.SLEEP)
|
||||
.attr(HealStatusEffectAttr, false, getNonVolatileStatusEffects())
|
||||
.target(MoveTarget.USER_AND_ALLIES),
|
||||
new AttackMove(Moves.WICKED_BLOW, Type.DARK, MoveCategory.PHYSICAL, 75, 100, 5, -1, 0, 8)
|
||||
.attr(CritOnlyAttr)
|
||||
@ -10305,12 +10319,12 @@ export function initMoves() {
|
||||
.target(MoveTarget.ALL_NEAR_ENEMIES),
|
||||
new StatusMove(Moves.LUNAR_BLESSING, Type.PSYCHIC, -1, 5, -1, 0, 8)
|
||||
.attr(HealAttr, 0.25, true, false)
|
||||
.attr(HealStatusEffectAttr, false, StatusEffect.PARALYSIS, StatusEffect.POISON, StatusEffect.TOXIC, StatusEffect.BURN, StatusEffect.SLEEP)
|
||||
.attr(HealStatusEffectAttr, false, getNonVolatileStatusEffects())
|
||||
.target(MoveTarget.USER_AND_ALLIES)
|
||||
.triageMove(),
|
||||
new SelfStatusMove(Moves.TAKE_HEART, Type.PSYCHIC, -1, 10, -1, 0, 8)
|
||||
.attr(StatStageChangeAttr, [ Stat.SPATK, Stat.SPDEF ], 1, true)
|
||||
.attr(HealStatusEffectAttr, true, StatusEffect.PARALYSIS, StatusEffect.POISON, StatusEffect.TOXIC, StatusEffect.BURN, StatusEffect.SLEEP),
|
||||
.attr(HealStatusEffectAttr, true, [ StatusEffect.PARALYSIS, StatusEffect.POISON, StatusEffect.TOXIC, StatusEffect.BURN, StatusEffect.SLEEP ]),
|
||||
/* Unused
|
||||
new AttackMove(Moves.G_MAX_WILDFIRE, Type.FIRE, MoveCategory.PHYSICAL, 10, -1, 10, -1, 0, 8)
|
||||
.target(MoveTarget.ALL_NEAR_ENEMIES)
|
||||
|
@ -505,11 +505,11 @@ export abstract class PokemonSpeciesForm {
|
||||
scene.anims.create({
|
||||
key: this.getSpriteKey(female, formIndex, shiny, variant),
|
||||
frames: frameNames,
|
||||
frameRate: 12,
|
||||
frameRate: 10,
|
||||
repeat: -1
|
||||
});
|
||||
} else {
|
||||
scene.anims.get(spriteKey).frameRate = 12;
|
||||
scene.anims.get(spriteKey).frameRate = 10;
|
||||
}
|
||||
let spritePath = this.getSpriteAtlasPath(female, formIndex, shiny, variant).replace("variant/", "").replace(/_[1-3]$/, "");
|
||||
const useExpSprite = scene.experimentalSprites && scene.hasExpSprite(spriteKey);
|
||||
|
@ -1841,21 +1841,25 @@ export const trainerConfigs: TrainerConfigs = {
|
||||
[TrainerType.RIVAL]: new TrainerConfig((t = TrainerType.RIVAL)).setName("Finn").setHasGenders("Ivy").setHasCharSprite().setTitle("Rival").setStaticParty().setEncounterBgm(TrainerType.RIVAL).setBattleBgm("battle_rival").setMixedBattleBgm("battle_rival").setPartyTemplates(trainerPartyTemplates.RIVAL)
|
||||
.setModifierRewardFuncs(() => modifierTypes.SUPER_EXP_CHARM, () => modifierTypes.EXP_SHARE)
|
||||
.setEventModifierRewardFuncs(() => modifierTypes.SHINY_CHARM, () => modifierTypes.ABILITY_CHARM)
|
||||
.setPartyMemberFunc(0, getRandomPartyMemberFunc([ Species.BULBASAUR, Species.CHARMANDER, Species.SQUIRTLE, Species.CHIKORITA, Species.CYNDAQUIL, Species.TOTODILE, Species.TREECKO, Species.TORCHIC, Species.MUDKIP, Species.TURTWIG, Species.CHIMCHAR, Species.PIPLUP, Species.SNIVY, Species.TEPIG, Species.OSHAWOTT, Species.CHESPIN, Species.FENNEKIN, Species.FROAKIE, Species.ROWLET, Species.LITTEN, Species.POPPLIO, Species.GROOKEY, Species.SCORBUNNY, Species.SOBBLE, Species.SPRIGATITO, Species.FUECOCO, Species.QUAXLY ], TrainerSlot.TRAINER, true))
|
||||
.setPartyMemberFunc(0, getRandomPartyMemberFunc([ Species.BULBASAUR, Species.CHARMANDER, Species.SQUIRTLE, Species.CHIKORITA, Species.CYNDAQUIL, Species.TOTODILE, Species.TREECKO, Species.TORCHIC, Species.MUDKIP, Species.TURTWIG, Species.CHIMCHAR, Species.PIPLUP, Species.SNIVY, Species.TEPIG, Species.OSHAWOTT, Species.CHESPIN, Species.FENNEKIN, Species.FROAKIE, Species.ROWLET, Species.LITTEN, Species.POPPLIO, Species.GROOKEY, Species.SCORBUNNY, Species.SOBBLE, Species.SPRIGATITO, Species.FUECOCO, Species.QUAXLY ], TrainerSlot.TRAINER, true,
|
||||
(p => p.abilityIndex = 0)))
|
||||
.setPartyMemberFunc(1, getRandomPartyMemberFunc([ Species.PIDGEY, Species.HOOTHOOT, Species.TAILLOW, Species.STARLY, Species.PIDOVE, Species.FLETCHLING, Species.PIKIPEK, Species.ROOKIDEE, Species.WATTREL ], TrainerSlot.TRAINER, true)),
|
||||
[TrainerType.RIVAL_2]: new TrainerConfig(++t).setName("Finn").setHasGenders("Ivy").setHasCharSprite().setTitle("Rival").setStaticParty().setMoneyMultiplier(1.25).setEncounterBgm(TrainerType.RIVAL).setBattleBgm("battle_rival").setMixedBattleBgm("battle_rival").setPartyTemplates(trainerPartyTemplates.RIVAL_2)
|
||||
.setModifierRewardFuncs(() => modifierTypes.EXP_SHARE)
|
||||
.setEventModifierRewardFuncs(() => modifierTypes.SHINY_CHARM)
|
||||
.setPartyMemberFunc(0, getRandomPartyMemberFunc([ Species.IVYSAUR, Species.CHARMELEON, Species.WARTORTLE, Species.BAYLEEF, Species.QUILAVA, Species.CROCONAW, Species.GROVYLE, Species.COMBUSKEN, Species.MARSHTOMP, Species.GROTLE, Species.MONFERNO, Species.PRINPLUP, Species.SERVINE, Species.PIGNITE, Species.DEWOTT, Species.QUILLADIN, Species.BRAIXEN, Species.FROGADIER, Species.DARTRIX, Species.TORRACAT, Species.BRIONNE, Species.THWACKEY, Species.RABOOT, Species.DRIZZILE, Species.FLORAGATO, Species.CROCALOR, Species.QUAXWELL ], TrainerSlot.TRAINER, true))
|
||||
.setPartyMemberFunc(0, getRandomPartyMemberFunc([ Species.IVYSAUR, Species.CHARMELEON, Species.WARTORTLE, Species.BAYLEEF, Species.QUILAVA, Species.CROCONAW, Species.GROVYLE, Species.COMBUSKEN, Species.MARSHTOMP, Species.GROTLE, Species.MONFERNO, Species.PRINPLUP, Species.SERVINE, Species.PIGNITE, Species.DEWOTT, Species.QUILLADIN, Species.BRAIXEN, Species.FROGADIER, Species.DARTRIX, Species.TORRACAT, Species.BRIONNE, Species.THWACKEY, Species.RABOOT, Species.DRIZZILE, Species.FLORAGATO, Species.CROCALOR, Species.QUAXWELL ], TrainerSlot.TRAINER, true,
|
||||
(p => p.abilityIndex = 0)))
|
||||
.setPartyMemberFunc(1, getRandomPartyMemberFunc([ Species.PIDGEOTTO, Species.HOOTHOOT, Species.TAILLOW, Species.STARAVIA, Species.TRANQUILL, Species.FLETCHINDER, Species.TRUMBEAK, Species.CORVISQUIRE, Species.WATTREL ], TrainerSlot.TRAINER, true))
|
||||
.setPartyMemberFunc(2, getSpeciesFilterRandomPartyMemberFunc((species: PokemonSpecies) => !pokemonEvolutions.hasOwnProperty(species.speciesId) && !pokemonPrevolutions.hasOwnProperty(species.speciesId) && species.baseTotal >= 450)),
|
||||
[TrainerType.RIVAL_3]: new TrainerConfig(++t).setName("Finn").setHasGenders("Ivy").setHasCharSprite().setTitle("Rival").setStaticParty().setMoneyMultiplier(1.5).setEncounterBgm(TrainerType.RIVAL).setBattleBgm("battle_rival").setMixedBattleBgm("battle_rival").setPartyTemplates(trainerPartyTemplates.RIVAL_3)
|
||||
.setPartyMemberFunc(0, getRandomPartyMemberFunc([ Species.VENUSAUR, Species.CHARIZARD, Species.BLASTOISE, Species.MEGANIUM, Species.TYPHLOSION, Species.FERALIGATR, Species.SCEPTILE, Species.BLAZIKEN, Species.SWAMPERT, Species.TORTERRA, Species.INFERNAPE, Species.EMPOLEON, Species.SERPERIOR, Species.EMBOAR, Species.SAMUROTT, Species.CHESNAUGHT, Species.DELPHOX, Species.GRENINJA, Species.DECIDUEYE, Species.INCINEROAR, Species.PRIMARINA, Species.RILLABOOM, Species.CINDERACE, Species.INTELEON, Species.MEOWSCARADA, Species.SKELEDIRGE, Species.QUAQUAVAL ], TrainerSlot.TRAINER, true))
|
||||
.setPartyMemberFunc(0, getRandomPartyMemberFunc([ Species.VENUSAUR, Species.CHARIZARD, Species.BLASTOISE, Species.MEGANIUM, Species.TYPHLOSION, Species.FERALIGATR, Species.SCEPTILE, Species.BLAZIKEN, Species.SWAMPERT, Species.TORTERRA, Species.INFERNAPE, Species.EMPOLEON, Species.SERPERIOR, Species.EMBOAR, Species.SAMUROTT, Species.CHESNAUGHT, Species.DELPHOX, Species.GRENINJA, Species.DECIDUEYE, Species.INCINEROAR, Species.PRIMARINA, Species.RILLABOOM, Species.CINDERACE, Species.INTELEON, Species.MEOWSCARADA, Species.SKELEDIRGE, Species.QUAQUAVAL ], TrainerSlot.TRAINER, true,
|
||||
(p => p.abilityIndex = 0)))
|
||||
.setPartyMemberFunc(1, getRandomPartyMemberFunc([ Species.PIDGEOT, Species.NOCTOWL, Species.SWELLOW, Species.STARAPTOR, Species.UNFEZANT, Species.TALONFLAME, Species.TOUCANNON, Species.CORVIKNIGHT, Species.KILOWATTREL ], TrainerSlot.TRAINER, true))
|
||||
.setPartyMemberFunc(2, getSpeciesFilterRandomPartyMemberFunc((species: PokemonSpecies) => !pokemonEvolutions.hasOwnProperty(species.speciesId) && !pokemonPrevolutions.hasOwnProperty(species.speciesId) && species.baseTotal >= 450))
|
||||
.setSpeciesFilter(species => species.baseTotal >= 540),
|
||||
[TrainerType.RIVAL_4]: new TrainerConfig(++t).setName("Finn").setHasGenders("Ivy").setHasCharSprite().setTitle("Rival").setBoss().setStaticParty().setMoneyMultiplier(1.75).setEncounterBgm(TrainerType.RIVAL).setBattleBgm("battle_rival_2").setMixedBattleBgm("battle_rival_2").setPartyTemplates(trainerPartyTemplates.RIVAL_4)
|
||||
.setPartyMemberFunc(0, getRandomPartyMemberFunc([ Species.VENUSAUR, Species.CHARIZARD, Species.BLASTOISE, Species.MEGANIUM, Species.TYPHLOSION, Species.FERALIGATR, Species.SCEPTILE, Species.BLAZIKEN, Species.SWAMPERT, Species.TORTERRA, Species.INFERNAPE, Species.EMPOLEON, Species.SERPERIOR, Species.EMBOAR, Species.SAMUROTT, Species.CHESNAUGHT, Species.DELPHOX, Species.GRENINJA, Species.DECIDUEYE, Species.INCINEROAR, Species.PRIMARINA, Species.RILLABOOM, Species.CINDERACE, Species.INTELEON, Species.MEOWSCARADA, Species.SKELEDIRGE, Species.QUAQUAVAL ], TrainerSlot.TRAINER, true))
|
||||
.setPartyMemberFunc(0, getRandomPartyMemberFunc([ Species.VENUSAUR, Species.CHARIZARD, Species.BLASTOISE, Species.MEGANIUM, Species.TYPHLOSION, Species.FERALIGATR, Species.SCEPTILE, Species.BLAZIKEN, Species.SWAMPERT, Species.TORTERRA, Species.INFERNAPE, Species.EMPOLEON, Species.SERPERIOR, Species.EMBOAR, Species.SAMUROTT, Species.CHESNAUGHT, Species.DELPHOX, Species.GRENINJA, Species.DECIDUEYE, Species.INCINEROAR, Species.PRIMARINA, Species.RILLABOOM, Species.CINDERACE, Species.INTELEON, Species.MEOWSCARADA, Species.SKELEDIRGE, Species.QUAQUAVAL ], TrainerSlot.TRAINER, true,
|
||||
(p => p.abilityIndex = 0)))
|
||||
.setPartyMemberFunc(1, getRandomPartyMemberFunc([ Species.PIDGEOT, Species.NOCTOWL, Species.SWELLOW, Species.STARAPTOR, Species.UNFEZANT, Species.TALONFLAME, Species.TOUCANNON, Species.CORVIKNIGHT, Species.KILOWATTREL ], TrainerSlot.TRAINER, true))
|
||||
.setPartyMemberFunc(2, getSpeciesFilterRandomPartyMemberFunc((species: PokemonSpecies) => !pokemonEvolutions.hasOwnProperty(species.speciesId) && !pokemonPrevolutions.hasOwnProperty(species.speciesId) && species.baseTotal >= 450))
|
||||
.setSpeciesFilter(species => species.baseTotal >= 540)
|
||||
@ -1865,7 +1869,10 @@ export const trainerConfigs: TrainerConfigs = {
|
||||
}),
|
||||
[TrainerType.RIVAL_5]: new TrainerConfig(++t).setName("Finn").setHasGenders("Ivy").setHasCharSprite().setTitle("Rival").setBoss().setStaticParty().setMoneyMultiplier(2.25).setEncounterBgm(TrainerType.RIVAL).setBattleBgm("battle_rival_3").setMixedBattleBgm("battle_rival_3").setPartyTemplates(trainerPartyTemplates.RIVAL_5)
|
||||
.setPartyMemberFunc(0, getRandomPartyMemberFunc([ Species.VENUSAUR, Species.CHARIZARD, Species.BLASTOISE, Species.MEGANIUM, Species.TYPHLOSION, Species.FERALIGATR, Species.SCEPTILE, Species.BLAZIKEN, Species.SWAMPERT, Species.TORTERRA, Species.INFERNAPE, Species.EMPOLEON, Species.SERPERIOR, Species.EMBOAR, Species.SAMUROTT, Species.CHESNAUGHT, Species.DELPHOX, Species.GRENINJA, Species.DECIDUEYE, Species.INCINEROAR, Species.PRIMARINA, Species.RILLABOOM, Species.CINDERACE, Species.INTELEON, Species.MEOWSCARADA, Species.SKELEDIRGE, Species.QUAQUAVAL ], TrainerSlot.TRAINER, true,
|
||||
p => p.setBoss(true, 2)))
|
||||
p => {
|
||||
p.setBoss(true, 2);
|
||||
p.abilityIndex = 0;
|
||||
}))
|
||||
.setPartyMemberFunc(1, getRandomPartyMemberFunc([ Species.PIDGEOT, Species.NOCTOWL, Species.SWELLOW, Species.STARAPTOR, Species.UNFEZANT, Species.TALONFLAME, Species.TOUCANNON, Species.CORVIKNIGHT, Species.KILOWATTREL ], TrainerSlot.TRAINER, true))
|
||||
.setPartyMemberFunc(2, getSpeciesFilterRandomPartyMemberFunc((species: PokemonSpecies) => !pokemonEvolutions.hasOwnProperty(species.speciesId) && !pokemonPrevolutions.hasOwnProperty(species.speciesId) && species.baseTotal >= 450))
|
||||
.setSpeciesFilter(species => species.baseTotal >= 540)
|
||||
@ -1883,6 +1890,7 @@ export const trainerConfigs: TrainerConfigs = {
|
||||
.setPartyMemberFunc(0, getRandomPartyMemberFunc([ Species.VENUSAUR, Species.CHARIZARD, Species.BLASTOISE, Species.MEGANIUM, Species.TYPHLOSION, Species.FERALIGATR, Species.SCEPTILE, Species.BLAZIKEN, Species.SWAMPERT, Species.TORTERRA, Species.INFERNAPE, Species.EMPOLEON, Species.SERPERIOR, Species.EMBOAR, Species.SAMUROTT, Species.CHESNAUGHT, Species.DELPHOX, Species.GRENINJA, Species.DECIDUEYE, Species.INCINEROAR, Species.PRIMARINA, Species.RILLABOOM, Species.CINDERACE, Species.INTELEON, Species.MEOWSCARADA, Species.SKELEDIRGE, Species.QUAQUAVAL ], TrainerSlot.TRAINER, true,
|
||||
p => {
|
||||
p.setBoss(true, 3);
|
||||
p.abilityIndex = 0;
|
||||
p.generateAndPopulateMoveset();
|
||||
}))
|
||||
.setPartyMemberFunc(1, getRandomPartyMemberFunc([ Species.PIDGEOT, Species.NOCTOWL, Species.SWELLOW, Species.STARAPTOR, Species.UNFEZANT, Species.TALONFLAME, Species.TOUCANNON, Species.CORVIKNIGHT, Species.KILOWATTREL ], TrainerSlot.TRAINER, true,
|
||||
|
@ -212,7 +212,7 @@ export default class MysteryEncounterIntroVisuals extends Phaser.GameObjects.Con
|
||||
this.scene.anims.create({
|
||||
key: config.spriteKey,
|
||||
frames: frameNames,
|
||||
frameRate: 12,
|
||||
frameRate: 10,
|
||||
repeat: -1
|
||||
});
|
||||
}
|
||||
|
@ -3,7 +3,7 @@ import BattleScene, { AnySound } from "#app/battle-scene";
|
||||
import { Variant, VariantSet, variantColorCache } from "#app/data/variant";
|
||||
import { variantData } from "#app/data/variant";
|
||||
import BattleInfo, { PlayerBattleInfo, EnemyBattleInfo } from "#app/ui/battle-info";
|
||||
import Move, { HighCritAttr, HitsTagAttr, applyMoveAttrs, FixedDamageAttr, VariableAtkAttr, allMoves, MoveCategory, TypelessAttr, CritOnlyAttr, getMoveTargets, OneHitKOAttr, VariableMoveTypeAttr, VariableDefAttr, AttackMove, ModifiedDamageAttr, VariableMoveTypeMultiplierAttr, IgnoreOpponentStatStagesAttr, SacrificialAttr, VariableMoveCategoryAttr, CounterDamageAttr, StatStageChangeAttr, RechargeAttr, IgnoreWeatherTypeDebuffAttr, BypassBurnDamageReductionAttr, SacrificialAttrOnHit, OneHitKOAccuracyAttr, RespectAttackTypeImmunityAttr, MoveTarget, CombinedPledgeStabBoostAttr } from "#app/data/move";
|
||||
import Move, { HighCritAttr, HitsTagAttr, applyMoveAttrs, FixedDamageAttr, VariableAtkAttr, allMoves, MoveCategory, TypelessAttr, CritOnlyAttr, getMoveTargets, OneHitKOAttr, VariableMoveTypeAttr, VariableDefAttr, AttackMove, ModifiedDamageAttr, VariableMoveTypeMultiplierAttr, IgnoreOpponentStatStagesAttr, SacrificialAttr, VariableMoveCategoryAttr, CounterDamageAttr, StatStageChangeAttr, RechargeAttr, IgnoreWeatherTypeDebuffAttr, BypassBurnDamageReductionAttr, SacrificialAttrOnHit, OneHitKOAccuracyAttr, RespectAttackTypeImmunityAttr, MoveTarget, CombinedPledgeStabBoostAttr, VariableMoveTypeChartAttr } from "#app/data/move";
|
||||
import { default as PokemonSpecies, PokemonSpeciesForm, getFusedSpeciesName, getPokemonSpecies, getPokemonSpeciesForm } from "#app/data/pokemon-species";
|
||||
import { CLASSIC_CANDY_FRIENDSHIP_MULTIPLIER, getStarterValueFriendshipCap, speciesStarterCosts } from "#app/data/balance/starters";
|
||||
import { starterPassiveAbilities } from "#app/data/balance/passives";
|
||||
@ -427,7 +427,7 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container {
|
||||
this.scene.anims.create({
|
||||
key: this.getBattleSpriteKey(),
|
||||
frames: battleFrameNames,
|
||||
frameRate: 12,
|
||||
frameRate: 10,
|
||||
repeat: -1
|
||||
});
|
||||
}
|
||||
@ -1632,7 +1632,7 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container {
|
||||
const moveType = source.getMoveType(move);
|
||||
|
||||
const typeMultiplier = new Utils.NumberHolder((move.category !== MoveCategory.STATUS || move.hasAttr(RespectAttackTypeImmunityAttr))
|
||||
? this.getAttackTypeEffectiveness(moveType, source, false, simulated)
|
||||
? this.getAttackTypeEffectiveness(moveType, source, false, simulated, move)
|
||||
: 1);
|
||||
|
||||
applyMoveAttrs(VariableMoveTypeMultiplierAttr, source, this, move, typeMultiplier);
|
||||
@ -1684,9 +1684,10 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container {
|
||||
* @param source {@linkcode Pokemon} the Pokemon using the move
|
||||
* @param ignoreStrongWinds whether or not this ignores strong winds (anticipation, forewarn, stealth rocks)
|
||||
* @param simulated tag to only apply the strong winds effect message when the move is used
|
||||
* @param move (optional) the move whose type effectiveness is to be checked. Used for applying {@linkcode VariableMoveTypeChartAttr}
|
||||
* @returns a multiplier for the type effectiveness
|
||||
*/
|
||||
getAttackTypeEffectiveness(moveType: Type, source?: Pokemon, ignoreStrongWinds: boolean = false, simulated: boolean = true): TypeDamageMultiplier {
|
||||
getAttackTypeEffectiveness(moveType: Type, source?: Pokemon, ignoreStrongWinds: boolean = false, simulated: boolean = true, move?: Move): TypeDamageMultiplier {
|
||||
if (moveType === Type.STELLAR) {
|
||||
return this.isTerastallized() ? 2 : 1;
|
||||
}
|
||||
@ -1705,6 +1706,9 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container {
|
||||
let multiplier = types.map(defType => {
|
||||
const multiplier = new Utils.NumberHolder(getTypeDamageMultiplier(moveType, defType));
|
||||
applyChallenges(this.scene.gameMode, ChallengeType.TYPE_EFFECTIVENESS, multiplier);
|
||||
if (move) {
|
||||
applyMoveAttrs(VariableMoveTypeChartAttr, null, this, move, multiplier, defType);
|
||||
}
|
||||
if (source) {
|
||||
const ignoreImmunity = new Utils.BooleanHolder(false);
|
||||
if (source.isActive(true) && source.hasAbilityWithAttr(IgnoreTypeImmunityAbAttr)) {
|
||||
@ -3608,7 +3612,7 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container {
|
||||
}
|
||||
this.status = null;
|
||||
if (lastStatus === StatusEffect.SLEEP) {
|
||||
this.setFrameRate(12);
|
||||
this.setFrameRate(10);
|
||||
if (this.getTag(BattlerTagType.NIGHTMARE)) {
|
||||
this.lapseTag(BattlerTagType.NIGHTMARE);
|
||||
}
|
||||
|
@ -243,7 +243,7 @@ export class GameMode implements GameModeConfig {
|
||||
* @returns true if waveIndex is a multiple of 50 in Endless
|
||||
*/
|
||||
isEndlessBoss(waveIndex: integer): boolean {
|
||||
return !!(waveIndex % 50) &&
|
||||
return waveIndex % 50 === 0 &&
|
||||
(this.modeId === GameModes.ENDLESS || this.modeId === GameModes.SPLICED_ENDLESS);
|
||||
}
|
||||
|
||||
|
@ -3631,7 +3631,7 @@ export class EnemyEndureChanceModifier extends EnemyPersistentModifier {
|
||||
super(type, stackCount || 10);
|
||||
|
||||
//Hardcode temporarily
|
||||
this.chance = .02;
|
||||
this.chance = 2;
|
||||
}
|
||||
|
||||
match(modifier: Modifier) {
|
||||
@ -3639,11 +3639,11 @@ export class EnemyEndureChanceModifier extends EnemyPersistentModifier {
|
||||
}
|
||||
|
||||
clone() {
|
||||
return new EnemyEndureChanceModifier(this.type, this.chance * 100, this.stackCount);
|
||||
return new EnemyEndureChanceModifier(this.type, this.chance, this.stackCount);
|
||||
}
|
||||
|
||||
getArgs(): any[] {
|
||||
return [ this.chance * 100 ];
|
||||
return [ this.chance ];
|
||||
}
|
||||
|
||||
/**
|
||||
@ -3652,7 +3652,7 @@ export class EnemyEndureChanceModifier extends EnemyPersistentModifier {
|
||||
* @returns `true` if {@linkcode Pokemon} endured
|
||||
*/
|
||||
override apply(target: Pokemon): boolean {
|
||||
if (target.battleData.endured || Phaser.Math.RND.realInRange(0, 1) >= (this.chance * this.getStackCount())) {
|
||||
if (target.battleData.endured || target.randSeedInt(100) >= (this.chance * this.getStackCount())) {
|
||||
return false;
|
||||
}
|
||||
|
||||
|
@ -47,7 +47,18 @@ class DefaultOverrides {
|
||||
/** a specific seed (default: a random string of 24 characters) */
|
||||
readonly SEED_OVERRIDE: string = "";
|
||||
readonly WEATHER_OVERRIDE: WeatherType = WeatherType.NONE;
|
||||
readonly BATTLE_TYPE_OVERRIDE: "double" | "single" | null = null;
|
||||
/**
|
||||
* If `null`, ignore this override.
|
||||
*
|
||||
* If `"single"`, set every non-trainer battle to be a single battle.
|
||||
*
|
||||
* If `"double"`, set every battle (including trainer battles) to be a double battle.
|
||||
*
|
||||
* If `"even-doubles"`, follow the `"double"` rule on even wave numbers, and follow the `"single"` rule on odd wave numbers.
|
||||
*
|
||||
* If `"odd-doubles"`, follow the `"double"` rule on odd wave numbers, and follow the `"single"` rule on even wave numbers.
|
||||
*/
|
||||
readonly BATTLE_TYPE_OVERRIDE: BattleStyle | null = null;
|
||||
readonly STARTING_WAVE_OVERRIDE: number = 0;
|
||||
readonly STARTING_BIOME_OVERRIDE: Biome = Biome.TOWN;
|
||||
readonly ARENA_TINT_OVERRIDE: TimeOfDay | null = null;
|
||||
@ -229,3 +240,5 @@ export default {
|
||||
...defaultOverrides,
|
||||
...overrides
|
||||
} satisfies InstanceType<typeof DefaultOverrides>;
|
||||
|
||||
export type BattleStyle = "double" | "single" | "even-doubles" | "odd-doubles";
|
||||
|
@ -26,6 +26,7 @@ import {
|
||||
applyMoveAttrs,
|
||||
AttackMove,
|
||||
DelayedAttackAttr,
|
||||
FlinchAttr,
|
||||
HitsTagAttr,
|
||||
MissEffectAttr,
|
||||
MoveAttr,
|
||||
@ -502,6 +503,10 @@ export class MoveEffectPhase extends PokemonPhase {
|
||||
*/
|
||||
protected applyHeldItemFlinchCheck(user: Pokemon, target: Pokemon, dealsDamage: boolean) : () => void {
|
||||
return () => {
|
||||
if (this.move.getMove().hasAttr(FlinchAttr)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (dealsDamage && !target.hasAbilityWithAttr(IgnoreMoveEffectsAbAttr) && !this.move.getMove().hitsSubstitute(user, target)) {
|
||||
const flinched = new BooleanHolder(false);
|
||||
user.scene.applyModifiers(FlinchChanceModifier, user.isPlayer(), user, flinched);
|
||||
|
@ -312,6 +312,10 @@ export class PokemonAnimPhase extends BattlePhase {
|
||||
// Note: unlike the other Commander animation, this is played through the
|
||||
// Dondozo instead of the Tatsugiri.
|
||||
const tatsugiri = this.pokemon.getAlly();
|
||||
if (isNullOrUndefined(tatsugiri)) {
|
||||
console.warn("Aborting COMMANDER_REMOVE anim: Tatsugiri is undefined");
|
||||
return this.end();
|
||||
}
|
||||
|
||||
const tatsuSprite = this.scene.addPokemonSprite(
|
||||
tatsugiri,
|
||||
|
@ -29,10 +29,14 @@ export class QuietFormChangePhase extends BattlePhase {
|
||||
|
||||
const preName = getPokemonNameWithAffix(this.pokemon);
|
||||
|
||||
if (!this.pokemon.isOnField() || this.pokemon.getTag(SemiInvulnerableTag)) {
|
||||
this.pokemon.changeForm(this.formChange).then(() => {
|
||||
this.scene.ui.showText(getSpeciesFormChangeMessage(this.pokemon, this.formChange, preName), null, () => this.end(), 1500);
|
||||
});
|
||||
if (!this.pokemon.isOnField() || this.pokemon.getTag(SemiInvulnerableTag) || this.pokemon.isFainted()) {
|
||||
if (this.pokemon.isPlayer() || this.pokemon.isActive()) {
|
||||
this.pokemon.changeForm(this.formChange).then(() => {
|
||||
this.scene.ui.showText(getSpeciesFormChangeMessage(this.pokemon, this.formChange, preName), null, () => this.end(), 1500);
|
||||
});
|
||||
} else {
|
||||
this.end();
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
|
@ -679,7 +679,7 @@ export class GameData {
|
||||
return ret;
|
||||
}
|
||||
|
||||
return k.endsWith("Attr") && ![ "natureAttr", "abilityAttr", "passiveAttr" ].includes(k) ? BigInt(v) : v;
|
||||
return k.endsWith("Attr") && ![ "natureAttr", "abilityAttr", "passiveAttr" ].includes(k) ? BigInt(v ?? 0) : v;
|
||||
}) as SystemSaveData;
|
||||
}
|
||||
|
||||
@ -1540,7 +1540,7 @@ export class GameData {
|
||||
entry.caughtAttr = defaultStarterAttr;
|
||||
entry.natureAttr = 1 << (defaultStarterNatures[ds] + 1);
|
||||
for (const i in entry.ivs) {
|
||||
entry.ivs[i] = 10;
|
||||
entry.ivs[i] = 15;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1,4 +1,6 @@
|
||||
import { Status } from "#app/data/status-effect";
|
||||
import { Abilities } from "#enums/abilities";
|
||||
import { GameModes, getGameMode } from "#app/game-mode";
|
||||
import { BattleEndPhase } from "#app/phases/battle-end-phase";
|
||||
import { TurnInitPhase } from "#app/phases/turn-init-phase";
|
||||
import { Moves } from "#enums/moves";
|
||||
@ -6,9 +8,11 @@ import { Species } from "#enums/species";
|
||||
import { StatusEffect } from "#enums/status-effect";
|
||||
import GameManager from "#test/utils/gameManager";
|
||||
import Phaser from "phaser";
|
||||
import { afterEach, beforeAll, beforeEach, describe, expect, it } from "vitest";
|
||||
import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
|
||||
describe("Double Battles", () => {
|
||||
const DOUBLE_CHANCE = 8; // Normal chance of double battle is 1/8
|
||||
|
||||
let phaserGame: Phaser.Game;
|
||||
let game: GameManager;
|
||||
|
||||
@ -56,4 +60,40 @@ describe("Double Battles", () => {
|
||||
await game.phaseInterceptor.to(TurnInitPhase);
|
||||
expect(game.scene.getPlayerField().filter(p => !p.isFainted())).toHaveLength(2);
|
||||
}, 20000);
|
||||
|
||||
it("randomly chooses between single and double battles if there is no battle type override", async () => {
|
||||
let rngSweepProgress = 0; // Will simulate RNG rolls by slowly increasing from 0 to 1
|
||||
let doubleCount = 0;
|
||||
let singleCount = 0;
|
||||
|
||||
vi.spyOn(Phaser.Math.RND, "realInRange").mockImplementation((min: number, max: number) => {
|
||||
return rngSweepProgress * (max - min) + min;
|
||||
});
|
||||
|
||||
game.override.enemyMoveset(Moves.SPLASH)
|
||||
.moveset(Moves.SPLASH)
|
||||
.enemyAbility(Abilities.BALL_FETCH)
|
||||
.ability(Abilities.BALL_FETCH);
|
||||
|
||||
// Play through endless, waves 1 to 9, counting number of double battles from waves 2 to 9
|
||||
await game.classicMode.startBattle([ Species.BULBASAUR ]);
|
||||
game.scene.gameMode = getGameMode(GameModes.ENDLESS);
|
||||
|
||||
for (let i = 0; i < DOUBLE_CHANCE; i++) {
|
||||
rngSweepProgress = (i + 0.5) / DOUBLE_CHANCE;
|
||||
|
||||
game.move.select(Moves.SPLASH);
|
||||
await game.doKillOpponents();
|
||||
await game.toNextWave();
|
||||
|
||||
if (game.scene.getEnemyParty().length === 1) {
|
||||
singleCount++;
|
||||
} else if (game.scene.getEnemyParty().length === 2) {
|
||||
doubleCount++;
|
||||
}
|
||||
}
|
||||
|
||||
expect(doubleCount).toBe(1);
|
||||
expect(singleCount).toBe(DOUBLE_CHANCE - 1);
|
||||
});
|
||||
});
|
||||
|
@ -2,6 +2,8 @@ import { BattlerIndex } from "#app/battle";
|
||||
import { Abilities } from "#app/enums/abilities";
|
||||
import { Moves } from "#app/enums/moves";
|
||||
import { Species } from "#app/enums/species";
|
||||
import { Type } from "#enums/type";
|
||||
import { Challenges } from "#enums/challenges";
|
||||
import GameManager from "#test/utils/gameManager";
|
||||
import Phaser from "phaser";
|
||||
import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
@ -28,7 +30,7 @@ describe("Moves - Freeze-Dry", () => {
|
||||
.enemyMoveset(Moves.SPLASH)
|
||||
.starterSpecies(Species.FEEBAS)
|
||||
.ability(Abilities.BALL_FETCH)
|
||||
.moveset([ Moves.FREEZE_DRY ]);
|
||||
.moveset([ Moves.FREEZE_DRY, Moves.FORESTS_CURSE, Moves.SOAK ]);
|
||||
});
|
||||
|
||||
it("should deal 2x damage to pure water types", async () => {
|
||||
@ -97,8 +99,72 @@ describe("Moves - Freeze-Dry", () => {
|
||||
expect(enemy.hp).toBeLessThan(enemy.getMaxHp());
|
||||
});
|
||||
|
||||
// enable if this is ever fixed (lol)
|
||||
it.todo("should deal 2x damage to water types under Normalize", async () => {
|
||||
it("should deal 8x damage to water/ground/grass type under Forest's Curse", async () => {
|
||||
game.override.enemySpecies(Species.QUAGSIRE);
|
||||
await game.classicMode.startBattle();
|
||||
|
||||
const enemy = game.scene.getEnemyPokemon()!;
|
||||
vi.spyOn(enemy, "getMoveEffectiveness");
|
||||
|
||||
game.move.select(Moves.FORESTS_CURSE);
|
||||
await game.toNextTurn();
|
||||
|
||||
game.move.select(Moves.FREEZE_DRY);
|
||||
await game.setTurnOrder([ BattlerIndex.PLAYER, BattlerIndex.ENEMY ]);
|
||||
await game.phaseInterceptor.to("MoveEffectPhase");
|
||||
|
||||
expect(enemy.getMoveEffectiveness).toHaveReturnedWith(8);
|
||||
});
|
||||
|
||||
it("should deal 2x damage to steel type terastallized into water", async () => {
|
||||
game.override.enemySpecies(Species.SKARMORY)
|
||||
.enemyHeldItems([{ name: "TERA_SHARD", type: Type.WATER }]);
|
||||
await game.classicMode.startBattle();
|
||||
|
||||
const enemy = game.scene.getEnemyPokemon()!;
|
||||
vi.spyOn(enemy, "getMoveEffectiveness");
|
||||
|
||||
game.move.select(Moves.FREEZE_DRY);
|
||||
await game.setTurnOrder([ BattlerIndex.PLAYER, BattlerIndex.ENEMY ]);
|
||||
await game.phaseInterceptor.to("MoveEffectPhase");
|
||||
|
||||
expect(enemy.getMoveEffectiveness).toHaveReturnedWith(2);
|
||||
});
|
||||
|
||||
it("should deal 0.5x damage to water type terastallized into fire", async () => {
|
||||
game.override.enemySpecies(Species.PELIPPER)
|
||||
.enemyHeldItems([{ name: "TERA_SHARD", type: Type.FIRE }]);
|
||||
await game.classicMode.startBattle();
|
||||
|
||||
const enemy = game.scene.getEnemyPokemon()!;
|
||||
vi.spyOn(enemy, "getMoveEffectiveness");
|
||||
|
||||
game.move.select(Moves.FREEZE_DRY);
|
||||
await game.setTurnOrder([ BattlerIndex.PLAYER, BattlerIndex.ENEMY ]);
|
||||
await game.phaseInterceptor.to("MoveEffectPhase");
|
||||
|
||||
expect(enemy.getMoveEffectiveness).toHaveReturnedWith(0.5);
|
||||
});
|
||||
|
||||
it("should deal 0.5x damage to water type Terapagos with Tera Shell", async () => {
|
||||
game.override.enemySpecies(Species.TERAPAGOS)
|
||||
.enemyAbility(Abilities.TERA_SHELL);
|
||||
await game.classicMode.startBattle();
|
||||
|
||||
const enemy = game.scene.getEnemyPokemon()!;
|
||||
vi.spyOn(enemy, "getMoveEffectiveness");
|
||||
|
||||
game.move.select(Moves.SOAK);
|
||||
await game.toNextTurn();
|
||||
|
||||
game.move.select(Moves.FREEZE_DRY);
|
||||
await game.setTurnOrder([ BattlerIndex.PLAYER, BattlerIndex.ENEMY ]);
|
||||
await game.phaseInterceptor.to("MoveEffectPhase");
|
||||
|
||||
expect(enemy.getMoveEffectiveness).toHaveReturnedWith(0.5);
|
||||
});
|
||||
|
||||
it("should deal 2x damage to water type under Normalize", async () => {
|
||||
game.override.ability(Abilities.NORMALIZE);
|
||||
await game.classicMode.startBattle();
|
||||
|
||||
@ -112,8 +178,39 @@ describe("Moves - Freeze-Dry", () => {
|
||||
expect(enemy.getMoveEffectiveness).toHaveReturnedWith(2);
|
||||
});
|
||||
|
||||
// enable once Electrify is implemented (and the interaction is fixed, as above)
|
||||
it.todo("should deal 2x damage to water types under Electrify", async () => {
|
||||
it("should deal 0.25x damage to rock/steel type under Normalize", async () => {
|
||||
game.override
|
||||
.ability(Abilities.NORMALIZE)
|
||||
.enemySpecies(Species.SHIELDON);
|
||||
await game.classicMode.startBattle();
|
||||
|
||||
const enemy = game.scene.getEnemyPokemon()!;
|
||||
vi.spyOn(enemy, "getMoveEffectiveness");
|
||||
|
||||
game.move.select(Moves.FREEZE_DRY);
|
||||
await game.setTurnOrder([ BattlerIndex.PLAYER, BattlerIndex.ENEMY ]);
|
||||
await game.phaseInterceptor.to("MoveEffectPhase");
|
||||
|
||||
expect(enemy.getMoveEffectiveness).toHaveReturnedWith(0.25);
|
||||
});
|
||||
|
||||
it("should deal 0x damage to water/ghost type under Normalize", async () => {
|
||||
game.override
|
||||
.ability(Abilities.NORMALIZE)
|
||||
.enemySpecies(Species.JELLICENT);
|
||||
await game.classicMode.startBattle();
|
||||
|
||||
const enemy = game.scene.getEnemyPokemon()!;
|
||||
vi.spyOn(enemy, "getMoveEffectiveness");
|
||||
|
||||
game.move.select(Moves.FREEZE_DRY);
|
||||
await game.setTurnOrder([ BattlerIndex.PLAYER, BattlerIndex.ENEMY ]);
|
||||
await game.phaseInterceptor.to("BerryPhase");
|
||||
|
||||
expect(enemy.getMoveEffectiveness).toHaveReturnedWith(0);
|
||||
});
|
||||
|
||||
it("should deal 2x damage to water type under Electrify", async () => {
|
||||
game.override.enemyMoveset([ Moves.ELECTRIFY ]);
|
||||
await game.classicMode.startBattle();
|
||||
|
||||
@ -126,4 +223,128 @@ describe("Moves - Freeze-Dry", () => {
|
||||
|
||||
expect(enemy.getMoveEffectiveness).toHaveReturnedWith(2);
|
||||
});
|
||||
|
||||
it("should deal 4x damage to water/flying type under Electrify", async () => {
|
||||
game.override
|
||||
.enemyMoveset([ Moves.ELECTRIFY ])
|
||||
.enemySpecies(Species.GYARADOS);
|
||||
await game.classicMode.startBattle();
|
||||
|
||||
const enemy = game.scene.getEnemyPokemon()!;
|
||||
vi.spyOn(enemy, "getMoveEffectiveness");
|
||||
|
||||
game.move.select(Moves.FREEZE_DRY);
|
||||
await game.setTurnOrder([ BattlerIndex.ENEMY, BattlerIndex.PLAYER ]);
|
||||
await game.phaseInterceptor.to("BerryPhase");
|
||||
|
||||
expect(enemy.getMoveEffectiveness).toHaveReturnedWith(4);
|
||||
});
|
||||
|
||||
it("should deal 0x damage to water/ground type under Electrify", async () => {
|
||||
game.override
|
||||
.enemyMoveset([ Moves.ELECTRIFY ])
|
||||
.enemySpecies(Species.BARBOACH);
|
||||
await game.classicMode.startBattle();
|
||||
|
||||
const enemy = game.scene.getEnemyPokemon()!;
|
||||
vi.spyOn(enemy, "getMoveEffectiveness");
|
||||
|
||||
game.move.select(Moves.FREEZE_DRY);
|
||||
await game.setTurnOrder([ BattlerIndex.ENEMY, BattlerIndex.PLAYER ]);
|
||||
await game.phaseInterceptor.to("BerryPhase");
|
||||
|
||||
expect(enemy.getMoveEffectiveness).toHaveReturnedWith(0);
|
||||
});
|
||||
|
||||
it("should deal 0.25x damage to Grass/Dragon type under Electrify", async () => {
|
||||
game.override
|
||||
.enemyMoveset([ Moves.ELECTRIFY ])
|
||||
.enemySpecies(Species.FLAPPLE);
|
||||
await game.classicMode.startBattle();
|
||||
|
||||
const enemy = game.scene.getEnemyPokemon()!;
|
||||
vi.spyOn(enemy, "getMoveEffectiveness");
|
||||
|
||||
game.move.select(Moves.FREEZE_DRY);
|
||||
await game.setTurnOrder([ BattlerIndex.ENEMY, BattlerIndex.PLAYER ]);
|
||||
await game.phaseInterceptor.to("BerryPhase");
|
||||
|
||||
expect(enemy.getMoveEffectiveness).toHaveReturnedWith(0.25);
|
||||
});
|
||||
|
||||
it("should deal 2x damage to Water type during inverse battle", async () => {
|
||||
game.override
|
||||
.moveset([ Moves.FREEZE_DRY ])
|
||||
.enemySpecies(Species.MAGIKARP);
|
||||
game.challengeMode.addChallenge(Challenges.INVERSE_BATTLE, 1, 1);
|
||||
|
||||
|
||||
await game.challengeMode.startBattle();
|
||||
|
||||
const enemy = game.scene.getEnemyPokemon()!;
|
||||
vi.spyOn(enemy, "getMoveEffectiveness");
|
||||
|
||||
game.move.select(Moves.FREEZE_DRY);
|
||||
await game.setTurnOrder([ BattlerIndex.PLAYER, BattlerIndex.ENEMY ]);
|
||||
await game.phaseInterceptor.to("MoveEffectPhase");
|
||||
|
||||
expect(enemy.getMoveEffectiveness).toHaveLastReturnedWith(2);
|
||||
});
|
||||
|
||||
it("should deal 2x damage to Water type during inverse battle under Normalize", async () => {
|
||||
game.override
|
||||
.moveset([ Moves.FREEZE_DRY ])
|
||||
.ability(Abilities.NORMALIZE)
|
||||
.enemySpecies(Species.MAGIKARP);
|
||||
game.challengeMode.addChallenge(Challenges.INVERSE_BATTLE, 1, 1);
|
||||
|
||||
await game.challengeMode.startBattle();
|
||||
|
||||
const enemy = game.scene.getEnemyPokemon()!;
|
||||
vi.spyOn(enemy, "getMoveEffectiveness");
|
||||
|
||||
game.move.select(Moves.FREEZE_DRY);
|
||||
await game.setTurnOrder([ BattlerIndex.PLAYER, BattlerIndex.ENEMY ]);
|
||||
await game.phaseInterceptor.to("MoveEffectPhase");
|
||||
|
||||
expect(enemy.getMoveEffectiveness).toHaveLastReturnedWith(2);
|
||||
});
|
||||
|
||||
it("should deal 2x damage to Water type during inverse battle under Electrify", async () => {
|
||||
game.override
|
||||
.moveset([ Moves.FREEZE_DRY ])
|
||||
.enemySpecies(Species.MAGIKARP)
|
||||
.enemyMoveset([ Moves.ELECTRIFY ]);
|
||||
game.challengeMode.addChallenge(Challenges.INVERSE_BATTLE, 1, 1);
|
||||
|
||||
await game.challengeMode.startBattle();
|
||||
|
||||
const enemy = game.scene.getEnemyPokemon()!;
|
||||
vi.spyOn(enemy, "getMoveEffectiveness");
|
||||
|
||||
game.move.select(Moves.FREEZE_DRY);
|
||||
await game.setTurnOrder([ BattlerIndex.ENEMY, BattlerIndex.PLAYER ]);
|
||||
await game.phaseInterceptor.to("MoveEffectPhase");
|
||||
|
||||
expect(enemy.getMoveEffectiveness).toHaveLastReturnedWith(2);
|
||||
});
|
||||
|
||||
it("should deal 1x damage to water/flying type during inverse battle under Electrify", async () => {
|
||||
game.override
|
||||
.enemyMoveset([ Moves.ELECTRIFY ])
|
||||
.enemySpecies(Species.GYARADOS);
|
||||
|
||||
game.challengeMode.addChallenge(Challenges.INVERSE_BATTLE, 1, 1);
|
||||
|
||||
await game.challengeMode.startBattle();
|
||||
|
||||
const enemy = game.scene.getEnemyPokemon()!;
|
||||
vi.spyOn(enemy, "getMoveEffectiveness");
|
||||
|
||||
game.move.select(Moves.FREEZE_DRY);
|
||||
await game.setTurnOrder([ BattlerIndex.ENEMY, BattlerIndex.PLAYER ]);
|
||||
await game.phaseInterceptor.to("BerryPhase");
|
||||
|
||||
expect(enemy.getMoveEffectiveness).toHaveReturnedWith(1);
|
||||
});
|
||||
});
|
||||
|
@ -62,7 +62,7 @@ describe("Moves - Gastro Acid", () => {
|
||||
});
|
||||
|
||||
it("fails if used on an enemy with an already-suppressed ability", async () => {
|
||||
game.override.battleType(null);
|
||||
game.override.battleType("single");
|
||||
|
||||
await game.startBattle();
|
||||
|
||||
|
59
src/test/moves/moongeist_beam.test.ts
Normal file
59
src/test/moves/moongeist_beam.test.ts
Normal file
@ -0,0 +1,59 @@
|
||||
import { allMoves, RandomMoveAttr } from "#app/data/move";
|
||||
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, vi } from "vitest";
|
||||
|
||||
describe("Moves - Moongeist Beam", () => {
|
||||
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.MOONGEIST_BEAM, Moves.METRONOME ])
|
||||
.ability(Abilities.BALL_FETCH)
|
||||
.startingLevel(200)
|
||||
.battleType("single")
|
||||
.disableCrits()
|
||||
.enemySpecies(Species.MAGIKARP)
|
||||
.enemyAbility(Abilities.STURDY)
|
||||
.enemyMoveset(Moves.SPLASH);
|
||||
});
|
||||
|
||||
// Also covers Photon Geyser and Sunsteel Strike
|
||||
it("should ignore enemy abilities", async () => {
|
||||
await game.classicMode.startBattle([ Species.MILOTIC ]);
|
||||
|
||||
const enemy = game.scene.getEnemyPokemon()!;
|
||||
|
||||
game.move.select(Moves.MOONGEIST_BEAM);
|
||||
await game.phaseInterceptor.to("BerryPhase");
|
||||
|
||||
expect(enemy.isFainted()).toBe(true);
|
||||
});
|
||||
|
||||
// Also covers Photon Geyser and Sunsteel Strike
|
||||
it("should not ignore enemy abilities when called by another move, such as metronome", async () => {
|
||||
await game.classicMode.startBattle([ Species.MILOTIC ]);
|
||||
vi.spyOn(allMoves[Moves.METRONOME].getAttrs(RandomMoveAttr)[0], "getMoveOverride").mockReturnValue(Moves.MOONGEIST_BEAM);
|
||||
|
||||
game.move.select(Moves.METRONOME);
|
||||
await game.phaseInterceptor.to("BerryPhase");
|
||||
|
||||
expect(game.scene.getEnemyPokemon()!.isFainted()).toBe(false);
|
||||
expect(game.scene.getPlayerPokemon()!.getLastXMoves()[0].move).toBe(Moves.MOONGEIST_BEAM);
|
||||
});
|
||||
});
|
@ -4,7 +4,7 @@ import { Abilities } from "#app/enums/abilities";
|
||||
import * as GameMode from "#app/game-mode";
|
||||
import { GameModes, getGameMode } from "#app/game-mode";
|
||||
import { ModifierOverride } from "#app/modifier/modifier-type";
|
||||
import Overrides from "#app/overrides";
|
||||
import Overrides, { BattleStyle } from "#app/overrides";
|
||||
import { Unlockables } from "#app/system/unlockables";
|
||||
import { Biome } from "#enums/biome";
|
||||
import { Moves } from "#enums/moves";
|
||||
@ -238,13 +238,14 @@ export class OverridesHelper extends GameManagerHelper {
|
||||
}
|
||||
|
||||
/**
|
||||
* Override the battle type (single or double)
|
||||
* Override the battle type (e.g., single or double).
|
||||
* @see {@linkcode Overrides.BATTLE_TYPE_OVERRIDE}
|
||||
* @param battleType battle type to set
|
||||
* @returns `this`
|
||||
*/
|
||||
public battleType(battleType: "single" | "double" | null): this {
|
||||
public battleType(battleType: BattleStyle | null): this {
|
||||
vi.spyOn(Overrides, "BATTLE_TYPE_OVERRIDE", "get").mockReturnValue(battleType);
|
||||
this.log(`Battle type set to ${battleType} only!`);
|
||||
this.log(battleType === null ? "Battle type override disabled!" : `Battle type set to ${battleType}!`);
|
||||
return this;
|
||||
}
|
||||
|
||||
|
@ -13,9 +13,11 @@ import { SubstituteTag } from "#app/data/battler-tags";
|
||||
export type TargetSelectCallback = (targets: BattlerIndex[]) => void;
|
||||
|
||||
export default class TargetSelectUiHandler extends UiHandler {
|
||||
private fieldIndex: integer;
|
||||
private fieldIndex: number;
|
||||
private move: Moves;
|
||||
private targetSelectCallback: TargetSelectCallback;
|
||||
private cursor0: number; // associated with BattlerIndex.PLAYER
|
||||
private cursor1: number; // associated with BattlerIndex.PLAYER_2
|
||||
|
||||
private isMultipleTargets: boolean = false;
|
||||
private targets: BattlerIndex[];
|
||||
@ -42,8 +44,9 @@ export default class TargetSelectUiHandler extends UiHandler {
|
||||
this.fieldIndex = args[0] as integer;
|
||||
this.move = args[1] as Moves;
|
||||
this.targetSelectCallback = args[2] as TargetSelectCallback;
|
||||
const user = this.scene.getPlayerField()[this.fieldIndex];
|
||||
|
||||
const moveTargets = getMoveTargets(this.scene.getPlayerField()[this.fieldIndex], this.move);
|
||||
const moveTargets = getMoveTargets(user, this.move);
|
||||
this.targets = moveTargets.targets;
|
||||
this.isMultipleTargets = moveTargets.multiple ?? false;
|
||||
|
||||
@ -53,11 +56,29 @@ export default class TargetSelectUiHandler extends UiHandler {
|
||||
|
||||
this.enemyModifiers = this.scene.getModifierBar(true);
|
||||
|
||||
this.setCursor(this.targets.includes(this.cursor) ? this.cursor : this.targets[0]);
|
||||
|
||||
if (this.fieldIndex === BattlerIndex.PLAYER) {
|
||||
this.resetCursor(this.cursor0, user);
|
||||
} else if (this.fieldIndex === BattlerIndex.PLAYER_2) {
|
||||
this.resetCursor(this.cursor1, user);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines what value to assign the main cursor based on the previous turn's target or the user's status
|
||||
* @param cursorN the cursor associated with the user's field index
|
||||
* @param user the Pokemon using the move
|
||||
*/
|
||||
resetCursor(cursorN: number, user: Pokemon): void {
|
||||
if (!Utils.isNullOrUndefined(cursorN)) {
|
||||
if ([ BattlerIndex.PLAYER, BattlerIndex.PLAYER_2 ].includes(cursorN) || user.battleSummonData.waveTurnCount === 1) {
|
||||
// Reset cursor on the first turn of a fight or if an ally was targeted last turn
|
||||
cursorN = -1;
|
||||
}
|
||||
}
|
||||
this.setCursor(this.targets.includes(cursorN) ? cursorN : this.targets[0]);
|
||||
}
|
||||
|
||||
processInput(button: Button): boolean {
|
||||
const ui = this.getUi();
|
||||
|
||||
@ -67,6 +88,15 @@ export default class TargetSelectUiHandler extends UiHandler {
|
||||
const targetIndexes: BattlerIndex[] = this.isMultipleTargets ? this.targets : [ this.cursor ];
|
||||
this.targetSelectCallback(button === Button.ACTION ? targetIndexes : []);
|
||||
success = true;
|
||||
if (this.fieldIndex === BattlerIndex.PLAYER) {
|
||||
if (Utils.isNullOrUndefined(this.cursor0) || this.cursor0 !== this.cursor) {
|
||||
this.cursor0 = this.cursor;
|
||||
}
|
||||
} else if (this.fieldIndex === BattlerIndex.PLAYER_2) {
|
||||
if (Utils.isNullOrUndefined(this.cursor1) || this.cursor1 !== this.cursor) {
|
||||
this.cursor1 = this.cursor;
|
||||
}
|
||||
}
|
||||
} else if (this.isMultipleTargets) {
|
||||
success = false;
|
||||
} else {
|
||||
@ -152,7 +182,6 @@ export default class TargetSelectUiHandler extends UiHandler {
|
||||
yoyo: true
|
||||
}));
|
||||
});
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
||||
@ -184,7 +213,6 @@ export default class TargetSelectUiHandler extends UiHandler {
|
||||
}
|
||||
|
||||
clear() {
|
||||
this.cursor = -1;
|
||||
super.clear();
|
||||
this.eraseCursor();
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user