Compare commits

...

4 Commits

Author SHA1 Message Date
ImperialSympathizer
7490699bef
[Feature] Adds Expert Pokemon Breeder Mystery Encounter to the game (#4328)
* Adds Expert Breeder Mystery Encounter to the game

* add achievement for Breeders in Space and remove redundant tests

* rename to Expert Pokemon Breeder

* remove unintentional test code

* remove unintentional test code

* test fix with breeder rename

---------

Co-authored-by: ImperialSympathizer <imperialsympathizer@gmail.com>
Co-authored-by: Jannik Tappert <38758606+CodeTappert@users.noreply.github.com>
2024-09-19 22:46:27 +02:00
Lugiad
528e231794
[Localization] Update pkmnems font (#4329) 2024-09-19 15:13:37 -04:00
Mumble
c4d5c923fc
[Bug] Run History Not Saving Correctly (#4248)
* Fix - need to test though

* New Line for Personal Best

* Run History basic Interpretation

* Stray console log

* Added personal best message fix

* argh

* removed a stray log

---------

Co-authored-by: frutescens <info@laptop>
Co-authored-by: flx-sta <50131232+flx-sta@users.noreply.github.com>
2024-09-19 10:42:29 -04:00
Tempoanon
714630c9de
[Refactor] Renamed the HitsTagAttr back to how it was (#4324) 2024-09-19 10:25:00 -04:00
50 changed files with 1030 additions and 453 deletions

Binary file not shown.

View File

@ -0,0 +1,41 @@
{
"textures": [
{
"image": "expert_pokemon_breeder.png",
"format": "RGBA8888",
"size": {
"w": 39,
"h": 75
},
"scale": 1,
"frames": [
{
"filename": "0001.png",
"rotated": false,
"trimmed": true,
"sourceSize": {
"w": 80,
"h": 80
},
"spriteSourceSize": {
"x": 21,
"y": 3,
"w": 39,
"h": 75
},
"frame": {
"x": 0,
"y": 0,
"w": 39,
"h": 75
}
}
]
}
],
"meta": {
"app": "https://www.codeandweb.com/texturepacker",
"version": "3.0",
"smartupdate": "$TexturePacker:SmartUpdate:cb681265d8dca038a518ab14076fd140:18ff41b1ef6967682643a11695926e58:c59ea3971195f5a395b75223a77d9068$"
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 1006 B

View File

@ -4859,16 +4859,18 @@ export class RemoveAllSubstitutesAttr extends MoveEffectAttr {
} }
/** /**
* Attribute used when a move hits a {@linkcode BattlerTagType} for double damage * Attribute used when a move can deal damage to {@linkcode BattlerTagType}
* Moves that always hit but do not deal double damage: Thunder, Fissure, Sky Uppercut,
* Smack Down, Hurricane, Thousand Arrows
* @extends MoveAttr * @extends MoveAttr
*/ */
export class DealsDoubleDamageToTagAttr extends MoveAttr { export class HitsTagAttr extends MoveAttr {
/** The {@linkcode BattlerTagType} this move hits */ /** The {@linkcode BattlerTagType} this move hits */
public tagType: BattlerTagType; public tagType: BattlerTagType;
/** Should this move deal double damage against {@linkcode DealsDoubleDamageToTagAttr.tagType}? */ /** Should this move deal double damage against {@linkcode HitsTagAttr.tagType}? */
public doubleDamage: boolean; public doubleDamage: boolean;
constructor(tagType: BattlerTagType, doubleDamage?: boolean) { constructor(tagType: BattlerTagType, doubleDamage: boolean = false) {
super(); super();
this.tagType = tagType; this.tagType = tagType;
@ -4880,6 +4882,17 @@ export class DealsDoubleDamageToTagAttr extends MoveAttr {
} }
} }
/**
* Used for moves that will always hit for a given tag but also doubles damage.
* Moves include: Gust, Stomp, Body Slam, Surf, Earthquake, Magnitude, Twister,
* Whirlpool, Dragon Rush, Heat Crash, Steam Roller, Flying Press
*/
export class HitsTagForDoubleDamageAttr extends HitsTagAttr {
constructor(tagType: BattlerTagType) {
super(tagType, true);
}
}
export class AddArenaTagAttr extends MoveEffectAttr { export class AddArenaTagAttr extends MoveEffectAttr {
public tagType: ArenaTagType; public tagType: ArenaTagType;
public turnCount: integer; public turnCount: integer;
@ -6759,7 +6772,7 @@ export function initMoves() {
new AttackMove(Moves.CUT, Type.NORMAL, MoveCategory.PHYSICAL, 50, 95, 30, -1, 0, 1) new AttackMove(Moves.CUT, Type.NORMAL, MoveCategory.PHYSICAL, 50, 95, 30, -1, 0, 1)
.slicingMove(), .slicingMove(),
new AttackMove(Moves.GUST, Type.FLYING, MoveCategory.SPECIAL, 40, 100, 35, -1, 0, 1) new AttackMove(Moves.GUST, Type.FLYING, MoveCategory.SPECIAL, 40, 100, 35, -1, 0, 1)
.attr(DealsDoubleDamageToTagAttr, BattlerTagType.FLYING, true) .attr(HitsTagForDoubleDamageAttr, BattlerTagType.FLYING)
.windMove(), .windMove(),
new AttackMove(Moves.WING_ATTACK, Type.FLYING, MoveCategory.PHYSICAL, 60, 100, 35, -1, 0, 1), new AttackMove(Moves.WING_ATTACK, Type.FLYING, MoveCategory.PHYSICAL, 60, 100, 35, -1, 0, 1),
new StatusMove(Moves.WHIRLWIND, Type.NORMAL, -1, 20, -1, -6, 1) new StatusMove(Moves.WHIRLWIND, Type.NORMAL, -1, 20, -1, -6, 1)
@ -6777,7 +6790,7 @@ export function initMoves() {
new AttackMove(Moves.VINE_WHIP, Type.GRASS, MoveCategory.PHYSICAL, 45, 100, 25, -1, 0, 1), new AttackMove(Moves.VINE_WHIP, Type.GRASS, MoveCategory.PHYSICAL, 45, 100, 25, -1, 0, 1),
new AttackMove(Moves.STOMP, Type.NORMAL, MoveCategory.PHYSICAL, 65, 100, 20, 30, 0, 1) new AttackMove(Moves.STOMP, Type.NORMAL, MoveCategory.PHYSICAL, 65, 100, 20, 30, 0, 1)
.attr(AlwaysHitMinimizeAttr) .attr(AlwaysHitMinimizeAttr)
.attr(DealsDoubleDamageToTagAttr, BattlerTagType.MINIMIZED, true) .attr(HitsTagForDoubleDamageAttr, BattlerTagType.MINIMIZED)
.attr(FlinchAttr), .attr(FlinchAttr),
new AttackMove(Moves.DOUBLE_KICK, Type.FIGHTING, MoveCategory.PHYSICAL, 30, 100, 30, -1, 0, 1) new AttackMove(Moves.DOUBLE_KICK, Type.FIGHTING, MoveCategory.PHYSICAL, 30, 100, 30, -1, 0, 1)
.attr(MultiHitAttr, MultiHitType._2), .attr(MultiHitAttr, MultiHitType._2),
@ -6802,7 +6815,7 @@ export function initMoves() {
new AttackMove(Moves.TACKLE, Type.NORMAL, MoveCategory.PHYSICAL, 40, 100, 35, -1, 0, 1), new AttackMove(Moves.TACKLE, Type.NORMAL, MoveCategory.PHYSICAL, 40, 100, 35, -1, 0, 1),
new AttackMove(Moves.BODY_SLAM, Type.NORMAL, MoveCategory.PHYSICAL, 85, 100, 15, 30, 0, 1) new AttackMove(Moves.BODY_SLAM, Type.NORMAL, MoveCategory.PHYSICAL, 85, 100, 15, 30, 0, 1)
.attr(AlwaysHitMinimizeAttr) .attr(AlwaysHitMinimizeAttr)
.attr(DealsDoubleDamageToTagAttr, BattlerTagType.MINIMIZED, true) .attr(HitsTagForDoubleDamageAttr, BattlerTagType.MINIMIZED)
.attr(StatusEffectAttr, StatusEffect.PARALYSIS), .attr(StatusEffectAttr, StatusEffect.PARALYSIS),
new AttackMove(Moves.WRAP, Type.NORMAL, MoveCategory.PHYSICAL, 15, 90, 20, -1, 0, 1) new AttackMove(Moves.WRAP, Type.NORMAL, MoveCategory.PHYSICAL, 15, 90, 20, -1, 0, 1)
.attr(TrapAttr, BattlerTagType.WRAP), .attr(TrapAttr, BattlerTagType.WRAP),
@ -6870,7 +6883,7 @@ export function initMoves() {
new AttackMove(Moves.HYDRO_PUMP, Type.WATER, MoveCategory.SPECIAL, 110, 80, 5, -1, 0, 1), new AttackMove(Moves.HYDRO_PUMP, Type.WATER, MoveCategory.SPECIAL, 110, 80, 5, -1, 0, 1),
new AttackMove(Moves.SURF, Type.WATER, MoveCategory.SPECIAL, 90, 100, 15, -1, 0, 1) new AttackMove(Moves.SURF, Type.WATER, MoveCategory.SPECIAL, 90, 100, 15, -1, 0, 1)
.target(MoveTarget.ALL_NEAR_OTHERS) .target(MoveTarget.ALL_NEAR_OTHERS)
.attr(DealsDoubleDamageToTagAttr, BattlerTagType.UNDERWATER, true) .attr(HitsTagForDoubleDamageAttr, BattlerTagType.UNDERWATER)
.attr(GulpMissileTagAttr), .attr(GulpMissileTagAttr),
new AttackMove(Moves.ICE_BEAM, Type.ICE, MoveCategory.SPECIAL, 90, 100, 10, 10, 0, 1) new AttackMove(Moves.ICE_BEAM, Type.ICE, MoveCategory.SPECIAL, 90, 100, 10, 10, 0, 1)
.attr(StatusEffectAttr, StatusEffect.FREEZE), .attr(StatusEffectAttr, StatusEffect.FREEZE),
@ -6953,18 +6966,18 @@ export function initMoves() {
new AttackMove(Moves.THUNDER, Type.ELECTRIC, MoveCategory.SPECIAL, 110, 70, 10, 30, 0, 1) new AttackMove(Moves.THUNDER, Type.ELECTRIC, MoveCategory.SPECIAL, 110, 70, 10, 30, 0, 1)
.attr(StatusEffectAttr, StatusEffect.PARALYSIS) .attr(StatusEffectAttr, StatusEffect.PARALYSIS)
.attr(ThunderAccuracyAttr) .attr(ThunderAccuracyAttr)
.attr(DealsDoubleDamageToTagAttr, BattlerTagType.FLYING, false), .attr(HitsTagAttr, BattlerTagType.FLYING),
new AttackMove(Moves.ROCK_THROW, Type.ROCK, MoveCategory.PHYSICAL, 50, 90, 15, -1, 0, 1) new AttackMove(Moves.ROCK_THROW, Type.ROCK, MoveCategory.PHYSICAL, 50, 90, 15, -1, 0, 1)
.makesContact(false), .makesContact(false),
new AttackMove(Moves.EARTHQUAKE, Type.GROUND, MoveCategory.PHYSICAL, 100, 100, 10, -1, 0, 1) new AttackMove(Moves.EARTHQUAKE, Type.GROUND, MoveCategory.PHYSICAL, 100, 100, 10, -1, 0, 1)
.attr(DealsDoubleDamageToTagAttr, BattlerTagType.UNDERGROUND, true) .attr(HitsTagForDoubleDamageAttr, BattlerTagType.UNDERGROUND)
.attr(MovePowerMultiplierAttr, (user, target, move) => user.scene.arena.getTerrainType() === TerrainType.GRASSY && target.isGrounded() ? 0.5 : 1) .attr(MovePowerMultiplierAttr, (user, target, move) => user.scene.arena.getTerrainType() === TerrainType.GRASSY && target.isGrounded() ? 0.5 : 1)
.makesContact(false) .makesContact(false)
.target(MoveTarget.ALL_NEAR_OTHERS), .target(MoveTarget.ALL_NEAR_OTHERS),
new AttackMove(Moves.FISSURE, Type.GROUND, MoveCategory.PHYSICAL, 200, 30, 5, -1, 0, 1) new AttackMove(Moves.FISSURE, Type.GROUND, MoveCategory.PHYSICAL, 200, 30, 5, -1, 0, 1)
.attr(OneHitKOAttr) .attr(OneHitKOAttr)
.attr(OneHitKOAccuracyAttr) .attr(OneHitKOAccuracyAttr)
.attr(DealsDoubleDamageToTagAttr, BattlerTagType.UNDERGROUND, false) .attr(HitsTagAttr, BattlerTagType.UNDERGROUND)
.makesContact(false), .makesContact(false),
new AttackMove(Moves.DIG, Type.GROUND, MoveCategory.PHYSICAL, 80, 100, 10, -1, 0, 1) new AttackMove(Moves.DIG, Type.GROUND, MoveCategory.PHYSICAL, 80, 100, 10, -1, 0, 1)
.attr(ChargeAttr, ChargeAnim.DIG_CHARGING, i18next.t("moveTriggers:dugAHole", {pokemonName: "{USER}"}), BattlerTagType.UNDERGROUND) .attr(ChargeAttr, ChargeAnim.DIG_CHARGING, i18next.t("moveTriggers:dugAHole", {pokemonName: "{USER}"}), BattlerTagType.UNDERGROUND)
@ -7353,7 +7366,7 @@ export function initMoves() {
.attr(PreMoveMessageAttr, magnitudeMessageFunc) .attr(PreMoveMessageAttr, magnitudeMessageFunc)
.attr(MagnitudePowerAttr) .attr(MagnitudePowerAttr)
.attr(MovePowerMultiplierAttr, (user, target, move) => user.scene.arena.getTerrainType() === TerrainType.GRASSY && target.isGrounded() ? 0.5 : 1) .attr(MovePowerMultiplierAttr, (user, target, move) => user.scene.arena.getTerrainType() === TerrainType.GRASSY && target.isGrounded() ? 0.5 : 1)
.attr(DealsDoubleDamageToTagAttr, BattlerTagType.UNDERGROUND, true) .attr(HitsTagForDoubleDamageAttr, BattlerTagType.UNDERGROUND)
.makesContact(false) .makesContact(false)
.target(MoveTarget.ALL_NEAR_OTHERS), .target(MoveTarget.ALL_NEAR_OTHERS),
new AttackMove(Moves.DYNAMIC_PUNCH, Type.FIGHTING, MoveCategory.PHYSICAL, 100, 50, 5, 100, 0, 2) new AttackMove(Moves.DYNAMIC_PUNCH, Type.FIGHTING, MoveCategory.PHYSICAL, 100, 50, 5, 100, 0, 2)
@ -7409,7 +7422,7 @@ export function initMoves() {
new AttackMove(Moves.CROSS_CHOP, Type.FIGHTING, MoveCategory.PHYSICAL, 100, 80, 5, -1, 0, 2) new AttackMove(Moves.CROSS_CHOP, Type.FIGHTING, MoveCategory.PHYSICAL, 100, 80, 5, -1, 0, 2)
.attr(HighCritAttr), .attr(HighCritAttr),
new AttackMove(Moves.TWISTER, Type.DRAGON, MoveCategory.SPECIAL, 40, 100, 20, 20, 0, 2) new AttackMove(Moves.TWISTER, Type.DRAGON, MoveCategory.SPECIAL, 40, 100, 20, 20, 0, 2)
.attr(DealsDoubleDamageToTagAttr, BattlerTagType.FLYING, true) .attr(HitsTagForDoubleDamageAttr, BattlerTagType.FLYING)
.attr(FlinchAttr) .attr(FlinchAttr)
.windMove() .windMove()
.target(MoveTarget.ALL_NEAR_ENEMIES), .target(MoveTarget.ALL_NEAR_ENEMIES),
@ -7441,7 +7454,7 @@ export function initMoves() {
.attr(StatStageChangeAttr, [ Stat.DEF ], -1), .attr(StatStageChangeAttr, [ Stat.DEF ], -1),
new AttackMove(Moves.WHIRLPOOL, Type.WATER, MoveCategory.SPECIAL, 35, 85, 15, -1, 0, 2) new AttackMove(Moves.WHIRLPOOL, Type.WATER, MoveCategory.SPECIAL, 35, 85, 15, -1, 0, 2)
.attr(TrapAttr, BattlerTagType.WHIRLPOOL) .attr(TrapAttr, BattlerTagType.WHIRLPOOL)
.attr(DealsDoubleDamageToTagAttr, BattlerTagType.UNDERWATER, true), .attr(HitsTagForDoubleDamageAttr, BattlerTagType.UNDERWATER),
new AttackMove(Moves.BEAT_UP, Type.DARK, MoveCategory.PHYSICAL, -1, 100, 10, -1, 0, 2) new AttackMove(Moves.BEAT_UP, Type.DARK, MoveCategory.PHYSICAL, -1, 100, 10, -1, 0, 2)
.attr(MultiHitAttr, MultiHitType.BEAT_UP) .attr(MultiHitAttr, MultiHitType.BEAT_UP)
.attr(BeatUpAttr) .attr(BeatUpAttr)
@ -7664,7 +7677,7 @@ export function initMoves() {
new AttackMove(Moves.EXTRASENSORY, Type.PSYCHIC, MoveCategory.SPECIAL, 80, 100, 20, 10, 0, 3) new AttackMove(Moves.EXTRASENSORY, Type.PSYCHIC, MoveCategory.SPECIAL, 80, 100, 20, 10, 0, 3)
.attr(FlinchAttr), .attr(FlinchAttr),
new AttackMove(Moves.SKY_UPPERCUT, Type.FIGHTING, MoveCategory.PHYSICAL, 85, 90, 15, -1, 0, 3) new AttackMove(Moves.SKY_UPPERCUT, Type.FIGHTING, MoveCategory.PHYSICAL, 85, 90, 15, -1, 0, 3)
.attr(DealsDoubleDamageToTagAttr, BattlerTagType.FLYING) .attr(HitsTagAttr, BattlerTagType.FLYING)
.punchingMove(), .punchingMove(),
new AttackMove(Moves.SAND_TOMB, Type.GROUND, MoveCategory.PHYSICAL, 35, 85, 15, -1, 0, 3) new AttackMove(Moves.SAND_TOMB, Type.GROUND, MoveCategory.PHYSICAL, 35, 85, 15, -1, 0, 3)
.attr(TrapAttr, BattlerTagType.SAND_TOMB) .attr(TrapAttr, BattlerTagType.SAND_TOMB)
@ -7896,7 +7909,7 @@ export function initMoves() {
.pulseMove(), .pulseMove(),
new AttackMove(Moves.DRAGON_RUSH, Type.DRAGON, MoveCategory.PHYSICAL, 100, 75, 10, 20, 0, 4) new AttackMove(Moves.DRAGON_RUSH, Type.DRAGON, MoveCategory.PHYSICAL, 100, 75, 10, 20, 0, 4)
.attr(AlwaysHitMinimizeAttr) .attr(AlwaysHitMinimizeAttr)
.attr(DealsDoubleDamageToTagAttr, BattlerTagType.MINIMIZED, true) .attr(HitsTagForDoubleDamageAttr, BattlerTagType.MINIMIZED)
.attr(FlinchAttr), .attr(FlinchAttr),
new AttackMove(Moves.POWER_GEM, Type.ROCK, MoveCategory.SPECIAL, 80, 100, 20, -1, 0, 4), new AttackMove(Moves.POWER_GEM, Type.ROCK, MoveCategory.SPECIAL, 80, 100, 20, -1, 0, 4),
new AttackMove(Moves.DRAIN_PUNCH, Type.FIGHTING, MoveCategory.PHYSICAL, 75, 100, 10, -1, 0, 4) new AttackMove(Moves.DRAIN_PUNCH, Type.FIGHTING, MoveCategory.PHYSICAL, 75, 100, 10, -1, 0, 4)
@ -8093,7 +8106,7 @@ export function initMoves() {
.attr(AddBattlerTagAttr, BattlerTagType.IGNORE_FLYING, false, false, 1, 1, true) .attr(AddBattlerTagAttr, BattlerTagType.IGNORE_FLYING, false, false, 1, 1, true)
.attr(AddBattlerTagAttr, BattlerTagType.INTERRUPTED) .attr(AddBattlerTagAttr, BattlerTagType.INTERRUPTED)
.attr(RemoveBattlerTagAttr, [BattlerTagType.FLYING, BattlerTagType.MAGNET_RISEN]) .attr(RemoveBattlerTagAttr, [BattlerTagType.FLYING, BattlerTagType.MAGNET_RISEN])
.attr(DealsDoubleDamageToTagAttr, BattlerTagType.FLYING, false) .attr(HitsTagAttr, BattlerTagType.FLYING)
.makesContact(false), .makesContact(false),
new AttackMove(Moves.STORM_THROW, Type.FIGHTING, MoveCategory.PHYSICAL, 60, 100, 10, -1, 0, 5) new AttackMove(Moves.STORM_THROW, Type.FIGHTING, MoveCategory.PHYSICAL, 60, 100, 10, -1, 0, 5)
.attr(CritOnlyAttr), .attr(CritOnlyAttr),
@ -8108,7 +8121,7 @@ export function initMoves() {
new AttackMove(Moves.HEAVY_SLAM, Type.STEEL, MoveCategory.PHYSICAL, -1, 100, 10, -1, 0, 5) new AttackMove(Moves.HEAVY_SLAM, Type.STEEL, MoveCategory.PHYSICAL, -1, 100, 10, -1, 0, 5)
.attr(AlwaysHitMinimizeAttr) .attr(AlwaysHitMinimizeAttr)
.attr(CompareWeightPowerAttr) .attr(CompareWeightPowerAttr)
.attr(DealsDoubleDamageToTagAttr, BattlerTagType.MINIMIZED, true), .attr(HitsTagForDoubleDamageAttr, BattlerTagType.MINIMIZED),
new AttackMove(Moves.SYNCHRONOISE, Type.PSYCHIC, MoveCategory.SPECIAL, 120, 100, 10, -1, 0, 5) new AttackMove(Moves.SYNCHRONOISE, Type.PSYCHIC, MoveCategory.SPECIAL, 120, 100, 10, -1, 0, 5)
.target(MoveTarget.ALL_NEAR_OTHERS) .target(MoveTarget.ALL_NEAR_OTHERS)
.condition(unknownTypeCondition) .condition(unknownTypeCondition)
@ -8261,12 +8274,12 @@ export function initMoves() {
new AttackMove(Moves.HEAT_CRASH, Type.FIRE, MoveCategory.PHYSICAL, -1, 100, 10, -1, 0, 5) new AttackMove(Moves.HEAT_CRASH, Type.FIRE, MoveCategory.PHYSICAL, -1, 100, 10, -1, 0, 5)
.attr(AlwaysHitMinimizeAttr) .attr(AlwaysHitMinimizeAttr)
.attr(CompareWeightPowerAttr) .attr(CompareWeightPowerAttr)
.attr(DealsDoubleDamageToTagAttr, BattlerTagType.MINIMIZED, true), .attr(HitsTagForDoubleDamageAttr, BattlerTagType.MINIMIZED),
new AttackMove(Moves.LEAF_TORNADO, Type.GRASS, MoveCategory.SPECIAL, 65, 90, 10, 50, 0, 5) new AttackMove(Moves.LEAF_TORNADO, Type.GRASS, MoveCategory.SPECIAL, 65, 90, 10, 50, 0, 5)
.attr(StatStageChangeAttr, [ Stat.ACC ], -1), .attr(StatStageChangeAttr, [ Stat.ACC ], -1),
new AttackMove(Moves.STEAMROLLER, Type.BUG, MoveCategory.PHYSICAL, 65, 100, 20, 30, 0, 5) new AttackMove(Moves.STEAMROLLER, Type.BUG, MoveCategory.PHYSICAL, 65, 100, 20, 30, 0, 5)
.attr(AlwaysHitMinimizeAttr) .attr(AlwaysHitMinimizeAttr)
.attr(DealsDoubleDamageToTagAttr, BattlerTagType.MINIMIZED, true) .attr(HitsTagForDoubleDamageAttr, BattlerTagType.MINIMIZED)
.attr(FlinchAttr), .attr(FlinchAttr),
new SelfStatusMove(Moves.COTTON_GUARD, Type.GRASS, -1, 10, -1, 0, 5) new SelfStatusMove(Moves.COTTON_GUARD, Type.GRASS, -1, 10, -1, 0, 5)
.attr(StatStageChangeAttr, [ Stat.DEF ], 3, true), .attr(StatStageChangeAttr, [ Stat.DEF ], 3, true),
@ -8279,7 +8292,7 @@ export function initMoves() {
new AttackMove(Moves.HURRICANE, Type.FLYING, MoveCategory.SPECIAL, 110, 70, 10, 30, 0, 5) new AttackMove(Moves.HURRICANE, Type.FLYING, MoveCategory.SPECIAL, 110, 70, 10, 30, 0, 5)
.attr(ThunderAccuracyAttr) .attr(ThunderAccuracyAttr)
.attr(ConfuseAttr) .attr(ConfuseAttr)
.attr(DealsDoubleDamageToTagAttr, BattlerTagType.FLYING, false) .attr(HitsTagAttr, BattlerTagType.FLYING)
.windMove(), .windMove(),
new AttackMove(Moves.HEAD_CHARGE, Type.NORMAL, MoveCategory.PHYSICAL, 120, 100, 15, -1, 0, 5) new AttackMove(Moves.HEAD_CHARGE, Type.NORMAL, MoveCategory.PHYSICAL, 120, 100, 15, -1, 0, 5)
.attr(RecoilAttr) .attr(RecoilAttr)
@ -8335,7 +8348,7 @@ export function initMoves() {
new AttackMove(Moves.FLYING_PRESS, Type.FIGHTING, MoveCategory.PHYSICAL, 100, 95, 10, -1, 0, 6) new AttackMove(Moves.FLYING_PRESS, Type.FIGHTING, MoveCategory.PHYSICAL, 100, 95, 10, -1, 0, 6)
.attr(AlwaysHitMinimizeAttr) .attr(AlwaysHitMinimizeAttr)
.attr(FlyingTypeMultiplierAttr) .attr(FlyingTypeMultiplierAttr)
.attr(DealsDoubleDamageToTagAttr, BattlerTagType.MINIMIZED, true) .attr(HitsTagForDoubleDamageAttr, BattlerTagType.MINIMIZED)
.condition(failOnGravityCondition), .condition(failOnGravityCondition),
new StatusMove(Moves.MAT_BLOCK, Type.FIGHTING, -1, 10, -1, 0, 6) new StatusMove(Moves.MAT_BLOCK, Type.FIGHTING, -1, 10, -1, 0, 6)
.target(MoveTarget.USER_SIDE) .target(MoveTarget.USER_SIDE)
@ -8506,8 +8519,8 @@ export function initMoves() {
new AttackMove(Moves.THOUSAND_ARROWS, Type.GROUND, MoveCategory.PHYSICAL, 90, 100, 10, -1, 0, 6) new AttackMove(Moves.THOUSAND_ARROWS, Type.GROUND, MoveCategory.PHYSICAL, 90, 100, 10, -1, 0, 6)
.attr(NeutralDamageAgainstFlyingTypeMultiplierAttr) .attr(NeutralDamageAgainstFlyingTypeMultiplierAttr)
.attr(AddBattlerTagAttr, BattlerTagType.IGNORE_FLYING, false, false, 1, 1, true) .attr(AddBattlerTagAttr, BattlerTagType.IGNORE_FLYING, false, false, 1, 1, true)
.attr(DealsDoubleDamageToTagAttr, BattlerTagType.FLYING, false) .attr(HitsTagAttr, BattlerTagType.FLYING)
.attr(DealsDoubleDamageToTagAttr, BattlerTagType.MAGNET_RISEN, false) .attr(HitsTagAttr, BattlerTagType.MAGNET_RISEN)
.attr(AddBattlerTagAttr, BattlerTagType.INTERRUPTED) .attr(AddBattlerTagAttr, BattlerTagType.INTERRUPTED)
.attr(RemoveBattlerTagAttr, [BattlerTagType.FLYING, BattlerTagType.MAGNET_RISEN]) .attr(RemoveBattlerTagAttr, [BattlerTagType.FLYING, BattlerTagType.MAGNET_RISEN])
.makesContact(false) .makesContact(false)
@ -8765,7 +8778,7 @@ export function initMoves() {
.ignoresVirtual(), .ignoresVirtual(),
new AttackMove(Moves.MALICIOUS_MOONSAULT, Type.DARK, MoveCategory.PHYSICAL, 180, -1, 1, -1, 0, 7) new AttackMove(Moves.MALICIOUS_MOONSAULT, Type.DARK, MoveCategory.PHYSICAL, 180, -1, 1, -1, 0, 7)
.attr(AlwaysHitMinimizeAttr) .attr(AlwaysHitMinimizeAttr)
.attr(DealsDoubleDamageToTagAttr, BattlerTagType.MINIMIZED, true) .attr(HitsTagAttr, BattlerTagType.MINIMIZED, true)
.partial() .partial()
.ignoresVirtual(), .ignoresVirtual(),
new AttackMove(Moves.OCEANIC_OPERETTA, Type.WATER, MoveCategory.SPECIAL, 195, -1, 1, -1, 0, 7) new AttackMove(Moves.OCEANIC_OPERETTA, Type.WATER, MoveCategory.SPECIAL, 195, -1, 1, -1, 0, 7)

View File

@ -9,6 +9,7 @@ import {
transitionMysteryEncounterIntroVisuals, transitionMysteryEncounterIntroVisuals,
} from "#app/data/mystery-encounters/utils/encounter-phase-utils"; } from "#app/data/mystery-encounters/utils/encounter-phase-utils";
import { import {
getRandomPartyMemberFunc,
trainerConfigs, trainerConfigs,
TrainerPartyCompoundTemplate, TrainerPartyCompoundTemplate,
TrainerPartyTemplate, TrainerPartyTemplate,
@ -17,14 +18,12 @@ import {
import { MysteryEncounterType } from "#enums/mystery-encounter-type"; import { MysteryEncounterType } from "#enums/mystery-encounter-type";
import { PartyMemberStrength } from "#enums/party-member-strength"; import { PartyMemberStrength } from "#enums/party-member-strength";
import BattleScene from "#app/battle-scene"; import BattleScene from "#app/battle-scene";
import * as Utils from "#app/utils";
import { isNullOrUndefined, randSeedInt, randSeedShuffle } from "#app/utils"; import { isNullOrUndefined, randSeedInt, randSeedShuffle } from "#app/utils";
import MysteryEncounter, { MysteryEncounterBuilder } from "#app/data/mystery-encounters/mystery-encounter"; import MysteryEncounter, { MysteryEncounterBuilder } from "#app/data/mystery-encounters/mystery-encounter";
import { MysteryEncounterTier } from "#enums/mystery-encounter-tier"; import { MysteryEncounterTier } from "#enums/mystery-encounter-tier";
import { TrainerType } from "#enums/trainer-type"; import { TrainerType } from "#enums/trainer-type";
import { Species } from "#enums/species"; import { Species } from "#enums/species";
import Pokemon, { EnemyPokemon, PlayerPokemon, PokemonMove } from "#app/field/pokemon"; import Pokemon, { PlayerPokemon, PokemonMove } from "#app/field/pokemon";
import { getPokemonSpecies } from "#app/data/pokemon-species";
import { getEncounterText, showEncounterDialogue } from "#app/data/mystery-encounters/utils/encounter-dialogue-utils"; import { getEncounterText, showEncounterDialogue } from "#app/data/mystery-encounters/utils/encounter-dialogue-utils";
import { LearnMovePhase } from "#app/phases/learn-move-phase"; import { LearnMovePhase } from "#app/phases/learn-move-phase";
import { Moves } from "#enums/moves"; import { Moves } from "#enums/moves";
@ -584,16 +583,6 @@ function getTrainerConfigForWave(waveIndex: number) {
return config; return config;
} }
function getRandomPartyMemberFunc(speciesPool: Species[], trainerSlot: TrainerSlot = TrainerSlot.TRAINER, ignoreEvolution: boolean = false, postProcess?: (enemyPokemon: EnemyPokemon) => void) {
return (scene: BattleScene, level: number, strength: PartyMemberStrength) => {
let species = Utils.randSeedItem(speciesPool);
if (!ignoreEvolution) {
species = getPokemonSpecies(species).getTrainerSpeciesForLevel(level, true, strength);
}
return scene.addEnemyPokemon(getPokemonSpecies(species), level, trainerSlot, undefined, undefined, postProcess);
};
}
function doBugTypeMoveTutor(scene: BattleScene): Promise<void> { function doBugTypeMoveTutor(scene: BattleScene): Promise<void> {
return new Promise<void>(async resolve => { return new Promise<void>(async resolve => {
const moveOptions = scene.currentBattle.mysteryEncounter!.misc.moveTutorOptions; const moveOptions = scene.currentBattle.mysteryEncounter!.misc.moveTutorOptions;

View File

@ -133,7 +133,7 @@ export const DancingLessonsEncounter: MysteryEncounter =
} }
const oricorioData = new PokemonData(enemyPokemon); const oricorioData = new PokemonData(enemyPokemon);
const oricorio = scene.addEnemyPokemon(species, scene.currentBattle.enemyLevels![0], TrainerSlot.NONE, false, oricorioData); const oricorio = scene.addEnemyPokemon(species, level, TrainerSlot.NONE, false, oricorioData);
// Adds a real Pokemon sprite to the field (required for the animation) // Adds a real Pokemon sprite to the field (required for the animation)
scene.getEnemyParty().forEach(enemyPokemon => { scene.getEnemyParty().forEach(enemyPokemon => {

View File

@ -0,0 +1,549 @@
import { EnemyPartyConfig, generateModifierType, initBattleWithEnemyConfig, setEncounterRewards, } from "#app/data/mystery-encounters/utils/encounter-phase-utils";
import { trainerConfigs } from "#app/data/trainer-config";
import { modifierTypes, PokemonHeldItemModifierType } from "#app/modifier/modifier-type";
import { MysteryEncounterType } from "#enums/mystery-encounter-type";
import BattleScene from "#app/battle-scene";
import { randSeedShuffle } from "#app/utils";
import MysteryEncounter, { MysteryEncounterBuilder } from "../mystery-encounter";
import { MysteryEncounterTier } from "#enums/mystery-encounter-tier";
import { CLASSIC_MODE_MYSTERY_ENCOUNTER_WAVES } from "#app/game-mode";
import { Biome } from "#enums/biome";
import { TrainerType } from "#enums/trainer-type";
import i18next from "i18next";
import { Species } from "#enums/species";
import { getPokemonSpecies, speciesStarters } from "#app/data/pokemon-species";
import { Nature } from "#enums/nature";
import { Moves } from "#enums/moves";
import { Type } from "#app/data/type";
import { Stat } from "#enums/stat";
import { PlayerPokemon } from "#app/field/pokemon";
import { getEncounterText } from "#app/data/mystery-encounters/utils/encounter-dialogue-utils";
import { IEggOptions } from "#app/data/egg";
import { EggSourceType } from "#enums/egg-source-types";
import { EggTier } from "#enums/egg-type";
import { MysteryEncounterOptionBuilder } from "#app/data/mystery-encounters/mystery-encounter-option";
import { MysteryEncounterOptionMode } from "#enums/mystery-encounter-option-mode";
import { achvs } from "#app/system/achv";
/** the i18n namespace for the encounter */
const namespace = "mysteryEncounter:expertPokemonBreeder";
const trainerNameKey = "trainerNames:expert_pokemon_breeder";
const FIRST_STAGE_EVOLUTION_WAVE = 30;
const SECOND_STAGE_EVOLUTION_WAVE = 45;
const FINAL_STAGE_EVOLUTION_WAVE = 60;
const FRIENDSHIP_ADDED = 20;
class BreederSpeciesEvolution {
species: Species;
evolution: number;
constructor(species: Species, evolution: number) {
this.species = species;
this.evolution = evolution;
}
}
const POOL_1_POKEMON: (Species | BreederSpeciesEvolution)[][] = [
[Species.MUNCHLAX, new BreederSpeciesEvolution(Species.SNORLAX, SECOND_STAGE_EVOLUTION_WAVE)],
[Species.HAPPINY, new BreederSpeciesEvolution(Species.CHANSEY, FIRST_STAGE_EVOLUTION_WAVE), new BreederSpeciesEvolution(Species.BLISSEY, FINAL_STAGE_EVOLUTION_WAVE)],
[Species.MAGBY, new BreederSpeciesEvolution(Species.MAGMAR, FIRST_STAGE_EVOLUTION_WAVE), new BreederSpeciesEvolution(Species.MAGMORTAR, FINAL_STAGE_EVOLUTION_WAVE)],
[Species.ELEKID, new BreederSpeciesEvolution(Species.ELECTABUZZ, FIRST_STAGE_EVOLUTION_WAVE), new BreederSpeciesEvolution(Species.ELECTIVIRE, FINAL_STAGE_EVOLUTION_WAVE)],
[Species.RIOLU, new BreederSpeciesEvolution(Species.LUCARIO, SECOND_STAGE_EVOLUTION_WAVE)],
[Species.BUDEW, new BreederSpeciesEvolution(Species.ROSELIA, FIRST_STAGE_EVOLUTION_WAVE), new BreederSpeciesEvolution(Species.ROSERADE, FINAL_STAGE_EVOLUTION_WAVE)],
[Species.TOXEL, new BreederSpeciesEvolution(Species.TOXTRICITY, SECOND_STAGE_EVOLUTION_WAVE)],
[Species.MIME_JR, new BreederSpeciesEvolution(Species.GALAR_MR_MIME, FIRST_STAGE_EVOLUTION_WAVE), new BreederSpeciesEvolution(Species.MR_RIME, FINAL_STAGE_EVOLUTION_WAVE)]
];
const POOL_2_POKEMON: (Species | BreederSpeciesEvolution)[][] = [
[Species.PICHU, new BreederSpeciesEvolution(Species.PIKACHU, FIRST_STAGE_EVOLUTION_WAVE), new BreederSpeciesEvolution(Species.RAICHU, FINAL_STAGE_EVOLUTION_WAVE)],
[Species.PICHU, new BreederSpeciesEvolution(Species.PIKACHU, FIRST_STAGE_EVOLUTION_WAVE), new BreederSpeciesEvolution(Species.ALOLA_RAICHU, FINAL_STAGE_EVOLUTION_WAVE)],
[Species.JYNX],
[Species.TYROGUE, new BreederSpeciesEvolution(Species.HITMONLEE, SECOND_STAGE_EVOLUTION_WAVE)],
[Species.TYROGUE, new BreederSpeciesEvolution(Species.HITMONCHAN, SECOND_STAGE_EVOLUTION_WAVE)],
[Species.TYROGUE, new BreederSpeciesEvolution(Species.HITMONTOP, SECOND_STAGE_EVOLUTION_WAVE)],
[Species.IGGLYBUFF, new BreederSpeciesEvolution(Species.JIGGLYPUFF, FIRST_STAGE_EVOLUTION_WAVE), new BreederSpeciesEvolution(Species.WIGGLYTUFF, FINAL_STAGE_EVOLUTION_WAVE)],
[Species.AZURILL, new BreederSpeciesEvolution(Species.MARILL, FIRST_STAGE_EVOLUTION_WAVE), new BreederSpeciesEvolution(Species.AZUMARILL, FINAL_STAGE_EVOLUTION_WAVE)],
[Species.WYNAUT, new BreederSpeciesEvolution(Species.WOBBUFFET, SECOND_STAGE_EVOLUTION_WAVE)],
[Species.CHINGLING, new BreederSpeciesEvolution(Species.CHIMECHO, SECOND_STAGE_EVOLUTION_WAVE)],
[Species.BONSLY, new BreederSpeciesEvolution(Species.SUDOWOODO, SECOND_STAGE_EVOLUTION_WAVE)],
[Species.MANTYKE, new BreederSpeciesEvolution(Species.MANTINE, SECOND_STAGE_EVOLUTION_WAVE)]
];
/**
* The Expert Pokémon Breeder encounter.
* @see {@link https://github.com/pagefaultgames/pokerogue/issues/3818 | GitHub Issue #3818}
* @see For biome requirements check {@linkcode mysteryEncountersByBiome}
*/
export const TheExpertPokemonBreederEncounter: MysteryEncounter =
MysteryEncounterBuilder.withEncounterType(MysteryEncounterType.THE_EXPERT_POKEMON_BREEDER)
.withEncounterTier(MysteryEncounterTier.ULTRA)
.withSceneWaveRangeRequirement(...CLASSIC_MODE_MYSTERY_ENCOUNTER_WAVES)
.withScenePartySizeRequirement(4, 6, true) // Must have at least 4 legal pokemon in party
.withIntroSpriteConfigs([]) // These are set in onInit()
.withIntroDialogue([
{
text: `${namespace}.intro`,
},
{
speaker: trainerNameKey,
text: `${namespace}.intro_dialogue`,
},
])
.withOnInit((scene: BattleScene) => {
const encounter = scene.currentBattle.mysteryEncounter!;
const waveIndex = scene.currentBattle.waveIndex;
// Calculates what trainers are available for battle in the encounter
// If player is in space biome, uses special "Space" version of the trainer
encounter.enemyPartyConfigs = [
getPartyConfig(scene)
];
const cleffaSpecies = waveIndex < FIRST_STAGE_EVOLUTION_WAVE ? Species.CLEFFA : waveIndex < FINAL_STAGE_EVOLUTION_WAVE ? Species.CLEFAIRY : Species.CLEFABLE;
encounter.spriteConfigs = [
{
spriteKey: cleffaSpecies.toString(),
fileRoot: "pokemon",
hasShadow: true,
repeat: true,
x: 14,
y: -2,
yShadow: -2
},
{
spriteKey: "expert_pokemon_breeder",
fileRoot: "trainer",
hasShadow: true,
x: -14,
y: 4,
yShadow: 2
},
];
// Determine the 3 pokemon the player can battle with
let partyCopy = scene.getParty().slice(0);
partyCopy = partyCopy
.filter(p => p.isAllowedInBattle())
.sort((a, b) => a.friendship - b.friendship);
const pokemon1 = partyCopy[0];
const pokemon2 = partyCopy[1];
const pokemon3 = partyCopy[2];
encounter.setDialogueToken("pokemon1Name", pokemon1.getNameToRender());
encounter.setDialogueToken("pokemon2Name", pokemon2.getNameToRender());
encounter.setDialogueToken("pokemon3Name", pokemon3.getNameToRender());
// Dialogue and egg calcs for Pokemon 1
const [pokemon1CommonEggs, pokemon1RareEggs] = calculateEggRewardsForPokemon(pokemon1);
let pokemon1Tooltip = getEncounterText(scene, `${namespace}.option.1.tooltip_base`)!;
if (pokemon1RareEggs > 0) {
const eggsText = i18next.t(`${namespace}.numEggs`, { count: pokemon1RareEggs, rarity: i18next.t("egg:greatTier") });
pokemon1Tooltip += i18next.t(`${namespace}.eggs_tooltip`, { eggs: eggsText });
encounter.setDialogueToken("pokemon1RareEggs", eggsText);
}
if (pokemon1CommonEggs > 0) {
const eggsText = i18next.t(`${namespace}.numEggs`, { count: pokemon1CommonEggs, rarity: i18next.t("egg:defaultTier") });
pokemon1Tooltip += i18next.t(`${namespace}.eggs_tooltip`, { eggs: eggsText });
encounter.setDialogueToken("pokemon1CommonEggs", eggsText);
}
encounter.options[0].dialogue!.buttonTooltip = pokemon1Tooltip;
// Dialogue and egg calcs for Pokemon 2
const [pokemon2CommonEggs, pokemon2RareEggs] = calculateEggRewardsForPokemon(pokemon2);
let pokemon2Tooltip = getEncounterText(scene, `${namespace}.option.2.tooltip_base`)!;
if (pokemon2RareEggs > 0) {
const eggsText = i18next.t(`${namespace}.numEggs`, { count: pokemon2RareEggs, rarity: i18next.t("egg:greatTier") });
pokemon2Tooltip += i18next.t(`${namespace}.eggs_tooltip`, { eggs: eggsText });
encounter.setDialogueToken("pokemon2RareEggs", eggsText);
}
if (pokemon2CommonEggs > 0) {
const eggsText = i18next.t(`${namespace}.numEggs`, { count: pokemon2CommonEggs, rarity: i18next.t("egg:defaultTier") });
pokemon2Tooltip += i18next.t(`${namespace}.eggs_tooltip`, { eggs: eggsText });
encounter.setDialogueToken("pokemon1CommonEggs", eggsText);
}
encounter.options[1].dialogue!.buttonTooltip = pokemon2Tooltip;
// Dialogue and egg calcs for Pokemon 3
const [pokemon3CommonEggs, pokemon3RareEggs] = calculateEggRewardsForPokemon(pokemon3);
let pokemon3Tooltip = getEncounterText(scene, `${namespace}.option.3.tooltip_base`)!;
if (pokemon3RareEggs > 0) {
const eggsText = i18next.t(`${namespace}.numEggs`, { count: pokemon3RareEggs, rarity: i18next.t("egg:greatTier") });
pokemon3Tooltip += i18next.t(`${namespace}.eggs_tooltip`, { eggs: eggsText });
encounter.setDialogueToken("pokemon3RareEggs", eggsText);
}
if (pokemon3CommonEggs > 0) {
const eggsText = i18next.t(`${namespace}.numEggs`, { count: pokemon3CommonEggs, rarity: i18next.t("egg:defaultTier") });
pokemon3Tooltip += i18next.t(`${namespace}.eggs_tooltip`, { eggs: eggsText });
encounter.setDialogueToken("pokemon3CommonEggs", eggsText);
}
encounter.options[2].dialogue!.buttonTooltip = pokemon3Tooltip;
encounter.misc = {
pokemon1,
pokemon1CommonEggs,
pokemon1RareEggs,
pokemon2,
pokemon2CommonEggs,
pokemon2RareEggs,
pokemon3,
pokemon3CommonEggs,
pokemon3RareEggs
};
return true;
})
.withTitle(`${namespace}.title`)
.withDescription(`${namespace}.description`)
.withQuery(`${namespace}.query`)
.withOption(
MysteryEncounterOptionBuilder
.newOptionWithMode(MysteryEncounterOptionMode.DEFAULT)
.withDialogue({
buttonLabel: `${namespace}.option.1.label`,
selected: [
{
speaker: trainerNameKey,
text: `${namespace}.option.selected`,
},
],
})
.withOptionPhase(async (scene: BattleScene) => {
const encounter = scene.currentBattle.mysteryEncounter!;
// Spawn battle with first pokemon
const config: EnemyPartyConfig = encounter.enemyPartyConfigs[0];
const { pokemon1, pokemon1CommonEggs, pokemon1RareEggs } = encounter.misc;
encounter.setDialogueToken("chosenPokemon", pokemon1.getNameToRender());
const eggOptions = getEggOptions(scene, pokemon1CommonEggs, pokemon1RareEggs);
setEncounterRewards(scene, { fillRemaining: true }, eggOptions);
// Remove all Pokemon from the party except the chosen Pokemon
removePokemonFromPartyAndStoreHeldItems(scene, encounter, pokemon1);
// Configure outro dialogue for egg rewards
encounter.dialogue.outro = [
{
speaker: trainerNameKey,
text: `${namespace}.outro`,
},
];
if (encounter.dialogueTokens.hasOwnProperty("pokemon1CommonEggs")) {
encounter.dialogue.outro.push({
text: i18next.t(`${namespace}.gained_eggs`, { numEggs: encounter.dialogueTokens["pokemon1CommonEggs"] }),
});
}
if (encounter.dialogueTokens.hasOwnProperty("pokemon1RareEggs")) {
encounter.dialogue.outro.push({
text: i18next.t(`${namespace}.gained_eggs`, { numEggs: encounter.dialogueTokens["pokemon1RareEggs"] }),
});
}
initBattleWithEnemyConfig(scene, config);
})
.withPostOptionPhase(async (scene: BattleScene) => {
// Give achievement if in Space biome
checkAchievement(scene);
// Give 20 friendship to the chosen pokemon
scene.currentBattle.mysteryEncounter!.misc.pokemon1.addFriendship(FRIENDSHIP_ADDED);
await restorePartyAndHeldItems(scene);
})
.build()
)
.withOption(
MysteryEncounterOptionBuilder
.newOptionWithMode(MysteryEncounterOptionMode.DEFAULT)
.withDialogue({
buttonLabel: `${namespace}.option.2.label`,
selected: [
{
speaker: trainerNameKey,
text: `${namespace}.option.selected`,
},
],
})
.withOptionPhase(async (scene: BattleScene) => {
const encounter = scene.currentBattle.mysteryEncounter!;
// Spawn battle with second pokemon
const config: EnemyPartyConfig = encounter.enemyPartyConfigs[0];
const { pokemon2, pokemon2CommonEggs, pokemon2RareEggs } = encounter.misc;
encounter.setDialogueToken("chosenPokemon", pokemon2.getNameToRender());
const eggOptions = getEggOptions(scene, pokemon2CommonEggs, pokemon2RareEggs);
setEncounterRewards(scene, { fillRemaining: true }, eggOptions);
// Remove all Pokemon from the party except the chosen Pokemon
removePokemonFromPartyAndStoreHeldItems(scene, encounter, pokemon2);
// Configure outro dialogue for egg rewards
encounter.dialogue.outro = [
{
speaker: trainerNameKey,
text: `${namespace}.outro`,
},
];
if (encounter.dialogueTokens.hasOwnProperty("pokemon2CommonEggs")) {
encounter.dialogue.outro.push({
text: i18next.t(`${namespace}.gained_eggs`, { numEggs: encounter.dialogueTokens["pokemon2CommonEggs"] }),
});
}
if (encounter.dialogueTokens.hasOwnProperty("pokemon2RareEggs")) {
encounter.dialogue.outro.push({
text: i18next.t(`${namespace}.gained_eggs`, { numEggs: encounter.dialogueTokens["pokemon2RareEggs"] }),
});
}
initBattleWithEnemyConfig(scene, config);
})
.withPostOptionPhase(async (scene: BattleScene) => {
// Give achievement if in Space biome
checkAchievement(scene);
// Give 20 friendship to the chosen pokemon
scene.currentBattle.mysteryEncounter!.misc.pokemon2.addFriendship(FRIENDSHIP_ADDED);
await restorePartyAndHeldItems(scene);
})
.build()
)
.withOption(
MysteryEncounterOptionBuilder
.newOptionWithMode(MysteryEncounterOptionMode.DEFAULT)
.withDialogue({
buttonLabel: `${namespace}.option.3.label`,
selected: [
{
speaker: trainerNameKey,
text: `${namespace}.option.selected`,
},
],
})
.withOptionPhase(async (scene: BattleScene) => {
const encounter = scene.currentBattle.mysteryEncounter!;
// Spawn battle with third pokemon
const config: EnemyPartyConfig = encounter.enemyPartyConfigs[0];
const { pokemon3, pokemon3CommonEggs, pokemon3RareEggs } = encounter.misc;
encounter.setDialogueToken("chosenPokemon", pokemon3.getNameToRender());
const eggOptions = getEggOptions(scene, pokemon3CommonEggs, pokemon3RareEggs);
setEncounterRewards(scene, { fillRemaining: true }, eggOptions);
// Remove all Pokemon from the party except the chosen Pokemon
removePokemonFromPartyAndStoreHeldItems(scene, encounter, pokemon3);
// Configure outro dialogue for egg rewards
encounter.dialogue.outro = [
{
speaker: trainerNameKey,
text: `${namespace}.outro`,
},
];
if (encounter.dialogueTokens.hasOwnProperty("pokemon3CommonEggs")) {
encounter.dialogue.outro.push({
text: i18next.t(`${namespace}.gained_eggs`, { numEggs: encounter.dialogueTokens["pokemon3CommonEggs"] }),
});
}
if (encounter.dialogueTokens.hasOwnProperty("pokemon3RareEggs")) {
encounter.dialogue.outro.push({
text: i18next.t(`${namespace}.gained_eggs`, { numEggs: encounter.dialogueTokens["pokemon3RareEggs"] }),
});
}
initBattleWithEnemyConfig(scene, config);
})
.withPostOptionPhase(async (scene: BattleScene) => {
// Give achievement if in Space biome
checkAchievement(scene);
// Give 20 friendship to the chosen pokemon
scene.currentBattle.mysteryEncounter!.misc.pokemon3.addFriendship(FRIENDSHIP_ADDED);
await restorePartyAndHeldItems(scene);
})
.build()
)
.withOutroDialogue([
{
text: `${namespace}.outro`,
},
])
.build();
function getPartyConfig(scene: BattleScene): EnemyPartyConfig {
// Bug type superfan trainer config
const waveIndex = scene.currentBattle.waveIndex;
const breederConfig = trainerConfigs[TrainerType.EXPERT_POKEMON_BREEDER].clone();
breederConfig.name = i18next.t(trainerNameKey);
// First mon is *always* this special cleffa
const cleffaSpecies = waveIndex < FIRST_STAGE_EVOLUTION_WAVE ? Species.CLEFFA : waveIndex < FINAL_STAGE_EVOLUTION_WAVE ? Species.CLEFAIRY : Species.CLEFABLE;
const baseConfig: EnemyPartyConfig = {
trainerType: TrainerType.EXPERT_POKEMON_BREEDER,
pokemonConfigs: [
{
nickname: i18next.t(`${namespace}.cleffa_1_nickname`),
species: getPokemonSpecies(cleffaSpecies),
isBoss: false,
abilityIndex: 1, // Magic Guard
shiny: false,
nature: Nature.ADAMANT,
moveSet: [Moves.METEOR_MASH, Moves.FIRE_PUNCH, Moves.ICE_PUNCH, Moves.THUNDER_PUNCH],
ivs: [31, 31, 31, 31, 31, 31],
modifierConfigs: [
{
modifier: generateModifierType(scene, modifierTypes.TERA_SHARD, [Type.STEEL]) as PokemonHeldItemModifierType,
},
{
modifier: generateModifierType(scene, modifierTypes.BASE_STAT_BOOSTER, [Stat.ATK]) as PokemonHeldItemModifierType,
stackCount: 1 + Math.floor(waveIndex / 20), // +1 Protein every 20 waves
},
{
modifier: generateModifierType(scene, modifierTypes.BASE_STAT_BOOSTER, [Stat.SPD]) as PokemonHeldItemModifierType,
stackCount: 1 + Math.floor(waveIndex / 40), // +1 Carbos every 40 waves
},
]
}
]
};
if (scene.arena.biomeType === Biome.SPACE) {
// All 3 members always Cleffa line, but different configs
baseConfig.pokemonConfigs!.push({
nickname: i18next.t(`${namespace}.cleffa_2_nickname`),
species: getPokemonSpecies(cleffaSpecies),
isBoss: false,
abilityIndex: 1, // Magic Guard
shiny: true,
variant: 1,
nature: Nature.MODEST,
moveSet: [Moves.MOONBLAST, Moves.MYSTICAL_FIRE, Moves.ICE_BEAM, Moves.THUNDERBOLT],
ivs: [31, 31, 31, 31, 31, 31]
},
{
nickname: i18next.t(`${namespace}.cleffa_3_nickname`, { speciesName: getPokemonSpecies(cleffaSpecies).getName() }),
species: getPokemonSpecies(cleffaSpecies),
isBoss: false,
abilityIndex: 2, // Friend Guard / Unaware
shiny: true,
variant: 2,
nature: Nature.BOLD,
moveSet: [Moves.TRI_ATTACK, Moves.STORED_POWER, Moves.TAKE_HEART, Moves.MOONLIGHT],
ivs: [31, 31, 31, 31, 31, 31]
});
} else {
// Second member from pool 1
const pool1Species = getSpeciesFromPool(POOL_1_POKEMON, waveIndex);
// Third member from pool 2
const pool2Species = getSpeciesFromPool(POOL_2_POKEMON, waveIndex);
baseConfig.pokemonConfigs!.push({
species: getPokemonSpecies(pool1Species),
isBoss: false,
ivs: [31, 31, 31, 31, 31, 31]
},
{
species: getPokemonSpecies(pool2Species),
isBoss: false,
ivs: [31, 31, 31, 31, 31, 31]
});
}
return baseConfig;
}
function getSpeciesFromPool(speciesPool: (Species | BreederSpeciesEvolution)[][], waveIndex: number): Species {
const poolCopy = speciesPool.slice(0);
randSeedShuffle(poolCopy);
const speciesEvolutions = poolCopy.pop()!.slice(0);
let speciesObject = speciesEvolutions.pop()!;
while (speciesObject instanceof BreederSpeciesEvolution && speciesObject.evolution > waveIndex) {
speciesObject = speciesEvolutions.pop()!;
}
return speciesObject instanceof BreederSpeciesEvolution ? speciesObject.species : speciesObject;
}
function calculateEggRewardsForPokemon(pokemon: PlayerPokemon): [number, number] {
const bst = pokemon.calculateBaseStats().reduce((a, b) => a + b, 0);
// 1 point for every 20 points below 680 BST the pokemon is, (max 18, min 1)
const pointsFromBst = Math.min(Math.max(Math.floor((680 - bst) / 20), 1), 18);
const rootSpecies = pokemon.species.getRootSpeciesId(true);
let pointsFromStarterTier = 0;
// 2 points for every 1 below 7 that the pokemon's starter tier is (max 12, min 0)
if (speciesStarters.hasOwnProperty(rootSpecies)) {
const starterTier = speciesStarters[rootSpecies];
pointsFromStarterTier = Math.min(Math.max(Math.floor(7 - starterTier) * 2, 0), 12);
}
// Maximum of 30 points
const totalPoints = Math.min(pointsFromStarterTier + pointsFromBst, 30);
// 1 Rare egg for every 6 points
const numRares = Math.floor(totalPoints / 6);
// 1 Common egg for every point leftover
const numCommons = totalPoints % 6;
return [numCommons, numRares];
}
function getEggOptions(scene: BattleScene, commonEggs: number, rareEggs: number) {
const eggDescription = i18next.t(`${namespace}.title`) + ":\n" + i18next.t(trainerNameKey);
const eggOptions: IEggOptions[] = [];
if (commonEggs > 0) {
for (let i = 0; i < commonEggs; i++) {
eggOptions.push({
scene,
pulled: false,
sourceType: EggSourceType.EVENT,
eggDescriptor: eggDescription,
tier: EggTier.COMMON
});
}
}
if (rareEggs > 0) {
for (let i = 0; i < rareEggs; i++) {
eggOptions.push({
scene,
pulled: false,
sourceType: EggSourceType.EVENT,
eggDescriptor: eggDescription,
tier: EggTier.GREAT
});
}
}
return eggOptions;
}
function removePokemonFromPartyAndStoreHeldItems(scene: BattleScene, encounter: MysteryEncounter, chosenPokemon: PlayerPokemon) {
const party = scene.getParty();
const chosenIndex = party.indexOf(chosenPokemon);
party[chosenIndex] = party[0];
party[0] = chosenPokemon;
encounter.misc.originalParty = scene.getParty().slice(1);
encounter.misc.originalPartyHeldItems = encounter.misc.originalParty
.map(p => p.getHeldItems());
scene["party"] = [
chosenPokemon
];
}
function checkAchievement(scene: BattleScene) {
if (scene.arena.biomeType === Biome.SPACE) {
scene.validateAchv(achvs.BREEDERS_IN_SPACE);
}
}
async function restorePartyAndHeldItems(scene: BattleScene) {
const encounter = scene.currentBattle.mysteryEncounter!;
// Restore original party
scene.getParty().push(...encounter.misc.originalParty);
// Restore held items
const originalHeldItems = encounter.misc.originalPartyHeldItems;
originalHeldItems.forEach(pokemonHeldItemsList => {
pokemonHeldItemsList.forEach(heldItem => {
scene.addModifier(heldItem, true, false, false, true);
});
});
await scene.updateModifiers(true);
}

View File

@ -31,6 +31,7 @@ import { BugTypeSuperfanEncounter } from "#app/data/mystery-encounters/encounter
import { FunAndGamesEncounter } from "#app/data/mystery-encounters/encounters/fun-and-games-encounter"; import { FunAndGamesEncounter } from "#app/data/mystery-encounters/encounters/fun-and-games-encounter";
import { UncommonBreedEncounter } from "#app/data/mystery-encounters/encounters/uncommon-breed-encounter"; import { UncommonBreedEncounter } from "#app/data/mystery-encounters/encounters/uncommon-breed-encounter";
import { GlobalTradeSystemEncounter } from "#app/data/mystery-encounters/encounters/global-trade-system-encounter"; import { GlobalTradeSystemEncounter } from "#app/data/mystery-encounters/encounters/global-trade-system-encounter";
import { TheExpertPokemonBreederEncounter } from "#app/data/mystery-encounters/encounters/the-expert-pokemon-breeder-encounter";
/** /**
* Spawn chance: (BASE_MYSTERY_ENCOUNTER_SPAWN_WEIGHT + WIGHT_INCREMENT_ON_SPAWN_MISS * <number of missed spawns>) / MYSTERY_ENCOUNTER_SPAWN_MAX_WEIGHT * Spawn chance: (BASE_MYSTERY_ENCOUNTER_SPAWN_WEIGHT + WIGHT_INCREMENT_ON_SPAWN_MISS * <number of missed spawns>) / MYSTERY_ENCOUNTER_SPAWN_MAX_WEIGHT
@ -184,7 +185,8 @@ const humanTransitableBiomeEncounters: MysteryEncounterType[] = [
MysteryEncounterType.SHADY_VITAMIN_DEALER, MysteryEncounterType.SHADY_VITAMIN_DEALER,
MysteryEncounterType.THE_POKEMON_SALESMAN, MysteryEncounterType.THE_POKEMON_SALESMAN,
MysteryEncounterType.AN_OFFER_YOU_CANT_REFUSE, MysteryEncounterType.AN_OFFER_YOU_CANT_REFUSE,
MysteryEncounterType.THE_WINSTRATE_CHALLENGE MysteryEncounterType.THE_WINSTRATE_CHALLENGE,
MysteryEncounterType.THE_EXPERT_POKEMON_BREEDER
]; ];
const civilizationBiomeEncounters: MysteryEncounterType[] = [ const civilizationBiomeEncounters: MysteryEncounterType[] = [
@ -238,7 +240,6 @@ export const mysteryEncountersByBiome = new Map<Biome, MysteryEncounterType[]>([
MysteryEncounterType.SAFARI_ZONE, MysteryEncounterType.SAFARI_ZONE,
MysteryEncounterType.ABSOLUTE_AVARICE MysteryEncounterType.ABSOLUTE_AVARICE
]], ]],
[Biome.SEA, [ [Biome.SEA, [
MysteryEncounterType.LOST_AT_SEA MysteryEncounterType.LOST_AT_SEA
]], ]],
@ -275,7 +276,9 @@ export const mysteryEncountersByBiome = new Map<Biome, MysteryEncounterType[]>([
[Biome.ABYSS, [ [Biome.ABYSS, [
MysteryEncounterType.DANCING_LESSONS MysteryEncounterType.DANCING_LESSONS
]], ]],
[Biome.SPACE, []], [Biome.SPACE, [
MysteryEncounterType.THE_EXPERT_POKEMON_BREEDER
]],
[Biome.CONSTRUCTION_SITE, []], [Biome.CONSTRUCTION_SITE, []],
[Biome.JUNGLE, [ [Biome.JUNGLE, [
MysteryEncounterType.SAFARI_ZONE MysteryEncounterType.SAFARI_ZONE
@ -319,6 +322,7 @@ export function initMysteryEncounters() {
allMysteryEncounters[MysteryEncounterType.FUN_AND_GAMES] = FunAndGamesEncounter; allMysteryEncounters[MysteryEncounterType.FUN_AND_GAMES] = FunAndGamesEncounter;
allMysteryEncounters[MysteryEncounterType.UNCOMMON_BREED] = UncommonBreedEncounter; allMysteryEncounters[MysteryEncounterType.UNCOMMON_BREED] = UncommonBreedEncounter;
allMysteryEncounters[MysteryEncounterType.GLOBAL_TRADE_SYSTEM] = GlobalTradeSystemEncounter; allMysteryEncounters[MysteryEncounterType.GLOBAL_TRADE_SYSTEM] = GlobalTradeSystemEncounter;
allMysteryEncounters[MysteryEncounterType.THE_EXPERT_POKEMON_BREEDER] = TheExpertPokemonBreederEncounter;
// Add extreme encounters to biome map // Add extreme encounters to biome map
extremeBiomeEncounters.forEach(encounter => { extremeBiomeEncounters.forEach(encounter => {

View File

@ -36,6 +36,7 @@ import { BattleEndPhase } from "#app/phases/battle-end-phase";
import { GameOverPhase } from "#app/phases/game-over-phase"; import { GameOverPhase } from "#app/phases/game-over-phase";
import { SelectModifierPhase } from "#app/phases/select-modifier-phase"; import { SelectModifierPhase } from "#app/phases/select-modifier-phase";
import { PartyExpPhase } from "#app/phases/party-exp-phase"; import { PartyExpPhase } from "#app/phases/party-exp-phase";
import { Variant } from "#app/data/variant";
/** /**
* Animates exclamation sprite over trainer's head at start of encounter * Animates exclamation sprite over trainer's head at start of encounter
@ -67,6 +68,7 @@ export function doTrainerExclamation(scene: BattleScene) {
export interface EnemyPokemonConfig { export interface EnemyPokemonConfig {
species: PokemonSpecies; species: PokemonSpecies;
isBoss: boolean; isBoss: boolean;
nickname?: string;
bossSegments?: number; bossSegments?: number;
bossSegmentModifier?: number; // Additive to the determined segment number bossSegmentModifier?: number; // Additive to the determined segment number
mysteryEncounterPokemonData?: MysteryEncounterPokemonData; mysteryEncounterPokemonData?: MysteryEncounterPokemonData;
@ -79,6 +81,8 @@ export interface EnemyPokemonConfig {
nature?: Nature; nature?: Nature;
ivs?: [number, number, number, number, number, number]; ivs?: [number, number, number, number, number, number];
shiny?: boolean; shiny?: boolean;
/** Is only checked if Pokemon is shiny */
variant?: Variant;
/** Can set just the status, or pass a timer on the status turns */ /** Can set just the status, or pass a timer on the status turns */
status?: StatusEffect | [StatusEffect, number]; status?: StatusEffect | [StatusEffect, number];
mysteryEncounterBattleEffects?: (pokemon: Pokemon) => void; mysteryEncounterBattleEffects?: (pokemon: Pokemon) => void;
@ -220,6 +224,11 @@ export async function initBattleWithEnemyConfig(scene: BattleScene, partyConfig:
if (partyConfig?.pokemonConfigs && e < partyConfig.pokemonConfigs.length) { if (partyConfig?.pokemonConfigs && e < partyConfig.pokemonConfigs.length) {
const config = partyConfig.pokemonConfigs[e]; const config = partyConfig.pokemonConfigs[e];
// Set form
if (!isNullOrUndefined(config.nickname)) {
enemyPokemon.nickname = btoa(unescape(encodeURIComponent(config.nickname!)));
}
// Generate new id, reset status and HP in case using data source // Generate new id, reset status and HP in case using data source
if (config.dataSource) { if (config.dataSource) {
enemyPokemon.id = Utils.randSeedInt(4294967296); enemyPokemon.id = Utils.randSeedInt(4294967296);
@ -235,6 +244,11 @@ export async function initBattleWithEnemyConfig(scene: BattleScene, partyConfig:
enemyPokemon.shiny = config.shiny!; enemyPokemon.shiny = config.shiny!;
} }
// Set Variant
if (enemyPokemon.shiny && !isNullOrUndefined(config.variant)) {
enemyPokemon.variant = config.variant!;
}
// Set custom mystery encounter data fields (such as sprite scale, custom abilities, types, etc.) // Set custom mystery encounter data fields (such as sprite scale, custom abilities, types, etc.)
if (!isNullOrUndefined(config.mysteryEncounterPokemonData)) { if (!isNullOrUndefined(config.mysteryEncounterPokemonData)) {
enemyPokemon.mysteryEncounterPokemonData = config.mysteryEncounterPokemonData!; enemyPokemon.mysteryEncounterPokemonData = config.mysteryEncounterPokemonData!;
@ -315,6 +329,9 @@ export async function initBattleWithEnemyConfig(scene: BattleScene, partyConfig:
// Requires re-priming summon data to update everything properly // Requires re-priming summon data to update everything properly
enemyPokemon.primeSummonData(enemyPokemon.summonData); enemyPokemon.primeSummonData(enemyPokemon.summonData);
if (enemyPokemon.isShiny() && !enemyPokemon["shinySparkle"]) {
enemyPokemon.initShinySparkle();
}
enemyPokemon.initBattleInfo(); enemyPokemon.initBattleInfo();
enemyPokemon.getBattleInfo().initInfo(enemyPokemon); enemyPokemon.getBattleInfo().initInfo(enemyPokemon);
enemyPokemon.generateName(); enemyPokemon.generateName();

View File

@ -1105,8 +1105,16 @@ function getGymLeaderPartyTemplate(scene: BattleScene) {
return getWavePartyTemplate(scene, trainerPartyTemplates.GYM_LEADER_1, trainerPartyTemplates.GYM_LEADER_2, trainerPartyTemplates.GYM_LEADER_3, trainerPartyTemplates.GYM_LEADER_4, trainerPartyTemplates.GYM_LEADER_5); return getWavePartyTemplate(scene, trainerPartyTemplates.GYM_LEADER_1, trainerPartyTemplates.GYM_LEADER_2, trainerPartyTemplates.GYM_LEADER_3, trainerPartyTemplates.GYM_LEADER_4, trainerPartyTemplates.GYM_LEADER_5);
} }
function getRandomPartyMemberFunc(speciesPool: Species[], trainerSlot: TrainerSlot = TrainerSlot.TRAINER, ignoreEvolution: boolean = false, postProcess?: (enemyPokemon: EnemyPokemon) => void): PartyMemberFunc { /**
return (scene: BattleScene, level: integer, strength: PartyMemberStrength) => { * Randomly selects one of the `Species` from `speciesPool`, determines its evolution, level, and strength.
* Then adds Pokemon to scene.
* @param speciesPool
* @param trainerSlot
* @param ignoreEvolution
* @param postProcess
*/
export function getRandomPartyMemberFunc(speciesPool: Species[], trainerSlot: TrainerSlot = TrainerSlot.TRAINER, ignoreEvolution: boolean = false, postProcess?: (enemyPokemon: EnemyPokemon) => void) {
return (scene: BattleScene, level: number, strength: PartyMemberStrength) => {
let species = Utils.randSeedItem(speciesPool); let species = Utils.randSeedItem(speciesPool);
if (!ignoreEvolution) { if (!ignoreEvolution) {
species = getPokemonSpecies(species).getTrainerSpeciesForLevel(level, true, strength, scene.currentBattle.waveIndex); species = getPokemonSpecies(species).getTrainerSpeciesForLevel(level, true, strength, scene.currentBattle.waveIndex);
@ -2302,6 +2310,8 @@ export const trainerConfigs: TrainerConfigs = {
.setMoneyMultiplier(2) .setMoneyMultiplier(2)
.setPartyTemplates(new TrainerPartyCompoundTemplate(new TrainerPartyTemplate(3, PartyMemberStrength.AVERAGE), new TrainerPartyTemplate(2, PartyMemberStrength.STRONG))), .setPartyTemplates(new TrainerPartyCompoundTemplate(new TrainerPartyTemplate(3, PartyMemberStrength.AVERAGE), new TrainerPartyTemplate(2, PartyMemberStrength.STRONG))),
[TrainerType.BUG_TYPE_SUPERFAN]: new TrainerConfig(++t).setMoneyMultiplier(2.25).setEncounterBgm(TrainerType.ACE_TRAINER) [TrainerType.BUG_TYPE_SUPERFAN]: new TrainerConfig(++t).setMoneyMultiplier(2.25).setEncounterBgm(TrainerType.ACE_TRAINER)
.setPartyTemplates(new TrainerPartyTemplate(2, PartyMemberStrength.AVERAGE)) .setPartyTemplates(new TrainerPartyTemplate(2, PartyMemberStrength.AVERAGE)),
[TrainerType.EXPERT_POKEMON_BREEDER]: new TrainerConfig(++t).setMoneyMultiplier(3).setEncounterBgm(TrainerType.ACE_TRAINER)
.setPartyTemplates(new TrainerPartyTemplate(3, PartyMemberStrength.STRONG))
}; };

View File

@ -28,5 +28,6 @@ export enum MysteryEncounterType {
BUG_TYPE_SUPERFAN, BUG_TYPE_SUPERFAN,
FUN_AND_GAMES, FUN_AND_GAMES,
UNCOMMON_BREED, UNCOMMON_BREED,
GLOBAL_TRADE_SYSTEM GLOBAL_TRADE_SYSTEM,
THE_EXPERT_POKEMON_BREEDER
} }

View File

@ -107,6 +107,7 @@ export enum TrainerType {
VICKY, VICKY,
VITO, VITO,
BUG_TYPE_SUPERFAN, BUG_TYPE_SUPERFAN,
EXPERT_POKEMON_BREEDER,
BROCK = 200, BROCK = 200,
MISTY, MISTY,

View File

@ -1,19 +1,19 @@
import BattleScene from "../battle-scene"; import BattleScene from "../battle-scene";
import { BiomePoolTier, PokemonPools, BiomeTierTrainerPools, biomePokemonPools, biomeTrainerPools } from "../data/biomes"; import { biomePokemonPools, BiomePoolTier, BiomeTierTrainerPools, biomeTrainerPools, PokemonPools } from "../data/biomes";
import { Constructor } from "#app/utils"; import { Constructor } from "#app/utils";
import * as Utils from "../utils"; import * as Utils from "../utils";
import PokemonSpecies, { getPokemonSpecies } from "../data/pokemon-species"; import PokemonSpecies, { getPokemonSpecies } from "../data/pokemon-species";
import { Weather, WeatherType, getTerrainClearMessage, getTerrainStartMessage, getWeatherClearMessage, getWeatherStartMessage } from "../data/weather"; import { getTerrainClearMessage, getTerrainStartMessage, getWeatherClearMessage, getWeatherStartMessage, Weather, WeatherType } from "../data/weather";
import { CommonAnim } from "../data/battle-anims"; import { CommonAnim } from "../data/battle-anims";
import { Type } from "../data/type"; import { Type } from "../data/type";
import Move from "../data/move"; import Move from "../data/move";
import { ArenaTag, ArenaTagSide, ArenaTrapTag, getArenaTag } from "../data/arena-tag"; import { ArenaTag, ArenaTagSide, ArenaTrapTag, getArenaTag } from "../data/arena-tag";
import { BattlerIndex } from "../battle"; import { BattlerIndex } from "../battle";
import { Terrain, TerrainType } from "../data/terrain"; import { Terrain, TerrainType } from "../data/terrain";
import { PostTerrainChangeAbAttr, PostWeatherChangeAbAttr, applyPostTerrainChangeAbAttrs, applyPostWeatherChangeAbAttrs } from "../data/ability"; import { applyPostTerrainChangeAbAttrs, applyPostWeatherChangeAbAttrs, PostTerrainChangeAbAttr, PostWeatherChangeAbAttr } from "../data/ability";
import Pokemon from "./pokemon"; import Pokemon from "./pokemon";
import Overrides from "#app/overrides"; import Overrides from "#app/overrides";
import { WeatherChangedEvent, TerrainChangedEvent, TagAddedEvent, TagRemovedEvent } from "../events/arena"; import { TagAddedEvent, TagRemovedEvent, TerrainChangedEvent, WeatherChangedEvent } from "../events/arena";
import { ArenaTagType } from "#enums/arena-tag-type"; import { ArenaTagType } from "#enums/arena-tag-type";
import { Biome } from "#enums/biome"; import { Biome } from "#enums/biome";
import { Moves } from "#enums/moves"; import { Moves } from "#enums/moves";

View File

@ -3,7 +3,7 @@ import BattleScene, { AnySound } from "../battle-scene";
import { Variant, VariantSet, variantColorCache } from "#app/data/variant"; import { Variant, VariantSet, variantColorCache } from "#app/data/variant";
import { variantData } from "#app/data/variant"; import { variantData } from "#app/data/variant";
import BattleInfo, { PlayerBattleInfo, EnemyBattleInfo } from "../ui/battle-info"; import BattleInfo, { PlayerBattleInfo, EnemyBattleInfo } from "../ui/battle-info";
import Move, { HighCritAttr, DealsDoubleDamageToTagAttr, applyMoveAttrs, FixedDamageAttr, VariableAtkAttr, allMoves, MoveCategory, TypelessAttr, CritOnlyAttr, getMoveTargets, OneHitKOAttr, VariableMoveTypeAttr, VariableDefAttr, AttackMove, ModifiedDamageAttr, VariableMoveTypeMultiplierAttr, IgnoreOpponentStatStagesAttr, SacrificialAttr, VariableMoveCategoryAttr, CounterDamageAttr, StatStageChangeAttr, RechargeAttr, ChargeAttr, IgnoreWeatherTypeDebuffAttr, BypassBurnDamageReductionAttr, SacrificialAttrOnHit, OneHitKOAccuracyAttr, RespectAttackTypeImmunityAttr, MoveTarget } from "../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, ChargeAttr, IgnoreWeatherTypeDebuffAttr, BypassBurnDamageReductionAttr, SacrificialAttrOnHit, OneHitKOAccuracyAttr, RespectAttackTypeImmunityAttr, MoveTarget } from "../data/move";
import { default as PokemonSpecies, PokemonSpeciesForm, SpeciesFormKey, getFusedSpeciesName, getPokemonSpecies, getPokemonSpeciesForm, getStarterValueFriendshipCap, speciesStarters, starterPassiveAbilities } from "../data/pokemon-species"; import { default as PokemonSpecies, PokemonSpeciesForm, SpeciesFormKey, getFusedSpeciesName, getPokemonSpecies, getPokemonSpeciesForm, getStarterValueFriendshipCap, speciesStarters, starterPassiveAbilities } from "../data/pokemon-species";
import { Constructor, isNullOrUndefined, randSeedInt } from "#app/utils"; import { Constructor, isNullOrUndefined, randSeedInt } from "#app/utils";
import * as Utils from "../utils"; import * as Utils from "../utils";
@ -1513,7 +1513,7 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container {
const immuneTags = this.findTags(tag => tag instanceof TypeImmuneTag && tag.immuneType === moveType); const immuneTags = this.findTags(tag => tag instanceof TypeImmuneTag && tag.immuneType === moveType);
for (const tag of immuneTags) { for (const tag of immuneTags) {
if (move && !move.getAttrs(DealsDoubleDamageToTagAttr).some(attr => attr.tagType === tag.tagType)) { if (move && !move.getAttrs(HitsTagAttr).some(attr => attr.tagType === tag.tagType)) {
typeMultiplier.value = 0; typeMultiplier.value = 0;
break; break;
} }
@ -2515,13 +2515,13 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container {
this.scene.arena.applyTagsForSide(WeakenMoveScreenTag, defendingSide, move.category, this.scene.currentBattle.double, screenMultiplier); this.scene.arena.applyTagsForSide(WeakenMoveScreenTag, defendingSide, move.category, this.scene.currentBattle.double, screenMultiplier);
/** /**
* For each {@linkcode DealsDoubleDamageToTagAttr} the move has, doubles the damage of the move if: * For each {@linkcode HitsTagAttr} the move has, doubles the damage of the move if:
* The target has a {@linkcode BattlerTagType} that this move interacts with * The target has a {@linkcode BattlerTagType} that this move interacts with
* AND * AND
* The move doubles damage when used against that tag * The move doubles damage when used against that tag
*/ */
const hitsTagMultiplier = new Utils.NumberHolder(1); const hitsTagMultiplier = new Utils.NumberHolder(1);
move.getAttrs(DealsDoubleDamageToTagAttr).filter(hta => hta.doubleDamage).forEach(hta => { move.getAttrs(HitsTagAttr).filter(hta => hta.doubleDamage).forEach(hta => {
if (this.getTag(hta.tagType)) { if (this.getTag(hta.tagType)) {
hitsTagMultiplier.value *= 2; hitsTagMultiplier.value *= 2;
} }

View File

@ -283,5 +283,9 @@
"INVERSE_BATTLE": { "INVERSE_BATTLE": {
"name": "Mirror rorriM", "name": "Mirror rorriM",
"description": "Complete the Inverse Battle challenge.\n.egnellahc elttaB esrevnI eht etelpmoC" "description": "Complete the Inverse Battle challenge.\n.egnellahc elttaB esrevnI eht etelpmoC"
},
"BREEDERS_IN_SPACE": {
"name": "Breeders in Space!",
"description": "Beat the Expert Pokémon Breeder in the Space Biome."
} }
} }

View File

@ -84,6 +84,7 @@ import bugTypeSuperfan from "#app/locales/en/mystery-encounters/bug-type-superfa
import funAndGames from "#app/locales/en/mystery-encounters/fun-and-games-dialogue.json"; import funAndGames from "#app/locales/en/mystery-encounters/fun-and-games-dialogue.json";
import uncommonBreed from "#app/locales/en/mystery-encounters/uncommon-breed-dialogue.json"; import uncommonBreed from "#app/locales/en/mystery-encounters/uncommon-breed-dialogue.json";
import globalTradeSystem from "#app/locales/en/mystery-encounters/global-trade-system-dialogue.json"; import globalTradeSystem from "#app/locales/en/mystery-encounters/global-trade-system-dialogue.json";
import expertPokemonBreeder from "#app/locales/en/mystery-encounters/the-expert-pokemon-breeder-dialogue.json";
/** /**
* Dialogue/Text token injection patterns that can be used: * Dialogue/Text token injection patterns that can be used:
@ -183,7 +184,8 @@ export const enConfig = {
bugTypeSuperfan, bugTypeSuperfan,
funAndGames, funAndGames,
uncommonBreed, uncommonBreed,
globalTradeSystem globalTradeSystem,
expertPokemonBreeder
}, },
mysteryEncounterMessages mysteryEncounterMessages
}; };

View File

@ -21,7 +21,7 @@
"label": "Sales Assistant", "label": "Sales Assistant",
"tooltip": "(-) Your {{option3PrimaryName}} uses {{option3PrimaryMove}}\n(+) Earn @[MONEY]{Money}", "tooltip": "(-) Your {{option3PrimaryName}} uses {{option3PrimaryMove}}\n(+) Earn @[MONEY]{Money}",
"disabled_tooltip": "Your Pokémon need to know certain moves for this job", "disabled_tooltip": "Your Pokémon need to know certain moves for this job",
"selected": "Your {{option3PrimaryName}} spends the day using {{option3PrimaryMove}} to attract customers to the business!" "selected": "Your {{option3PrimaryName}} spends the day using {{option3PrimaryMove}} to draw customers to the business!"
} }
}, },
"job_complete_good": "Thanks for the assistance!\nYour {{selectedPokemon}} was incredibly helpful!$Here's your check for the day.", "job_complete_good": "Thanks for the assistance!\nYour {{selectedPokemon}} was incredibly helpful!$Here's your check for the day.",

View File

@ -0,0 +1,30 @@
{
"intro": "It's a trainer carrying tons of Pokémon Eggs!",
"intro_dialogue": "Hey there, trainer!$It looks like some of your\npartner Pokémon are feeling a little down.$Why not have a battle with me to cheer them up?",
"title": "The Expert Pokémon Breeder",
"description": "You've been challenged to a battle where @[TOOLTIP_TITLE]{you can only use a single Pokémon}. It might be tough, but it would surely deepen the bond you have with the Pokémon you choose!\nThe breeder will also give you some @[TOOLTIP_TITLE]{Pokémon Eggs} if you win!",
"query": "Who will you battle with?",
"cleffa_1_nickname": "Ace",
"cleffa_2_nickname": "Clefablest",
"cleffa_3_nickname": "{{speciesName}} the Great",
"option": {
"1": {
"label": "{{pokemon1Name}}",
"tooltip_base": "(-) Tough Battle\n(+) Gain Friendship with {{pokemon1Name}}"
},
"2": {
"label": "{{pokemon2Name}}",
"tooltip_base": "(-) Tough Battle\n(+) Gain Friendship with {{pokemon2Name}}"
},
"3": {
"label": "{{pokemon3Name}}",
"tooltip_base": "(-) Tough Battle\n(+) Gain Friendship with {{pokemon3Name}}"
},
"selected": "Let's do this!"
},
"outro": "Look how happy your {{chosenPokemon}} is now!$Here, you can have these as well.",
"gained_eggs": "@s{item_fanfare}You received {{numEggs}}!",
"eggs_tooltip": "\n(+) Earn {{eggs}}",
"numEggs_one": "{{count}} {{rarity}} Egg",
"numEggs_other": "{{count}} {{rarity}} Eggs"
}

View File

@ -172,5 +172,6 @@
"vivi": "Vivi", "vivi": "Vivi",
"vicky": "Vicky", "vicky": "Vicky",
"vito": "Vito", "vito": "Vito",
"bug_type_superfan": "Bug-Type Superfan" "bug_type_superfan": "Bug-Type Superfan",
"expert_pokemon_breeder": "Expert Pokémon Breeder"
} }

View File

@ -243,7 +243,7 @@ export class GameOverPhase extends BattlePhase {
gameVersion: this.scene.game.config.gameVersion, gameVersion: this.scene.game.config.gameVersion,
timestamp: new Date().getTime(), timestamp: new Date().getTime(),
challenges: this.scene.gameMode.challenges.map(c => new ChallengeData(c)), challenges: this.scene.gameMode.challenges.map(c => new ChallengeData(c)),
mysteryEncounterType: this.scene.currentBattle.mysteryEncounter?.encounterType, mysteryEncounterType: this.scene.currentBattle.mysteryEncounter?.encounterType ?? -1,
mysteryEncounterSaveData: this.scene.mysteryEncounterSaveData mysteryEncounterSaveData: this.scene.mysteryEncounterSaveData
} as SessionSaveData; } as SessionSaveData;
} }

View File

@ -4,7 +4,7 @@ import { applyPreAttackAbAttrs, AddSecondStrikeAbAttr, IgnoreMoveEffectsAbAttr,
import { ArenaTagSide, ConditionalProtectTag } from "#app/data/arena-tag"; import { ArenaTagSide, ConditionalProtectTag } from "#app/data/arena-tag";
import { MoveAnim } from "#app/data/battle-anims"; import { MoveAnim } from "#app/data/battle-anims";
import { BattlerTagLapseType, DamageProtectedTag, ProtectedTag, SemiInvulnerableTag, SubstituteTag } from "#app/data/battler-tags"; import { BattlerTagLapseType, DamageProtectedTag, ProtectedTag, SemiInvulnerableTag, SubstituteTag } from "#app/data/battler-tags";
import { MoveTarget, applyMoveAttrs, OverrideMoveEffectAttr, MultiHitAttr, AttackMove, FixedDamageAttr, VariableTargetAttr, MissEffectAttr, MoveFlags, applyFilteredMoveAttrs, MoveAttr, MoveEffectAttr, MoveEffectTrigger, ChargeAttr, MoveCategory, NoEffectAttr, DealsDoubleDamageToTagAttr } from "#app/data/move"; import { MoveTarget, applyMoveAttrs, OverrideMoveEffectAttr, MultiHitAttr, AttackMove, FixedDamageAttr, VariableTargetAttr, MissEffectAttr, MoveFlags, applyFilteredMoveAttrs, MoveAttr, MoveEffectAttr, MoveEffectTrigger, ChargeAttr, MoveCategory, NoEffectAttr, HitsTagAttr } from "#app/data/move";
import { SpeciesFormChangePostMoveTrigger } from "#app/data/pokemon-forms"; import { SpeciesFormChangePostMoveTrigger } from "#app/data/pokemon-forms";
import { BattlerTagType } from "#app/enums/battler-tag-type"; import { BattlerTagType } from "#app/enums/battler-tag-type";
import { Moves } from "#app/enums/moves"; import { Moves } from "#app/enums/moves";
@ -394,7 +394,7 @@ export class MoveEffectPhase extends PokemonPhase {
} }
const semiInvulnerableTag = target.getTag(SemiInvulnerableTag); const semiInvulnerableTag = target.getTag(SemiInvulnerableTag);
if (semiInvulnerableTag && !this.move.getMove().getAttrs(DealsDoubleDamageToTagAttr).some(hta => hta.tagType === semiInvulnerableTag.tagType)) { if (semiInvulnerableTag && !this.move.getMove().getAttrs(HitsTagAttr).some(hta => hta.tagType === semiInvulnerableTag.tagType)) {
return false; return false;
} }

View File

@ -279,6 +279,8 @@ export function getAchievementDescription(localizationKey: string): string {
return i18next.t("achv:FRESH_START.description", { context: genderStr }); return i18next.t("achv:FRESH_START.description", { context: genderStr });
case "INVERSE_BATTLE": case "INVERSE_BATTLE":
return i18next.t("achv:INVERSE_BATTLE.description", { context: genderStr }); return i18next.t("achv:INVERSE_BATTLE.description", { context: genderStr });
case "BREEDERS_IN_SPACE":
return i18next.t("achv:BREEDERS_IN_SPACE.description", { context: genderStr });
default: default:
return ""; return "";
} }
@ -356,6 +358,7 @@ export const achvs = {
MONO_FAIRY: new ChallengeAchv("MONO_FAIRY", "", "MONO_FAIRY.description", "fairy_feather", 100, (c, scene) => c instanceof SingleTypeChallenge && c.value === 18 && !scene.gameMode.challenges.some(c => c.id === Challenges.INVERSE_BATTLE && c.value > 0)), MONO_FAIRY: new ChallengeAchv("MONO_FAIRY", "", "MONO_FAIRY.description", "fairy_feather", 100, (c, scene) => c instanceof SingleTypeChallenge && c.value === 18 && !scene.gameMode.challenges.some(c => c.id === Challenges.INVERSE_BATTLE && c.value > 0)),
FRESH_START: new ChallengeAchv("FRESH_START", "", "FRESH_START.description", "reviver_seed", 100, (c, scene) => c instanceof FreshStartChallenge && c.value > 0 && !scene.gameMode.challenges.some(c => c.id === Challenges.INVERSE_BATTLE && c.value > 0)), FRESH_START: new ChallengeAchv("FRESH_START", "", "FRESH_START.description", "reviver_seed", 100, (c, scene) => c instanceof FreshStartChallenge && c.value > 0 && !scene.gameMode.challenges.some(c => c.id === Challenges.INVERSE_BATTLE && c.value > 0)),
INVERSE_BATTLE: new ChallengeAchv("INVERSE_BATTLE", "", "INVERSE_BATTLE.description", "inverse", 100, c => c instanceof InverseBattleChallenge && c.value > 0), INVERSE_BATTLE: new ChallengeAchv("INVERSE_BATTLE", "", "INVERSE_BATTLE.description", "inverse", 100, c => c instanceof InverseBattleChallenge && c.value > 0),
BREEDERS_IN_SPACE: new Achv("BREEDERS_IN_SPACE", "", "BREEDERS_IN_SPACE.description", "moon_stone", 100).setSecret(),
}; };
export function initAchievements() { export function initAchievements() {

View File

@ -69,22 +69,6 @@ describe("A Trainer's Test - Mystery Encounter", () => {
expect(ATrainersTestEncounter.options.length).toBe(2); expect(ATrainersTestEncounter.options.length).toBe(2);
}); });
it("should not run below wave 10", async () => {
game.override.startingWave(9);
await game.runToMysteryEncounter();
expect(scene.currentBattle?.mysteryEncounter?.encounterType).not.toBe(MysteryEncounterType.A_TRAINERS_TEST);
});
it("should not run above wave 179", async () => {
game.override.startingWave(181);
await game.runToMysteryEncounter();
expect(scene.currentBattle.mysteryEncounter).toBeUndefined();
});
it("should initialize fully ", async () => { it("should initialize fully ", async () => {
initSceneWithoutEncounterPhase(scene, defaultParty); initSceneWithoutEncounterPhase(scene, defaultParty);
scene.currentBattle.mysteryEncounter = ATrainersTestEncounter; scene.currentBattle.mysteryEncounter = ATrainersTestEncounter;

View File

@ -66,22 +66,6 @@ describe("Absolute Avarice - Mystery Encounter", () => {
expect(AbsoluteAvariceEncounter.options.length).toBe(3); expect(AbsoluteAvariceEncounter.options.length).toBe(3);
}); });
it("should not run below wave 10", async () => {
game.override.startingWave(9);
await game.runToMysteryEncounter();
expect(scene.currentBattle?.mysteryEncounter?.encounterType).not.toBe(MysteryEncounterType.ABSOLUTE_AVARICE);
});
it("should not run above wave 179", async () => {
game.override.startingWave(181);
await game.runToMysteryEncounter();
expect(scene.currentBattle.mysteryEncounter).toBeUndefined();
});
it("should not spawn outside of proper biomes", async () => { it("should not spawn outside of proper biomes", async () => {
game.override.mysteryEncounterTier(MysteryEncounterTier.GREAT); game.override.mysteryEncounterTier(MysteryEncounterTier.GREAT);
game.override.startingBiome(Biome.VOLCANO); game.override.startingBiome(Biome.VOLCANO);

View File

@ -80,22 +80,6 @@ describe("An Offer You Can't Refuse - Mystery Encounter", () => {
expect(scene.currentBattle?.mysteryEncounter?.encounterType).not.toBe(MysteryEncounterType.AN_OFFER_YOU_CANT_REFUSE); expect(scene.currentBattle?.mysteryEncounter?.encounterType).not.toBe(MysteryEncounterType.AN_OFFER_YOU_CANT_REFUSE);
}); });
it("should not run below wave 10", async () => {
game.override.startingWave(9);
await game.runToMysteryEncounter();
expect(scene.currentBattle?.mysteryEncounter?.encounterType).not.toBe(MysteryEncounterType.AN_OFFER_YOU_CANT_REFUSE);
});
it("should not run above wave 179", async () => {
game.override.startingWave(181);
await game.runToMysteryEncounter();
expect(scene.currentBattle.mysteryEncounter).toBeUndefined();
});
it("should initialize fully ", async () => { it("should initialize fully ", async () => {
initSceneWithoutEncounterPhase(scene, defaultParty); initSceneWithoutEncounterPhase(scene, defaultParty);
scene.currentBattle.mysteryEncounter = AnOfferYouCantRefuseEncounter; scene.currentBattle.mysteryEncounter = AnOfferYouCantRefuseEncounter;

View File

@ -69,22 +69,6 @@ describe("Berries Abound - Mystery Encounter", () => {
expect(BerriesAboundEncounter.options.length).toBe(3); expect(BerriesAboundEncounter.options.length).toBe(3);
}); });
it("should not run below wave 10", async () => {
game.override.startingWave(9);
await game.runToMysteryEncounter();
expect(scene.currentBattle?.mysteryEncounter?.encounterType).not.toBe(MysteryEncounterType.BERRIES_ABOUND);
});
it("should not run above wave 179", async () => {
game.override.startingWave(181);
await game.runToMysteryEncounter();
expect(scene.currentBattle.mysteryEncounter).toBeUndefined();
});
it("should initialize fully", async () => { it("should initialize fully", async () => {
initSceneWithoutEncounterPhase(scene, defaultParty); initSceneWithoutEncounterPhase(scene, defaultParty);
scene.currentBattle.mysteryEncounter = BerriesAboundEncounter; scene.currentBattle.mysteryEncounter = BerriesAboundEncounter;

View File

@ -201,22 +201,6 @@ describe("Bug-Type Superfan - Mystery Encounter", () => {
expect(BugTypeSuperfanEncounter.options.length).toBe(3); expect(BugTypeSuperfanEncounter.options.length).toBe(3);
}); });
it("should not run below wave 10", async () => {
game.override.startingWave(9);
await game.runToMysteryEncounter();
expect(scene.currentBattle?.mysteryEncounter?.encounterType).not.toBe(MysteryEncounterType.BUG_TYPE_SUPERFAN);
});
it("should not run above wave 179", async () => {
game.override.startingWave(181);
await game.runToMysteryEncounter();
expect(scene.currentBattle.mysteryEncounter).toBeUndefined();
});
it("should initialize fully", async () => { it("should initialize fully", async () => {
initSceneWithoutEncounterPhase(scene, defaultParty); initSceneWithoutEncounterPhase(scene, defaultParty);
scene.currentBattle.mysteryEncounter = BugTypeSuperfanEncounter; scene.currentBattle.mysteryEncounter = BugTypeSuperfanEncounter;

View File

@ -95,14 +95,6 @@ describe("Clowning Around - Mystery Encounter", () => {
expect(scene.currentBattle?.mysteryEncounter?.encounterType).not.toBe(MysteryEncounterType.CLOWNING_AROUND); expect(scene.currentBattle?.mysteryEncounter?.encounterType).not.toBe(MysteryEncounterType.CLOWNING_AROUND);
}); });
it("should not run above wave 179", async () => {
game.override.startingWave(181);
await game.runToMysteryEncounter();
expect(scene.currentBattle.mysteryEncounter).toBeUndefined();
});
it("should initialize fully", async () => { it("should initialize fully", async () => {
initSceneWithoutEncounterPhase(scene, defaultParty); initSceneWithoutEncounterPhase(scene, defaultParty);
scene.currentBattle.mysteryEncounter = ClowningAroundEncounter; scene.currentBattle.mysteryEncounter = ClowningAroundEncounter;

View File

@ -69,22 +69,6 @@ describe("Dancing Lessons - Mystery Encounter", () => {
expect(DancingLessonsEncounter.options.length).toBe(3); expect(DancingLessonsEncounter.options.length).toBe(3);
}); });
it("should not run below wave 10", async () => {
game.override.startingWave(9);
await game.runToMysteryEncounter();
expect(scene.currentBattle?.mysteryEncounter?.encounterType).not.toBe(MysteryEncounterType.DANCING_LESSONS);
});
it("should not run above wave 179", async () => {
game.override.startingWave(181);
await game.runToMysteryEncounter();
expect(scene.currentBattle.mysteryEncounter).toBeUndefined();
});
it("should not spawn outside of proper biomes", async () => { it("should not spawn outside of proper biomes", async () => {
game.override.mysteryEncounterTier(MysteryEncounterTier.GREAT); game.override.mysteryEncounterTier(MysteryEncounterTier.GREAT);
game.override.startingBiome(Biome.SPACE); game.override.startingBiome(Biome.SPACE);

View File

@ -66,22 +66,6 @@ describe("Delibird-y - Mystery Encounter", () => {
expect(DelibirdyEncounter.options.length).toBe(3); expect(DelibirdyEncounter.options.length).toBe(3);
}); });
it("should not run below wave 10", async () => {
game.override.startingWave(9);
await game.runToMysteryEncounter();
expect(scene.currentBattle?.mysteryEncounter?.encounterType).not.toBe(MysteryEncounterType.DELIBIRDY);
});
it("should not run above wave 179", async () => {
game.override.startingWave(181);
await game.runToMysteryEncounter();
expect(scene.currentBattle.mysteryEncounter).toBeUndefined();
});
it("should not spawn if player does not have enough money", async () => { it("should not spawn if player does not have enough money", async () => {
scene.money = 0; scene.money = 0;

View File

@ -79,22 +79,6 @@ describe("Department Store Sale - Mystery Encounter", () => {
expect(scene.currentBattle?.mysteryEncounter?.encounterType).not.toBe(MysteryEncounterType.DEPARTMENT_STORE_SALE); expect(scene.currentBattle?.mysteryEncounter?.encounterType).not.toBe(MysteryEncounterType.DEPARTMENT_STORE_SALE);
}); });
it("should not run below wave 10", async () => {
game.override.startingWave(9);
await game.runToMysteryEncounter();
expect(scene.currentBattle?.mysteryEncounter?.encounterType).not.toBe(MysteryEncounterType.DEPARTMENT_STORE_SALE);
});
it("should not run above wave 179", async () => {
game.override.startingWave(181);
await game.runToMysteryEncounter();
expect(scene.currentBattle.mysteryEncounter).toBeUndefined();
});
describe("Option 1 - TM Shop", () => { describe("Option 1 - TM Shop", () => {
it("should have the correct properties", () => { it("should have the correct properties", () => {
const option = DepartmentStoreSaleEncounter.options[0]; const option = DepartmentStoreSaleEncounter.options[0];

View File

@ -72,22 +72,6 @@ describe("Field Trip - Mystery Encounter", () => {
expect(FieldTripEncounter.options.length).toBe(3); expect(FieldTripEncounter.options.length).toBe(3);
}); });
it("should not run below wave 10", async () => {
game.override.startingWave(9);
await game.runToMysteryEncounter();
expect(scene.currentBattle?.mysteryEncounter?.encounterType).not.toBe(MysteryEncounterType.FIELD_TRIP);
});
it("should not run above wave 179", async () => {
game.override.startingWave(181);
await game.runToMysteryEncounter();
expect(scene.currentBattle.mysteryEncounter).toBeUndefined();
});
describe("Option 1 - Show off a physical move", () => { describe("Option 1 - Show off a physical move", () => {
it("should have the correct properties", () => { it("should have the correct properties", () => {
const option = FieldTripEncounter.options[0]; const option = FieldTripEncounter.options[0];

View File

@ -88,14 +88,6 @@ describe("Fiery Fallout - Mystery Encounter", () => {
expect(scene.currentBattle?.mysteryEncounter?.encounterType).not.toBe(MysteryEncounterType.FIERY_FALLOUT); expect(scene.currentBattle?.mysteryEncounter?.encounterType).not.toBe(MysteryEncounterType.FIERY_FALLOUT);
}); });
it("should not run above wave 179", async () => {
game.override.startingWave(181);
await game.runToMysteryEncounter();
expect(scene.currentBattle.mysteryEncounter).toBeUndefined();
});
it("should initialize fully ", async () => { it("should initialize fully ", async () => {
initSceneWithoutEncounterPhase(scene, defaultParty); initSceneWithoutEncounterPhase(scene, defaultParty);
scene.currentBattle.mysteryEncounter = FieryFalloutEncounter; scene.currentBattle.mysteryEncounter = FieryFalloutEncounter;

View File

@ -67,22 +67,6 @@ describe("Fight or Flight - Mystery Encounter", () => {
expect(FightOrFlightEncounter.options.length).toBe(3); expect(FightOrFlightEncounter.options.length).toBe(3);
}); });
it("should not run below wave 10", async () => {
game.override.startingWave(9);
await game.runToMysteryEncounter();
expect(scene.currentBattle?.mysteryEncounter?.encounterType).not.toBe(MysteryEncounterType.FIGHT_OR_FLIGHT);
});
it("should not run above wave 179", async () => {
game.override.startingWave(181);
await game.runToMysteryEncounter();
expect(scene.currentBattle.mysteryEncounter).toBeUndefined();
});
it("should initialize fully", async () => { it("should initialize fully", async () => {
initSceneWithoutEncounterPhase(scene, defaultParty); initSceneWithoutEncounterPhase(scene, defaultParty);
scene.currentBattle.mysteryEncounter = FightOrFlightEncounter; scene.currentBattle.mysteryEncounter = FightOrFlightEncounter;

View File

@ -85,22 +85,6 @@ describe("Fun And Games! - Mystery Encounter", () => {
expect(scene.currentBattle?.mysteryEncounter?.encounterType).not.toBe(MysteryEncounterType.FUN_AND_GAMES); expect(scene.currentBattle?.mysteryEncounter?.encounterType).not.toBe(MysteryEncounterType.FUN_AND_GAMES);
}); });
it("should not run below wave 10", async () => {
game.override.startingWave(9);
await game.runToMysteryEncounter();
expect(scene.currentBattle?.mysteryEncounter?.encounterType).not.toBe(MysteryEncounterType.FUN_AND_GAMES);
});
it("should not run above wave 179", async () => {
game.override.startingWave(181);
await game.runToMysteryEncounter();
expect(scene.currentBattle.mysteryEncounter).toBeUndefined();
});
it("should initialize fully", async () => { it("should initialize fully", async () => {
initSceneWithoutEncounterPhase(scene, defaultParty); initSceneWithoutEncounterPhase(scene, defaultParty);
scene.currentBattle.mysteryEncounter = new MysteryEncounter(FunAndGamesEncounter); scene.currentBattle.mysteryEncounter = new MysteryEncounter(FunAndGamesEncounter);

View File

@ -69,22 +69,6 @@ describe("Global Trade System - Mystery Encounter", () => {
expect(GlobalTradeSystemEncounter.options.length).toBe(4); expect(GlobalTradeSystemEncounter.options.length).toBe(4);
}); });
it("should not run below wave 10", async () => {
game.override.startingWave(9);
await game.runToMysteryEncounter();
expect(scene.currentBattle?.mysteryEncounter?.encounterType).not.toBe(MysteryEncounterType.GLOBAL_TRADE_SYSTEM);
});
it("should not run above wave 179", async () => {
game.override.startingWave(181);
await game.runToMysteryEncounter();
expect(scene.currentBattle.mysteryEncounter).toBeUndefined();
});
it("should not spawn outside of CIVILIZATION_ENCOUNTER_BIOMES", async () => { it("should not spawn outside of CIVILIZATION_ENCOUNTER_BIOMES", async () => {
game.override.mysteryEncounterTier(MysteryEncounterTier.COMMON); game.override.mysteryEncounterTier(MysteryEncounterTier.COMMON);
game.override.startingBiome(Biome.VOLCANO); game.override.startingBiome(Biome.VOLCANO);

View File

@ -74,22 +74,6 @@ describe("Lost at Sea - Mystery Encounter", () => {
expect(game.scene.currentBattle.mysteryEncounter?.encounterType).not.toBe(MysteryEncounterType.LOST_AT_SEA); expect(game.scene.currentBattle.mysteryEncounter?.encounterType).not.toBe(MysteryEncounterType.LOST_AT_SEA);
}); });
it("should not run below wave 11", async () => {
game.override.startingWave(9);
await game.runToMysteryEncounter();
expect(game.scene.currentBattle.mysteryEncounter).toBeUndefined();
});
it("should not run above wave 179", async () => {
game.override.startingWave(181);
await game.runToMysteryEncounter();
expect(game.scene.currentBattle.mysteryEncounter).toBeUndefined();
});
it("should initialize fully", () => { it("should initialize fully", () => {
initSceneWithoutEncounterPhase(scene, defaultParty); initSceneWithoutEncounterPhase(scene, defaultParty);
scene.currentBattle.mysteryEncounter = LostAtSeaEncounter; scene.currentBattle.mysteryEncounter = LostAtSeaEncounter;

View File

@ -79,22 +79,6 @@ describe("Mysterious Challengers - Mystery Encounter", () => {
expect(scene.currentBattle?.mysteryEncounter?.encounterType).not.toBe(MysteryEncounterType.MYSTERIOUS_CHALLENGERS); expect(scene.currentBattle?.mysteryEncounter?.encounterType).not.toBe(MysteryEncounterType.MYSTERIOUS_CHALLENGERS);
}); });
it("should not run below wave 10", async () => {
game.override.startingWave(9);
await game.runToMysteryEncounter();
expect(scene.currentBattle?.mysteryEncounter?.encounterType).not.toBe(MysteryEncounterType.MYSTERIOUS_CHALLENGERS);
});
it("should not run above wave 179", async () => {
game.override.startingWave(181);
await game.runToMysteryEncounter();
expect(scene.currentBattle.mysteryEncounter).toBeUndefined();
});
it("should initialize fully", async () => { it("should initialize fully", async () => {
initSceneWithoutEncounterPhase(scene, defaultParty); initSceneWithoutEncounterPhase(scene, defaultParty);
scene.currentBattle.mysteryEncounter = new MysteryEncounter(MysteriousChallengersEncounter); scene.currentBattle.mysteryEncounter = new MysteryEncounter(MysteriousChallengersEncounter);

View File

@ -80,22 +80,6 @@ describe("Part-Timer - Mystery Encounter", () => {
expect(scene.currentBattle?.mysteryEncounter?.encounterType).not.toBe(MysteryEncounterType.PART_TIMER); expect(scene.currentBattle?.mysteryEncounter?.encounterType).not.toBe(MysteryEncounterType.PART_TIMER);
}); });
it("should not run below wave 10", async () => {
game.override.startingWave(9);
await game.runToMysteryEncounter();
expect(scene.currentBattle?.mysteryEncounter?.encounterType).not.toBe(MysteryEncounterType.PART_TIMER);
});
it("should not run above wave 179", async () => {
game.override.startingWave(181);
await game.runToMysteryEncounter();
expect(scene.currentBattle.mysteryEncounter).toBeUndefined();
});
describe("Option 1 - Make Deliveries", () => { describe("Option 1 - Make Deliveries", () => {
it("should have the correct properties", () => { it("should have the correct properties", () => {
const option = PartTimerEncounter.options[0]; const option = PartTimerEncounter.options[0];

View File

@ -67,22 +67,6 @@ describe("Teleporting Hijinks - Mystery Encounter", () => {
expect(TeleportingHijinksEncounter.options.length).toBe(3); expect(TeleportingHijinksEncounter.options.length).toBe(3);
}); });
it("should not run below wave 10", async () => {
game.override.startingWave(9);
await game.runToMysteryEncounter();
expect(scene.currentBattle?.mysteryEncounter?.encounterType).not.toBe(MysteryEncounterType.TELEPORTING_HIJINKS);
});
it("should not run above wave 179", async () => {
game.override.startingWave(181);
await game.runToMysteryEncounter();
expect(scene.currentBattle.mysteryEncounter).toBeUndefined();
});
it("should run in waves that are X1", async () => { it("should run in waves that are X1", async () => {
game.override.startingWave(11); game.override.startingWave(11);
game.override.mysteryEncounterTier(MysteryEncounterTier.COMMON); game.override.mysteryEncounterTier(MysteryEncounterTier.COMMON);

View File

@ -0,0 +1,283 @@
import * as MysteryEncounters from "#app/data/mystery-encounters/mystery-encounters";
import { HUMAN_TRANSITABLE_BIOMES } from "#app/data/mystery-encounters/mystery-encounters";
import { Biome } from "#app/enums/biome";
import { MysteryEncounterType } from "#app/enums/mystery-encounter-type";
import { Species } from "#app/enums/species";
import GameManager from "#app/test/utils/gameManager";
import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest";
import { runMysteryEncounterToEnd, skipBattleRunMysteryEncounterRewardsPhase } from "#test/mystery-encounter/encounter-test-utils";
import BattleScene from "#app/battle-scene";
import { MysteryEncounterOptionMode } from "#enums/mystery-encounter-option-mode";
import { MysteryEncounterTier } from "#enums/mystery-encounter-tier";
import { initSceneWithoutEncounterPhase } from "#test/utils/gameManagerUtils";
import { MysteryEncounterMode } from "#enums/mystery-encounter-mode";
import MysteryEncounter from "#app/data/mystery-encounters/mystery-encounter";
import { CommandPhase } from "#app/phases/command-phase";
import { SelectModifierPhase } from "#app/phases/select-modifier-phase";
import { TheExpertPokemonBreederEncounter } from "#app/data/mystery-encounters/encounters/the-expert-pokemon-breeder-encounter";
import { TrainerType } from "#enums/trainer-type";
import { EggTier } from "#enums/egg-type";
import { PostMysteryEncounterPhase } from "#app/phases/mystery-encounter-phases";
const namespace = "mysteryEncounter:expertPokemonBreeder";
const defaultParty = [Species.LAPRAS, Species.GENGAR, Species.ABRA];
const defaultBiome = Biome.CAVE;
const defaultWave = 45;
describe("The Expert Pokémon Breeder - Mystery Encounter", () => {
let phaserGame: Phaser.Game;
let game: GameManager;
let scene: BattleScene;
beforeAll(() => {
phaserGame = new Phaser.Game({ type: Phaser.HEADLESS });
});
beforeEach(async () => {
game = new GameManager(phaserGame);
scene = game.scene;
game.override.mysteryEncounterChance(100);
game.override.startingWave(defaultWave);
game.override.startingBiome(defaultBiome);
game.override.disableTrainerWaves();
const biomeMap = new Map<Biome, MysteryEncounterType[]>([
[Biome.VOLCANO, [MysteryEncounterType.FIGHT_OR_FLIGHT]],
]);
HUMAN_TRANSITABLE_BIOMES.forEach(biome => {
biomeMap.set(biome, [MysteryEncounterType.THE_EXPERT_POKEMON_BREEDER]);
});
vi.spyOn(MysteryEncounters, "mysteryEncountersByBiome", "get").mockReturnValue(biomeMap);
});
afterEach(() => {
game.phaseInterceptor.restoreOg();
vi.clearAllMocks();
vi.resetAllMocks();
});
it("should have the correct properties", async () => {
await game.runToMysteryEncounter(MysteryEncounterType.THE_EXPERT_POKEMON_BREEDER, defaultParty);
expect(TheExpertPokemonBreederEncounter.encounterType).toBe(MysteryEncounterType.THE_EXPERT_POKEMON_BREEDER);
expect(TheExpertPokemonBreederEncounter.encounterTier).toBe(MysteryEncounterTier.ULTRA);
expect(TheExpertPokemonBreederEncounter.dialogue).toBeDefined();
expect(TheExpertPokemonBreederEncounter.dialogue.intro).toStrictEqual([
{
text: `${namespace}.intro`
},
{
speaker: "trainerNames:expert_pokemon_breeder",
text: `${namespace}.intro_dialogue`
},
]);
expect(TheExpertPokemonBreederEncounter.dialogue.encounterOptionsDialogue?.title).toBe(`${namespace}.title`);
expect(TheExpertPokemonBreederEncounter.dialogue.encounterOptionsDialogue?.description).toBe(`${namespace}.description`);
expect(TheExpertPokemonBreederEncounter.dialogue.encounterOptionsDialogue?.query).toBe(`${namespace}.query`);
expect(TheExpertPokemonBreederEncounter.options.length).toBe(3);
});
it("should not spawn outside of HUMAN_TRANSITABLE_BIOMES", async () => {
game.override.mysteryEncounterTier(MysteryEncounterTier.GREAT);
game.override.startingBiome(Biome.VOLCANO);
await game.runToMysteryEncounter();
expect(scene.currentBattle?.mysteryEncounter?.encounterType).not.toBe(MysteryEncounterType.THE_EXPERT_POKEMON_BREEDER);
});
it("should initialize fully", async () => {
initSceneWithoutEncounterPhase(scene, defaultParty);
scene.currentBattle.mysteryEncounter = new MysteryEncounter(TheExpertPokemonBreederEncounter);
const encounter = scene.currentBattle.mysteryEncounter!;
scene.currentBattle.waveIndex = defaultWave;
const { onInit } = encounter;
expect(encounter.onInit).toBeDefined();
encounter.populateDialogueTokensFromRequirements(scene);
const onInitResult = onInit!(scene);
expect(encounter.enemyPartyConfigs).toBeDefined();
expect(encounter.enemyPartyConfigs.length).toBe(1);
expect(encounter.enemyPartyConfigs[0].trainerType).toBe(TrainerType.EXPERT_POKEMON_BREEDER);
expect(encounter.enemyPartyConfigs[0].pokemonConfigs?.length).toBe(3);
expect(encounter.spriteConfigs).toBeDefined();
expect(encounter.spriteConfigs.length).toBe(2);
expect(onInitResult).toBe(true);
});
describe("Option 1 - Battle with Pokemon 1", () => {
it("should have the correct properties", () => {
const option = TheExpertPokemonBreederEncounter.options[0];
expect(option.optionMode).toBe(MysteryEncounterOptionMode.DEFAULT);
expect(option.dialogue).toBeDefined();
expect(option.dialogue).toStrictEqual({
buttonLabel: `${namespace}.option.1.label`,
buttonTooltip: expect.any(String), // Varies based on pokemon
selected: [
{
speaker: "trainerNames:expert_pokemon_breeder",
text: `${namespace}.option.selected`,
},
],
});
});
it("should start battle against the trainer", async () => {
await game.runToMysteryEncounter(MysteryEncounterType.THE_EXPERT_POKEMON_BREEDER, defaultParty);
await runMysteryEncounterToEnd(game, 1, undefined, true);
expect(scene.getCurrentPhase()?.constructor.name).toBe(CommandPhase.name);
expect(scene.currentBattle.trainer).toBeDefined();
expect(scene.currentBattle.mysteryEncounter?.encounterMode).toBe(MysteryEncounterMode.TRAINER_BATTLE);
expect(scene.getParty().length).toBe(1);
});
it("Should reward the player with friendship and eggs based on pokemon selected", async () => {
await game.runToMysteryEncounter(MysteryEncounterType.THE_EXPERT_POKEMON_BREEDER, defaultParty);
const friendshipBefore = scene.currentBattle.mysteryEncounter!.misc.pokemon1.friendship;
scene.gameData.eggs = [];
const eggsBefore = scene.gameData.eggs;
expect(eggsBefore).toBeDefined();
const eggsBeforeLength = eggsBefore.length;
await runMysteryEncounterToEnd(game, 1, undefined, true);
await skipBattleRunMysteryEncounterRewardsPhase(game);
await game.phaseInterceptor.to(SelectModifierPhase, false);
expect(scene.getCurrentPhase()?.constructor.name).toBe(SelectModifierPhase.name);
const eggsAfter = scene.gameData.eggs;
const commonEggs = scene.currentBattle.mysteryEncounter!.misc.pokemon1CommonEggs;
const rareEggs = scene.currentBattle.mysteryEncounter!.misc.pokemon1RareEggs;
expect(eggsAfter).toBeDefined();
expect(eggsBeforeLength + commonEggs + rareEggs).toBe(eggsAfter.length);
expect(eggsAfter.filter(egg => egg.tier === EggTier.COMMON).length).toBe(commonEggs);
expect(eggsAfter.filter(egg => egg.tier === EggTier.GREAT).length).toBe(rareEggs);
game.phaseInterceptor.superEndPhase();
await game.phaseInterceptor.to(PostMysteryEncounterPhase);
const friendshipAfter = scene.currentBattle.mysteryEncounter!.misc.pokemon1.friendship;
expect(friendshipAfter).toBe(friendshipBefore + 20 + 2); // +2 extra for friendship gained from winning battle
});
});
describe("Option 2 - Battle with Pokemon 2", () => {
it("should have the correct properties", () => {
const option = TheExpertPokemonBreederEncounter.options[1];
expect(option.optionMode).toBe(MysteryEncounterOptionMode.DEFAULT);
expect(option.dialogue).toBeDefined();
expect(option.dialogue).toStrictEqual({
buttonLabel: `${namespace}.option.2.label`,
buttonTooltip: expect.any(String), // Varies based on pokemon
selected: [
{
speaker: "trainerNames:expert_pokemon_breeder",
text: `${namespace}.option.selected`,
},
],
});
});
it("should start battle against the trainer", async () => {
await game.runToMysteryEncounter(MysteryEncounterType.THE_EXPERT_POKEMON_BREEDER, defaultParty);
await runMysteryEncounterToEnd(game, 2, undefined, true);
expect(scene.getCurrentPhase()?.constructor.name).toBe(CommandPhase.name);
expect(scene.currentBattle.trainer).toBeDefined();
expect(scene.currentBattle.mysteryEncounter?.encounterMode).toBe(MysteryEncounterMode.TRAINER_BATTLE);
expect(scene.getParty().length).toBe(1);
});
it("Should reward the player with friendship and eggs based on pokemon selected", async () => {
await game.runToMysteryEncounter(MysteryEncounterType.THE_EXPERT_POKEMON_BREEDER, defaultParty);
const friendshipBefore = scene.currentBattle.mysteryEncounter!.misc.pokemon2.friendship;
scene.gameData.eggs = [];
const eggsBefore = scene.gameData.eggs;
expect(eggsBefore).toBeDefined();
const eggsBeforeLength = eggsBefore.length;
await runMysteryEncounterToEnd(game, 2, undefined, true);
await skipBattleRunMysteryEncounterRewardsPhase(game);
await game.phaseInterceptor.to(SelectModifierPhase, false);
expect(scene.getCurrentPhase()?.constructor.name).toBe(SelectModifierPhase.name);
const eggsAfter = scene.gameData.eggs;
const commonEggs = scene.currentBattle.mysteryEncounter!.misc.pokemon2CommonEggs;
const rareEggs = scene.currentBattle.mysteryEncounter!.misc.pokemon2RareEggs;
expect(eggsAfter).toBeDefined();
expect(eggsBeforeLength + commonEggs + rareEggs).toBe(eggsAfter.length);
expect(eggsAfter.filter(egg => egg.tier === EggTier.COMMON).length).toBe(commonEggs);
expect(eggsAfter.filter(egg => egg.tier === EggTier.GREAT).length).toBe(rareEggs);
game.phaseInterceptor.superEndPhase();
await game.phaseInterceptor.to(PostMysteryEncounterPhase);
const friendshipAfter = scene.currentBattle.mysteryEncounter!.misc.pokemon2.friendship;
expect(friendshipAfter).toBe(friendshipBefore + 20 + 2); // +2 extra for friendship gained from winning battle
});
});
describe("Option 3 - Battle with Pokemon 3", () => {
it("should have the correct properties", () => {
const option = TheExpertPokemonBreederEncounter.options[2];
expect(option.optionMode).toBe(MysteryEncounterOptionMode.DEFAULT);
expect(option.dialogue).toBeDefined();
expect(option.dialogue).toStrictEqual({
buttonLabel: `${namespace}.option.3.label`,
buttonTooltip: expect.any(String), // Varies based on pokemon
selected: [
{
speaker: "trainerNames:expert_pokemon_breeder",
text: `${namespace}.option.selected`,
},
],
});
});
it("should start battle against the trainer", async () => {
await game.runToMysteryEncounter(MysteryEncounterType.THE_EXPERT_POKEMON_BREEDER, defaultParty);
await runMysteryEncounterToEnd(game, 3, undefined, true);
expect(scene.getCurrentPhase()?.constructor.name).toBe(CommandPhase.name);
expect(scene.currentBattle.trainer).toBeDefined();
expect(scene.currentBattle.mysteryEncounter?.encounterMode).toBe(MysteryEncounterMode.TRAINER_BATTLE);
expect(scene.getParty().length).toBe(1);
});
it("Should reward the player with friendship and eggs based on pokemon selected", async () => {
await game.runToMysteryEncounter(MysteryEncounterType.THE_EXPERT_POKEMON_BREEDER, defaultParty);
const friendshipBefore = scene.currentBattle.mysteryEncounter!.misc.pokemon3.friendship;
scene.gameData.eggs = [];
const eggsBefore = scene.gameData.eggs;
expect(eggsBefore).toBeDefined();
const eggsBeforeLength = eggsBefore.length;
await runMysteryEncounterToEnd(game, 3, undefined, true);
await skipBattleRunMysteryEncounterRewardsPhase(game);
await game.phaseInterceptor.to(SelectModifierPhase, false);
expect(scene.getCurrentPhase()?.constructor.name).toBe(SelectModifierPhase.name);
const eggsAfter = scene.gameData.eggs;
const commonEggs = scene.currentBattle.mysteryEncounter!.misc.pokemon3CommonEggs;
const rareEggs = scene.currentBattle.mysteryEncounter!.misc.pokemon3RareEggs;
expect(eggsAfter).toBeDefined();
expect(eggsBeforeLength + commonEggs + rareEggs).toBe(eggsAfter.length);
expect(eggsAfter.filter(egg => egg.tier === EggTier.COMMON).length).toBe(commonEggs);
expect(eggsAfter.filter(egg => egg.tier === EggTier.GREAT).length).toBe(rareEggs);
game.phaseInterceptor.superEndPhase();
await game.phaseInterceptor.to(PostMysteryEncounterPhase);
const friendshipAfter = scene.currentBattle.mysteryEncounter!.misc.pokemon3.friendship;
expect(friendshipAfter).toBe(friendshipBefore + 20 + 2); // +2 extra for friendship gained from winning battle
});
});
});

View File

@ -76,22 +76,6 @@ describe("The Pokemon Salesman - Mystery Encounter", () => {
expect(scene.currentBattle?.mysteryEncounter?.encounterType).not.toBe(MysteryEncounterType.THE_POKEMON_SALESMAN); expect(scene.currentBattle?.mysteryEncounter?.encounterType).not.toBe(MysteryEncounterType.THE_POKEMON_SALESMAN);
}); });
it("should not run below wave 10", async () => {
game.override.startingWave(9);
await game.runToMysteryEncounter();
expect(scene.currentBattle?.mysteryEncounter?.encounterType).not.toBe(MysteryEncounterType.THE_POKEMON_SALESMAN);
});
it("should not run above wave 179", async () => {
game.override.startingWave(181);
await game.runToMysteryEncounter();
expect(scene.currentBattle.mysteryEncounter).toBeUndefined();
});
it("should initialize fully ", async () => { it("should initialize fully ", async () => {
initSceneWithoutEncounterPhase(scene, defaultParty); initSceneWithoutEncounterPhase(scene, defaultParty);
scene.currentBattle.mysteryEncounter = ThePokemonSalesmanEncounter; scene.currentBattle.mysteryEncounter = ThePokemonSalesmanEncounter;

View File

@ -83,22 +83,6 @@ describe("The Strong Stuff - Mystery Encounter", () => {
expect(scene.currentBattle?.mysteryEncounter?.encounterType).not.toBe(MysteryEncounterType.THE_STRONG_STUFF); expect(scene.currentBattle?.mysteryEncounter?.encounterType).not.toBe(MysteryEncounterType.THE_STRONG_STUFF);
}); });
it("should not run below wave 10", async () => {
game.override.startingWave(9);
await game.runToMysteryEncounter();
expect(scene.currentBattle?.mysteryEncounter?.encounterType).not.toBe(MysteryEncounterType.THE_STRONG_STUFF);
});
it("should not run above wave 179", async () => {
game.override.startingWave(181);
await game.runToMysteryEncounter();
expect(scene.currentBattle.mysteryEncounter).toBeUndefined();
});
it("should initialize fully ", async () => { it("should initialize fully ", async () => {
initSceneWithoutEncounterPhase(scene, defaultParty); initSceneWithoutEncounterPhase(scene, defaultParty);
scene.currentBattle.mysteryEncounter = TheStrongStuffEncounter; scene.currentBattle.mysteryEncounter = TheStrongStuffEncounter;

View File

@ -90,22 +90,6 @@ describe("The Winstrate Challenge - Mystery Encounter", () => {
expect(scene.currentBattle?.mysteryEncounter?.encounterType).not.toBe(MysteryEncounterType.THE_WINSTRATE_CHALLENGE); expect(scene.currentBattle?.mysteryEncounter?.encounterType).not.toBe(MysteryEncounterType.THE_WINSTRATE_CHALLENGE);
}); });
it("should not run below wave 10", async () => {
game.override.startingWave(9);
await game.runToMysteryEncounter();
expect(scene.currentBattle?.mysteryEncounter?.encounterType).not.toBe(MysteryEncounterType.THE_WINSTRATE_CHALLENGE);
});
it("should not run above wave 179", async () => {
game.override.startingWave(181);
await game.runToMysteryEncounter();
expect(scene.currentBattle.mysteryEncounter).toBeUndefined();
});
it("should initialize fully", async () => { it("should initialize fully", async () => {
initSceneWithoutEncounterPhase(scene, defaultParty); initSceneWithoutEncounterPhase(scene, defaultParty);
scene.currentBattle.mysteryEncounter = new MysteryEncounter(TheWinstrateChallengeEncounter); scene.currentBattle.mysteryEncounter = new MysteryEncounter(TheWinstrateChallengeEncounter);

View File

@ -71,22 +71,6 @@ describe("Trash to Treasure - Mystery Encounter", () => {
expect(TrashToTreasureEncounter.options.length).toBe(2); expect(TrashToTreasureEncounter.options.length).toBe(2);
}); });
it("should not run below wave 10", async () => {
game.override.startingWave(9);
await game.runToMysteryEncounter();
expect(scene.currentBattle?.mysteryEncounter?.encounterType).not.toBe(MysteryEncounterType.TRASH_TO_TREASURE);
});
it("should not run above wave 179", async () => {
game.override.startingWave(181);
await game.runToMysteryEncounter();
expect(scene.currentBattle.mysteryEncounter).toBeUndefined();
});
it("should initialize fully", async () => { it("should initialize fully", async () => {
initSceneWithoutEncounterPhase(scene, defaultParty); initSceneWithoutEncounterPhase(scene, defaultParty);
scene.currentBattle.mysteryEncounter = TrashToTreasureEncounter; scene.currentBattle.mysteryEncounter = TrashToTreasureEncounter;

View File

@ -74,22 +74,6 @@ describe("Uncommon Breed - Mystery Encounter", () => {
expect(UncommonBreedEncounter.options.length).toBe(3); expect(UncommonBreedEncounter.options.length).toBe(3);
}); });
it("should not run below wave 10", async () => {
game.override.startingWave(9);
await game.runToMysteryEncounter();
expect(scene.currentBattle?.mysteryEncounter?.encounterType).not.toBe(MysteryEncounterType.UNCOMMON_BREED);
});
it("should not run above wave 179", async () => {
game.override.startingWave(181);
await game.runToMysteryEncounter();
expect(scene.currentBattle.mysteryEncounter).toBeUndefined();
});
it("should initialize fully", async () => { it("should initialize fully", async () => {
initSceneWithoutEncounterPhase(scene, defaultParty); initSceneWithoutEncounterPhase(scene, defaultParty);
scene.currentBattle.mysteryEncounter = UncommonBreedEncounter; scene.currentBattle.mysteryEncounter = UncommonBreedEncounter;

View File

@ -73,22 +73,6 @@ describe("Weird Dream - Mystery Encounter", () => {
expect(WeirdDreamEncounter.options.length).toBe(2); expect(WeirdDreamEncounter.options.length).toBe(2);
}); });
it("should not run below wave 10", async () => {
game.override.startingWave(9);
await game.runToMysteryEncounter();
expect(scene.currentBattle?.mysteryEncounter?.encounterType).not.toBe(MysteryEncounterType.WEIRD_DREAM);
});
it("should not run above wave 179", async () => {
game.override.startingWave(181);
await game.runToMysteryEncounter();
expect(scene.currentBattle.mysteryEncounter).toBeUndefined();
});
it("should initialize fully", async () => { it("should initialize fully", async () => {
initSceneWithoutEncounterPhase(scene, defaultParty); initSceneWithoutEncounterPhase(scene, defaultParty);
scene.currentBattle.mysteryEncounter = WeirdDreamEncounter; scene.currentBattle.mysteryEncounter = WeirdDreamEncounter;

View File

@ -4,10 +4,12 @@ import Phaser from "phaser";
import { Species } from "#enums/species"; import { Species } from "#enums/species";
import { MysteryEncounterPhase } from "#app/phases/mystery-encounter-phases"; import { MysteryEncounterPhase } from "#app/phases/mystery-encounter-phases";
import { MysteryEncounterType } from "#enums/mystery-encounter-type"; import { MysteryEncounterType } from "#enums/mystery-encounter-type";
import BattleScene from "#app/battle-scene";
describe("Mystery Encounters", () => { describe("Mystery Encounters", () => {
let phaserGame: Phaser.Game; let phaserGame: Phaser.Game;
let game: GameManager; let game: GameManager;
let scene: BattleScene;
beforeAll(() => { beforeAll(() => {
phaserGame = new Phaser.Game({ phaserGame = new Phaser.Game({
@ -21,6 +23,7 @@ describe("Mystery Encounters", () => {
beforeEach(() => { beforeEach(() => {
game = new GameManager(phaserGame); game = new GameManager(phaserGame);
scene = game.scene;
game.override.startingWave(11); game.override.startingWave(11);
game.override.mysteryEncounterChance(100); game.override.mysteryEncounterChance(100);
}); });
@ -32,23 +35,20 @@ describe("Mystery Encounters", () => {
expect(game.scene.getCurrentPhase()!.constructor.name).toBe(MysteryEncounterPhase.name); expect(game.scene.getCurrentPhase()!.constructor.name).toBe(MysteryEncounterPhase.name);
}); });
it("", async () => { it("Encounters should not run below wave 10", async () => {
await game.runToMysteryEncounter(MysteryEncounterType.MYSTERIOUS_CHALLENGERS, [Species.CHARIZARD, Species.VOLCARONA]); game.override.startingWave(9);
await game.phaseInterceptor.to(MysteryEncounterPhase, false); await game.runToMysteryEncounter();
expect(game.scene.getCurrentPhase()!.constructor.name).toBe(MysteryEncounterPhase.name);
expect(scene.currentBattle?.mysteryEncounter?.encounterType).not.toBe(MysteryEncounterType.MYSTERIOUS_CHALLENGERS);
}); });
it("spawns mysterious challengers encounter", async () => { it("Encounters should not run above wave 180", async () => {
}); game.override.startingWave(181);
it("spawns mysterious chest encounter", async () => { await game.runToMysteryEncounter();
});
it("spawns dark deal encounter", async () => { expect(scene.currentBattle.mysteryEncounter).toBeUndefined();
});
it("spawns fight or flight encounter", async () => {
}); });
}); });

View File

@ -281,7 +281,7 @@ class RunEntryContainer extends Phaser.GameObjects.Container {
const genderIndex = this.scene.gameData.gender ?? PlayerGender.UNSET; const genderIndex = this.scene.gameData.gender ?? PlayerGender.UNSET;
const genderStr = PlayerGender[genderIndex].toLowerCase(); const genderStr = PlayerGender[genderIndex].toLowerCase();
// Defeats from wild Pokemon battles will show the Pokemon responsible by the text of the run result. // Defeats from wild Pokemon battles will show the Pokemon responsible by the text of the run result.
if (data.battleType === BattleType.WILD) { if (data.battleType === BattleType.WILD || (data.battleType === BattleType.MYSTERY_ENCOUNTER && !data.trainer)) {
const enemyContainer = this.scene.add.container(8, 5); const enemyContainer = this.scene.add.container(8, 5);
const gameOutcomeLabel = addTextObject(this.scene, 0, 0, `${i18next.t("runHistory:defeatedWild", { context: genderStr })}`, TextStyle.WINDOW); const gameOutcomeLabel = addTextObject(this.scene, 0, 0, `${i18next.t("runHistory:defeatedWild", { context: genderStr })}`, TextStyle.WINDOW);
enemyContainer.add(gameOutcomeLabel); enemyContainer.add(gameOutcomeLabel);
@ -302,7 +302,7 @@ class RunEntryContainer extends Phaser.GameObjects.Container {
enemy.destroy(); enemy.destroy();
}); });
this.add(enemyContainer); this.add(enemyContainer);
} else if (data.battleType === BattleType.TRAINER) { // Defeats from Trainers show the trainer's title and name } else if (data.battleType === BattleType.TRAINER || (data.battleType === BattleType.MYSTERY_ENCOUNTER && data.trainer)) { // Defeats from Trainers show the trainer's title and name
const tObj = data.trainer.toTrainer(this.scene); const tObj = data.trainer.toTrainer(this.scene);
// Because of the interesting mechanics behind rival names, the rival name and title have to be retrieved differently // Because of the interesting mechanics behind rival names, the rival name and title have to be retrieved differently
const RIVAL_TRAINER_ID_THRESHOLD = 375; const RIVAL_TRAINER_ID_THRESHOLD = 375;

View File

@ -211,7 +211,7 @@ export default class RunInfoUiHandler extends UiHandler {
if (!this.isVictory) { if (!this.isVictory) {
const enemyContainer = this.scene.add.container(0, 0); const enemyContainer = this.scene.add.container(0, 0);
// Wild - Single and Doubles // Wild - Single and Doubles
if (this.runInfo.battleType === BattleType.WILD) { if (this.runInfo.battleType === BattleType.WILD || (this.runInfo.battleType === BattleType.MYSTERY_ENCOUNTER && !this.runInfo.trainer)) {
switch (this.runInfo.enemyParty.length) { switch (this.runInfo.enemyParty.length) {
case 1: case 1:
// Wild - Singles // Wild - Singles
@ -222,7 +222,7 @@ export default class RunInfoUiHandler extends UiHandler {
this.parseWildDoubleDefeat(enemyContainer); this.parseWildDoubleDefeat(enemyContainer);
break; break;
} }
} else if (this.runInfo.battleType === BattleType.TRAINER) { } else if (this.runInfo.battleType === BattleType.TRAINER || (this.runInfo.battleType === BattleType.MYSTERY_ENCOUNTER && this.runInfo.trainer)) {
this.parseTrainerDefeat(enemyContainer); this.parseTrainerDefeat(enemyContainer);
} }
this.runResultContainer.add(enemyContainer); this.runResultContainer.add(enemyContainer);
@ -381,10 +381,6 @@ export default class RunInfoUiHandler extends UiHandler {
break; break;
case GameModes.SPLICED_ENDLESS: case GameModes.SPLICED_ENDLESS:
modeText.appendText(`${i18next.t("gameMode:endlessSpliced")}`, false); modeText.appendText(`${i18next.t("gameMode:endlessSpliced")}`, false);
if (this.runInfo.waveIndex === this.scene.gameData.gameStats.highestEndlessWave) {
modeText.appendText(` [${i18next.t("runHistory:personalBest")}]`, false);
modeText.setTint(0xffef5c, 0x47ff69, 0x6b6bff, 0xff6969);
}
break; break;
case GameModes.CHALLENGE: case GameModes.CHALLENGE:
modeText.appendText(`${i18next.t("gameMode:challenge")}`, false); modeText.appendText(`${i18next.t("gameMode:challenge")}`, false);
@ -403,17 +399,18 @@ export default class RunInfoUiHandler extends UiHandler {
break; break;
case GameModes.ENDLESS: case GameModes.ENDLESS:
modeText.appendText(`${i18next.t("gameMode:endless")}`, false); modeText.appendText(`${i18next.t("gameMode:endless")}`, false);
// If the player achieves a personal best in Endless, the mode text will be tinted similarly to SSS luck to celebrate their achievement.
if (this.runInfo.waveIndex === this.scene.gameData.gameStats.highestEndlessWave) {
modeText.appendText(` [${i18next.t("runHistory:personalBest")}]`, false);
modeText.setTint(0xffef5c, 0x47ff69, 0x6b6bff, 0xff6969);
}
break; break;
case GameModes.CLASSIC: case GameModes.CLASSIC:
modeText.appendText(`${i18next.t("gameMode:classic")}`, false); modeText.appendText(`${i18next.t("gameMode:classic")}`, false);
break; break;
} }
// If the player achieves a personal best in Endless, the mode text will be tinted similarly to SSS luck to celebrate their achievement.
if ((this.runInfo.gameMode === GameModes.ENDLESS || this.runInfo.gameMode === GameModes.SPLICED_ENDLESS) && this.runInfo.waveIndex === this.scene.gameData.gameStats.highestEndlessWave) {
modeText.appendText(` [${i18next.t("runHistory:personalBest")}]`);
modeText.setTint(0xffef5c, 0x47ff69, 0x6b6bff, 0xff6969);
}
// Duration + Money // Duration + Money
const runInfoTextContainer = this.scene.add.container(0, 0); const runInfoTextContainer = this.scene.add.container(0, 0);
// Japanese is set to a greater line spacing of 35px in addBBCodeTextObject() if lineSpacing < 12. // Japanese is set to a greater line spacing of 35px in addBBCodeTextObject() if lineSpacing < 12.