Merge branch 'beta' into hebrew-pr

This commit is contained in:
Lugiad 2025-04-03 16:29:47 +02:00 committed by GitHub
commit d84b8c5249
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
60 changed files with 1052 additions and 1517 deletions

1595
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -1,7 +1,7 @@
{
"name": "pokemon-rogue-battle",
"private": true,
"version": "1.7.7",
"version": "1.8.4",
"type": "module",
"scripts": {
"start": "vite",
@ -62,7 +62,7 @@
"i18next-korean-postposition-processor": "^1.0.0",
"json-stable-stringify": "^1.2.0",
"jszip": "^3.10.1",
"phaser": "^3.88.2",
"phaser": "^3.70.0",
"phaser3-rex-plugins": "^1.80.14"
},
"engines": {

Binary file not shown.

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 26 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 26 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 39 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 32 KiB

@ -1 +1 @@
Subproject commit e599780a369f87a96ab0469a8908cea86628145f
Subproject commit e98f0eb9c2022bc78b53f0444424c636498e725a

View File

@ -170,6 +170,7 @@ import { StatusEffect } from "#enums/status-effect";
import { initGlobalScene } from "#app/global-scene";
import { ShowAbilityPhase } from "#app/phases/show-ability-phase";
import { HideAbilityPhase } from "#app/phases/hide-ability-phase";
import { timedEventManager } from "./global-event-manager";
export const bypassLogin = import.meta.env.VITE_BYPASS_LOGIN === "1";
@ -2268,6 +2269,9 @@ export default class BattleScene extends SceneBase {
if (bgmName === undefined) {
bgmName = this.currentBattle?.getBgmOverride() || this.arena?.bgm;
}
bgmName = timedEventManager.getEventBgmReplacement(bgmName);
if (this.bgm && bgmName === this.bgm.key) {
if (!this.bgm.isPlaying) {
this.bgm.play({
@ -2660,6 +2664,10 @@ export default class BattleScene extends SceneBase {
return 41.42;
case "mystery_encounter_delibirdy": // Firel Delibirdy
return 82.28;
case "title_afd": // Andr06 - PokéRogue Title Remix (AFD)
return 47.66;
case "battle_rival_3_afd": // Andr06 - Final N Battle Remix (AFD)
return 49.147;
}
return 0;
@ -2929,14 +2937,19 @@ export default class BattleScene extends SceneBase {
* @param show Whether to show or hide the bar
*/
public queueAbilityDisplay(pokemon: Pokemon, passive: boolean, show: boolean): void {
this.unshiftPhase(
show
? new ShowAbilityPhase(pokemon.getBattlerIndex(), passive)
: new HideAbilityPhase(pokemon.getBattlerIndex(), passive),
);
this.unshiftPhase(show ? new ShowAbilityPhase(pokemon.getBattlerIndex(), passive) : new HideAbilityPhase());
this.clearPhaseQueueSplice();
}
/**
* Hides the ability bar if it is currently visible
*/
public hideAbilityBar(): void {
if (this.abilityBar.isVisible()) {
this.unshiftPhase(new HideAbilityPhase());
}
}
/**
* Moves everything from nextCommandPhaseQueue to phaseQueue (keeping order)
*/

View File

@ -2,7 +2,7 @@
export const PLAYER_PARTY_MAX_SIZE: number = 6;
/** Whether to use seasonal splash messages in general */
export const USE_SEASONAL_SPLASH_MESSAGES: boolean = false;
export const USE_SEASONAL_SPLASH_MESSAGES: boolean = true;
/** Name of the session ID cookie */
export const SESSION_ID_COOKIE_NAME: string = "pokerogue_sessionId";

View File

@ -854,7 +854,8 @@ export class PostDefendStatStageChangeAbAttr extends PostDefendAbAttr {
}
if (this.allOthers) {
const otherPokemon = pokemon.getAlly() ? pokemon.getOpponents().concat([ pokemon.getAlly() ]) : pokemon.getOpponents();
const ally = pokemon.getAlly();
const otherPokemon = !Utils.isNullOrUndefined(ally) ? pokemon.getOpponents().concat([ ally ]) : pokemon.getOpponents();
for (const other of otherPokemon) {
globalScene.unshiftPhase(new StatStageChangePhase((other).getBattlerIndex(), false, [ this.stat ], this.stages));
}
@ -2460,12 +2461,12 @@ export class PostSummonAllyHealAbAttr extends PostSummonAbAttr {
}
override canApplyPostSummon(pokemon: Pokemon, passive: boolean, simulated: boolean, args: any[]): boolean {
return pokemon.getAlly()?.isActive(true);
return pokemon.getAlly()?.isActive(true) ?? false;
}
override applyPostSummon(pokemon: Pokemon, passive: boolean, simulated: boolean, args: any[]): void {
const target = pokemon.getAlly();
if (!simulated) {
if (!simulated && !Utils.isNullOrUndefined(target)) {
globalScene.unshiftPhase(new PokemonHealPhase(target.getBattlerIndex(),
Utils.toDmgValue(pokemon.getMaxHp() / this.healRatio), i18next.t("abilityTriggers:postSummonAllyHeal", { pokemonNameWithAffix: getPokemonNameWithAffix(target), pokemonName: pokemon.name }), true, !this.showAnim));
}
@ -2486,12 +2487,12 @@ export class PostSummonClearAllyStatStagesAbAttr extends PostSummonAbAttr {
}
override canApplyPostSummon(pokemon: Pokemon, passive: boolean, simulated: boolean, args: any[]): boolean {
return pokemon.getAlly()?.isActive(true);
return pokemon.getAlly()?.isActive(true) ?? false;
}
override applyPostSummon(pokemon: Pokemon, passive: boolean, simulated: boolean, args: any[]): void {
const target = pokemon.getAlly();
if (!simulated) {
if (!simulated && !Utils.isNullOrUndefined(target)) {
for (const s of BATTLE_STATS) {
target.setStatStage(s, 0);
}
@ -2712,7 +2713,7 @@ export class PostSummonCopyAllyStatsAbAttr extends PostSummonAbAttr {
}
const ally = pokemon.getAlly();
if (!ally || ally.getStatStages().every(s => s === 0)) {
if (Utils.isNullOrUndefined(ally) || ally.getStatStages().every(s => s === 0)) {
return false;
}
@ -2721,7 +2722,7 @@ export class PostSummonCopyAllyStatsAbAttr extends PostSummonAbAttr {
override applyPostSummon(pokemon: Pokemon, passive: boolean, simulated: boolean, args: any[]): void {
const ally = pokemon.getAlly();
if (!simulated) {
if (!simulated && !Utils.isNullOrUndefined(ally)) {
for (const s of BATTLE_STATS) {
pokemon.setStatStage(s, ally.getStatStage(s));
}
@ -2822,7 +2823,7 @@ export class PostSummonFormChangeByWeatherAbAttr extends PostSummonAbAttr {
private ability: Abilities;
constructor(ability: Abilities) {
super(false);
super(true);
this.ability = ability;
}
@ -2846,7 +2847,6 @@ export class PostSummonFormChangeByWeatherAbAttr extends PostSummonAbAttr {
if (!simulated) {
globalScene.triggerPokemonFormChange(pokemon, SpeciesFormChangeWeatherTrigger);
globalScene.triggerPokemonFormChange(pokemon, SpeciesFormChangeRevertWeatherFormTrigger);
globalScene.queueAbilityDisplay(pokemon, passive, true);
}
}
}
@ -2867,8 +2867,9 @@ export class CommanderAbAttr extends AbAttr {
// another Pokemon, this effect cannot apply.
// TODO: Should this work with X + Dondozo fusions?
return globalScene.currentBattle?.double && pokemon.getAlly()?.species.speciesId === Species.DONDOZO
&& !(pokemon.getAlly().isFainted() || pokemon.getAlly().getTag(BattlerTagType.COMMANDED));
const ally = pokemon.getAlly();
return globalScene.currentBattle?.double && !Utils.isNullOrUndefined(ally) && ally.species.speciesId === Species.DONDOZO
&& !(ally.isFainted() || ally.getTag(BattlerTagType.COMMANDED));
}
override apply(pokemon: Pokemon, passive: boolean, simulated: boolean, cancelled: null, args: any[]): void {
@ -2878,7 +2879,7 @@ export class CommanderAbAttr extends AbAttr {
// Play an animation of the source jumping into the ally Dondozo's mouth
globalScene.triggerPokemonBattleAnim(pokemon, PokemonAnimType.COMMANDER_APPLY);
// Apply boosts from this effect to the ally Dondozo
pokemon.getAlly().addTag(BattlerTagType.COMMANDED, 0, Moves.NONE, pokemon.id);
pokemon.getAlly()?.addTag(BattlerTagType.COMMANDED, 0, Moves.NONE, pokemon.id);
// Cancel the source Pokemon's next move (if a move is queued)
globalScene.tryRemovePhase((phase) => phase instanceof MovePhase && phase.pokemon === pokemon);
}
@ -4078,7 +4079,7 @@ export class PostTurnStatusHealAbAttr extends PostTurnAbAttr {
*/
export class PostTurnResetStatusAbAttr extends PostTurnAbAttr {
private allyTarget: boolean;
private target: Pokemon;
private target: Pokemon | undefined;
constructor(allyTarget = false) {
super(true);
@ -4091,11 +4092,11 @@ export class PostTurnResetStatusAbAttr extends PostTurnAbAttr {
} else {
this.target = pokemon;
}
return !Utils.isNullOrUndefined(this.target.status);
return !Utils.isNullOrUndefined(this.target?.status);
}
override applyPostTurn(pokemon: Pokemon, passive: boolean, simulated: boolean, args: any[]): void {
if (!simulated && this.target.status) {
if (!simulated && this.target?.status) {
globalScene.queueMessage(getStatusEffectHealText(this.target.status?.effect, getPokemonNameWithAffix(this.target)));
this.target.resetStatus(false);
this.target.updateInfo();
@ -5441,6 +5442,8 @@ class ForceSwitchOutHelper {
* It will not flee if it is a Mystery Encounter with fleeing disabled (checked in `getSwitchOutCondition()`) or if it is a wave 10x wild boss
*/
} else {
const allyPokemon = switchOutTarget.getAlly();
if (!globalScene.currentBattle.waveIndex || globalScene.currentBattle.waveIndex % 10 === 0) {
return false;
}
@ -5448,14 +5451,12 @@ class ForceSwitchOutHelper {
if (switchOutTarget.hp > 0) {
switchOutTarget.leaveField(false);
globalScene.queueMessage(i18next.t("moveTriggers:fled", { pokemonName: getPokemonNameWithAffix(switchOutTarget) }), null, true, 500);
if (globalScene.currentBattle.double) {
const allyPokemon = switchOutTarget.getAlly();
if (globalScene.currentBattle.double && !Utils.isNullOrUndefined(allyPokemon)) {
globalScene.redirectPokemonMoves(switchOutTarget, allyPokemon);
}
}
if (!switchOutTarget.getAlly()?.isActive(true)) {
if (!allyPokemon?.isActive(true)) {
globalScene.clearEnemyHeldItemModifiers();
if (switchOutTarget.hp) {
@ -6441,9 +6442,9 @@ export function initAbilities() {
new Ability(Abilities.CUTE_CHARM, 3)
.attr(PostDefendContactApplyTagChanceAbAttr, 30, BattlerTagType.INFATUATED),
new Ability(Abilities.PLUS, 3)
.conditionalAttr(p => globalScene.currentBattle.double && [ Abilities.PLUS, Abilities.MINUS ].some(a => p.getAlly().hasAbility(a)), StatMultiplierAbAttr, Stat.SPATK, 1.5),
.conditionalAttr(p => globalScene.currentBattle.double && [ Abilities.PLUS, Abilities.MINUS ].some(a => (p.getAlly()?.hasAbility(a) ?? false)), StatMultiplierAbAttr, Stat.SPATK, 1.5),
new Ability(Abilities.MINUS, 3)
.conditionalAttr(p => globalScene.currentBattle.double && [ Abilities.PLUS, Abilities.MINUS ].some(a => p.getAlly().hasAbility(a)), StatMultiplierAbAttr, Stat.SPATK, 1.5),
.conditionalAttr(p => globalScene.currentBattle.double && [ Abilities.PLUS, Abilities.MINUS ].some(a => (p.getAlly()?.hasAbility(a) ?? false)), StatMultiplierAbAttr, Stat.SPATK, 1.5),
new Ability(Abilities.FORECAST, 3)
.uncopiable()
.unreplaceable()
@ -6670,7 +6671,7 @@ export function initAbilities() {
.attr(PostDefendMoveDisableAbAttr, 30)
.bypassFaint(),
new Ability(Abilities.HEALER, 5)
.conditionalAttr(pokemon => pokemon.getAlly() && Utils.randSeedInt(10) < 3, PostTurnResetStatusAbAttr, true),
.conditionalAttr(pokemon => !Utils.isNullOrUndefined(pokemon.getAlly()) && Utils.randSeedInt(10) < 3, PostTurnResetStatusAbAttr, true),
new Ability(Abilities.FRIEND_GUARD, 5)
.attr(AlliedFieldDamageReductionAbAttr, 0.75)
.ignorable(),

View File

@ -28,7 +28,6 @@ import { BattlerTagType } from "#enums/battler-tag-type";
import { Moves } from "#enums/moves";
import { MoveEffectPhase } from "#app/phases/move-effect-phase";
import { PokemonHealPhase } from "#app/phases/pokemon-heal-phase";
import { ShowAbilityPhase } from "#app/phases/show-ability-phase";
import { StatStageChangePhase } from "#app/phases/stat-stage-change-phase";
import { CommonAnimPhase } from "#app/phases/common-anim-phase";
@ -1160,9 +1159,11 @@ class TailwindTag extends ArenaTag {
);
}
// Raise attack by one stage if party member has WIND_RIDER ability
// TODO: Ability displays should be handled by the ability
if (pokemon.hasAbility(Abilities.WIND_RIDER)) {
globalScene.unshiftPhase(new ShowAbilityPhase(pokemon.getBattlerIndex()));
globalScene.queueAbilityDisplay(pokemon, false, true);
globalScene.unshiftPhase(new StatStageChangePhase(pokemon.getBattlerIndex(), true, [Stat.ATK], 1, true));
globalScene.queueAbilityDisplay(pokemon, false, false);
}
}
}

View File

@ -15,7 +15,7 @@ export const starterPassiveAbilities: StarterPassiveAbilities = {
[Species.VENUSAUR]: { 0: Abilities.GRASSY_SURGE, 1: Abilities.SEED_SOWER, 2: Abilities.FLOWER_VEIL },
[Species.CHARMANDER]: { 0: Abilities.SHEER_FORCE },
[Species.CHARMELEON]: { 0: Abilities.BEAST_BOOST },
[Species.CHARIZARD]: { 0: Abilities.BEAST_BOOST, 1: Abilities.LEVITATE, 2: Abilities.INTIMIDATE, 3: Abilities.UNNERVE },
[Species.CHARIZARD]: { 0: Abilities.BEAST_BOOST, 1: Abilities.LEVITATE, 2: Abilities.TURBOBLAZE, 3: Abilities.UNNERVE },
[Species.SQUIRTLE]: { 0: Abilities.DAUNTLESS_SHIELD },
[Species.WARTORTLE]: { 0: Abilities.DAUNTLESS_SHIELD },
[Species.BLASTOISE]: { 0: Abilities.DAUNTLESS_SHIELD, 1: Abilities.BULLETPROOF, 2: Abilities.BULLETPROOF },
@ -154,14 +154,14 @@ export const starterPassiveAbilities: StarterPassiveAbilities = {
[Species.LEAFEON]: { 0: Abilities.GRASSY_SURGE },
[Species.GLACEON]: { 0: Abilities.SNOW_WARNING },
[Species.SYLVEON]: { 0: Abilities.COMPETITIVE },
[Species.PORYGON]: { 0: Abilities.LEVITATE },
[Species.PORYGON2]: { 0: Abilities.LEVITATE },
[Species.PORYGON]: { 0: Abilities.TRANSISTOR },
[Species.PORYGON2]: { 0: Abilities.TRANSISTOR },
[Species.PORYGON_Z]: { 0: Abilities.PROTEAN },
[Species.OMANYTE]: { 0: Abilities.STURDY },
[Species.OMASTAR]: { 0: Abilities.STURDY },
[Species.KABUTO]: { 0: Abilities.TOUGH_CLAWS },
[Species.KABUTOPS]: { 0: Abilities.TOUGH_CLAWS },
[Species.AERODACTYL]: { 0: Abilities.INTIMIDATE, 1: Abilities.DELTA_STREAM },
[Species.AERODACTYL]: { 0: Abilities.INTIMIDATE, 1: Abilities.INTIMIDATE },
[Species.ARTICUNO]: { 0: Abilities.SNOW_WARNING },
[Species.ZAPDOS]: { 0: Abilities.DRIZZLE },
[Species.MOLTRES]: { 0: Abilities.DROUGHT },
@ -309,7 +309,7 @@ export const starterPassiveAbilities: StarterPassiveAbilities = {
[Species.SHIFTRY]: { 0: Abilities.SHARPNESS },
[Species.TAILLOW]: { 0: Abilities.AERILATE },
[Species.SWELLOW]: { 0: Abilities.AERILATE },
[Species.WINGULL]: { 0: Abilities.DRIZZLE },
[Species.WINGULL]: { 0: Abilities.WATER_ABSORB },
[Species.PELIPPER]: { 0: Abilities.SWIFT_SWIM },
[Species.RALTS]: { 0: Abilities.NEUROFORCE },
[Species.KIRLIA]: { 0: Abilities.NEUROFORCE },
@ -612,8 +612,8 @@ export const starterPassiveAbilities: StarterPassiveAbilities = {
[Species.REUNICLUS]: { 0: Abilities.PSYCHIC_SURGE },
[Species.DUCKLETT]: { 0: Abilities.DRIZZLE },
[Species.SWANNA]: { 0: Abilities.DRIZZLE },
[Species.VANILLITE]: { 0: Abilities.SNOW_WARNING },
[Species.VANILLISH]: { 0: Abilities.SNOW_WARNING },
[Species.VANILLITE]: { 0: Abilities.REFRIGERATE },
[Species.VANILLISH]: { 0: Abilities.REFRIGERATE },
[Species.VANILLUXE]: { 0: Abilities.SLUSH_RUSH },
[Species.DEERLING]: { 0: Abilities.FLOWER_VEIL, 1: Abilities.CUD_CHEW, 2: Abilities.HARVEST, 3: Abilities.FUR_COAT },
[Species.SAWSBUCK]: { 0: Abilities.FLOWER_VEIL, 1: Abilities.CUD_CHEW, 2: Abilities.HARVEST, 3: Abilities.FUR_COAT },
@ -838,7 +838,7 @@ export const starterPassiveAbilities: StarterPassiveAbilities = {
[Species.CELESTEELA]: { 0: Abilities.HEATPROOF },
[Species.KARTANA]: { 0: Abilities.TECHNICIAN },
[Species.GUZZLORD]: { 0: Abilities.POISON_HEAL },
[Species.NECROZMA]: { 0: Abilities.BEAST_BOOST, 1: Abilities.FULL_METAL_BODY, 2: Abilities.SHADOW_SHIELD, 3: Abilities.PRISM_ARMOR },
[Species.NECROZMA]: { 0: Abilities.BEAST_BOOST, 1: Abilities.FULL_METAL_BODY, 2: Abilities.SHADOW_SHIELD, 3: Abilities.UNNERVE },
[Species.MAGEARNA]: { 0: Abilities.STEELY_SPIRIT, 1: Abilities.STEELY_SPIRIT },
[Species.MARSHADOW]: { 0: Abilities.IRON_FIST },
[Species.POIPOLE]: { 0: Abilities.LEVITATE },

View File

@ -30,7 +30,6 @@ import { CommonAnimPhase } from "#app/phases/common-anim-phase";
import { MoveEffectPhase } from "#app/phases/move-effect-phase";
import { MovePhase } from "#app/phases/move-phase";
import { PokemonHealPhase } from "#app/phases/pokemon-heal-phase";
import { ShowAbilityPhase } from "#app/phases/show-ability-phase";
import type { StatStageChangeCallback } from "#app/phases/stat-stage-change-phase";
import { StatStageChangePhase } from "#app/phases/stat-stage-change-phase";
import i18next from "#app/plugins/i18n";
@ -1901,12 +1900,14 @@ export class TruantTag extends AbilityBattlerTag {
if (lastMove && lastMove.move !== Moves.NONE) {
(globalScene.getCurrentPhase() as MovePhase).cancel();
globalScene.unshiftPhase(new ShowAbilityPhase(pokemon.id, passive));
// TODO: Ability displays should be handled by the ability
globalScene.queueAbilityDisplay(pokemon, passive, true);
globalScene.queueMessage(
i18next.t("battlerTags:truantLapse", {
pokemonNameWithAffix: getPokemonNameWithAffix(pokemon),
}),
);
globalScene.queueAbilityDisplay(pokemon, passive, false);
}
return true;

View File

@ -1272,23 +1272,22 @@ function checkSpeciesValidForChallenge(species: PokemonSpecies, props: DexAttrPr
if (soft && isValidForChallenge.value) {
return true;
}
pokemonFormChanges[species.speciesId].forEach(f1 => {
// Exclude form changes that require the mon to be on the field to begin with,
// such as Castform
if (!("item" in f1)) {
return;
const result = pokemonFormChanges[species.speciesId].some(f1 => {
// Exclude form changes that require the mon to be on the field to begin with
if (!("item" in f1.trigger)) {
return false;
}
species.forms.forEach((f2, formIndex) => {
return species.forms.some((f2, formIndex) => {
if (f1.formKey === f2.formKey) {
const formProps = { ...props };
formProps.formIndex = formIndex;
const formProps = { ...props, formIndex };
const isFormValidForChallenge = new Utils.BooleanHolder(true);
applyChallenges(ChallengeType.STARTER_CHOICE, species, isFormValidForChallenge, formProps);
if (isFormValidForChallenge.value) {
return true;
}
return isFormValidForChallenge.value;
}
return false;
});
});
return false;
return result;
}

View File

@ -106,7 +106,6 @@ import { PokemonHealPhase } from "#app/phases/pokemon-heal-phase";
import { StatStageChangePhase } from "#app/phases/stat-stage-change-phase";
import { SwitchPhase } from "#app/phases/switch-phase";
import { SwitchSummonPhase } from "#app/phases/switch-summon-phase";
import { ShowAbilityPhase } from "#app/phases/show-ability-phase";
import { SpeciesFormChangeRevertWeatherFormTrigger } from "../pokemon-forms";
import type { GameMode } from "#app/game-mode";
import { applyChallenges, ChallengeType } from "../challenge";
@ -789,9 +788,9 @@ export default class Move implements Localizable {
}
applyPreAttackAbAttrs(VariableMovePowerAbAttr, source, target, this, simulated, power);
if (source.getAlly()) {
applyPreAttackAbAttrs(AllyMoveCategoryPowerBoostAbAttr, source.getAlly(), target, this, simulated, power);
const ally = source.getAlly();
if (!Utils.isNullOrUndefined(ally)) {
applyPreAttackAbAttrs(AllyMoveCategoryPowerBoostAbAttr, ally, target, this, simulated, power);
}
const fieldAuras = new Set(
@ -912,7 +911,8 @@ export default class Move implements Localizable {
];
// ...and cannot enhance Pollen Puff when targeting an ally.
const exceptPollenPuffAlly: boolean = this.id === Moves.POLLEN_PUFF && targets.includes(user.getAlly().getBattlerIndex())
const ally = user.getAlly();
const exceptPollenPuffAlly: boolean = this.id === Moves.POLLEN_PUFF && !Utils.isNullOrUndefined(ally) && targets.includes(ally.getBattlerIndex())
return (!restrictSpread || !isMultiTarget)
&& !this.isChargingMove()
@ -1924,7 +1924,9 @@ export class PartyStatusCureAttr extends MoveEffectAttr {
pokemon.resetStatus();
pokemon.updateInfo();
} else {
globalScene.unshiftPhase(new ShowAbilityPhase(pokemon.id, pokemon.getPassiveAbility()?.id === this.abilityCondition));
// TODO: Ability displays should be handled by the ability
globalScene.queueAbilityDisplay(pokemon, pokemon.getPassiveAbility()?.id === this.abilityCondition, true);
globalScene.queueAbilityDisplay(pokemon, pokemon.getPassiveAbility()?.id === this.abilityCondition, false);
}
}
}
@ -1945,7 +1947,7 @@ export class FlameBurstAttr extends MoveEffectAttr {
const targetAlly = target.getAlly();
const cancelled = new Utils.BooleanHolder(false);
if (targetAlly) {
if (!Utils.isNullOrUndefined(targetAlly)) {
applyAbAttrs(BlockNonDirectDamageAbAttr, targetAlly, cancelled);
}
@ -1958,7 +1960,7 @@ export class FlameBurstAttr extends MoveEffectAttr {
}
getTargetBenefitScore(user: Pokemon, target: Pokemon, move: Move): number {
return target.getAlly() ? -5 : 0;
return !Utils.isNullOrUndefined(target.getAlly()) ? -5 : 0;
}
}
@ -4356,10 +4358,10 @@ export class LastMoveDoublePowerAttr extends VariablePowerAttr {
const userAlly = user.getAlly();
const enemyAlly = enemy?.getAlly();
if (userAlly && userAlly.turnData.acted) {
if (!Utils.isNullOrUndefined(userAlly) && userAlly.turnData.acted) {
pokemonActed.push(userAlly);
}
if (enemyAlly && enemyAlly.turnData.acted) {
if (!Utils.isNullOrUndefined(enemyAlly) && enemyAlly.turnData.acted) {
pokemonActed.push(enemyAlly);
}
}
@ -6161,9 +6163,8 @@ export class RevivalBlessingAttr extends MoveEffectAttr {
pokemon.resetStatus();
pokemon.heal(Math.min(Utils.toDmgValue(0.5 * pokemon.getMaxHp()), pokemon.getMaxHp()));
globalScene.queueMessage(i18next.t("moveTriggers:revivalBlessing", { pokemonName: getPokemonNameWithAffix(pokemon) }), 0, true);
if (globalScene.currentBattle.double && globalScene.getEnemyParty().length > 1) {
const allyPokemon = user.getAlly();
const allyPokemon = user.getAlly();
if (globalScene.currentBattle.double && globalScene.getEnemyParty().length > 1 && !Utils.isNullOrUndefined(allyPokemon)) {
// Handle cases where revived pokemon needs to get switched in on same turn
if (allyPokemon.isFainted() || allyPokemon === pokemon) {
// Enemy switch phase should be removed and replaced with the revived pkmn switching in
@ -6342,18 +6343,19 @@ export class ForceSwitchOutAttr extends MoveEffectAttr {
return false;
}
const allyPokemon = switchOutTarget.getAlly();
if (switchOutTarget.hp > 0) {
switchOutTarget.leaveField(false);
globalScene.queueMessage(i18next.t("moveTriggers:fled", { pokemonName: getPokemonNameWithAffix(switchOutTarget) }), null, true, 500);
// in double battles redirect potential moves off fled pokemon
if (globalScene.currentBattle.double) {
const allyPokemon = switchOutTarget.getAlly();
if (globalScene.currentBattle.double && !Utils.isNullOrUndefined(allyPokemon)) {
globalScene.redirectPokemonMoves(switchOutTarget, allyPokemon);
}
}
if (!switchOutTarget.getAlly()?.isActive(true)) {
if (!allyPokemon?.isActive(true)) {
globalScene.clearEnemyHeldItemModifiers();
if (switchOutTarget.hp) {
@ -7029,7 +7031,7 @@ export class RepeatMoveAttr extends MoveEffectAttr {
const firstTarget = globalScene.getField()[moveTargets[0]];
if (globalScene.currentBattle.double && moveTargets.length === 1 && firstTarget.isFainted() && firstTarget !== target.getAlly()) {
const ally = firstTarget.getAlly();
if (ally.isActive()) { // ally exists, is not dead and can sponge the blast
if (!Utils.isNullOrUndefined(ally) && ally.isActive()) { // ally exists, is not dead and can sponge the blast
moveTargets = [ ally.getBattlerIndex() ];
}
}
@ -7403,10 +7405,11 @@ export class AbilityCopyAttr extends MoveEffectAttr {
globalScene.queueMessage(i18next.t("moveTriggers:copiedTargetAbility", { pokemonName: getPokemonNameWithAffix(user), targetName: getPokemonNameWithAffix(target), abilityName: allAbilities[target.getAbility().id].name }));
user.setTempAbility(target.getAbility());
const ally = user.getAlly();
if (this.copyToPartner && globalScene.currentBattle?.double && user.getAlly().hp) {
globalScene.queueMessage(i18next.t("moveTriggers:copiedTargetAbility", { pokemonName: getPokemonNameWithAffix(user.getAlly()), targetName: getPokemonNameWithAffix(target), abilityName: allAbilities[target.getAbility().id].name }));
user.getAlly().setTempAbility(target.getAbility());
if (this.copyToPartner && globalScene.currentBattle?.double && !Utils.isNullOrUndefined(ally) && ally.hp) { // TODO is this the best way to check that the ally is active?
globalScene.queueMessage(i18next.t("moveTriggers:copiedTargetAbility", { pokemonName: getPokemonNameWithAffix(ally), targetName: getPokemonNameWithAffix(target), abilityName: allAbilities[target.getAbility().id].name }));
ally.setTempAbility(target.getAbility());
}
return true;
@ -7414,9 +7417,10 @@ export class AbilityCopyAttr extends MoveEffectAttr {
getCondition(): MoveConditionFunc {
return (user, target, move) => {
const ally = user.getAlly();
let ret = target.getAbility().isCopiable && user.getAbility().isReplaceable;
if (this.copyToPartner && globalScene.currentBattle?.double) {
ret = ret && (!user.getAlly().hp || user.getAlly().getAbility().isReplaceable);
ret = ret && (!ally?.hp || ally?.getAbility().isReplaceable);
} else {
ret = ret && user.getAbility().id !== target.getAbility().id;
}
@ -8187,6 +8191,7 @@ export function getMoveTargets(user: Pokemon, move: Moves, replaceTarget?: MoveT
let set: Pokemon[] = [];
let multiple = false;
const ally: Pokemon | undefined = user.getAlly();
switch (moveTarget) {
case MoveTarget.USER:
@ -8197,7 +8202,7 @@ export function getMoveTargets(user: Pokemon, move: Moves, replaceTarget?: MoveT
case MoveTarget.OTHER:
case MoveTarget.ALL_NEAR_OTHERS:
case MoveTarget.ALL_OTHERS:
set = (opponents.concat([ user.getAlly() ]));
set = !Utils.isNullOrUndefined(ally) ? (opponents.concat([ ally ])) : opponents;
multiple = moveTarget === MoveTarget.ALL_NEAR_OTHERS || moveTarget === MoveTarget.ALL_OTHERS;
break;
case MoveTarget.NEAR_ENEMY:
@ -8214,21 +8219,22 @@ export function getMoveTargets(user: Pokemon, move: Moves, replaceTarget?: MoveT
return { targets: [ -1 as BattlerIndex ], multiple: false };
case MoveTarget.NEAR_ALLY:
case MoveTarget.ALLY:
set = [ user.getAlly() ];
set = !Utils.isNullOrUndefined(ally) ? [ ally ] : [];
break;
case MoveTarget.USER_OR_NEAR_ALLY:
case MoveTarget.USER_AND_ALLIES:
case MoveTarget.USER_SIDE:
set = [ user, user.getAlly() ];
set = !Utils.isNullOrUndefined(ally) ? [ user, ally ] : [ user ];
multiple = moveTarget !== MoveTarget.USER_OR_NEAR_ALLY;
break;
case MoveTarget.ALL:
case MoveTarget.BOTH_SIDES:
set = [ user, user.getAlly() ].concat(opponents);
set = (!Utils.isNullOrUndefined(ally) ? [ user, ally ] : [ user ]).concat(opponents);
multiple = true;
break;
case MoveTarget.CURSE:
set = user.getTypes(true).includes(PokemonType.GHOST) ? (opponents.concat([ user.getAlly() ])) : [ user ];
const extraTargets = !Utils.isNullOrUndefined(ally) ? [ ally ] : [];
set = user.getTypes(true).includes(PokemonType.GHOST) ? (opponents.concat(extraTargets)) : [ user ];
break;
}
@ -10122,7 +10128,7 @@ export function initMoves() {
.attr(StatStageChangeAttr, [ Stat.DEF, Stat.SPDEF ], 1, false, { condition: (user, target, move) => !![ Abilities.PLUS, Abilities.MINUS ].find(a => target.hasAbility(a, false)) })
.ignoresSubstitute()
.target(MoveTarget.USER_AND_ALLIES)
.condition((user, target, move) => !![ user, user.getAlly() ].filter(p => p?.isActive()).find(p => !![ Abilities.PLUS, Abilities.MINUS ].find(a => p.hasAbility(a, false)))),
.condition((user, target, move) => !![ user, user.getAlly() ].filter(p => p?.isActive()).find(p => !![ Abilities.PLUS, Abilities.MINUS ].find(a => p?.hasAbility(a, false)))),
new StatusMove(Moves.HAPPY_HOUR, PokemonType.NORMAL, -1, 30, -1, 0, 6) // No animation
.attr(AddArenaTagAttr, ArenaTagType.HAPPY_HOUR, null, true)
.target(MoveTarget.USER_SIDE),
@ -10312,7 +10318,7 @@ export function initMoves() {
.attr(StatStageChangeAttr, [ Stat.ATK, Stat.SPATK ], 1, false, { condition: (user, target, move) => !![ Abilities.PLUS, Abilities.MINUS ].find(a => target.hasAbility(a, false)) })
.ignoresSubstitute()
.target(MoveTarget.USER_AND_ALLIES)
.condition((user, target, move) => !![ user, user.getAlly() ].filter(p => p?.isActive()).find(p => !![ Abilities.PLUS, Abilities.MINUS ].find(a => p.hasAbility(a, false)))),
.condition((user, target, move) => !![ user, user.getAlly() ].filter(p => p?.isActive()).find(p => !![ Abilities.PLUS, Abilities.MINUS ].find(a => p?.hasAbility(a, false)))),
new AttackMove(Moves.THROAT_CHOP, PokemonType.DARK, MoveCategory.PHYSICAL, 80, 100, 15, 100, 0, 7)
.attr(AddBattlerTagAttr, BattlerTagType.THROAT_CHOPPED),
new AttackMove(Moves.POLLEN_PUFF, PokemonType.BUG, MoveCategory.SPECIAL, 90, 100, 15, -1, 0, 7)

View File

@ -37,6 +37,7 @@ import { MysteryEncounterOptionMode } from "#enums/mystery-encounter-option-mode
import { MysteryEncounterTier } from "#enums/mystery-encounter-tier";
import { MysteryEncounterType } from "#enums/mystery-encounter-type";
import { Species } from "#enums/species";
import { timedEventManager } from "#app/global-event-manager";
/** the i18n namespace for this encounter */
const namespace = "mysteryEncounters/delibirdy";
@ -56,7 +57,7 @@ const OPTION_3_DISALLOWED_MODIFIERS = [
const DELIBIRDY_MONEY_PRICE_MULTIPLIER = 2;
const doEventReward = () => {
const event_buff = globalScene.eventManager.getDelibirdyBuff();
const event_buff = timedEventManager.getDelibirdyBuff();
if (event_buff.length > 0) {
const candidates = event_buff.filter(c => {
const mtype = generateModifierType(modifierTypes[c]);

View File

@ -46,6 +46,7 @@ import { addPokemonDataToDexAndValidateAchievements } from "#app/data/mystery-en
import type { PokeballType } from "#enums/pokeball";
import { doShinySparkleAnim } from "#app/field/anims";
import { TrainerType } from "#enums/trainer-type";
import { timedEventManager } from "#app/global-event-manager";
/** the i18n namespace for the encounter */
const namespace = "mysteryEncounters/globalTradeSystem";
@ -273,8 +274,8 @@ export const GlobalTradeSystemEncounter: MysteryEncounter = MysteryEncounterBuil
// Extra shiny roll at 1/128 odds (boosted by events and charms)
if (!tradePokemon.shiny) {
const shinyThreshold = new NumberHolder(WONDER_TRADE_SHINY_CHANCE);
if (globalScene.eventManager.isEventActive()) {
shinyThreshold.value *= globalScene.eventManager.getShinyMultiplier();
if (timedEventManager.isEventActive()) {
shinyThreshold.value *= timedEventManager.getShinyMultiplier();
}
globalScene.applyModifiers(ShinyRateBoosterModifier, true, shinyThreshold);
@ -984,12 +985,11 @@ function doTradeReceivedSequence(
function generateRandomTraderName() {
const length = TrainerType.YOUNGSTER - TrainerType.ACE_TRAINER + 1;
// +1 avoids TrainerType.UNKNOWN
const trainerTypePool = i18next.t("trainersCommon:" + TrainerType[randInt(length) + 1], { returnObjects: true });
const classKey = `trainersCommon:${TrainerType[randInt(length) + 1]}`;
// Some trainers have 2 gendered pools, some do not
const gender = randInt(2) === 0 ? "MALE" : "FEMALE";
const trainerNameString = randSeedItem(
Object.values(trainerTypePool.hasOwnProperty(gender) ? trainerTypePool[gender] : trainerTypePool),
) as string;
const genderKey = i18next.exists(`${classKey}.MALE`) ? (randInt(2) === 0 ? ".MALE" : ".FEMALE") : "";
const trainerNameKey = randSeedItem(Object.keys(i18next.t(`${classKey}${genderKey}`, { returnObjects: true })));
const trainerNameString = i18next.t(`${classKey}${genderKey}.${trainerNameKey}`);
// Some names have an '&' symbol and need to be trimmed to a single name instead of a double name
const trainerNames = trainerNameString.split(" & ");
return trainerNames[randInt(trainerNames.length)];

View File

@ -93,7 +93,7 @@ export const TheStrongStuffEncounter: MysteryEncounter = MysteryEncounterBuilder
bossSegments: 5,
shiny: false, // Shiny lock because shiny is rolled only if the battle option is picked
customPokemonData: new CustomPokemonData({ spriteScale: 1.25 }),
nature: Nature.BOLD,
nature: Nature.HARDY,
moveSet: [Moves.INFESTATION, Moves.SALT_CURE, Moves.GASTRO_ACID, Moves.HEAL_ORDER],
modifierConfigs: [
{
@ -117,7 +117,7 @@ export const TheStrongStuffEncounter: MysteryEncounter = MysteryEncounterBuilder
mysteryEncounterBattleEffects: (pokemon: Pokemon) => {
queueEncounterMessage(`${namespace}:option.2.stat_boost`);
globalScene.unshiftPhase(
new StatStageChangePhase(pokemon.getBattlerIndex(), true, [Stat.DEF, Stat.SPDEF], 2),
new StatStageChangePhase(pokemon.getBattlerIndex(), true, [Stat.DEF, Stat.SPDEF], 1),
);
},
},

View File

@ -27,6 +27,7 @@ import { Moves } from "#enums/moves";
import { BattlerIndex } from "#app/battle";
import { PokemonMove } from "#app/field/pokemon";
import { CLASSIC_MODE_MYSTERY_ENCOUNTER_WAVES } from "#app/game-mode";
import { randSeedInt } from "#app/utils";
/** the i18n namespace for this encounter */
const namespace = "mysteryEncounters/trashToTreasure";
@ -80,7 +81,43 @@ export const TrashToTreasureEncounter: MysteryEncounter = MysteryEncounterBuilde
shiny: false, // Shiny lock because of custom intro sprite
formIndex: 1, // Gmax
bossSegmentModifier: 1, // +1 Segment from normal
moveSet: [Moves.PAYBACK, Moves.GUNK_SHOT, Moves.STOMPING_TANTRUM, Moves.DRAIN_PUNCH],
moveSet: [Moves.GUNK_SHOT, Moves.STOMPING_TANTRUM, Moves.HAMMER_ARM, Moves.PAYBACK],
modifierConfigs: [
{
modifier: generateModifierType(modifierTypes.BERRY) as PokemonHeldItemModifierType,
},
{
modifier: generateModifierType(modifierTypes.BERRY) as PokemonHeldItemModifierType,
},
{
modifier: generateModifierType(modifierTypes.BERRY) as PokemonHeldItemModifierType,
},
{
modifier: generateModifierType(modifierTypes.BERRY) as PokemonHeldItemModifierType,
},
{
modifier: generateModifierType(modifierTypes.BASE_STAT_BOOSTER) as PokemonHeldItemModifierType,
},
{
modifier: generateModifierType(modifierTypes.BASE_STAT_BOOSTER) as PokemonHeldItemModifierType,
},
{
modifier: generateModifierType(modifierTypes.TOXIC_ORB) as PokemonHeldItemModifierType,
stackCount: randSeedInt(2, 0),
},
{
modifier: generateModifierType(modifierTypes.SOOTHE_BELL) as PokemonHeldItemModifierType,
stackCount: randSeedInt(2, 1),
},
{
modifier: generateModifierType(modifierTypes.LUCKY_EGG) as PokemonHeldItemModifierType,
stackCount: randSeedInt(3, 1),
},
{
modifier: generateModifierType(modifierTypes.GOLDEN_EGG) as PokemonHeldItemModifierType,
stackCount: randSeedInt(2, 0),
},
],
};
const config: EnemyPartyConfig = {
levelAdditiveModifier: 0.5,
@ -90,7 +127,7 @@ export const TrashToTreasureEncounter: MysteryEncounter = MysteryEncounterBuilde
encounter.enemyPartyConfigs = [config];
// Load animations/sfx for Garbodor fight start moves
loadCustomMovesForEncounter([Moves.TOXIC, Moves.AMNESIA]);
loadCustomMovesForEncounter([Moves.TOXIC, Moves.STOCKPILE]);
globalScene.loadSe("PRSFX- Dig2", "battle_anims", "PRSFX- Dig2.wav");
globalScene.loadSe("PRSFX- Venom Drench", "battle_anims", "PRSFX- Venom Drench.wav");
@ -115,7 +152,7 @@ export const TrashToTreasureEncounter: MysteryEncounter = MysteryEncounterBuilde
doGarbageDig();
})
.withOptionPhase(async () => {
// Gain 2 Leftovers and 2 Shell Bell
// Gain 2 Leftovers and 1 Shell Bell
await transitionMysteryEncounterIntroVisuals();
await tryApplyDigRewardItems();
@ -175,7 +212,7 @@ export const TrashToTreasureEncounter: MysteryEncounter = MysteryEncounterBuilde
{
sourceBattlerIndex: BattlerIndex.ENEMY,
targets: [BattlerIndex.ENEMY],
move: new PokemonMove(Moves.AMNESIA),
move: new PokemonMove(Moves.STOCKPILE),
ignorePp: true,
},
);
@ -231,21 +268,7 @@ async function tryApplyDigRewardItems() {
true,
);
// First Shell bell
for (const pokemon of party) {
const heldItems = globalScene.findModifiers(
m => m instanceof PokemonHeldItemModifier && m.pokemonId === pokemon.id,
true,
) as PokemonHeldItemModifier[];
const existingShellBell = heldItems.find(m => m instanceof HitHealModifier) as HitHealModifier;
if (!existingShellBell || existingShellBell.getStackCount() < existingShellBell.getMaxStackCount()) {
await applyModifierTypeToPlayerPokemon(pokemon, shellBell);
break;
}
}
// Second Shell bell
// Only Shell bell
for (const pokemon of party) {
const heldItems = globalScene.findModifiers(
m => m instanceof PokemonHeldItemModifier && m.pokemonId === pokemon.id,
@ -263,7 +286,7 @@ async function tryApplyDigRewardItems() {
await showEncounterText(
i18next.t("battle:rewardGainCount", {
modifierName: shellBell.name,
count: 2,
count: 1,
}),
null,
undefined,

View File

@ -65,6 +65,7 @@ import { getPokemonSpecies } from "#app/data/pokemon-species";
import { PokemonType } from "#enums/pokemon-type";
import { getNatureName } from "#app/data/nature";
import { getPokemonNameWithAffix } from "#app/messages";
import { timedEventManager } from "#app/global-event-manager";
/**
* Animates exclamation sprite over trainer's head at start of encounter
@ -1046,7 +1047,7 @@ export function handleMysteryEncounterTurnStartEffects(): boolean {
export function getRandomEncounterSpecies(level: number, isBoss = false, rerollHidden = false): EnemyPokemon {
let bossSpecies: PokemonSpecies;
let isEventEncounter = false;
const eventEncounters = globalScene.eventManager.getEventEncounters();
const eventEncounters = timedEventManager.getEventEncounters();
let formIndex: number | undefined;
if (eventEncounters.length > 0 && randSeedInt(2) === 1) {

View File

@ -3054,11 +3054,11 @@ export function initSpecies() {
new PokemonSpecies(Species.DIPPLIN, 9, false, false, false, "Candy Apple Pokémon", PokemonType.GRASS, PokemonType.DRAGON, 0.4, 9.7, Abilities.SUPERSWEET_SYRUP, Abilities.GLUTTONY, Abilities.STICKY_HOLD, 485, 80, 80, 110, 95, 80, 40, 45, 50, 170, GrowthRate.ERRATIC, 50, false),
new PokemonSpecies(Species.POLTCHAGEIST, 9, false, false, false, "Matcha Pokémon", PokemonType.GRASS, PokemonType.GHOST, 0.1, 1.1, Abilities.HOSPITALITY, Abilities.NONE, Abilities.HEATPROOF, 308, 40, 45, 45, 74, 54, 50, 120, 50, 62, GrowthRate.SLOW, null, false, false,
new PokemonForm("Counterfeit Form", "counterfeit", PokemonType.GRASS, PokemonType.GHOST, 0.1, 1.1, Abilities.HOSPITALITY, Abilities.NONE, Abilities.HEATPROOF, 308, 40, 45, 45, 74, 54, 50, 120, 50, 62, false, null, true),
new PokemonForm("Artisan Form", "artisan", PokemonType.GRASS, PokemonType.GHOST, 0.1, 1.1, Abilities.HOSPITALITY, Abilities.NONE, Abilities.HEATPROOF, 308, 40, 45, 45, 74, 54, 50, 120, 50, 62, false, null, true),
new PokemonForm("Artisan Form", "artisan", PokemonType.GRASS, PokemonType.GHOST, 0.1, 1.1, Abilities.HOSPITALITY, Abilities.NONE, Abilities.HEATPROOF, 308, 40, 45, 45, 74, 54, 50, 120, 50, 62, false, null, false, true),
),
new PokemonSpecies(Species.SINISTCHA, 9, false, false, false, "Matcha Pokémon", PokemonType.GRASS, PokemonType.GHOST, 0.2, 2.2, Abilities.HOSPITALITY, Abilities.NONE, Abilities.HEATPROOF, 508, 71, 60, 106, 121, 80, 70, 60, 50, 178, GrowthRate.SLOW, null, false, false,
new PokemonForm("Unremarkable Form", "unremarkable", PokemonType.GRASS, PokemonType.GHOST, 0.2, 2.2, Abilities.HOSPITALITY, Abilities.NONE, Abilities.HEATPROOF, 508, 71, 60, 106, 121, 80, 70, 60, 50, 178),
new PokemonForm("Masterpiece Form", "masterpiece", PokemonType.GRASS, PokemonType.GHOST, 0.2, 2.2, Abilities.HOSPITALITY, Abilities.NONE, Abilities.HEATPROOF, 508, 71, 60, 106, 121, 80, 70, 60, 50, 178),
new PokemonForm("Masterpiece Form", "masterpiece", PokemonType.GRASS, PokemonType.GHOST, 0.2, 2.2, Abilities.HOSPITALITY, Abilities.NONE, Abilities.HEATPROOF, 508, 71, 60, 106, 121, 80, 70, 60, 50, 178, false, null, false, true),
),
new PokemonSpecies(Species.OKIDOGI, 9, true, false, false, "Retainer Pokémon", PokemonType.POISON, PokemonType.FIGHTING, 1.8, 92.2, Abilities.TOXIC_CHAIN, Abilities.NONE, Abilities.GUARD_DOG, 555, 88, 128, 115, 58, 86, 80, 3, 0, 276, GrowthRate.SLOW, 100, false),
new PokemonSpecies(Species.MUNKIDORI, 9, true, false, false, "Retainer Pokémon", PokemonType.POISON, PokemonType.PSYCHIC, 1, 12.2, Abilities.TOXIC_CHAIN, Abilities.NONE, Abilities.FRISK, 555, 88, 75, 66, 130, 90, 106, 3, 0, 276, GrowthRate.SLOW, 100, false),

View File

@ -32,6 +32,7 @@ import { TeraAIMode } from "#enums/tera-ai-mode";
import { TrainerPoolTier } from "#enums/trainer-pool-tier";
import { TrainerSlot } from "#enums/trainer-slot";
import { TrainerType } from "#enums/trainer-type";
import { timedEventManager } from "#app/global-event-manager";
// Type imports
import type { PokemonSpeciesFilter } from "#app/data/pokemon-species";
@ -516,13 +517,13 @@ export class TrainerConfig {
// return ret;
// }
setEventModifierRewardFuncs(...modifierTypeFuncs: (() => ModifierTypeFunc)[]): TrainerConfig {
this.eventRewardFuncs = modifierTypeFuncs.map(func => () => {
const modifierTypeFunc = func();
const modifierType = modifierTypeFunc();
modifierType.withIdFromFunc(modifierTypeFunc);
return modifierType;
});
/**
* Sets eventRewardFuncs to the active event rewards for the specified wave
* @param wave Associated with {@linkcode getFixedBattleEventRewards}
* @returns this
*/
setEventModifierRewardFuncs(wave: number): TrainerConfig {
this.eventRewardFuncs = timedEventManager.getFixedBattleEventRewards(wave).map(r => modifierTypes[r]);
return this;
}
@ -3696,11 +3697,7 @@ export const trainerConfigs: TrainerConfigs = {
() => modifierTypes.SUPER_EXP_CHARM,
() => modifierTypes.EXP_SHARE,
)
.setEventModifierRewardFuncs(
() => modifierTypes.SHINY_CHARM,
() => modifierTypes.ABILITY_CHARM,
() => modifierTypes.CATCHING_CHARM,
)
.setEventModifierRewardFuncs(8)
.setPartyMemberFunc(
0,
getRandomPartyMemberFunc(
@ -3768,7 +3765,7 @@ export const trainerConfigs: TrainerConfigs = {
.setMixedBattleBgm("battle_rival")
.setPartyTemplates(trainerPartyTemplates.RIVAL_2)
.setModifierRewardFuncs(() => modifierTypes.EXP_SHARE)
.setEventModifierRewardFuncs(() => modifierTypes.SHINY_CHARM)
.setEventModifierRewardFuncs(25)
.setPartyMemberFunc(
0,
getRandomPartyMemberFunc(
@ -4077,7 +4074,7 @@ export const trainerConfigs: TrainerConfigs = {
getRandomPartyMemberFunc([Species.RAYQUAZA], TrainerSlot.TRAINER, true, p => {
p.setBoss(true, 3);
p.pokeball = PokeballType.MASTER_BALL;
p.shiny = true;
p.shiny = timedEventManager.getClassicTrainerShinyChance() === 0;
p.variant = 1;
}),
)
@ -4174,7 +4171,7 @@ export const trainerConfigs: TrainerConfigs = {
p.setBoss();
p.generateAndPopulateMoveset();
p.pokeball = PokeballType.MASTER_BALL;
p.shiny = true;
p.shiny = timedEventManager.getClassicTrainerShinyChance() === 0;
p.variant = 1;
p.formIndex = 1; // Mega Rayquaza
p.generateName();

View File

@ -11,6 +11,7 @@ import { TerrainType, getTerrainName } from "./terrain";
import i18next from "i18next";
import { globalScene } from "#app/global-scene";
import type { Arena } from "#app/field/arena";
import { timedEventManager } from "#app/global-event-manager";
export class Weather {
public weatherType: WeatherType;
@ -405,8 +406,8 @@ export function getRandomWeatherType(arena: Arena): WeatherType {
break;
}
if (arena.biomeType === Biome.TOWN && globalScene.eventManager.isEventActive()) {
globalScene.eventManager.getWeather()?.map(w => weatherPool.push(w));
if (arena.biomeType === Biome.TOWN && timedEventManager.isEventActive()) {
timedEventManager.getWeather()?.map(w => weatherPool.push(w));
}
if (weatherPool.length > 1) {

View File

@ -40,7 +40,6 @@ import { TrainerType } from "#enums/trainer-type";
import { Abilities } from "#enums/abilities";
import { SpeciesFormChangeRevertWeatherFormTrigger, SpeciesFormChangeWeatherTrigger } from "#app/data/pokemon-forms";
import { CommonAnimPhase } from "#app/phases/common-anim-phase";
import { ShowAbilityPhase } from "#app/phases/show-ability-phase";
import { WeatherType } from "#enums/weather-type";
import { FieldEffectModifier } from "#app/modifier/modifier";
@ -378,7 +377,6 @@ export class Arena {
const isCherrimWithFlowerGift = p.hasAbility(Abilities.FLOWER_GIFT) && p.species.speciesId === Species.CHERRIM;
if (isCastformWithForecast || isCherrimWithFlowerGift) {
new ShowAbilityPhase(p.getBattlerIndex());
globalScene.triggerPokemonFormChange(p, SpeciesFormChangeWeatherTrigger);
}
});
@ -395,7 +393,6 @@ export class Arena {
p.hasAbility(Abilities.FLOWER_GIFT, false, true) && p.species.speciesId === Species.CHERRIM;
if (isCastformWithForecast || isCherrimWithFlowerGift) {
new ShowAbilityPhase(p.getBattlerIndex());
return globalScene.triggerPokemonFormChange(p, SpeciesFormChangeRevertWeatherFormTrigger);
}
});

View File

@ -263,6 +263,7 @@ import { Nature } from "#enums/nature";
import { StatusEffect } from "#enums/status-effect";
import { doShinySparkleAnim } from "#app/field/anims";
import { MoveFlags } from "#enums/MoveFlags";
import { timedEventManager } from "#app/global-event-manager";
export enum LearnMoveSituation {
MISC,
@ -1446,7 +1447,7 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container {
}
const ally = this.getAlly();
if (ally) {
if (!Utils.isNullOrUndefined(ally)) {
applyAllyStatMultiplierAbAttrs(AllyStatMultiplierAbAttr, ally, stat, statValue, simulated, this, move?.hasFlag(MoveFlags.IGNORE_ABILITIES) || ignoreAllyAbility);
}
@ -2983,8 +2984,12 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container {
const shinyThreshold = new Utils.NumberHolder(BASE_SHINY_CHANCE);
if (thresholdOverride === undefined) {
if (globalScene.eventManager.isEventActive()) {
shinyThreshold.value *= globalScene.eventManager.getShinyMultiplier();
if (timedEventManager.isEventActive()) {
const tchance = timedEventManager.getClassicTrainerShinyChance();
shinyThreshold.value *= timedEventManager.getShinyMultiplier();
if (this.hasTrainer() && tchance > 0) {
shinyThreshold.value = Math.max(tchance, shinyThreshold.value); // Choose the higher boost
}
}
if (!this.hasTrainer()) {
globalScene.applyModifiers(
@ -3025,8 +3030,8 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container {
if (thresholdOverride !== undefined && applyModifiersToOverride) {
shinyThreshold.value = thresholdOverride;
}
if (globalScene.eventManager.isEventActive()) {
shinyThreshold.value *= globalScene.eventManager.getShinyMultiplier();
if (timedEventManager.isEventActive()) {
shinyThreshold.value *= timedEventManager.getShinyMultiplier();
}
if (!this.hasTrainer()) {
globalScene.applyModifiers(
@ -3709,7 +3714,7 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container {
: i18next.t("arenaTag:yourTeam");
}
getAlly(): Pokemon {
getAlly(): Pokemon | undefined {
return (
this.isPlayer()
? globalScene.getPlayerField()
@ -3895,7 +3900,7 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container {
);
const ally = this.getAlly();
if (ally) {
if (!isNullOrUndefined(ally)) {
const ignore = this.hasAbilityWithAttr(MoveAbilityBypassAbAttr) || sourceMove.hasFlag(MoveFlags.IGNORE_ABILITIES);
applyAllyStatMultiplierAbAttrs(AllyStatMultiplierAbAttr, ally, Stat.ACC, accuracyMultiplier, false, this, ignore);
applyAllyStatMultiplierAbAttrs(AllyStatMultiplierAbAttr, ally, Stat.EVA, evasionMultiplier, false, this, ignore);
@ -4331,11 +4336,12 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container {
damage,
);
const ally = this.getAlly();
/** Additionally apply friend guard damage reduction if ally has it. */
if (globalScene.currentBattle.double && this.getAlly()?.isActive(true)) {
if (globalScene.currentBattle.double && !isNullOrUndefined(ally) && ally.isActive(true)) {
applyPreDefendAbAttrs(
AlliedFieldDamageReductionAbAttr,
this.getAlly(),
ally,
source,
move,
cancelled,
@ -6469,10 +6475,10 @@ export class PlayerPokemon extends Pokemon {
amount,
);
const candyFriendshipMultiplier = globalScene.gameMode.isClassic
? globalScene.eventManager.getClassicFriendshipMultiplier()
? timedEventManager.getClassicFriendshipMultiplier()
: 1;
const fusionReduction = fusionStarterSpeciesId
? globalScene.eventManager.areFusionsBoosted()
? timedEventManager.areFusionsBoosted()
? 1.5 // Divide candy gain for fusions by 1.5 during events
: 2 // 2 for fusions outside events
: 1; // 1 for non-fused mons

View File

@ -33,14 +33,16 @@ export default class Trainer extends Phaser.GameObjects.Container {
public partyTemplateIndex: number;
public name: string;
public partnerName: string;
public nameKey: string;
public partnerNameKey: string | undefined;
public originalIndexes: { [key: number]: number } = {};
constructor(
trainerType: TrainerType,
variant: TrainerVariant,
partyTemplateIndex?: number,
name?: string,
partnerName?: string,
nameKey?: string,
partnerNameKey?: string,
trainerConfigOverride?: TrainerConfig,
) {
super(globalScene, -72, 80);
@ -59,28 +61,41 @@ export default class Trainer extends Phaser.GameObjects.Container {
: Utils.randSeedWeightedItem(this.config.partyTemplates.map((_, i) => i)),
this.config.partyTemplates.length - 1,
);
if (i18next.exists("trainersCommon:" + TrainerType[trainerType], { returnObjects: true })) {
const namePool = i18next.t("trainersCommon:" + TrainerType[trainerType], { returnObjects: true });
this.name =
name ||
Utils.randSeedItem(
Object.values(
namePool.hasOwnProperty("MALE")
? namePool[variant === TrainerVariant.FEMALE ? "FEMALE" : "MALE"]
: namePool,
),
const classKey = `trainersCommon:${TrainerType[trainerType]}`;
if (i18next.exists(classKey, { returnObjects: true })) {
if (nameKey) {
this.nameKey = nameKey;
} else {
const genderKey = i18next.exists(`${classKey}.MALE`)
? variant === TrainerVariant.FEMALE
? ".FEMALE"
: ".MALE"
: "";
const trainerKey = Utils.randSeedItem(
Object.keys(i18next.t(`${classKey}${genderKey}`, { returnObjects: true })),
);
this.nameKey = `${classKey}${genderKey}.${trainerKey}`;
}
this.name = i18next.t(this.nameKey);
if (variant === TrainerVariant.DOUBLE) {
if (this.config.doubleOnly) {
if (partnerName) {
this.partnerName = partnerName;
if (partnerNameKey) {
this.partnerNameKey = partnerNameKey;
this.partnerName = i18next.t(this.partnerNameKey);
} else {
[this.name, this.partnerName] = this.name.split(" & ");
}
} else {
this.partnerName =
partnerName ||
Utils.randSeedItem(Object.values(namePool.hasOwnProperty("FEMALE") ? namePool["FEMALE"] : namePool));
const partnerGenderKey = i18next.exists(`${classKey}.FEMALE`) ? ".FEMALE" : "";
const partnerTrainerKey = Utils.randSeedItem(
Object.keys(
i18next.t(`${classKey}${partnerGenderKey}`, {
returnObjects: true,
}),
),
);
this.partnerNameKey = `${classKey}${partnerGenderKey}.${partnerTrainerKey}`;
this.partnerName = i18next.t(this.partnerNameKey);
}
}
}

View File

@ -68,6 +68,19 @@ export class GameMode implements GameModeConfig {
this.battleConfig = battleConfig || {};
}
/**
* Enables challenges if they are disabled and sets the specified challenge's value
* @param challenge The challenge to set
* @param value The value to give the challenge. Impact depends on the specific challenge
*/
setChallengeValue(challenge: Challenges, value: number) {
if (!this.isChallenge) {
this.isChallenge = true;
this.challenges = allChallenges.map(c => copyChallenge(c));
}
this.challenges.filter((chal: Challenge) => chal.id === challenge).map((chal: Challenge) => (chal.value = value));
}
/**
* Helper function to see if a GameMode has a specific challenge type
* @param challenge the Challenges it looks for

View File

@ -0,0 +1,3 @@
import { TimedEventManager } from "./timed-event-manager";
export const timedEventManager = new TimedEventManager();

View File

@ -20,6 +20,7 @@ import { initStatsKeys } from "#app/ui/game-stats-ui-handler";
import { initVouchers } from "#app/system/voucher";
import { Biome } from "#enums/biome";
import { initMysteryEncounters } from "#app/data/mystery-encounters/mystery-encounters";
import { timedEventManager } from "./global-event-manager";
export class LoadingScene extends SceneBase {
public static readonly KEY = "loading";
@ -250,11 +251,13 @@ export class LoadingScene extends SceneBase {
this.loadAtlas("statuses", "");
this.loadAtlas("types", "");
}
const availableLangs = ["en", "de", "it", "fr", "ja", "ko", "es-ES", "es-MX", "pt-BR", "zh-CN", "zh-TW", "ca-ES"];
if (lang && availableLangs.includes(lang)) {
this.loadImage(`pkmnday2025event-${lang}`, "events");
} else {
this.loadImage("pkmnday2025event-en", "events");
if (timedEventManager.activeEventHasBanner()) {
const availableLangs = timedEventManager.getEventBannerLangs();
if (lang && availableLangs.includes(lang)) {
this.loadImage(`${timedEventManager.getEventBannerFilename()}-${lang}`, "events");
} else {
this.loadImage(`${timedEventManager.getEventBannerFilename()}-en`, "events");
}
}
this.loadAtlas("statuses", "");

View File

@ -127,6 +127,7 @@ import type { PermanentStat, TempBattleStat } from "#enums/stat";
import { getStatKey, Stat, TEMP_BATTLE_STATS } from "#enums/stat";
import { StatusEffect } from "#enums/status-effect";
import i18next from "i18next";
import { timedEventManager } from "#app/global-event-manager";
const outputModifierData = false;
const useMaxWeightForOutput = false;
@ -2655,7 +2656,7 @@ const modifierPool: ModifierPool = {
if (globalScene.gameMode.isSplicedOnly) {
return 4;
}
if (globalScene.gameMode.isClassic && globalScene.eventManager.areFusionsBoosted()) {
if (globalScene.gameMode.isClassic && timedEventManager.areFusionsBoosted()) {
return 2;
}
}
@ -2822,37 +2823,48 @@ const modifierPool: ModifierPool = {
modifierTypes.MYSTICAL_ROCK,
(party: Pokemon[]) => {
return party.some(p => {
const moveset = p.getMoveset(true).map(m => m.moveId);
let isHoldingMax = false;
for (const i of p.getHeldItems()) {
if (i.type.id === "MYSTICAL_ROCK") {
isHoldingMax = i.getStackCount() === i.getMaxStackCount();
break;
}
}
const hasAbility = [
Abilities.DRIZZLE,
Abilities.ORICHALCUM_PULSE,
Abilities.DRIZZLE,
Abilities.SAND_STREAM,
Abilities.SAND_SPIT,
Abilities.SNOW_WARNING,
Abilities.ELECTRIC_SURGE,
Abilities.HADRON_ENGINE,
Abilities.PSYCHIC_SURGE,
Abilities.GRASSY_SURGE,
Abilities.SEED_SOWER,
Abilities.MISTY_SURGE,
].some(a => p.hasAbility(a, false, true));
if (!isHoldingMax) {
const moveset = p.getMoveset(true).map(m => m.moveId);
const hasMoves = [
Moves.SUNNY_DAY,
Moves.RAIN_DANCE,
Moves.SANDSTORM,
Moves.SNOWSCAPE,
Moves.HAIL,
Moves.CHILLY_RECEPTION,
Moves.ELECTRIC_TERRAIN,
Moves.PSYCHIC_TERRAIN,
Moves.GRASSY_TERRAIN,
Moves.MISTY_TERRAIN,
].some(m => moveset.includes(m));
const hasAbility = [
Abilities.DROUGHT,
Abilities.ORICHALCUM_PULSE,
Abilities.DRIZZLE,
Abilities.SAND_STREAM,
Abilities.SAND_SPIT,
Abilities.SNOW_WARNING,
Abilities.ELECTRIC_SURGE,
Abilities.HADRON_ENGINE,
Abilities.PSYCHIC_SURGE,
Abilities.GRASSY_SURGE,
Abilities.SEED_SOWER,
Abilities.MISTY_SURGE,
].some(a => p.hasAbility(a, false, true));
return hasAbility || hasMoves;
const hasMoves = [
Moves.SUNNY_DAY,
Moves.RAIN_DANCE,
Moves.SANDSTORM,
Moves.SNOWSCAPE,
Moves.HAIL,
Moves.CHILLY_RECEPTION,
Moves.ELECTRIC_TERRAIN,
Moves.PSYCHIC_TERRAIN,
Moves.GRASSY_TERRAIN,
Moves.MISTY_TERRAIN,
].some(m => moveset.includes(m));
return hasAbility || hasMoves;
}
return false;
})
? 10
: 0;
@ -2939,7 +2951,7 @@ const modifierPool: ModifierPool = {
new WeightedModifierType(
modifierTypes.DNA_SPLICERS,
(party: Pokemon[]) =>
!(globalScene.gameMode.isClassic && globalScene.eventManager.areFusionsBoosted()) &&
!(globalScene.gameMode.isClassic && timedEventManager.areFusionsBoosted()) &&
!globalScene.gameMode.isSplicedOnly &&
party.filter(p => !p.fusionSpecies).length > 1
? 24
@ -3703,7 +3715,7 @@ export function getPartyLuckValue(party: Pokemon[]): number {
);
return DailyLuck.value;
}
const eventSpecies = globalScene.eventManager.getEventLuckBoostedSpecies();
const eventSpecies = timedEventManager.getEventLuckBoostedSpecies();
const luck = Phaser.Math.Clamp(
party
.map(p => (p.isAllowedInBattle() ? p.getLuck() + (eventSpecies.includes(p.species.speciesId) ? 1 : 0) : 0))
@ -3711,7 +3723,7 @@ export function getPartyLuckValue(party: Pokemon[]): number {
0,
14,
);
return Math.min(globalScene.eventManager.getEventLuckBoost() + (luck ?? 0), 14);
return Math.min(timedEventManager.getEventLuckBoost() + (luck ?? 0), 14);
}
export function getLuckString(luckValue: number): string {

View File

@ -39,7 +39,7 @@ export class EnemyCommandPhase extends FieldPhase {
if (
battle.double &&
enemyPokemon.hasAbility(Abilities.COMMANDER) &&
enemyPokemon.getAlly().getTag(BattlerTagType.COMMANDED)
enemyPokemon.getAlly()?.getTag(BattlerTagType.COMMANDED)
) {
this.skipTurn = true;
}

View File

@ -209,8 +209,8 @@ export class FaintPhase extends PokemonPhase {
}
// in double battles redirect potential moves off fainted pokemon
if (globalScene.currentBattle.double) {
const allyPokemon = pokemon.getAlly();
const allyPokemon = pokemon.getAlly();
if (globalScene.currentBattle.double && !isNullOrUndefined(allyPokemon)) {
globalScene.redirectPokemonMoves(pokemon, allyPokemon);
}

View File

@ -45,6 +45,8 @@ export class GameOverPhase extends BattlePhase {
start() {
super.start();
globalScene.hideAbilityBar();
// Failsafe if players somehow skip floor 200 in classic mode
if (globalScene.gameMode.isClassic && globalScene.currentBattle.waveIndex > 200) {
this.isVictory = true;

View File

@ -1,27 +1,12 @@
import { globalScene } from "#app/global-scene";
import type { BattlerIndex } from "#app/battle";
import { PokemonPhase } from "./pokemon-phase";
export class HideAbilityPhase extends PokemonPhase {
private passive: boolean;
constructor(battlerIndex: BattlerIndex, passive = false) {
super(battlerIndex);
this.passive = passive;
}
import { Phase } from "#app/phase";
export class HideAbilityPhase extends Phase {
start() {
super.start();
const pokemon = this.getPokemon();
if (pokemon) {
globalScene.abilityBar.hide().then(() => {
this.end();
});
} else {
globalScene.abilityBar.hide().then(() => {
this.end();
}
});
}
}

View File

@ -69,6 +69,7 @@ import type { Phase } from "#app/phase";
import { ShowAbilityPhase } from "./show-ability-phase";
import { MovePhase } from "./move-phase";
import { MoveEndPhase } from "./move-end-phase";
import { HideAbilityPhase } from "#app/phases/hide-ability-phase";
export class MoveEffectPhase extends PokemonPhase {
public move: PokemonMove;
@ -326,12 +327,14 @@ export class MoveEffectPhase extends PokemonPhase {
? getMoveTargets(target, move.id).targets
: [user.getBattlerIndex()];
if (!isReflecting) {
// TODO: Ability displays should be handled by the ability
queuedPhases.push(
new ShowAbilityPhase(
target.getBattlerIndex(),
target.getPassiveAbility().hasAttr(ReflectStatusMoveAbAttr),
),
);
queuedPhases.push(new HideAbilityPhase());
}
queuedPhases.push(

View File

@ -42,7 +42,6 @@ import { CommonAnimPhase } from "#app/phases/common-anim-phase";
import { MoveChargePhase } from "#app/phases/move-charge-phase";
import { MoveEffectPhase } from "#app/phases/move-effect-phase";
import { MoveEndPhase } from "#app/phases/move-end-phase";
import { ShowAbilityPhase } from "#app/phases/show-ability-phase";
import { NumberHolder } from "#app/utils";
import { Abilities } from "#enums/abilities";
import { ArenaTagType } from "#enums/arena-tag-type";
@ -535,11 +534,16 @@ export class MovePhase extends BattlePhase {
if (this.pokemon.hasAbilityWithAttr(BlockRedirectAbAttr)) {
redirectTarget.value = currentTarget;
globalScene.unshiftPhase(
new ShowAbilityPhase(
this.pokemon.getBattlerIndex(),
this.pokemon.getPassiveAbility().hasAttr(BlockRedirectAbAttr),
),
// TODO: Ability displays should be handled by the ability
globalScene.queueAbilityDisplay(
this.pokemon,
this.pokemon.getPassiveAbility().hasAttr(BlockRedirectAbAttr),
true,
);
globalScene.queueAbilityDisplay(
this.pokemon,
this.pokemon.getPassiveAbility().hasAttr(BlockRedirectAbAttr),
false,
);
}

View File

@ -42,8 +42,12 @@ export class RevivalBlessingPhase extends BattlePhase {
true,
);
if (globalScene.currentBattle.double && globalScene.getPlayerParty().length > 1) {
const allyPokemon = this.user.getAlly();
const allyPokemon = this.user.getAlly();
if (
globalScene.currentBattle.double &&
globalScene.getPlayerParty().length > 1 &&
!Utils.isNullOrUndefined(allyPokemon)
) {
if (slotIndex <= 1) {
// Revived ally pokemon
globalScene.unshiftPhase(

View File

@ -35,7 +35,7 @@ export class ShowAbilityPhase extends PokemonPhase {
// If the bar is already out, hide it before showing the new one
if (globalScene.abilityBar.isVisible()) {
globalScene.unshiftPhase(new HideAbilityPhase(this.battlerIndex, this.passive));
globalScene.unshiftPhase(new HideAbilityPhase());
globalScene.unshiftPhase(new ShowAbilityPhase(this.battlerIndex, this.passive));
return this.end();
}

View File

@ -17,7 +17,7 @@ import type Pokemon from "#app/field/pokemon";
import { getPokemonNameWithAffix } from "#app/messages";
import { ResetNegativeStatStageModifier } from "#app/modifier/modifier";
import { handleTutorial, Tutorial } from "#app/tutorial";
import { NumberHolder, BooleanHolder } from "#app/utils";
import { NumberHolder, BooleanHolder, isNullOrUndefined } from "#app/utils";
import i18next from "i18next";
import { PokemonPhase } from "./pokemon-phase";
import { Stat, type BattleStat, getStatKey, getStatStageChangeDescriptionKey } from "#enums/stat";
@ -161,7 +161,7 @@ export class StatStageChangePhase extends PokemonPhase {
pokemon,
);
const ally = pokemon.getAlly();
if (ally) {
if (!isNullOrUndefined(ally)) {
applyPreStatStageChangeAbAttrs(
ConditionalUserFieldProtectStatAbAttr,
ally,

View File

@ -212,6 +212,8 @@ export class TitlePhase extends Phase {
const generateDaily = (seed: string) => {
globalScene.gameMode = getGameMode(GameModes.DAILY);
// Daily runs don't support all challenges yet (starter select restrictions aren't considered)
globalScene.eventManager.startEventChallenges();
globalScene.setSeed(seed);
globalScene.resetSeed(0);

View File

@ -11,6 +11,7 @@ import { TrainerSlot } from "#enums/trainer-slot";
import { globalScene } from "#app/global-scene";
import { Biome } from "#app/enums/biome";
import { achvs } from "#app/system/achv";
import { timedEventManager } from "#app/global-event-manager";
export class TrainerVictoryPhase extends BattlePhase {
constructor() {
@ -29,7 +30,7 @@ export class TrainerVictoryPhase extends BattlePhase {
globalScene.unshiftPhase(new ModifierRewardPhase(modifierRewardFunc));
}
if (globalScene.eventManager.isEventActive()) {
if (timedEventManager.isEventActive()) {
for (const rewardFunc of globalScene.currentBattle.trainer?.config.eventRewardFuncs!) {
globalScene.unshiftPhase(new ModifierRewardPhase(rewardFunc));
}
@ -42,7 +43,7 @@ export class TrainerVictoryPhase extends BattlePhase {
!globalScene.validateVoucher(vouchers[TrainerType[trainerType]]) &&
globalScene.currentBattle.trainer?.config.isBoss
) {
if (globalScene.eventManager.getUpgradeUnlockedVouchers()) {
if (timedEventManager.getUpgradeUnlockedVouchers()) {
globalScene.unshiftPhase(
new ModifierRewardPhase(
[

View File

@ -28,6 +28,8 @@ export class TurnEndPhase extends FieldPhase {
globalScene.currentBattle.incrementTurn();
globalScene.eventTarget.dispatchEvent(new TurnEndEvent(globalScene.currentBattle.turn));
globalScene.hideAbilityBar();
const handlePokemon = (pokemon: Pokemon) => {
if (!pokemon.switchOutStatus) {
pokemon.lapseTags(BattlerTagLapseType.TURN_END);

View File

@ -1793,7 +1793,9 @@ export class GameData {
const dexEntry = this.dexData[species.speciesId];
const caughtAttr = dexEntry.caughtAttr;
const formIndex = pokemon.formIndex;
const dexAttr = pokemon.getDexAttr();
// This makes sure that we do not try to unlock data which cannot be unlocked
const dexAttr = pokemon.getDexAttr() & species.getFullUnlocksData();
// Mark as caught
dexEntry.caughtAttr |= dexAttr;
@ -1803,6 +1805,10 @@ export class GameData {
// always true except for the case of Urshifu.
const formKey = pokemon.getFormKey();
if (formIndex > 0) {
// In case a Pikachu with formIndex > 0 was unlocked, base form Pichu is also unlocked
if (pokemon.species.speciesId === Species.PIKACHU && species.speciesId === Species.PICHU) {
dexEntry.caughtAttr |= globalScene.gameData.getFormAttr(0);
}
if (pokemon.species.speciesId === Species.URSHIFU) {
if (formIndex === 2) {
dexEntry.caughtAttr |= globalScene.gameData.getFormAttr(0);

View File

@ -5,8 +5,8 @@ export default class TrainerData {
public trainerType: TrainerType;
public variant: TrainerVariant;
public partyTemplateIndex: number;
public name: string;
public partnerName: string;
public nameKey: string;
public partnerNameKey: string | undefined;
constructor(source: Trainer | any) {
const sourceTrainer = source instanceof Trainer ? (source as Trainer) : null;
@ -17,11 +17,11 @@ export default class TrainerData {
? TrainerVariant.FEMALE
: TrainerVariant.DEFAULT;
this.partyTemplateIndex = source.partyMemberTemplateIndex;
this.name = source.name;
this.partnerName = source.partnerName;
this.nameKey = source.nameKey;
this.partnerNameKey = source.partnerNameKey;
}
toTrainer(): Trainer {
return new Trainer(this.trainerType, this.variant, this.partyTemplateIndex, this.name, this.partnerName);
return new Trainer(this.trainerType, this.variant, this.partyTemplateIndex, this.nameKey, this.partnerNameKey);
}
}

View File

@ -10,6 +10,9 @@ import * as v1_1_0 from "./versions/v1_1_0";
// --- v1.7.0 PATCHES --- //
import * as v1_7_0 from "./versions/v1_7_0";
// --- v1.8.3 PATCHES --- //
import * as v1_8_3 from "./versions/v1_8_3";
const LATEST_VERSION = version.split(".").map(value => Number.parseInt(value));
/**
@ -174,6 +177,12 @@ class SystemVersionConverter extends VersionConverter {
console.log("Applying v1.7.0 system data migration!");
this.callMigrators(data, v1_7_0.systemMigrators);
}
if (curMinor === 8) {
if (curPatch <= 2) {
console.log("Applying v1.8.3 system data migration!");
this.callMigrators(data, v1_8_3.systemMigrators);
}
}
}
console.log(`System data successfully migrated to v${version}!`);

View File

@ -0,0 +1,30 @@
import { getPokemonSpecies } from "#app/data/pokemon-species";
import { DexAttr, type SystemSaveData } from "#app/system/game-data";
import { Species } from "#enums/species";
export const systemMigrators = [
/**
* If a starter is caught, but the only forms registered as caught are not starterSelectable,
* unlock the default form.
* @param data {@linkcode SystemSaveData}
*/
function migratePichuForms(data: SystemSaveData) {
if (data.starterData && data.dexData) {
// This is Pichu's Pokédex number
const sd = 172;
const caughtAttr = data.dexData[sd]?.caughtAttr;
const species = getPokemonSpecies(sd);
// An extra check because you never know
if (species.speciesId === Species.PICHU && caughtAttr) {
// Ensuring that only existing forms are unlocked
data.dexData[sd].caughtAttr &= species.getFullUnlocksData();
// If no forms are unlocked now, since Pichu is caught, we unlock form 0
data.dexData[sd].caughtAttr |= DexAttr.DEFAULT_FORM;
}
}
},
] as const;
export const settingsMigrators = [] as const;
export const sessionMigrators = [] as const;

View File

@ -9,6 +9,7 @@ import { WeatherType } from "#enums/weather-type";
import { CLASSIC_CANDY_FRIENDSHIP_MULTIPLIER } from "./data/balance/starters";
import { MysteryEncounterType } from "./enums/mystery-encounter-type";
import { MysteryEncounterTier } from "./enums/mystery-encounter-tier";
import { Challenges } from "#enums/challenges";
export enum EventType {
SHINY,
@ -36,6 +37,18 @@ interface EventMysteryEncounterTier {
disable?: boolean;
}
interface EventWaveReward {
wave: number;
type: string;
}
type EventMusicReplacement = [string, string];
interface EventChallenge {
challenge: Challenges;
value: number;
}
interface TimedEvent extends EventBanner {
name: string;
eventType: EventType;
@ -51,6 +64,10 @@ interface TimedEvent extends EventBanner {
mysteryEncounterTierChanges?: EventMysteryEncounterTier[];
luckBoostedSpecies?: Species[];
boostFusions?: boolean; //MODIFIER REWORK PLEASE
classicWaveRewards?: EventWaveReward[]; // Rival battle rewards
trainerShinyChance?: number; // Odds over 65536 of trainer mon generating as shiny
music?: EventMusicReplacement[];
dailyRunChallenges?: EventChallenge[];
}
const timedEvents: TimedEvent[] = [
@ -61,7 +78,7 @@ const timedEvents: TimedEvent[] = [
upgradeUnlockedVouchers: true,
startDate: new Date(Date.UTC(2024, 11, 21, 0)),
endDate: new Date(Date.UTC(2025, 0, 4, 0)),
bannerKey: "winter_holidays2024-event-",
bannerKey: "winter_holidays2024-event",
scale: 0.21,
availableLangs: ["en", "de", "it", "fr", "ja", "ko", "es-ES", "pt-BR", "zh-CN"],
eventEncounters: [
@ -104,6 +121,12 @@ const timedEvents: TimedEvent[] = [
disable: true,
},
],
classicWaveRewards: [
{ wave: 8, type: "SHINY_CHARM" },
{ wave: 8, type: "ABILITY_CHARM" },
{ wave: 8, type: "CATCHING_CHARM" },
{ wave: 25, type: "SHINY_CHARM" },
],
},
{
name: "Year of the Snake",
@ -111,7 +134,7 @@ const timedEvents: TimedEvent[] = [
luckBoost: 1,
startDate: new Date(Date.UTC(2025, 0, 29, 0)),
endDate: new Date(Date.UTC(2025, 1, 3, 0)),
bannerKey: "yearofthesnakeevent-",
bannerKey: "yearofthesnakeevent",
scale: 0.21,
availableLangs: ["en", "de", "it", "fr", "ja", "ko", "es-ES", "pt-BR", "zh-CN"],
eventEncounters: [
@ -169,6 +192,12 @@ const timedEvents: TimedEvent[] = [
Species.ROARING_MOON,
Species.BLOODMOON_URSALUNA,
],
classicWaveRewards: [
{ wave: 8, type: "SHINY_CHARM" },
{ wave: 8, type: "ABILITY_CHARM" },
{ wave: 8, type: "CATCHING_CHARM" },
{ wave: 25, type: "SHINY_CHARM" },
],
},
{
name: "Valentine",
@ -177,7 +206,7 @@ const timedEvents: TimedEvent[] = [
endDate: new Date(Date.UTC(2025, 1, 21)),
boostFusions: true,
shinyMultiplier: 2,
bannerKey: "valentines2025event-",
bannerKey: "valentines2025event",
scale: 0.21,
availableLangs: ["en", "de", "it", "fr", "ja", "ko", "es-ES", "pt-BR", "zh-CN"],
eventEncounters: [
@ -203,6 +232,12 @@ const timedEvents: TimedEvent[] = [
{ species: Species.ENAMORUS },
],
luckBoostedSpecies: [Species.LUVDISC],
classicWaveRewards: [
{ wave: 8, type: "SHINY_CHARM" },
{ wave: 8, type: "ABILITY_CHARM" },
{ wave: 8, type: "CATCHING_CHARM" },
{ wave: 25, type: "SHINY_CHARM" },
],
},
{
name: "PKMNDAY2025",
@ -210,7 +245,7 @@ const timedEvents: TimedEvent[] = [
startDate: new Date(Date.UTC(2025, 1, 27)),
endDate: new Date(Date.UTC(2025, 2, 4)),
classicFriendshipMultiplier: 4,
bannerKey: "pkmnday2025event-",
bannerKey: "pkmnday2025event",
scale: 0.21,
availableLangs: ["en", "de", "it", "fr", "ja", "ko", "es-ES", "pt-BR", "zh-CN"],
eventEncounters: [
@ -248,6 +283,32 @@ const timedEvents: TimedEvent[] = [
Species.ZYGARDE,
Species.ETERNAL_FLOETTE,
],
classicWaveRewards: [
{ wave: 8, type: "SHINY_CHARM" },
{ wave: 8, type: "ABILITY_CHARM" },
{ wave: 8, type: "CATCHING_CHARM" },
{ wave: 25, type: "SHINY_CHARM" },
],
},
{
name: "April Fools 2025",
eventType: EventType.LUCK,
startDate: new Date(Date.UTC(2025, 2, 31)),
endDate: new Date(Date.UTC(2025, 3, 3)),
bannerKey: "aprf25",
scale: 0.21,
availableLangs: ["en", "de", "it", "fr", "ja", "ko", "es-ES", "es-MX", "pt-BR", "zh-CN"],
trainerShinyChance: 13107, // 13107/65536 = 1/5
music: [
["title", "title_afd"],
["battle_rival_3", "battle_rival_3_afd"],
],
dailyRunChallenges: [
{
challenge: Challenges.INVERSE_BATTLE,
value: 1,
},
],
},
];
@ -265,7 +326,7 @@ export class TimedEventManager {
}
activeEventHasBanner(): boolean {
const activeEvents = timedEvents.filter(te => this.isActive(te) && te.hasOwnProperty("bannerFilename"));
const activeEvents = timedEvents.filter(te => this.isActive(te) && te.hasOwnProperty("bannerKey"));
return activeEvents.length > 0;
}
@ -283,6 +344,12 @@ export class TimedEventManager {
return timedEvents.find((te: TimedEvent) => this.isActive(te))?.bannerKey ?? "";
}
getEventBannerLangs(): string[] {
const ret: string[] = [];
ret.push(...timedEvents.find(te => this.isActive(te) && !isNullOrUndefined(te.availableLangs))?.availableLangs!);
return ret;
}
getEventEncounters(): EventEncounter[] {
const ret: EventEncounter[] = [];
timedEvents
@ -417,6 +484,55 @@ export class TimedEventManager {
areFusionsBoosted(): boolean {
return timedEvents.some(te => this.isActive(te) && te.boostFusions);
}
/**
* Gets all the modifier types associated with a certain wave during an event
* @see EventWaveReward
* @param wave the wave to check for associated rewards
* @returns array of strings of the event modifier reward types
*/
getFixedBattleEventRewards(wave: number): string[] {
const ret: string[] = [];
timedEvents
.filter(te => this.isActive(te) && !isNullOrUndefined(te.classicWaveRewards))
.map(te => {
ret.push(...te.classicWaveRewards!.filter(cwr => cwr.wave === wave).map(cwr => cwr.type));
});
return ret;
}
// Gets the extra shiny chance for trainers due to event (odds/65536)
getClassicTrainerShinyChance(): number {
let ret = 0;
const tsEvents = timedEvents.filter(te => this.isActive(te) && !isNullOrUndefined(te.trainerShinyChance));
tsEvents.map(t => (ret += t.trainerShinyChance!));
return ret;
}
getEventBgmReplacement(bgm: string): string {
let ret = bgm;
timedEvents.map(te => {
if (this.isActive(te) && !isNullOrUndefined(te.music)) {
te.music.map(mr => {
if (mr[0] === bgm) {
console.log(`it is ${te.name} so instead of ${mr[0]} we play ${mr[1]}`);
ret = mr[1];
}
});
}
});
return ret;
}
/**
* Activates any challenges on {@linkcode globalScene.gameMode} for the currently active event
*/
startEventChallenges(): void {
const challenges = this.activeEvent()?.dailyRunChallenges;
challenges?.forEach((eventChal: EventChallenge) =>
globalScene.gameMode.setChallengeValue(eventChal.challenge, eventChal.value),
);
}
}
export class TimedEventDisplay extends Phaser.GameObjects.Container {
@ -456,11 +572,12 @@ export class TimedEventDisplay extends Phaser.GameObjects.Container {
let key = this.event.bannerKey;
if (lang && this.event.availableLangs && this.event.availableLangs.length > 0) {
if (this.event.availableLangs.includes(lang)) {
key += lang;
key += "-" + lang;
} else {
key += "en";
key += "-en";
}
}
console.log(key);
console.log(this.event.bannerKey);
const padding = 5;
const showTimer = this.event.eventType !== EventType.NO_TIMER_DISPLAY;

View File

@ -11,6 +11,7 @@ import { globalScene } from "#app/global-scene";
import type { Species } from "#enums/species";
import { getPokemonSpecies } from "#app/data/pokemon-species";
import { PlayerGender } from "#enums/player-gender";
import { timedEventManager } from "#app/global-event-manager";
export default class TitleUiHandler extends OptionSelectUiHandler {
/** If the stats can not be retrieved, use this fallback value */
@ -43,8 +44,8 @@ export default class TitleUiHandler extends OptionSelectUiHandler {
logo.setOrigin(0.5, 0);
this.titleContainer.add(logo);
if (globalScene.eventManager.isEventActive()) {
this.eventDisplay = new TimedEventDisplay(0, 0, globalScene.eventManager.activeEvent());
if (timedEventManager.isEventActive()) {
this.eventDisplay = new TimedEventDisplay(0, 0, timedEventManager.activeEvent());
this.eventDisplay.setup();
this.titleContainer.add(this.eventDisplay);
}
@ -142,7 +143,7 @@ export default class TitleUiHandler extends OptionSelectUiHandler {
const ui = this.getUi();
if (globalScene.eventManager.isEventActive()) {
if (timedEventManager.isEventActive()) {
this.eventDisplay.setWidth(globalScene.scaledCanvas.width - this.optionSelectBg.width - this.optionSelectBg.x);
this.eventDisplay.show();
}

View File

@ -9,7 +9,6 @@ import { StatusEffect } from "#enums/status-effect";
import GameManager from "#test/testUtils/gameManager";
import Phaser from "phaser";
import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest";
import { BattlerIndex } from "#app/battle";
describe("Abilities - Parental Bond", () => {
let phaserGame: Phaser.Game;
@ -427,21 +426,4 @@ describe("Abilities - Parental Bond", () => {
// TODO: Update hit count to 1 once Future Sight is fixed to not activate abilities if user is off the field
expect(enemyPokemon.damageAndUpdate).toHaveBeenCalledTimes(2);
});
it("should not allow Pollen Puff to heal ally more than once", async () => {
game.override.battleType("double").moveset([Moves.POLLEN_PUFF, Moves.ENDURE]);
await game.classicMode.startBattle([Species.BULBASAUR, Species.OMANYTE]);
const [, rightPokemon] = game.scene.getPlayerField();
rightPokemon.damageAndUpdate(rightPokemon.hp - 1);
game.move.select(Moves.POLLEN_PUFF, 0, BattlerIndex.PLAYER_2);
game.move.select(Moves.ENDURE, 1);
await game.toNextTurn();
// Pollen Puff heals with a ratio of 0.5, as long as Pollen Puff triggers only once the pokemon will always be <= (0.5 * Max HP) + 1
expect(rightPokemon.hp).toBeLessThanOrEqual(0.5 * rightPokemon.getMaxHp() + 1);
});
});

View File

@ -0,0 +1,64 @@
import { BattlerIndex } from "#app/battle";
import { Abilities } from "#enums/abilities";
import { Moves } from "#enums/moves";
import { Species } from "#enums/species";
import GameManager from "#test/testUtils/gameManager";
import Phaser from "phaser";
import { afterEach, beforeAll, beforeEach, describe, expect, it } from "vitest";
describe("Moves - Pollen Puff", () => {
let phaserGame: Phaser.Game;
let game: GameManager;
beforeAll(() => {
phaserGame = new Phaser.Game({
type: Phaser.HEADLESS,
});
});
afterEach(() => {
game.phaseInterceptor.restoreOg();
});
beforeEach(() => {
game = new GameManager(phaserGame);
game.override
.moveset([Moves.POLLEN_PUFF])
.ability(Abilities.BALL_FETCH)
.battleType("single")
.disableCrits()
.enemySpecies(Species.MAGIKARP)
.enemyAbility(Abilities.BALL_FETCH)
.enemyMoveset(Moves.SPLASH);
});
it("should not heal more than once when the user has a source of multi-hit", async () => {
game.override.battleType("double").moveset([Moves.POLLEN_PUFF, Moves.ENDURE]).ability(Abilities.PARENTAL_BOND);
await game.classicMode.startBattle([Species.BULBASAUR, Species.OMANYTE]);
const [_, rightPokemon] = game.scene.getPlayerField();
rightPokemon.damageAndUpdate(rightPokemon.hp - 1);
game.move.select(Moves.POLLEN_PUFF, 0, BattlerIndex.PLAYER_2);
game.move.select(Moves.ENDURE, 1);
await game.phaseInterceptor.to("BerryPhase");
// Pollen Puff heals with a ratio of 0.5, as long as Pollen Puff triggers only once the pokemon will always be <= (0.5 * Max HP) + 1
expect(rightPokemon.hp).toBeLessThanOrEqual(0.5 * rightPokemon.getMaxHp() + 1);
});
it("should damage an enemy multiple times when the user has a source of multi-hit", async () => {
game.override.moveset([Moves.POLLEN_PUFF]).ability(Abilities.PARENTAL_BOND).enemyLevel(100);
await game.classicMode.startBattle([Species.MAGIKARP]);
const target = game.scene.getEnemyPokemon()!;
game.move.select(Moves.POLLEN_PUFF);
await game.phaseInterceptor.to("BerryPhase");
expect(target.battleData.hitCount).toBe(2);
});
});

View File

@ -114,7 +114,7 @@ describe("The Strong Stuff - Mystery Encounter", () => {
bossSegments: 5,
shiny: false,
customPokemonData: new CustomPokemonData({ spriteScale: 1.25 }),
nature: Nature.BOLD,
nature: Nature.HARDY,
moveSet: [Moves.INFESTATION, Moves.SALT_CURE, Moves.GASTRO_ACID, Moves.HEAL_ORDER],
modifierConfigs: expect.any(Array),
tags: [BattlerTagType.MYSTERY_ENCOUNTER_POST_SUMMON],
@ -198,7 +198,7 @@ describe("The Strong Stuff - Mystery Encounter", () => {
expect(scene.getCurrentPhase()?.constructor.name).toBe(CommandPhase.name);
expect(enemyField.length).toBe(1);
expect(enemyField[0].species.speciesId).toBe(Species.SHUCKLE);
expect(enemyField[0].summonData.statStages).toEqual([0, 2, 0, 2, 0, 0, 0]);
expect(enemyField[0].summonData.statStages).toEqual([0, 1, 0, 1, 0, 0, 0]);
const shuckleItems = enemyField[0].getHeldItems();
expect(shuckleItems.length).toBe(5);
expect(shuckleItems.find(m => m instanceof BerryModifier && m.berryType === BerryType.SITRUS)?.stackCount).toBe(

View File

@ -1,30 +1,37 @@
import type BattleScene from "#app/battle-scene";
import * as BattleAnims from "#app/data/battle-anims";
import { TrashToTreasureEncounter } from "#app/data/mystery-encounters/encounters/trash-to-treasure-encounter";
import * as MysteryEncounters from "#app/data/mystery-encounters/mystery-encounters";
import * as EncounterPhaseUtils from "#app/data/mystery-encounters/utils/encounter-phase-utils";
import {
type EnemyPartyConfig,
type EnemyPokemonConfig,
generateModifierType,
} from "#app/data/mystery-encounters/utils/encounter-phase-utils";
import { getPokemonSpecies } from "#app/data/pokemon-species";
import { Biome } from "#app/enums/biome";
import { MysteryEncounterType } from "#app/enums/mystery-encounter-type";
import { Species } from "#app/enums/species";
import GameManager from "#test/testUtils/gameManager";
import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest";
import { getPokemonSpecies } from "#app/data/pokemon-species";
import * as BattleAnims from "#app/data/battle-anims";
import * as EncounterPhaseUtils from "#app/data/mystery-encounters/utils/encounter-phase-utils";
import { PokemonMove } from "#app/field/pokemon";
import { HealShopCostModifier, HitHealModifier, TurnHealModifier } from "#app/modifier/modifier";
import { ModifierTier } from "#app/modifier/modifier-tier";
import { modifierTypes, type PokemonHeldItemModifierType } from "#app/modifier/modifier-type";
import { CommandPhase } from "#app/phases/command-phase";
import { MovePhase } from "#app/phases/move-phase";
import { SelectModifierPhase } from "#app/phases/select-modifier-phase";
import ModifierSelectUiHandler from "#app/ui/modifier-select-ui-handler";
import { Mode } from "#app/ui/ui";
import * as Utils from "#app/utils";
import { Moves } from "#enums/moves";
import { MysteryEncounterOptionMode } from "#enums/mystery-encounter-option-mode";
import { MysteryEncounterTier } from "#enums/mystery-encounter-tier";
import {
runMysteryEncounterToEnd,
skipBattleRunMysteryEncounterRewardsPhase,
} from "#test/mystery-encounter/encounter-test-utils";
import { Moves } from "#enums/moves";
import type BattleScene from "#app/battle-scene";
import { PokemonMove } from "#app/field/pokemon";
import { Mode } from "#app/ui/ui";
import ModifierSelectUiHandler from "#app/ui/modifier-select-ui-handler";
import { HitHealModifier, HealShopCostModifier, TurnHealModifier } from "#app/modifier/modifier";
import { MysteryEncounterOptionMode } from "#enums/mystery-encounter-option-mode";
import { MysteryEncounterTier } from "#enums/mystery-encounter-tier";
import GameManager from "#test/testUtils/gameManager";
import { initSceneWithoutEncounterPhase } from "#test/testUtils/gameManagerUtils";
import { TrashToTreasureEncounter } from "#app/data/mystery-encounters/encounters/trash-to-treasure-encounter";
import { ModifierTier } from "#app/modifier/modifier-tier";
import { SelectModifierPhase } from "#app/phases/select-modifier-phase";
import { CommandPhase } from "#app/phases/command-phase";
import { MovePhase } from "#app/phases/move-phase";
import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest";
const namespace = "mysteryEncounters/trashToTreasure";
const defaultParty = [Species.LAPRAS, Species.GENGAR, Species.ABRA];
@ -73,6 +80,7 @@ describe("Trash to Treasure - Mystery Encounter", () => {
});
it("should initialize fully", async () => {
vi.spyOn(Utils, "randSeedInt").mockImplementation((range, min = 0) => min + range - 1);
initSceneWithoutEncounterPhase(scene, defaultParty);
scene.currentBattle.mysteryEncounter = TrashToTreasureEncounter;
const moveInitSpy = vi.spyOn(BattleAnims, "initMoveAnim");
@ -85,22 +93,61 @@ describe("Trash to Treasure - Mystery Encounter", () => {
TrashToTreasureEncounter.populateDialogueTokensFromRequirements();
const onInitResult = onInit!();
expect(TrashToTreasureEncounter.enemyPartyConfigs).toEqual([
{
levelAdditiveModifier: 0.5,
disableSwitch: true,
pokemonConfigs: [
{
species: getPokemonSpecies(Species.GARBODOR),
isBoss: true,
shiny: false,
formIndex: 1,
bossSegmentModifier: 1,
moveSet: [Moves.PAYBACK, Moves.GUNK_SHOT, Moves.STOMPING_TANTRUM, Moves.DRAIN_PUNCH],
},
],
},
]);
const bossSpecies = getPokemonSpecies(Species.GARBODOR);
const pokemonConfig: EnemyPokemonConfig = {
species: bossSpecies,
isBoss: true,
shiny: false, // Shiny lock because of custom intro sprite
formIndex: 1, // Gmax
bossSegmentModifier: 1, // +1 Segment from normal
moveSet: [Moves.GUNK_SHOT, Moves.STOMPING_TANTRUM, Moves.HAMMER_ARM, Moves.PAYBACK],
modifierConfigs: [
{
modifier: generateModifierType(modifierTypes.BERRY) as PokemonHeldItemModifierType,
},
{
modifier: generateModifierType(modifierTypes.BERRY) as PokemonHeldItemModifierType,
},
{
modifier: generateModifierType(modifierTypes.BERRY) as PokemonHeldItemModifierType,
},
{
modifier: generateModifierType(modifierTypes.BERRY) as PokemonHeldItemModifierType,
},
{
modifier: generateModifierType(modifierTypes.BASE_STAT_BOOSTER) as PokemonHeldItemModifierType,
},
{
modifier: generateModifierType(modifierTypes.BASE_STAT_BOOSTER) as PokemonHeldItemModifierType,
},
{
modifier: generateModifierType(modifierTypes.TOXIC_ORB) as PokemonHeldItemModifierType,
stackCount: Utils.randSeedInt(2, 0),
},
{
modifier: generateModifierType(modifierTypes.SOOTHE_BELL) as PokemonHeldItemModifierType,
stackCount: Utils.randSeedInt(2, 1),
},
{
modifier: generateModifierType(modifierTypes.LUCKY_EGG) as PokemonHeldItemModifierType,
stackCount: Utils.randSeedInt(3, 1),
},
{
modifier: generateModifierType(modifierTypes.GOLDEN_EGG) as PokemonHeldItemModifierType,
stackCount: Utils.randSeedInt(2, 0),
},
],
};
const config: EnemyPartyConfig = {
levelAdditiveModifier: 0.5,
pokemonConfigs: [pokemonConfig],
disableSwitch: true,
};
const enemyPartyConfigs = [config];
expect(JSON.stringify(TrashToTreasureEncounter.enemyPartyConfigs, undefined, 2)).toEqual(
JSON.stringify(enemyPartyConfigs, undefined, 2),
);
await vi.waitFor(() => expect(moveInitSpy).toHaveBeenCalled());
await vi.waitFor(() => expect(moveLoadSpy).toHaveBeenCalled());
expect(onInitResult).toBe(true);
@ -122,7 +169,7 @@ describe("Trash to Treasure - Mystery Encounter", () => {
});
});
it("should give 2 Leftovers, 2 Shell Bell, and Black Sludge", async () => {
it("should give 2 Leftovers, 1 Shell Bell, and Black Sludge", async () => {
await game.runToMysteryEncounter(MysteryEncounterType.TRASH_TO_TREASURE, defaultParty);
await runMysteryEncounterToEnd(game, 1);
await game.phaseInterceptor.to(SelectModifierPhase, false);
@ -134,7 +181,7 @@ describe("Trash to Treasure - Mystery Encounter", () => {
const shellBell = scene.findModifier(m => m instanceof HitHealModifier) as HitHealModifier;
expect(shellBell).toBeDefined();
expect(shellBell?.stackCount).toBe(2);
expect(shellBell?.stackCount).toBe(1);
const blackSludge = scene.findModifier(m => m instanceof HealShopCostModifier) as HealShopCostModifier;
expect(blackSludge).toBeDefined();
@ -178,17 +225,17 @@ describe("Trash to Treasure - Mystery Encounter", () => {
expect(enemyField.length).toBe(1);
expect(enemyField[0].species.speciesId).toBe(Species.GARBODOR);
expect(enemyField[0].moveset).toEqual([
new PokemonMove(Moves.PAYBACK),
new PokemonMove(Moves.GUNK_SHOT),
new PokemonMove(Moves.STOMPING_TANTRUM),
new PokemonMove(Moves.DRAIN_PUNCH),
new PokemonMove(Moves.HAMMER_ARM),
new PokemonMove(Moves.PAYBACK),
]);
// Should have used moves pre-battle
const movePhases = phaseSpy.mock.calls.filter(p => p[0] instanceof MovePhase).map(p => p[0]);
expect(movePhases.length).toBe(2);
expect(movePhases.filter(p => (p as MovePhase).move.moveId === Moves.TOXIC).length).toBe(1);
expect(movePhases.filter(p => (p as MovePhase).move.moveId === Moves.AMNESIA).length).toBe(1);
expect(movePhases.filter(p => (p as MovePhase).move.moveId === Moves.STOCKPILE).length).toBe(1);
});
it("should have 2 Rogue, 1 Ultra, 1 Great in rewards", async () => {