Merge branch 'beta' into romanian-workspace

This commit is contained in:
Lugiad 2025-04-03 16:30:40 +02:00 committed by GitHub
commit b4c1d91f8d
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", "name": "pokemon-rogue-battle",
"private": true, "private": true,
"version": "1.7.7", "version": "1.8.4",
"type": "module", "type": "module",
"scripts": { "scripts": {
"start": "vite", "start": "vite",
@ -62,7 +62,7 @@
"i18next-korean-postposition-processor": "^1.0.0", "i18next-korean-postposition-processor": "^1.0.0",
"json-stable-stringify": "^1.2.0", "json-stable-stringify": "^1.2.0",
"jszip": "^3.10.1", "jszip": "^3.10.1",
"phaser": "^3.88.2", "phaser": "^3.70.0",
"phaser3-rex-plugins": "^1.80.14" "phaser3-rex-plugins": "^1.80.14"
}, },
"engines": { "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 { initGlobalScene } from "#app/global-scene";
import { ShowAbilityPhase } from "#app/phases/show-ability-phase"; import { ShowAbilityPhase } from "#app/phases/show-ability-phase";
import { HideAbilityPhase } from "#app/phases/hide-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"; export const bypassLogin = import.meta.env.VITE_BYPASS_LOGIN === "1";
@ -2268,6 +2269,9 @@ export default class BattleScene extends SceneBase {
if (bgmName === undefined) { if (bgmName === undefined) {
bgmName = this.currentBattle?.getBgmOverride() || this.arena?.bgm; bgmName = this.currentBattle?.getBgmOverride() || this.arena?.bgm;
} }
bgmName = timedEventManager.getEventBgmReplacement(bgmName);
if (this.bgm && bgmName === this.bgm.key) { if (this.bgm && bgmName === this.bgm.key) {
if (!this.bgm.isPlaying) { if (!this.bgm.isPlaying) {
this.bgm.play({ this.bgm.play({
@ -2660,6 +2664,10 @@ export default class BattleScene extends SceneBase {
return 41.42; return 41.42;
case "mystery_encounter_delibirdy": // Firel Delibirdy case "mystery_encounter_delibirdy": // Firel Delibirdy
return 82.28; 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; return 0;
@ -2929,14 +2937,19 @@ export default class BattleScene extends SceneBase {
* @param show Whether to show or hide the bar * @param show Whether to show or hide the bar
*/ */
public queueAbilityDisplay(pokemon: Pokemon, passive: boolean, show: boolean): void { public queueAbilityDisplay(pokemon: Pokemon, passive: boolean, show: boolean): void {
this.unshiftPhase( this.unshiftPhase(show ? new ShowAbilityPhase(pokemon.getBattlerIndex(), passive) : new HideAbilityPhase());
show
? new ShowAbilityPhase(pokemon.getBattlerIndex(), passive)
: new HideAbilityPhase(pokemon.getBattlerIndex(), passive),
);
this.clearPhaseQueueSplice(); 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) * Moves everything from nextCommandPhaseQueue to phaseQueue (keeping order)
*/ */

View File

@ -2,7 +2,7 @@
export const PLAYER_PARTY_MAX_SIZE: number = 6; export const PLAYER_PARTY_MAX_SIZE: number = 6;
/** Whether to use seasonal splash messages in general */ /** 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 */ /** Name of the session ID cookie */
export const SESSION_ID_COOKIE_NAME: string = "pokerogue_sessionId"; export const SESSION_ID_COOKIE_NAME: string = "pokerogue_sessionId";

View File

@ -854,7 +854,8 @@ export class PostDefendStatStageChangeAbAttr extends PostDefendAbAttr {
} }
if (this.allOthers) { 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) { for (const other of otherPokemon) {
globalScene.unshiftPhase(new StatStageChangePhase((other).getBattlerIndex(), false, [ this.stat ], this.stages)); 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 { 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 { override applyPostSummon(pokemon: Pokemon, passive: boolean, simulated: boolean, args: any[]): void {
const target = pokemon.getAlly(); const target = pokemon.getAlly();
if (!simulated) { if (!simulated && !Utils.isNullOrUndefined(target)) {
globalScene.unshiftPhase(new PokemonHealPhase(target.getBattlerIndex(), 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)); 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 { 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 { override applyPostSummon(pokemon: Pokemon, passive: boolean, simulated: boolean, args: any[]): void {
const target = pokemon.getAlly(); const target = pokemon.getAlly();
if (!simulated) { if (!simulated && !Utils.isNullOrUndefined(target)) {
for (const s of BATTLE_STATS) { for (const s of BATTLE_STATS) {
target.setStatStage(s, 0); target.setStatStage(s, 0);
} }
@ -2712,7 +2713,7 @@ export class PostSummonCopyAllyStatsAbAttr extends PostSummonAbAttr {
} }
const ally = pokemon.getAlly(); const ally = pokemon.getAlly();
if (!ally || ally.getStatStages().every(s => s === 0)) { if (Utils.isNullOrUndefined(ally) || ally.getStatStages().every(s => s === 0)) {
return false; return false;
} }
@ -2721,7 +2722,7 @@ export class PostSummonCopyAllyStatsAbAttr extends PostSummonAbAttr {
override applyPostSummon(pokemon: Pokemon, passive: boolean, simulated: boolean, args: any[]): void { override applyPostSummon(pokemon: Pokemon, passive: boolean, simulated: boolean, args: any[]): void {
const ally = pokemon.getAlly(); const ally = pokemon.getAlly();
if (!simulated) { if (!simulated && !Utils.isNullOrUndefined(ally)) {
for (const s of BATTLE_STATS) { for (const s of BATTLE_STATS) {
pokemon.setStatStage(s, ally.getStatStage(s)); pokemon.setStatStage(s, ally.getStatStage(s));
} }
@ -2822,7 +2823,7 @@ export class PostSummonFormChangeByWeatherAbAttr extends PostSummonAbAttr {
private ability: Abilities; private ability: Abilities;
constructor(ability: Abilities) { constructor(ability: Abilities) {
super(false); super(true);
this.ability = ability; this.ability = ability;
} }
@ -2846,7 +2847,6 @@ export class PostSummonFormChangeByWeatherAbAttr extends PostSummonAbAttr {
if (!simulated) { if (!simulated) {
globalScene.triggerPokemonFormChange(pokemon, SpeciesFormChangeWeatherTrigger); globalScene.triggerPokemonFormChange(pokemon, SpeciesFormChangeWeatherTrigger);
globalScene.triggerPokemonFormChange(pokemon, SpeciesFormChangeRevertWeatherFormTrigger); globalScene.triggerPokemonFormChange(pokemon, SpeciesFormChangeRevertWeatherFormTrigger);
globalScene.queueAbilityDisplay(pokemon, passive, true);
} }
} }
} }
@ -2867,8 +2867,9 @@ export class CommanderAbAttr extends AbAttr {
// another Pokemon, this effect cannot apply. // another Pokemon, this effect cannot apply.
// TODO: Should this work with X + Dondozo fusions? // TODO: Should this work with X + Dondozo fusions?
return globalScene.currentBattle?.double && pokemon.getAlly()?.species.speciesId === Species.DONDOZO const ally = pokemon.getAlly();
&& !(pokemon.getAlly().isFainted() || pokemon.getAlly().getTag(BattlerTagType.COMMANDED)); 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 { 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 // Play an animation of the source jumping into the ally Dondozo's mouth
globalScene.triggerPokemonBattleAnim(pokemon, PokemonAnimType.COMMANDER_APPLY); globalScene.triggerPokemonBattleAnim(pokemon, PokemonAnimType.COMMANDER_APPLY);
// Apply boosts from this effect to the ally Dondozo // 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) // Cancel the source Pokemon's next move (if a move is queued)
globalScene.tryRemovePhase((phase) => phase instanceof MovePhase && phase.pokemon === pokemon); globalScene.tryRemovePhase((phase) => phase instanceof MovePhase && phase.pokemon === pokemon);
} }
@ -4078,7 +4079,7 @@ export class PostTurnStatusHealAbAttr extends PostTurnAbAttr {
*/ */
export class PostTurnResetStatusAbAttr extends PostTurnAbAttr { export class PostTurnResetStatusAbAttr extends PostTurnAbAttr {
private allyTarget: boolean; private allyTarget: boolean;
private target: Pokemon; private target: Pokemon | undefined;
constructor(allyTarget = false) { constructor(allyTarget = false) {
super(true); super(true);
@ -4091,11 +4092,11 @@ export class PostTurnResetStatusAbAttr extends PostTurnAbAttr {
} else { } else {
this.target = pokemon; 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 { 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))); globalScene.queueMessage(getStatusEffectHealText(this.target.status?.effect, getPokemonNameWithAffix(this.target)));
this.target.resetStatus(false); this.target.resetStatus(false);
this.target.updateInfo(); 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 * 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 { } else {
const allyPokemon = switchOutTarget.getAlly();
if (!globalScene.currentBattle.waveIndex || globalScene.currentBattle.waveIndex % 10 === 0) { if (!globalScene.currentBattle.waveIndex || globalScene.currentBattle.waveIndex % 10 === 0) {
return false; return false;
} }
@ -5448,14 +5451,12 @@ class ForceSwitchOutHelper {
if (switchOutTarget.hp > 0) { if (switchOutTarget.hp > 0) {
switchOutTarget.leaveField(false); switchOutTarget.leaveField(false);
globalScene.queueMessage(i18next.t("moveTriggers:fled", { pokemonName: getPokemonNameWithAffix(switchOutTarget) }), null, true, 500); globalScene.queueMessage(i18next.t("moveTriggers:fled", { pokemonName: getPokemonNameWithAffix(switchOutTarget) }), null, true, 500);
if (globalScene.currentBattle.double && !Utils.isNullOrUndefined(allyPokemon)) {
if (globalScene.currentBattle.double) {
const allyPokemon = switchOutTarget.getAlly();
globalScene.redirectPokemonMoves(switchOutTarget, allyPokemon); globalScene.redirectPokemonMoves(switchOutTarget, allyPokemon);
} }
} }
if (!switchOutTarget.getAlly()?.isActive(true)) { if (!allyPokemon?.isActive(true)) {
globalScene.clearEnemyHeldItemModifiers(); globalScene.clearEnemyHeldItemModifiers();
if (switchOutTarget.hp) { if (switchOutTarget.hp) {
@ -6441,9 +6442,9 @@ export function initAbilities() {
new Ability(Abilities.CUTE_CHARM, 3) new Ability(Abilities.CUTE_CHARM, 3)
.attr(PostDefendContactApplyTagChanceAbAttr, 30, BattlerTagType.INFATUATED), .attr(PostDefendContactApplyTagChanceAbAttr, 30, BattlerTagType.INFATUATED),
new Ability(Abilities.PLUS, 3) 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) 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) new Ability(Abilities.FORECAST, 3)
.uncopiable() .uncopiable()
.unreplaceable() .unreplaceable()
@ -6670,7 +6671,7 @@ export function initAbilities() {
.attr(PostDefendMoveDisableAbAttr, 30) .attr(PostDefendMoveDisableAbAttr, 30)
.bypassFaint(), .bypassFaint(),
new Ability(Abilities.HEALER, 5) 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) new Ability(Abilities.FRIEND_GUARD, 5)
.attr(AlliedFieldDamageReductionAbAttr, 0.75) .attr(AlliedFieldDamageReductionAbAttr, 0.75)
.ignorable(), .ignorable(),

View File

@ -28,7 +28,6 @@ import { BattlerTagType } from "#enums/battler-tag-type";
import { Moves } from "#enums/moves"; import { Moves } from "#enums/moves";
import { MoveEffectPhase } from "#app/phases/move-effect-phase"; import { MoveEffectPhase } from "#app/phases/move-effect-phase";
import { PokemonHealPhase } from "#app/phases/pokemon-heal-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 { StatStageChangePhase } from "#app/phases/stat-stage-change-phase";
import { CommonAnimPhase } from "#app/phases/common-anim-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 // 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)) { 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.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.VENUSAUR]: { 0: Abilities.GRASSY_SURGE, 1: Abilities.SEED_SOWER, 2: Abilities.FLOWER_VEIL },
[Species.CHARMANDER]: { 0: Abilities.SHEER_FORCE }, [Species.CHARMANDER]: { 0: Abilities.SHEER_FORCE },
[Species.CHARMELEON]: { 0: Abilities.BEAST_BOOST }, [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.SQUIRTLE]: { 0: Abilities.DAUNTLESS_SHIELD },
[Species.WARTORTLE]: { 0: Abilities.DAUNTLESS_SHIELD }, [Species.WARTORTLE]: { 0: Abilities.DAUNTLESS_SHIELD },
[Species.BLASTOISE]: { 0: Abilities.DAUNTLESS_SHIELD, 1: Abilities.BULLETPROOF, 2: Abilities.BULLETPROOF }, [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.LEAFEON]: { 0: Abilities.GRASSY_SURGE },
[Species.GLACEON]: { 0: Abilities.SNOW_WARNING }, [Species.GLACEON]: { 0: Abilities.SNOW_WARNING },
[Species.SYLVEON]: { 0: Abilities.COMPETITIVE }, [Species.SYLVEON]: { 0: Abilities.COMPETITIVE },
[Species.PORYGON]: { 0: Abilities.LEVITATE }, [Species.PORYGON]: { 0: Abilities.TRANSISTOR },
[Species.PORYGON2]: { 0: Abilities.LEVITATE }, [Species.PORYGON2]: { 0: Abilities.TRANSISTOR },
[Species.PORYGON_Z]: { 0: Abilities.PROTEAN }, [Species.PORYGON_Z]: { 0: Abilities.PROTEAN },
[Species.OMANYTE]: { 0: Abilities.STURDY }, [Species.OMANYTE]: { 0: Abilities.STURDY },
[Species.OMASTAR]: { 0: Abilities.STURDY }, [Species.OMASTAR]: { 0: Abilities.STURDY },
[Species.KABUTO]: { 0: Abilities.TOUGH_CLAWS }, [Species.KABUTO]: { 0: Abilities.TOUGH_CLAWS },
[Species.KABUTOPS]: { 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.ARTICUNO]: { 0: Abilities.SNOW_WARNING },
[Species.ZAPDOS]: { 0: Abilities.DRIZZLE }, [Species.ZAPDOS]: { 0: Abilities.DRIZZLE },
[Species.MOLTRES]: { 0: Abilities.DROUGHT }, [Species.MOLTRES]: { 0: Abilities.DROUGHT },
@ -309,7 +309,7 @@ export const starterPassiveAbilities: StarterPassiveAbilities = {
[Species.SHIFTRY]: { 0: Abilities.SHARPNESS }, [Species.SHIFTRY]: { 0: Abilities.SHARPNESS },
[Species.TAILLOW]: { 0: Abilities.AERILATE }, [Species.TAILLOW]: { 0: Abilities.AERILATE },
[Species.SWELLOW]: { 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.PELIPPER]: { 0: Abilities.SWIFT_SWIM },
[Species.RALTS]: { 0: Abilities.NEUROFORCE }, [Species.RALTS]: { 0: Abilities.NEUROFORCE },
[Species.KIRLIA]: { 0: Abilities.NEUROFORCE }, [Species.KIRLIA]: { 0: Abilities.NEUROFORCE },
@ -612,8 +612,8 @@ export const starterPassiveAbilities: StarterPassiveAbilities = {
[Species.REUNICLUS]: { 0: Abilities.PSYCHIC_SURGE }, [Species.REUNICLUS]: { 0: Abilities.PSYCHIC_SURGE },
[Species.DUCKLETT]: { 0: Abilities.DRIZZLE }, [Species.DUCKLETT]: { 0: Abilities.DRIZZLE },
[Species.SWANNA]: { 0: Abilities.DRIZZLE }, [Species.SWANNA]: { 0: Abilities.DRIZZLE },
[Species.VANILLITE]: { 0: Abilities.SNOW_WARNING }, [Species.VANILLITE]: { 0: Abilities.REFRIGERATE },
[Species.VANILLISH]: { 0: Abilities.SNOW_WARNING }, [Species.VANILLISH]: { 0: Abilities.REFRIGERATE },
[Species.VANILLUXE]: { 0: Abilities.SLUSH_RUSH }, [Species.VANILLUXE]: { 0: Abilities.SLUSH_RUSH },
[Species.DEERLING]: { 0: Abilities.FLOWER_VEIL, 1: Abilities.CUD_CHEW, 2: Abilities.HARVEST, 3: Abilities.FUR_COAT }, [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 }, [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.CELESTEELA]: { 0: Abilities.HEATPROOF },
[Species.KARTANA]: { 0: Abilities.TECHNICIAN }, [Species.KARTANA]: { 0: Abilities.TECHNICIAN },
[Species.GUZZLORD]: { 0: Abilities.POISON_HEAL }, [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.MAGEARNA]: { 0: Abilities.STEELY_SPIRIT, 1: Abilities.STEELY_SPIRIT },
[Species.MARSHADOW]: { 0: Abilities.IRON_FIST }, [Species.MARSHADOW]: { 0: Abilities.IRON_FIST },
[Species.POIPOLE]: { 0: Abilities.LEVITATE }, [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 { MoveEffectPhase } from "#app/phases/move-effect-phase";
import { MovePhase } from "#app/phases/move-phase"; import { MovePhase } from "#app/phases/move-phase";
import { PokemonHealPhase } from "#app/phases/pokemon-heal-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 type { StatStageChangeCallback } from "#app/phases/stat-stage-change-phase";
import { StatStageChangePhase } from "#app/phases/stat-stage-change-phase"; import { StatStageChangePhase } from "#app/phases/stat-stage-change-phase";
import i18next from "#app/plugins/i18n"; import i18next from "#app/plugins/i18n";
@ -1901,12 +1900,14 @@ export class TruantTag extends AbilityBattlerTag {
if (lastMove && lastMove.move !== Moves.NONE) { if (lastMove && lastMove.move !== Moves.NONE) {
(globalScene.getCurrentPhase() as MovePhase).cancel(); (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( globalScene.queueMessage(
i18next.t("battlerTags:truantLapse", { i18next.t("battlerTags:truantLapse", {
pokemonNameWithAffix: getPokemonNameWithAffix(pokemon), pokemonNameWithAffix: getPokemonNameWithAffix(pokemon),
}), }),
); );
globalScene.queueAbilityDisplay(pokemon, passive, false);
} }
return true; return true;

View File

@ -1272,23 +1272,22 @@ function checkSpeciesValidForChallenge(species: PokemonSpecies, props: DexAttrPr
if (soft && isValidForChallenge.value) { if (soft && isValidForChallenge.value) {
return true; return true;
} }
pokemonFormChanges[species.speciesId].forEach(f1 => {
// Exclude form changes that require the mon to be on the field to begin with, const result = pokemonFormChanges[species.speciesId].some(f1 => {
// such as Castform // Exclude form changes that require the mon to be on the field to begin with
if (!("item" in f1)) { if (!("item" in f1.trigger)) {
return;
}
species.forms.forEach((f2, formIndex) => {
if (f1.formKey === f2.formKey) {
const formProps = { ...props };
formProps.formIndex = formIndex;
const isFormValidForChallenge = new Utils.BooleanHolder(true);
applyChallenges(ChallengeType.STARTER_CHOICE, species, isFormValidForChallenge, formProps);
if (isFormValidForChallenge.value) {
return true;
}
}
});
});
return false; return false;
} }
return species.forms.some((f2, formIndex) => {
if (f1.formKey === f2.formKey) {
const formProps = { ...props, formIndex };
const isFormValidForChallenge = new Utils.BooleanHolder(true);
applyChallenges(ChallengeType.STARTER_CHOICE, species, isFormValidForChallenge, formProps);
return isFormValidForChallenge.value;
}
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 { StatStageChangePhase } from "#app/phases/stat-stage-change-phase";
import { SwitchPhase } from "#app/phases/switch-phase"; import { SwitchPhase } from "#app/phases/switch-phase";
import { SwitchSummonPhase } from "#app/phases/switch-summon-phase"; import { SwitchSummonPhase } from "#app/phases/switch-summon-phase";
import { ShowAbilityPhase } from "#app/phases/show-ability-phase";
import { SpeciesFormChangeRevertWeatherFormTrigger } from "../pokemon-forms"; import { SpeciesFormChangeRevertWeatherFormTrigger } from "../pokemon-forms";
import type { GameMode } from "#app/game-mode"; import type { GameMode } from "#app/game-mode";
import { applyChallenges, ChallengeType } from "../challenge"; import { applyChallenges, ChallengeType } from "../challenge";
@ -789,9 +788,9 @@ export default class Move implements Localizable {
} }
applyPreAttackAbAttrs(VariableMovePowerAbAttr, source, target, this, simulated, power); applyPreAttackAbAttrs(VariableMovePowerAbAttr, source, target, this, simulated, power);
const ally = source.getAlly();
if (source.getAlly()) { if (!Utils.isNullOrUndefined(ally)) {
applyPreAttackAbAttrs(AllyMoveCategoryPowerBoostAbAttr, source.getAlly(), target, this, simulated, power); applyPreAttackAbAttrs(AllyMoveCategoryPowerBoostAbAttr, ally, target, this, simulated, power);
} }
const fieldAuras = new Set( const fieldAuras = new Set(
@ -912,7 +911,8 @@ export default class Move implements Localizable {
]; ];
// ...and cannot enhance Pollen Puff when targeting an ally. // ...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) return (!restrictSpread || !isMultiTarget)
&& !this.isChargingMove() && !this.isChargingMove()
@ -1924,7 +1924,9 @@ export class PartyStatusCureAttr extends MoveEffectAttr {
pokemon.resetStatus(); pokemon.resetStatus();
pokemon.updateInfo(); pokemon.updateInfo();
} else { } 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 targetAlly = target.getAlly();
const cancelled = new Utils.BooleanHolder(false); const cancelled = new Utils.BooleanHolder(false);
if (targetAlly) { if (!Utils.isNullOrUndefined(targetAlly)) {
applyAbAttrs(BlockNonDirectDamageAbAttr, targetAlly, cancelled); applyAbAttrs(BlockNonDirectDamageAbAttr, targetAlly, cancelled);
} }
@ -1958,7 +1960,7 @@ export class FlameBurstAttr extends MoveEffectAttr {
} }
getTargetBenefitScore(user: Pokemon, target: Pokemon, move: Move): number { 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 userAlly = user.getAlly();
const enemyAlly = enemy?.getAlly(); const enemyAlly = enemy?.getAlly();
if (userAlly && userAlly.turnData.acted) { if (!Utils.isNullOrUndefined(userAlly) && userAlly.turnData.acted) {
pokemonActed.push(userAlly); pokemonActed.push(userAlly);
} }
if (enemyAlly && enemyAlly.turnData.acted) { if (!Utils.isNullOrUndefined(enemyAlly) && enemyAlly.turnData.acted) {
pokemonActed.push(enemyAlly); pokemonActed.push(enemyAlly);
} }
} }
@ -6161,9 +6163,8 @@ export class RevivalBlessingAttr extends MoveEffectAttr {
pokemon.resetStatus(); pokemon.resetStatus();
pokemon.heal(Math.min(Utils.toDmgValue(0.5 * pokemon.getMaxHp()), pokemon.getMaxHp())); pokemon.heal(Math.min(Utils.toDmgValue(0.5 * pokemon.getMaxHp()), pokemon.getMaxHp()));
globalScene.queueMessage(i18next.t("moveTriggers:revivalBlessing", { pokemonName: getPokemonNameWithAffix(pokemon) }), 0, true); 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 // Handle cases where revived pokemon needs to get switched in on same turn
if (allyPokemon.isFainted() || allyPokemon === pokemon) { if (allyPokemon.isFainted() || allyPokemon === pokemon) {
// Enemy switch phase should be removed and replaced with the revived pkmn switching in // 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; return false;
} }
const allyPokemon = switchOutTarget.getAlly();
if (switchOutTarget.hp > 0) { if (switchOutTarget.hp > 0) {
switchOutTarget.leaveField(false); switchOutTarget.leaveField(false);
globalScene.queueMessage(i18next.t("moveTriggers:fled", { pokemonName: getPokemonNameWithAffix(switchOutTarget) }), null, true, 500); globalScene.queueMessage(i18next.t("moveTriggers:fled", { pokemonName: getPokemonNameWithAffix(switchOutTarget) }), null, true, 500);
// in double battles redirect potential moves off fled pokemon // in double battles redirect potential moves off fled pokemon
if (globalScene.currentBattle.double) { if (globalScene.currentBattle.double && !Utils.isNullOrUndefined(allyPokemon)) {
const allyPokemon = switchOutTarget.getAlly();
globalScene.redirectPokemonMoves(switchOutTarget, allyPokemon); globalScene.redirectPokemonMoves(switchOutTarget, allyPokemon);
} }
} }
if (!switchOutTarget.getAlly()?.isActive(true)) { if (!allyPokemon?.isActive(true)) {
globalScene.clearEnemyHeldItemModifiers(); globalScene.clearEnemyHeldItemModifiers();
if (switchOutTarget.hp) { if (switchOutTarget.hp) {
@ -7029,7 +7031,7 @@ export class RepeatMoveAttr extends MoveEffectAttr {
const firstTarget = globalScene.getField()[moveTargets[0]]; const firstTarget = globalScene.getField()[moveTargets[0]];
if (globalScene.currentBattle.double && moveTargets.length === 1 && firstTarget.isFainted() && firstTarget !== target.getAlly()) { if (globalScene.currentBattle.double && moveTargets.length === 1 && firstTarget.isFainted() && firstTarget !== target.getAlly()) {
const ally = firstTarget.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() ]; 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 })); globalScene.queueMessage(i18next.t("moveTriggers:copiedTargetAbility", { pokemonName: getPokemonNameWithAffix(user), targetName: getPokemonNameWithAffix(target), abilityName: allAbilities[target.getAbility().id].name }));
user.setTempAbility(target.getAbility()); user.setTempAbility(target.getAbility());
const ally = user.getAlly();
if (this.copyToPartner && globalScene.currentBattle?.double && user.getAlly().hp) { 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(user.getAlly()), targetName: getPokemonNameWithAffix(target), abilityName: allAbilities[target.getAbility().id].name })); globalScene.queueMessage(i18next.t("moveTriggers:copiedTargetAbility", { pokemonName: getPokemonNameWithAffix(ally), targetName: getPokemonNameWithAffix(target), abilityName: allAbilities[target.getAbility().id].name }));
user.getAlly().setTempAbility(target.getAbility()); ally.setTempAbility(target.getAbility());
} }
return true; return true;
@ -7414,9 +7417,10 @@ export class AbilityCopyAttr extends MoveEffectAttr {
getCondition(): MoveConditionFunc { getCondition(): MoveConditionFunc {
return (user, target, move) => { return (user, target, move) => {
const ally = user.getAlly();
let ret = target.getAbility().isCopiable && user.getAbility().isReplaceable; let ret = target.getAbility().isCopiable && user.getAbility().isReplaceable;
if (this.copyToPartner && globalScene.currentBattle?.double) { if (this.copyToPartner && globalScene.currentBattle?.double) {
ret = ret && (!user.getAlly().hp || user.getAlly().getAbility().isReplaceable); ret = ret && (!ally?.hp || ally?.getAbility().isReplaceable);
} else { } else {
ret = ret && user.getAbility().id !== target.getAbility().id; 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 set: Pokemon[] = [];
let multiple = false; let multiple = false;
const ally: Pokemon | undefined = user.getAlly();
switch (moveTarget) { switch (moveTarget) {
case MoveTarget.USER: case MoveTarget.USER:
@ -8197,7 +8202,7 @@ export function getMoveTargets(user: Pokemon, move: Moves, replaceTarget?: MoveT
case MoveTarget.OTHER: case MoveTarget.OTHER:
case MoveTarget.ALL_NEAR_OTHERS: case MoveTarget.ALL_NEAR_OTHERS:
case MoveTarget.ALL_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; multiple = moveTarget === MoveTarget.ALL_NEAR_OTHERS || moveTarget === MoveTarget.ALL_OTHERS;
break; break;
case MoveTarget.NEAR_ENEMY: case MoveTarget.NEAR_ENEMY:
@ -8214,21 +8219,22 @@ export function getMoveTargets(user: Pokemon, move: Moves, replaceTarget?: MoveT
return { targets: [ -1 as BattlerIndex ], multiple: false }; return { targets: [ -1 as BattlerIndex ], multiple: false };
case MoveTarget.NEAR_ALLY: case MoveTarget.NEAR_ALLY:
case MoveTarget.ALLY: case MoveTarget.ALLY:
set = [ user.getAlly() ]; set = !Utils.isNullOrUndefined(ally) ? [ ally ] : [];
break; break;
case MoveTarget.USER_OR_NEAR_ALLY: case MoveTarget.USER_OR_NEAR_ALLY:
case MoveTarget.USER_AND_ALLIES: case MoveTarget.USER_AND_ALLIES:
case MoveTarget.USER_SIDE: case MoveTarget.USER_SIDE:
set = [ user, user.getAlly() ]; set = !Utils.isNullOrUndefined(ally) ? [ user, ally ] : [ user ];
multiple = moveTarget !== MoveTarget.USER_OR_NEAR_ALLY; multiple = moveTarget !== MoveTarget.USER_OR_NEAR_ALLY;
break; break;
case MoveTarget.ALL: case MoveTarget.ALL:
case MoveTarget.BOTH_SIDES: case MoveTarget.BOTH_SIDES:
set = [ user, user.getAlly() ].concat(opponents); set = (!Utils.isNullOrUndefined(ally) ? [ user, ally ] : [ user ]).concat(opponents);
multiple = true; multiple = true;
break; break;
case MoveTarget.CURSE: 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; 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)) }) .attr(StatStageChangeAttr, [ Stat.DEF, Stat.SPDEF ], 1, false, { condition: (user, target, move) => !![ Abilities.PLUS, Abilities.MINUS ].find(a => target.hasAbility(a, false)) })
.ignoresSubstitute() .ignoresSubstitute()
.target(MoveTarget.USER_AND_ALLIES) .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 new StatusMove(Moves.HAPPY_HOUR, PokemonType.NORMAL, -1, 30, -1, 0, 6) // No animation
.attr(AddArenaTagAttr, ArenaTagType.HAPPY_HOUR, null, true) .attr(AddArenaTagAttr, ArenaTagType.HAPPY_HOUR, null, true)
.target(MoveTarget.USER_SIDE), .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)) }) .attr(StatStageChangeAttr, [ Stat.ATK, Stat.SPATK ], 1, false, { condition: (user, target, move) => !![ Abilities.PLUS, Abilities.MINUS ].find(a => target.hasAbility(a, false)) })
.ignoresSubstitute() .ignoresSubstitute()
.target(MoveTarget.USER_AND_ALLIES) .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) new AttackMove(Moves.THROAT_CHOP, PokemonType.DARK, MoveCategory.PHYSICAL, 80, 100, 15, 100, 0, 7)
.attr(AddBattlerTagAttr, BattlerTagType.THROAT_CHOPPED), .attr(AddBattlerTagAttr, BattlerTagType.THROAT_CHOPPED),
new AttackMove(Moves.POLLEN_PUFF, PokemonType.BUG, MoveCategory.SPECIAL, 90, 100, 15, -1, 0, 7) 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 { MysteryEncounterTier } from "#enums/mystery-encounter-tier";
import { MysteryEncounterType } from "#enums/mystery-encounter-type"; import { MysteryEncounterType } from "#enums/mystery-encounter-type";
import { Species } from "#enums/species"; import { Species } from "#enums/species";
import { timedEventManager } from "#app/global-event-manager";
/** the i18n namespace for this encounter */ /** the i18n namespace for this encounter */
const namespace = "mysteryEncounters/delibirdy"; const namespace = "mysteryEncounters/delibirdy";
@ -56,7 +57,7 @@ const OPTION_3_DISALLOWED_MODIFIERS = [
const DELIBIRDY_MONEY_PRICE_MULTIPLIER = 2; const DELIBIRDY_MONEY_PRICE_MULTIPLIER = 2;
const doEventReward = () => { const doEventReward = () => {
const event_buff = globalScene.eventManager.getDelibirdyBuff(); const event_buff = timedEventManager.getDelibirdyBuff();
if (event_buff.length > 0) { if (event_buff.length > 0) {
const candidates = event_buff.filter(c => { const candidates = event_buff.filter(c => {
const mtype = generateModifierType(modifierTypes[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 type { PokeballType } from "#enums/pokeball";
import { doShinySparkleAnim } from "#app/field/anims"; import { doShinySparkleAnim } from "#app/field/anims";
import { TrainerType } from "#enums/trainer-type"; import { TrainerType } from "#enums/trainer-type";
import { timedEventManager } from "#app/global-event-manager";
/** the i18n namespace for the encounter */ /** the i18n namespace for the encounter */
const namespace = "mysteryEncounters/globalTradeSystem"; 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) // Extra shiny roll at 1/128 odds (boosted by events and charms)
if (!tradePokemon.shiny) { if (!tradePokemon.shiny) {
const shinyThreshold = new NumberHolder(WONDER_TRADE_SHINY_CHANCE); const shinyThreshold = new NumberHolder(WONDER_TRADE_SHINY_CHANCE);
if (globalScene.eventManager.isEventActive()) { if (timedEventManager.isEventActive()) {
shinyThreshold.value *= globalScene.eventManager.getShinyMultiplier(); shinyThreshold.value *= timedEventManager.getShinyMultiplier();
} }
globalScene.applyModifiers(ShinyRateBoosterModifier, true, shinyThreshold); globalScene.applyModifiers(ShinyRateBoosterModifier, true, shinyThreshold);
@ -984,12 +985,11 @@ function doTradeReceivedSequence(
function generateRandomTraderName() { function generateRandomTraderName() {
const length = TrainerType.YOUNGSTER - TrainerType.ACE_TRAINER + 1; const length = TrainerType.YOUNGSTER - TrainerType.ACE_TRAINER + 1;
// +1 avoids TrainerType.UNKNOWN // +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 // Some trainers have 2 gendered pools, some do not
const gender = randInt(2) === 0 ? "MALE" : "FEMALE"; const genderKey = i18next.exists(`${classKey}.MALE`) ? (randInt(2) === 0 ? ".MALE" : ".FEMALE") : "";
const trainerNameString = randSeedItem( const trainerNameKey = randSeedItem(Object.keys(i18next.t(`${classKey}${genderKey}`, { returnObjects: true })));
Object.values(trainerTypePool.hasOwnProperty(gender) ? trainerTypePool[gender] : trainerTypePool), const trainerNameString = i18next.t(`${classKey}${genderKey}.${trainerNameKey}`);
) as string;
// Some names have an '&' symbol and need to be trimmed to a single name instead of a double name // Some names have an '&' symbol and need to be trimmed to a single name instead of a double name
const trainerNames = trainerNameString.split(" & "); const trainerNames = trainerNameString.split(" & ");
return trainerNames[randInt(trainerNames.length)]; return trainerNames[randInt(trainerNames.length)];

View File

@ -93,7 +93,7 @@ export const TheStrongStuffEncounter: MysteryEncounter = MysteryEncounterBuilder
bossSegments: 5, bossSegments: 5,
shiny: false, // Shiny lock because shiny is rolled only if the battle option is picked shiny: false, // Shiny lock because shiny is rolled only if the battle option is picked
customPokemonData: new CustomPokemonData({ spriteScale: 1.25 }), customPokemonData: new CustomPokemonData({ spriteScale: 1.25 }),
nature: Nature.BOLD, nature: Nature.HARDY,
moveSet: [Moves.INFESTATION, Moves.SALT_CURE, Moves.GASTRO_ACID, Moves.HEAL_ORDER], moveSet: [Moves.INFESTATION, Moves.SALT_CURE, Moves.GASTRO_ACID, Moves.HEAL_ORDER],
modifierConfigs: [ modifierConfigs: [
{ {
@ -117,7 +117,7 @@ export const TheStrongStuffEncounter: MysteryEncounter = MysteryEncounterBuilder
mysteryEncounterBattleEffects: (pokemon: Pokemon) => { mysteryEncounterBattleEffects: (pokemon: Pokemon) => {
queueEncounterMessage(`${namespace}:option.2.stat_boost`); queueEncounterMessage(`${namespace}:option.2.stat_boost`);
globalScene.unshiftPhase( 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 { BattlerIndex } from "#app/battle";
import { PokemonMove } from "#app/field/pokemon"; import { PokemonMove } from "#app/field/pokemon";
import { CLASSIC_MODE_MYSTERY_ENCOUNTER_WAVES } from "#app/game-mode"; import { CLASSIC_MODE_MYSTERY_ENCOUNTER_WAVES } from "#app/game-mode";
import { randSeedInt } from "#app/utils";
/** the i18n namespace for this encounter */ /** the i18n namespace for this encounter */
const namespace = "mysteryEncounters/trashToTreasure"; const namespace = "mysteryEncounters/trashToTreasure";
@ -80,7 +81,43 @@ export const TrashToTreasureEncounter: MysteryEncounter = MysteryEncounterBuilde
shiny: false, // Shiny lock because of custom intro sprite shiny: false, // Shiny lock because of custom intro sprite
formIndex: 1, // Gmax formIndex: 1, // Gmax
bossSegmentModifier: 1, // +1 Segment from normal 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 = { const config: EnemyPartyConfig = {
levelAdditiveModifier: 0.5, levelAdditiveModifier: 0.5,
@ -90,7 +127,7 @@ export const TrashToTreasureEncounter: MysteryEncounter = MysteryEncounterBuilde
encounter.enemyPartyConfigs = [config]; encounter.enemyPartyConfigs = [config];
// Load animations/sfx for Garbodor fight start moves // 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- Dig2", "battle_anims", "PRSFX- Dig2.wav");
globalScene.loadSe("PRSFX- Venom Drench", "battle_anims", "PRSFX- Venom Drench.wav"); globalScene.loadSe("PRSFX- Venom Drench", "battle_anims", "PRSFX- Venom Drench.wav");
@ -115,7 +152,7 @@ export const TrashToTreasureEncounter: MysteryEncounter = MysteryEncounterBuilde
doGarbageDig(); doGarbageDig();
}) })
.withOptionPhase(async () => { .withOptionPhase(async () => {
// Gain 2 Leftovers and 2 Shell Bell // Gain 2 Leftovers and 1 Shell Bell
await transitionMysteryEncounterIntroVisuals(); await transitionMysteryEncounterIntroVisuals();
await tryApplyDigRewardItems(); await tryApplyDigRewardItems();
@ -175,7 +212,7 @@ export const TrashToTreasureEncounter: MysteryEncounter = MysteryEncounterBuilde
{ {
sourceBattlerIndex: BattlerIndex.ENEMY, sourceBattlerIndex: BattlerIndex.ENEMY,
targets: [BattlerIndex.ENEMY], targets: [BattlerIndex.ENEMY],
move: new PokemonMove(Moves.AMNESIA), move: new PokemonMove(Moves.STOCKPILE),
ignorePp: true, ignorePp: true,
}, },
); );
@ -231,21 +268,7 @@ async function tryApplyDigRewardItems() {
true, true,
); );
// First Shell bell // Only 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
for (const pokemon of party) { for (const pokemon of party) {
const heldItems = globalScene.findModifiers( const heldItems = globalScene.findModifiers(
m => m instanceof PokemonHeldItemModifier && m.pokemonId === pokemon.id, m => m instanceof PokemonHeldItemModifier && m.pokemonId === pokemon.id,
@ -263,7 +286,7 @@ async function tryApplyDigRewardItems() {
await showEncounterText( await showEncounterText(
i18next.t("battle:rewardGainCount", { i18next.t("battle:rewardGainCount", {
modifierName: shellBell.name, modifierName: shellBell.name,
count: 2, count: 1,
}), }),
null, null,
undefined, undefined,

View File

@ -65,6 +65,7 @@ import { getPokemonSpecies } from "#app/data/pokemon-species";
import { PokemonType } from "#enums/pokemon-type"; import { PokemonType } from "#enums/pokemon-type";
import { getNatureName } from "#app/data/nature"; import { getNatureName } from "#app/data/nature";
import { getPokemonNameWithAffix } from "#app/messages"; import { getPokemonNameWithAffix } from "#app/messages";
import { timedEventManager } from "#app/global-event-manager";
/** /**
* Animates exclamation sprite over trainer's head at start of encounter * 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 { export function getRandomEncounterSpecies(level: number, isBoss = false, rerollHidden = false): EnemyPokemon {
let bossSpecies: PokemonSpecies; let bossSpecies: PokemonSpecies;
let isEventEncounter = false; let isEventEncounter = false;
const eventEncounters = globalScene.eventManager.getEventEncounters(); const eventEncounters = timedEventManager.getEventEncounters();
let formIndex: number | undefined; let formIndex: number | undefined;
if (eventEncounters.length > 0 && randSeedInt(2) === 1) { 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.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 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("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 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("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.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), 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 { TrainerPoolTier } from "#enums/trainer-pool-tier";
import { TrainerSlot } from "#enums/trainer-slot"; import { TrainerSlot } from "#enums/trainer-slot";
import { TrainerType } from "#enums/trainer-type"; import { TrainerType } from "#enums/trainer-type";
import { timedEventManager } from "#app/global-event-manager";
// Type imports // Type imports
import type { PokemonSpeciesFilter } from "#app/data/pokemon-species"; import type { PokemonSpeciesFilter } from "#app/data/pokemon-species";
@ -516,13 +517,13 @@ export class TrainerConfig {
// return ret; // return ret;
// } // }
setEventModifierRewardFuncs(...modifierTypeFuncs: (() => ModifierTypeFunc)[]): TrainerConfig { /**
this.eventRewardFuncs = modifierTypeFuncs.map(func => () => { * Sets eventRewardFuncs to the active event rewards for the specified wave
const modifierTypeFunc = func(); * @param wave Associated with {@linkcode getFixedBattleEventRewards}
const modifierType = modifierTypeFunc(); * @returns this
modifierType.withIdFromFunc(modifierTypeFunc); */
return modifierType; setEventModifierRewardFuncs(wave: number): TrainerConfig {
}); this.eventRewardFuncs = timedEventManager.getFixedBattleEventRewards(wave).map(r => modifierTypes[r]);
return this; return this;
} }
@ -3696,11 +3697,7 @@ export const trainerConfigs: TrainerConfigs = {
() => modifierTypes.SUPER_EXP_CHARM, () => modifierTypes.SUPER_EXP_CHARM,
() => modifierTypes.EXP_SHARE, () => modifierTypes.EXP_SHARE,
) )
.setEventModifierRewardFuncs( .setEventModifierRewardFuncs(8)
() => modifierTypes.SHINY_CHARM,
() => modifierTypes.ABILITY_CHARM,
() => modifierTypes.CATCHING_CHARM,
)
.setPartyMemberFunc( .setPartyMemberFunc(
0, 0,
getRandomPartyMemberFunc( getRandomPartyMemberFunc(
@ -3768,7 +3765,7 @@ export const trainerConfigs: TrainerConfigs = {
.setMixedBattleBgm("battle_rival") .setMixedBattleBgm("battle_rival")
.setPartyTemplates(trainerPartyTemplates.RIVAL_2) .setPartyTemplates(trainerPartyTemplates.RIVAL_2)
.setModifierRewardFuncs(() => modifierTypes.EXP_SHARE) .setModifierRewardFuncs(() => modifierTypes.EXP_SHARE)
.setEventModifierRewardFuncs(() => modifierTypes.SHINY_CHARM) .setEventModifierRewardFuncs(25)
.setPartyMemberFunc( .setPartyMemberFunc(
0, 0,
getRandomPartyMemberFunc( getRandomPartyMemberFunc(
@ -4077,7 +4074,7 @@ export const trainerConfigs: TrainerConfigs = {
getRandomPartyMemberFunc([Species.RAYQUAZA], TrainerSlot.TRAINER, true, p => { getRandomPartyMemberFunc([Species.RAYQUAZA], TrainerSlot.TRAINER, true, p => {
p.setBoss(true, 3); p.setBoss(true, 3);
p.pokeball = PokeballType.MASTER_BALL; p.pokeball = PokeballType.MASTER_BALL;
p.shiny = true; p.shiny = timedEventManager.getClassicTrainerShinyChance() === 0;
p.variant = 1; p.variant = 1;
}), }),
) )
@ -4174,7 +4171,7 @@ export const trainerConfigs: TrainerConfigs = {
p.setBoss(); p.setBoss();
p.generateAndPopulateMoveset(); p.generateAndPopulateMoveset();
p.pokeball = PokeballType.MASTER_BALL; p.pokeball = PokeballType.MASTER_BALL;
p.shiny = true; p.shiny = timedEventManager.getClassicTrainerShinyChance() === 0;
p.variant = 1; p.variant = 1;
p.formIndex = 1; // Mega Rayquaza p.formIndex = 1; // Mega Rayquaza
p.generateName(); p.generateName();

View File

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

View File

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

View File

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

View File

@ -33,14 +33,16 @@ export default class Trainer extends Phaser.GameObjects.Container {
public partyTemplateIndex: number; public partyTemplateIndex: number;
public name: string; public name: string;
public partnerName: string; public partnerName: string;
public nameKey: string;
public partnerNameKey: string | undefined;
public originalIndexes: { [key: number]: number } = {}; public originalIndexes: { [key: number]: number } = {};
constructor( constructor(
trainerType: TrainerType, trainerType: TrainerType,
variant: TrainerVariant, variant: TrainerVariant,
partyTemplateIndex?: number, partyTemplateIndex?: number,
name?: string, nameKey?: string,
partnerName?: string, partnerNameKey?: string,
trainerConfigOverride?: TrainerConfig, trainerConfigOverride?: TrainerConfig,
) { ) {
super(globalScene, -72, 80); super(globalScene, -72, 80);
@ -59,28 +61,41 @@ export default class Trainer extends Phaser.GameObjects.Container {
: Utils.randSeedWeightedItem(this.config.partyTemplates.map((_, i) => i)), : Utils.randSeedWeightedItem(this.config.partyTemplates.map((_, i) => i)),
this.config.partyTemplates.length - 1, this.config.partyTemplates.length - 1,
); );
if (i18next.exists("trainersCommon:" + TrainerType[trainerType], { returnObjects: true })) { const classKey = `trainersCommon:${TrainerType[trainerType]}`;
const namePool = i18next.t("trainersCommon:" + TrainerType[trainerType], { returnObjects: true }); if (i18next.exists(classKey, { returnObjects: true })) {
this.name = if (nameKey) {
name || this.nameKey = nameKey;
Utils.randSeedItem( } else {
Object.values( const genderKey = i18next.exists(`${classKey}.MALE`)
namePool.hasOwnProperty("MALE") ? variant === TrainerVariant.FEMALE
? namePool[variant === TrainerVariant.FEMALE ? "FEMALE" : "MALE"] ? ".FEMALE"
: namePool, : ".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 (variant === TrainerVariant.DOUBLE) {
if (this.config.doubleOnly) { if (this.config.doubleOnly) {
if (partnerName) { if (partnerNameKey) {
this.partnerName = partnerName; this.partnerNameKey = partnerNameKey;
this.partnerName = i18next.t(this.partnerNameKey);
} else { } else {
[this.name, this.partnerName] = this.name.split(" & "); [this.name, this.partnerName] = this.name.split(" & ");
} }
} else { } else {
this.partnerName = const partnerGenderKey = i18next.exists(`${classKey}.FEMALE`) ? ".FEMALE" : "";
partnerName || const partnerTrainerKey = Utils.randSeedItem(
Utils.randSeedItem(Object.values(namePool.hasOwnProperty("FEMALE") ? namePool["FEMALE"] : namePool)); 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 || {}; 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 * Helper function to see if a GameMode has a specific challenge type
* @param challenge the Challenges it looks for * @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 { initVouchers } from "#app/system/voucher";
import { Biome } from "#enums/biome"; import { Biome } from "#enums/biome";
import { initMysteryEncounters } from "#app/data/mystery-encounters/mystery-encounters"; import { initMysteryEncounters } from "#app/data/mystery-encounters/mystery-encounters";
import { timedEventManager } from "./global-event-manager";
export class LoadingScene extends SceneBase { export class LoadingScene extends SceneBase {
public static readonly KEY = "loading"; public static readonly KEY = "loading";
@ -250,11 +251,13 @@ export class LoadingScene extends SceneBase {
this.loadAtlas("statuses", ""); this.loadAtlas("statuses", "");
this.loadAtlas("types", ""); this.loadAtlas("types", "");
} }
const availableLangs = ["en", "de", "it", "fr", "ja", "ko", "es-ES", "es-MX", "pt-BR", "zh-CN", "zh-TW", "ca-ES"]; if (timedEventManager.activeEventHasBanner()) {
const availableLangs = timedEventManager.getEventBannerLangs();
if (lang && availableLangs.includes(lang)) { if (lang && availableLangs.includes(lang)) {
this.loadImage(`pkmnday2025event-${lang}`, "events"); this.loadImage(`${timedEventManager.getEventBannerFilename()}-${lang}`, "events");
} else { } else {
this.loadImage("pkmnday2025event-en", "events"); this.loadImage(`${timedEventManager.getEventBannerFilename()}-en`, "events");
}
} }
this.loadAtlas("statuses", ""); 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 { getStatKey, Stat, TEMP_BATTLE_STATS } from "#enums/stat";
import { StatusEffect } from "#enums/status-effect"; import { StatusEffect } from "#enums/status-effect";
import i18next from "i18next"; import i18next from "i18next";
import { timedEventManager } from "#app/global-event-manager";
const outputModifierData = false; const outputModifierData = false;
const useMaxWeightForOutput = false; const useMaxWeightForOutput = false;
@ -2655,7 +2656,7 @@ const modifierPool: ModifierPool = {
if (globalScene.gameMode.isSplicedOnly) { if (globalScene.gameMode.isSplicedOnly) {
return 4; return 4;
} }
if (globalScene.gameMode.isClassic && globalScene.eventManager.areFusionsBoosted()) { if (globalScene.gameMode.isClassic && timedEventManager.areFusionsBoosted()) {
return 2; return 2;
} }
} }
@ -2822,10 +2823,19 @@ const modifierPool: ModifierPool = {
modifierTypes.MYSTICAL_ROCK, modifierTypes.MYSTICAL_ROCK,
(party: Pokemon[]) => { (party: Pokemon[]) => {
return party.some(p => { return party.some(p => {
let isHoldingMax = false;
for (const i of p.getHeldItems()) {
if (i.type.id === "MYSTICAL_ROCK") {
isHoldingMax = i.getStackCount() === i.getMaxStackCount();
break;
}
}
if (!isHoldingMax) {
const moveset = p.getMoveset(true).map(m => m.moveId); const moveset = p.getMoveset(true).map(m => m.moveId);
const hasAbility = [ const hasAbility = [
Abilities.DRIZZLE, Abilities.DROUGHT,
Abilities.ORICHALCUM_PULSE, Abilities.ORICHALCUM_PULSE,
Abilities.DRIZZLE, Abilities.DRIZZLE,
Abilities.SAND_STREAM, Abilities.SAND_STREAM,
@ -2853,6 +2863,8 @@ const modifierPool: ModifierPool = {
].some(m => moveset.includes(m)); ].some(m => moveset.includes(m));
return hasAbility || hasMoves; return hasAbility || hasMoves;
}
return false;
}) })
? 10 ? 10
: 0; : 0;
@ -2939,7 +2951,7 @@ const modifierPool: ModifierPool = {
new WeightedModifierType( new WeightedModifierType(
modifierTypes.DNA_SPLICERS, modifierTypes.DNA_SPLICERS,
(party: Pokemon[]) => (party: Pokemon[]) =>
!(globalScene.gameMode.isClassic && globalScene.eventManager.areFusionsBoosted()) && !(globalScene.gameMode.isClassic && timedEventManager.areFusionsBoosted()) &&
!globalScene.gameMode.isSplicedOnly && !globalScene.gameMode.isSplicedOnly &&
party.filter(p => !p.fusionSpecies).length > 1 party.filter(p => !p.fusionSpecies).length > 1
? 24 ? 24
@ -3703,7 +3715,7 @@ export function getPartyLuckValue(party: Pokemon[]): number {
); );
return DailyLuck.value; return DailyLuck.value;
} }
const eventSpecies = globalScene.eventManager.getEventLuckBoostedSpecies(); const eventSpecies = timedEventManager.getEventLuckBoostedSpecies();
const luck = Phaser.Math.Clamp( const luck = Phaser.Math.Clamp(
party party
.map(p => (p.isAllowedInBattle() ? p.getLuck() + (eventSpecies.includes(p.species.speciesId) ? 1 : 0) : 0)) .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, 0,
14, 14,
); );
return Math.min(globalScene.eventManager.getEventLuckBoost() + (luck ?? 0), 14); return Math.min(timedEventManager.getEventLuckBoost() + (luck ?? 0), 14);
} }
export function getLuckString(luckValue: number): string { export function getLuckString(luckValue: number): string {

View File

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

View File

@ -209,8 +209,8 @@ export class FaintPhase extends PokemonPhase {
} }
// in double battles redirect potential moves off fainted pokemon // 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); globalScene.redirectPokemonMoves(pokemon, allyPokemon);
} }

View File

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

View File

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

View File

@ -69,6 +69,7 @@ import type { Phase } from "#app/phase";
import { ShowAbilityPhase } from "./show-ability-phase"; import { ShowAbilityPhase } from "./show-ability-phase";
import { MovePhase } from "./move-phase"; import { MovePhase } from "./move-phase";
import { MoveEndPhase } from "./move-end-phase"; import { MoveEndPhase } from "./move-end-phase";
import { HideAbilityPhase } from "#app/phases/hide-ability-phase";
export class MoveEffectPhase extends PokemonPhase { export class MoveEffectPhase extends PokemonPhase {
public move: PokemonMove; public move: PokemonMove;
@ -326,12 +327,14 @@ export class MoveEffectPhase extends PokemonPhase {
? getMoveTargets(target, move.id).targets ? getMoveTargets(target, move.id).targets
: [user.getBattlerIndex()]; : [user.getBattlerIndex()];
if (!isReflecting) { if (!isReflecting) {
// TODO: Ability displays should be handled by the ability
queuedPhases.push( queuedPhases.push(
new ShowAbilityPhase( new ShowAbilityPhase(
target.getBattlerIndex(), target.getBattlerIndex(),
target.getPassiveAbility().hasAttr(ReflectStatusMoveAbAttr), target.getPassiveAbility().hasAttr(ReflectStatusMoveAbAttr),
), ),
); );
queuedPhases.push(new HideAbilityPhase());
} }
queuedPhases.push( 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 { MoveChargePhase } from "#app/phases/move-charge-phase";
import { MoveEffectPhase } from "#app/phases/move-effect-phase"; import { MoveEffectPhase } from "#app/phases/move-effect-phase";
import { MoveEndPhase } from "#app/phases/move-end-phase"; import { MoveEndPhase } from "#app/phases/move-end-phase";
import { ShowAbilityPhase } from "#app/phases/show-ability-phase";
import { NumberHolder } from "#app/utils"; import { NumberHolder } from "#app/utils";
import { Abilities } from "#enums/abilities"; import { Abilities } from "#enums/abilities";
import { ArenaTagType } from "#enums/arena-tag-type"; import { ArenaTagType } from "#enums/arena-tag-type";
@ -535,11 +534,16 @@ export class MovePhase extends BattlePhase {
if (this.pokemon.hasAbilityWithAttr(BlockRedirectAbAttr)) { if (this.pokemon.hasAbilityWithAttr(BlockRedirectAbAttr)) {
redirectTarget.value = currentTarget; redirectTarget.value = currentTarget;
globalScene.unshiftPhase( // TODO: Ability displays should be handled by the ability
new ShowAbilityPhase( globalScene.queueAbilityDisplay(
this.pokemon.getBattlerIndex(), this.pokemon,
this.pokemon.getPassiveAbility().hasAttr(BlockRedirectAbAttr), 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, 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) { if (slotIndex <= 1) {
// Revived ally pokemon // Revived ally pokemon
globalScene.unshiftPhase( 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 the bar is already out, hide it before showing the new one
if (globalScene.abilityBar.isVisible()) { if (globalScene.abilityBar.isVisible()) {
globalScene.unshiftPhase(new HideAbilityPhase(this.battlerIndex, this.passive)); globalScene.unshiftPhase(new HideAbilityPhase());
globalScene.unshiftPhase(new ShowAbilityPhase(this.battlerIndex, this.passive)); globalScene.unshiftPhase(new ShowAbilityPhase(this.battlerIndex, this.passive));
return this.end(); return this.end();
} }

View File

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

View File

@ -212,6 +212,8 @@ export class TitlePhase extends Phase {
const generateDaily = (seed: string) => { const generateDaily = (seed: string) => {
globalScene.gameMode = getGameMode(GameModes.DAILY); 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.setSeed(seed);
globalScene.resetSeed(0); globalScene.resetSeed(0);

View File

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

View File

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

View File

@ -1793,7 +1793,9 @@ export class GameData {
const dexEntry = this.dexData[species.speciesId]; const dexEntry = this.dexData[species.speciesId];
const caughtAttr = dexEntry.caughtAttr; const caughtAttr = dexEntry.caughtAttr;
const formIndex = pokemon.formIndex; 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 // Mark as caught
dexEntry.caughtAttr |= dexAttr; dexEntry.caughtAttr |= dexAttr;
@ -1803,6 +1805,10 @@ export class GameData {
// always true except for the case of Urshifu. // always true except for the case of Urshifu.
const formKey = pokemon.getFormKey(); const formKey = pokemon.getFormKey();
if (formIndex > 0) { 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 (pokemon.species.speciesId === Species.URSHIFU) {
if (formIndex === 2) { if (formIndex === 2) {
dexEntry.caughtAttr |= globalScene.gameData.getFormAttr(0); dexEntry.caughtAttr |= globalScene.gameData.getFormAttr(0);

View File

@ -5,8 +5,8 @@ export default class TrainerData {
public trainerType: TrainerType; public trainerType: TrainerType;
public variant: TrainerVariant; public variant: TrainerVariant;
public partyTemplateIndex: number; public partyTemplateIndex: number;
public name: string; public nameKey: string;
public partnerName: string; public partnerNameKey: string | undefined;
constructor(source: Trainer | any) { constructor(source: Trainer | any) {
const sourceTrainer = source instanceof Trainer ? (source as Trainer) : null; const sourceTrainer = source instanceof Trainer ? (source as Trainer) : null;
@ -17,11 +17,11 @@ export default class TrainerData {
? TrainerVariant.FEMALE ? TrainerVariant.FEMALE
: TrainerVariant.DEFAULT; : TrainerVariant.DEFAULT;
this.partyTemplateIndex = source.partyMemberTemplateIndex; this.partyTemplateIndex = source.partyMemberTemplateIndex;
this.name = source.name; this.nameKey = source.nameKey;
this.partnerName = source.partnerName; this.partnerNameKey = source.partnerNameKey;
} }
toTrainer(): Trainer { 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 --- // // --- v1.7.0 PATCHES --- //
import * as v1_7_0 from "./versions/v1_7_0"; 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)); 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!"); console.log("Applying v1.7.0 system data migration!");
this.callMigrators(data, v1_7_0.systemMigrators); 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}!`); 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 { CLASSIC_CANDY_FRIENDSHIP_MULTIPLIER } from "./data/balance/starters";
import { MysteryEncounterType } from "./enums/mystery-encounter-type"; import { MysteryEncounterType } from "./enums/mystery-encounter-type";
import { MysteryEncounterTier } from "./enums/mystery-encounter-tier"; import { MysteryEncounterTier } from "./enums/mystery-encounter-tier";
import { Challenges } from "#enums/challenges";
export enum EventType { export enum EventType {
SHINY, SHINY,
@ -36,6 +37,18 @@ interface EventMysteryEncounterTier {
disable?: boolean; disable?: boolean;
} }
interface EventWaveReward {
wave: number;
type: string;
}
type EventMusicReplacement = [string, string];
interface EventChallenge {
challenge: Challenges;
value: number;
}
interface TimedEvent extends EventBanner { interface TimedEvent extends EventBanner {
name: string; name: string;
eventType: EventType; eventType: EventType;
@ -51,6 +64,10 @@ interface TimedEvent extends EventBanner {
mysteryEncounterTierChanges?: EventMysteryEncounterTier[]; mysteryEncounterTierChanges?: EventMysteryEncounterTier[];
luckBoostedSpecies?: Species[]; luckBoostedSpecies?: Species[];
boostFusions?: boolean; //MODIFIER REWORK PLEASE 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[] = [ const timedEvents: TimedEvent[] = [
@ -61,7 +78,7 @@ const timedEvents: TimedEvent[] = [
upgradeUnlockedVouchers: true, upgradeUnlockedVouchers: true,
startDate: new Date(Date.UTC(2024, 11, 21, 0)), startDate: new Date(Date.UTC(2024, 11, 21, 0)),
endDate: new Date(Date.UTC(2025, 0, 4, 0)), endDate: new Date(Date.UTC(2025, 0, 4, 0)),
bannerKey: "winter_holidays2024-event-", bannerKey: "winter_holidays2024-event",
scale: 0.21, scale: 0.21,
availableLangs: ["en", "de", "it", "fr", "ja", "ko", "es-ES", "pt-BR", "zh-CN"], availableLangs: ["en", "de", "it", "fr", "ja", "ko", "es-ES", "pt-BR", "zh-CN"],
eventEncounters: [ eventEncounters: [
@ -104,6 +121,12 @@ const timedEvents: TimedEvent[] = [
disable: true, 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", name: "Year of the Snake",
@ -111,7 +134,7 @@ const timedEvents: TimedEvent[] = [
luckBoost: 1, luckBoost: 1,
startDate: new Date(Date.UTC(2025, 0, 29, 0)), startDate: new Date(Date.UTC(2025, 0, 29, 0)),
endDate: new Date(Date.UTC(2025, 1, 3, 0)), endDate: new Date(Date.UTC(2025, 1, 3, 0)),
bannerKey: "yearofthesnakeevent-", bannerKey: "yearofthesnakeevent",
scale: 0.21, scale: 0.21,
availableLangs: ["en", "de", "it", "fr", "ja", "ko", "es-ES", "pt-BR", "zh-CN"], availableLangs: ["en", "de", "it", "fr", "ja", "ko", "es-ES", "pt-BR", "zh-CN"],
eventEncounters: [ eventEncounters: [
@ -169,6 +192,12 @@ const timedEvents: TimedEvent[] = [
Species.ROARING_MOON, Species.ROARING_MOON,
Species.BLOODMOON_URSALUNA, 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", name: "Valentine",
@ -177,7 +206,7 @@ const timedEvents: TimedEvent[] = [
endDate: new Date(Date.UTC(2025, 1, 21)), endDate: new Date(Date.UTC(2025, 1, 21)),
boostFusions: true, boostFusions: true,
shinyMultiplier: 2, shinyMultiplier: 2,
bannerKey: "valentines2025event-", bannerKey: "valentines2025event",
scale: 0.21, scale: 0.21,
availableLangs: ["en", "de", "it", "fr", "ja", "ko", "es-ES", "pt-BR", "zh-CN"], availableLangs: ["en", "de", "it", "fr", "ja", "ko", "es-ES", "pt-BR", "zh-CN"],
eventEncounters: [ eventEncounters: [
@ -203,6 +232,12 @@ const timedEvents: TimedEvent[] = [
{ species: Species.ENAMORUS }, { species: Species.ENAMORUS },
], ],
luckBoostedSpecies: [Species.LUVDISC], 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", name: "PKMNDAY2025",
@ -210,7 +245,7 @@ const timedEvents: TimedEvent[] = [
startDate: new Date(Date.UTC(2025, 1, 27)), startDate: new Date(Date.UTC(2025, 1, 27)),
endDate: new Date(Date.UTC(2025, 2, 4)), endDate: new Date(Date.UTC(2025, 2, 4)),
classicFriendshipMultiplier: 4, classicFriendshipMultiplier: 4,
bannerKey: "pkmnday2025event-", bannerKey: "pkmnday2025event",
scale: 0.21, scale: 0.21,
availableLangs: ["en", "de", "it", "fr", "ja", "ko", "es-ES", "pt-BR", "zh-CN"], availableLangs: ["en", "de", "it", "fr", "ja", "ko", "es-ES", "pt-BR", "zh-CN"],
eventEncounters: [ eventEncounters: [
@ -248,6 +283,32 @@ const timedEvents: TimedEvent[] = [
Species.ZYGARDE, Species.ZYGARDE,
Species.ETERNAL_FLOETTE, 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 { 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; return activeEvents.length > 0;
} }
@ -283,6 +344,12 @@ export class TimedEventManager {
return timedEvents.find((te: TimedEvent) => this.isActive(te))?.bannerKey ?? ""; 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[] { getEventEncounters(): EventEncounter[] {
const ret: EventEncounter[] = []; const ret: EventEncounter[] = [];
timedEvents timedEvents
@ -417,6 +484,55 @@ export class TimedEventManager {
areFusionsBoosted(): boolean { areFusionsBoosted(): boolean {
return timedEvents.some(te => this.isActive(te) && te.boostFusions); 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 { export class TimedEventDisplay extends Phaser.GameObjects.Container {
@ -456,11 +572,12 @@ export class TimedEventDisplay extends Phaser.GameObjects.Container {
let key = this.event.bannerKey; let key = this.event.bannerKey;
if (lang && this.event.availableLangs && this.event.availableLangs.length > 0) { if (lang && this.event.availableLangs && this.event.availableLangs.length > 0) {
if (this.event.availableLangs.includes(lang)) { if (this.event.availableLangs.includes(lang)) {
key += lang; key += "-" + lang;
} else { } else {
key += "en"; key += "-en";
} }
} }
console.log(key);
console.log(this.event.bannerKey); console.log(this.event.bannerKey);
const padding = 5; const padding = 5;
const showTimer = this.event.eventType !== EventType.NO_TIMER_DISPLAY; 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 type { Species } from "#enums/species";
import { getPokemonSpecies } from "#app/data/pokemon-species"; import { getPokemonSpecies } from "#app/data/pokemon-species";
import { PlayerGender } from "#enums/player-gender"; import { PlayerGender } from "#enums/player-gender";
import { timedEventManager } from "#app/global-event-manager";
export default class TitleUiHandler extends OptionSelectUiHandler { export default class TitleUiHandler extends OptionSelectUiHandler {
/** If the stats can not be retrieved, use this fallback value */ /** 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); logo.setOrigin(0.5, 0);
this.titleContainer.add(logo); this.titleContainer.add(logo);
if (globalScene.eventManager.isEventActive()) { if (timedEventManager.isEventActive()) {
this.eventDisplay = new TimedEventDisplay(0, 0, globalScene.eventManager.activeEvent()); this.eventDisplay = new TimedEventDisplay(0, 0, timedEventManager.activeEvent());
this.eventDisplay.setup(); this.eventDisplay.setup();
this.titleContainer.add(this.eventDisplay); this.titleContainer.add(this.eventDisplay);
} }
@ -142,7 +143,7 @@ export default class TitleUiHandler extends OptionSelectUiHandler {
const ui = this.getUi(); 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.setWidth(globalScene.scaledCanvas.width - this.optionSelectBg.width - this.optionSelectBg.x);
this.eventDisplay.show(); this.eventDisplay.show();
} }

View File

@ -9,7 +9,6 @@ import { StatusEffect } from "#enums/status-effect";
import GameManager from "#test/testUtils/gameManager"; import GameManager from "#test/testUtils/gameManager";
import Phaser from "phaser"; import Phaser from "phaser";
import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest"; import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest";
import { BattlerIndex } from "#app/battle";
describe("Abilities - Parental Bond", () => { describe("Abilities - Parental Bond", () => {
let phaserGame: Phaser.Game; 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 // 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); 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, bossSegments: 5,
shiny: false, shiny: false,
customPokemonData: new CustomPokemonData({ spriteScale: 1.25 }), customPokemonData: new CustomPokemonData({ spriteScale: 1.25 }),
nature: Nature.BOLD, nature: Nature.HARDY,
moveSet: [Moves.INFESTATION, Moves.SALT_CURE, Moves.GASTRO_ACID, Moves.HEAL_ORDER], moveSet: [Moves.INFESTATION, Moves.SALT_CURE, Moves.GASTRO_ACID, Moves.HEAL_ORDER],
modifierConfigs: expect.any(Array), modifierConfigs: expect.any(Array),
tags: [BattlerTagType.MYSTERY_ENCOUNTER_POST_SUMMON], 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(scene.getCurrentPhase()?.constructor.name).toBe(CommandPhase.name);
expect(enemyField.length).toBe(1); expect(enemyField.length).toBe(1);
expect(enemyField[0].species.speciesId).toBe(Species.SHUCKLE); 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(); const shuckleItems = enemyField[0].getHeldItems();
expect(shuckleItems.length).toBe(5); expect(shuckleItems.length).toBe(5);
expect(shuckleItems.find(m => m instanceof BerryModifier && m.berryType === BerryType.SITRUS)?.stackCount).toBe( 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 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 { Biome } from "#app/enums/biome";
import { MysteryEncounterType } from "#app/enums/mystery-encounter-type"; import { MysteryEncounterType } from "#app/enums/mystery-encounter-type";
import { Species } from "#app/enums/species"; import { Species } from "#app/enums/species";
import GameManager from "#test/testUtils/gameManager"; import { PokemonMove } from "#app/field/pokemon";
import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest"; import { HealShopCostModifier, HitHealModifier, TurnHealModifier } from "#app/modifier/modifier";
import { getPokemonSpecies } from "#app/data/pokemon-species"; import { ModifierTier } from "#app/modifier/modifier-tier";
import * as BattleAnims from "#app/data/battle-anims"; import { modifierTypes, type PokemonHeldItemModifierType } from "#app/modifier/modifier-type";
import * as EncounterPhaseUtils from "#app/data/mystery-encounters/utils/encounter-phase-utils"; 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 { import {
runMysteryEncounterToEnd, runMysteryEncounterToEnd,
skipBattleRunMysteryEncounterRewardsPhase, skipBattleRunMysteryEncounterRewardsPhase,
} from "#test/mystery-encounter/encounter-test-utils"; } from "#test/mystery-encounter/encounter-test-utils";
import { Moves } from "#enums/moves"; import GameManager from "#test/testUtils/gameManager";
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 { initSceneWithoutEncounterPhase } from "#test/testUtils/gameManagerUtils"; import { initSceneWithoutEncounterPhase } from "#test/testUtils/gameManagerUtils";
import { TrashToTreasureEncounter } from "#app/data/mystery-encounters/encounters/trash-to-treasure-encounter"; import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest";
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";
const namespace = "mysteryEncounters/trashToTreasure"; const namespace = "mysteryEncounters/trashToTreasure";
const defaultParty = [Species.LAPRAS, Species.GENGAR, Species.ABRA]; const defaultParty = [Species.LAPRAS, Species.GENGAR, Species.ABRA];
@ -73,6 +80,7 @@ describe("Trash to Treasure - Mystery Encounter", () => {
}); });
it("should initialize fully", async () => { it("should initialize fully", async () => {
vi.spyOn(Utils, "randSeedInt").mockImplementation((range, min = 0) => min + range - 1);
initSceneWithoutEncounterPhase(scene, defaultParty); initSceneWithoutEncounterPhase(scene, defaultParty);
scene.currentBattle.mysteryEncounter = TrashToTreasureEncounter; scene.currentBattle.mysteryEncounter = TrashToTreasureEncounter;
const moveInitSpy = vi.spyOn(BattleAnims, "initMoveAnim"); const moveInitSpy = vi.spyOn(BattleAnims, "initMoveAnim");
@ -85,22 +93,61 @@ describe("Trash to Treasure - Mystery Encounter", () => {
TrashToTreasureEncounter.populateDialogueTokensFromRequirements(); TrashToTreasureEncounter.populateDialogueTokensFromRequirements();
const onInitResult = onInit!(); const onInitResult = onInit!();
expect(TrashToTreasureEncounter.enemyPartyConfigs).toEqual([ const bossSpecies = getPokemonSpecies(Species.GARBODOR);
{ const pokemonConfig: EnemyPokemonConfig = {
levelAdditiveModifier: 0.5, species: bossSpecies,
disableSwitch: true,
pokemonConfigs: [
{
species: getPokemonSpecies(Species.GARBODOR),
isBoss: true, isBoss: true,
shiny: false, shiny: false, // Shiny lock because of custom intro sprite
formIndex: 1, formIndex: 1, // Gmax
bossSegmentModifier: 1, 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: 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(moveInitSpy).toHaveBeenCalled());
await vi.waitFor(() => expect(moveLoadSpy).toHaveBeenCalled()); await vi.waitFor(() => expect(moveLoadSpy).toHaveBeenCalled());
expect(onInitResult).toBe(true); 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 game.runToMysteryEncounter(MysteryEncounterType.TRASH_TO_TREASURE, defaultParty);
await runMysteryEncounterToEnd(game, 1); await runMysteryEncounterToEnd(game, 1);
await game.phaseInterceptor.to(SelectModifierPhase, false); 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; const shellBell = scene.findModifier(m => m instanceof HitHealModifier) as HitHealModifier;
expect(shellBell).toBeDefined(); expect(shellBell).toBeDefined();
expect(shellBell?.stackCount).toBe(2); expect(shellBell?.stackCount).toBe(1);
const blackSludge = scene.findModifier(m => m instanceof HealShopCostModifier) as HealShopCostModifier; const blackSludge = scene.findModifier(m => m instanceof HealShopCostModifier) as HealShopCostModifier;
expect(blackSludge).toBeDefined(); expect(blackSludge).toBeDefined();
@ -178,17 +225,17 @@ describe("Trash to Treasure - Mystery Encounter", () => {
expect(enemyField.length).toBe(1); expect(enemyField.length).toBe(1);
expect(enemyField[0].species.speciesId).toBe(Species.GARBODOR); expect(enemyField[0].species.speciesId).toBe(Species.GARBODOR);
expect(enemyField[0].moveset).toEqual([ expect(enemyField[0].moveset).toEqual([
new PokemonMove(Moves.PAYBACK),
new PokemonMove(Moves.GUNK_SHOT), new PokemonMove(Moves.GUNK_SHOT),
new PokemonMove(Moves.STOMPING_TANTRUM), 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 // Should have used moves pre-battle
const movePhases = phaseSpy.mock.calls.filter(p => p[0] instanceof MovePhase).map(p => p[0]); const movePhases = phaseSpy.mock.calls.filter(p => p[0] instanceof MovePhase).map(p => p[0]);
expect(movePhases.length).toBe(2); 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.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 () => { it("should have 2 Rogue, 1 Ultra, 1 Great in rewards", async () => {