Merge branch 'beta' into pr-illusion

This commit is contained in:
Lylian BALL 2024-10-17 11:01:06 +02:00 committed by GitHub
commit 68165c8225
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
41 changed files with 1001 additions and 312 deletions

4
package-lock.json generated
View File

@ -1,12 +1,12 @@
{ {
"name": "pokemon-rogue-battle", "name": "pokemon-rogue-battle",
"version": "1.0.4", "version": "1.1.0",
"lockfileVersion": 3, "lockfileVersion": 3,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "pokemon-rogue-battle", "name": "pokemon-rogue-battle",
"version": "1.0.4", "version": "1.1.0",
"hasInstallScript": true, "hasInstallScript": true,
"dependencies": { "dependencies": {
"@material/material-color-utilities": "^0.2.7", "@material/material-color-utilities": "^0.2.7",

View File

@ -1,7 +1,7 @@
{ {
"name": "pokemon-rogue-battle", "name": "pokemon-rogue-battle",
"private": true, "private": true,
"version": "1.0.4", "version": "1.1.0",
"type": "module", "type": "module",
"scripts": { "scripts": {
"start": "vite", "start": "vite",
@ -20,7 +20,7 @@
"depcruise": "depcruise src", "depcruise": "depcruise src",
"depcruise:graph": "depcruise src --output-type dot | node dependency-graph.js > dependency-graph.svg", "depcruise:graph": "depcruise src --output-type dot | node dependency-graph.js > dependency-graph.svg",
"create-test": "node ./create-test-boilerplate.js", "create-test": "node ./create-test-boilerplate.js",
"postinstall": "npx lefthook install && npx lefthook run post-merge" "postinstall": "npx lefthook install && npx lefthook run post-merge"
}, },
"devDependencies": { "devDependencies": {
"@eslint/js": "^9.3.0", "@eslint/js": "^9.3.0",

Binary file not shown.

Before

Width:  |  Height:  |  Size: 441 B

After

Width:  |  Height:  |  Size: 2.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.4 KiB

After

Width:  |  Height:  |  Size: 6.2 KiB

View File

@ -95,6 +95,7 @@ import { ExpPhase } from "#app/phases/exp-phase";
import { ShowPartyExpBarPhase } from "#app/phases/show-party-exp-bar-phase"; import { ShowPartyExpBarPhase } from "#app/phases/show-party-exp-bar-phase";
import { MysteryEncounterMode } from "#enums/mystery-encounter-mode"; import { MysteryEncounterMode } from "#enums/mystery-encounter-mode";
import { ExpGainsSpeed } from "#enums/exp-gains-speed"; import { ExpGainsSpeed } from "#enums/exp-gains-speed";
import { FRIENDSHIP_GAIN_FROM_BATTLE } from "#app/data/balance/starters";
export const bypassLogin = import.meta.env.VITE_BYPASS_LOGIN === "1"; export const bypassLogin = import.meta.env.VITE_BYPASS_LOGIN === "1";
@ -789,7 +790,7 @@ export default class BattleScene extends SceneBase {
} }
getEnemyParty(): EnemyPokemon[] { getEnemyParty(): EnemyPokemon[] {
return this.currentBattle?.enemyParty || []; return this.currentBattle?.enemyParty ?? [];
} }
getEnemyPokemon(): EnemyPokemon | undefined { getEnemyPokemon(): EnemyPokemon | undefined {
@ -3054,7 +3055,7 @@ export default class BattleScene extends SceneBase {
const pId = partyMember.id; const pId = partyMember.id;
const participated = participantIds.has(pId); const participated = participantIds.has(pId);
if (participated && pokemonDefeated) { if (participated && pokemonDefeated) {
partyMember.addFriendship(2); partyMember.addFriendship(FRIENDSHIP_GAIN_FROM_BATTLE);
const machoBraceModifier = partyMember.getHeldItems().find(m => m instanceof PokemonIncrementingStatModifier); const machoBraceModifier = partyMember.getHeldItems().find(m => m instanceof PokemonIncrementingStatModifier);
if (machoBraceModifier && machoBraceModifier.stackCount < machoBraceModifier.getMaxStackCount(this)) { if (machoBraceModifier && machoBraceModifier.stackCount < machoBraceModifier.getMaxStackCount(this)) {
machoBraceModifier.stackCount++; machoBraceModifier.stackCount++;

View File

@ -5024,8 +5024,7 @@ export function initAbilities() {
.attr(TypeImmunityAddBattlerTagAbAttr, Type.FIRE, BattlerTagType.FIRE_BOOST, 1) .attr(TypeImmunityAddBattlerTagAbAttr, Type.FIRE, BattlerTagType.FIRE_BOOST, 1)
.ignorable(), .ignorable(),
new Ability(Abilities.SHIELD_DUST, 3) new Ability(Abilities.SHIELD_DUST, 3)
.attr(IgnoreMoveEffectsAbAttr) .attr(IgnoreMoveEffectsAbAttr),
.edgeCase(), // Does not work with secret power (unimplemented)
new Ability(Abilities.OWN_TEMPO, 3) new Ability(Abilities.OWN_TEMPO, 3)
.attr(BattlerTagImmunityAbAttr, BattlerTagType.CONFUSED) .attr(BattlerTagImmunityAbAttr, BattlerTagType.CONFUSED)
.attr(IntimidateImmunityAbAttr) .attr(IntimidateImmunityAbAttr)
@ -5069,8 +5068,7 @@ export function initAbilities() {
.attr(TypeImmunityStatStageChangeAbAttr, Type.ELECTRIC, Stat.SPATK, 1) .attr(TypeImmunityStatStageChangeAbAttr, Type.ELECTRIC, Stat.SPATK, 1)
.ignorable(), .ignorable(),
new Ability(Abilities.SERENE_GRACE, 3) new Ability(Abilities.SERENE_GRACE, 3)
.attr(MoveEffectChanceMultiplierAbAttr, 2) .attr(MoveEffectChanceMultiplierAbAttr, 2),
.edgeCase(), // does not work with secret power (unimplemented)
new Ability(Abilities.SWIFT_SWIM, 3) new Ability(Abilities.SWIFT_SWIM, 3)
.attr(StatMultiplierAbAttr, Stat.SPD, 2) .attr(StatMultiplierAbAttr, Stat.SPD, 2)
.condition(getWeatherCondition(WeatherType.RAIN, WeatherType.HEAVY_RAIN)), .condition(getWeatherCondition(WeatherType.RAIN, WeatherType.HEAVY_RAIN)),

View File

@ -2,6 +2,12 @@ import { Species } from "#enums/species";
export const POKERUS_STARTER_COUNT = 5; export const POKERUS_STARTER_COUNT = 5;
// #region Friendship constants
export const CLASSIC_CANDY_FRIENDSHIP_MULTIPLIER = 2;
export const FRIENDSHIP_GAIN_FROM_BATTLE = 2;
export const FRIENDSHIP_GAIN_FROM_RARE_CANDY = 5;
export const FRIENDSHIP_LOSS_FROM_FAINT = 10;
/** /**
* Function to get the cumulative friendship threshold at which a candy is earned * Function to get the cumulative friendship threshold at which a candy is earned
* @param starterCost The cost of the starter, found in {@linkcode speciesStarterCosts} * @param starterCost The cost of the starter, found in {@linkcode speciesStarterCosts}

View File

@ -1024,7 +1024,7 @@ export class MoveEffectAttr extends MoveAttr {
applyAbAttrs(MoveEffectChanceMultiplierAbAttr, user, null, false, moveChance, move, target, selfEffect, showAbility); applyAbAttrs(MoveEffectChanceMultiplierAbAttr, user, null, false, moveChance, move, target, selfEffect, showAbility);
if (!move.hasAttr(FlinchAttr) || moveChance.value <= move.chance) { if ((!move.hasAttr(FlinchAttr) || moveChance.value <= move.chance) && !move.hasAttr(SecretPowerAttr)) {
const userSide = user.isPlayer() ? ArenaTagSide.PLAYER : ArenaTagSide.ENEMY; const userSide = user.isPlayer() ? ArenaTagSide.PLAYER : ArenaTagSide.ENEMY;
user.scene.arena.applyTagsForSide(ArenaTagType.WATER_FIRE_PLEDGE, userSide, false, moveChance); user.scene.arena.applyTagsForSide(ArenaTagType.WATER_FIRE_PLEDGE, userSide, false, moveChance);
} }
@ -2875,6 +2875,162 @@ export class StatStageChangeAttr extends MoveEffectAttr {
} }
} }
/**
* Attribute used to determine the Biome/Terrain-based secondary effect of Secret Power
*/
export class SecretPowerAttr extends MoveEffectAttr {
constructor() {
super(false);
}
/**
* Used to determine if the move should apply a secondary effect based on Secret Power's 30% chance
* @returns `true` if the move's secondary effect should apply
*/
override canApply(user: Pokemon, target: Pokemon, move: Move, args?: any[]): boolean {
this.effectChanceOverride = move.chance;
const moveChance = this.getMoveChance(user, target, move, this.selfTarget);
if (moveChance < 0 || moveChance === 100 || user.randSeedInt(100) < moveChance) {
return true;
} else {
return false;
}
}
/**
* Used to apply the secondary effect to the target Pokemon
* @returns `true` if a secondary effect is successfully applied
*/
override apply(user: Pokemon, target: Pokemon, move: Move, args?: any[]): boolean | Promise<boolean> {
if (!super.apply(user, target, move, args)) {
return false;
}
let secondaryEffect: MoveEffectAttr;
const terrain = user.scene.arena.getTerrainType();
if (terrain !== TerrainType.NONE) {
secondaryEffect = this.determineTerrainEffect(terrain);
} else {
const biome = user.scene.arena.biomeType;
secondaryEffect = this.determineBiomeEffect(biome);
}
// effectChanceOverride used in the application of the actual secondary effect
secondaryEffect.effectChanceOverride = 100;
return secondaryEffect.apply(user, target, move, []);
}
/**
* Determines the secondary effect based on terrain.
* Takes precedence over biome-based effects.
* ```
* Electric Terrain | Paralysis
* Misty Terrain | SpAtk -1
* Grassy Terrain | Sleep
* Psychic Terrain | Speed -1
* ```
* @param terrain - {@linkcode TerrainType} The current terrain
* @returns the chosen secondary effect {@linkcode MoveEffectAttr}
*/
private determineTerrainEffect(terrain: TerrainType): MoveEffectAttr {
let secondaryEffect: MoveEffectAttr;
switch (terrain) {
case TerrainType.ELECTRIC:
default:
secondaryEffect = new StatusEffectAttr(StatusEffect.PARALYSIS, false);
break;
case TerrainType.MISTY:
secondaryEffect = new StatStageChangeAttr([ Stat.SPATK ], -1, false);
break;
case TerrainType.GRASSY:
secondaryEffect = new StatusEffectAttr(StatusEffect.SLEEP, false);
break;
case TerrainType.PSYCHIC:
secondaryEffect = new StatStageChangeAttr([ Stat.SPD ], -1, false);
break;
}
return secondaryEffect;
}
/**
* Determines the secondary effect based on biome
* ```
* Town, Metropolis, Slum, Dojo, Laboratory, Power Plant + Default | Paralysis
* Plains, Grass, Tall Grass, Forest, Jungle, Meadow | Sleep
* Swamp, Mountain, Temple, Ruins | Speed -1
* Ice Cave, Snowy Forest | Freeze
* Volcano | Burn
* Fairy Cave | SpAtk -1
* Desert, Construction Site, Beach, Island, Badlands | Accuracy -1
* Sea, Lake, Seabed | Atk -1
* Cave, Wasteland, Graveyard, Abyss, Space | Flinch
* End | Def -1
* ```
* @param biome - The current {@linkcode Biome} the battle is set in
* @returns the chosen secondary effect {@linkcode MoveEffectAttr}
*/
private determineBiomeEffect(biome: Biome): MoveEffectAttr {
let secondaryEffect: MoveEffectAttr;
switch (biome) {
case Biome.PLAINS:
case Biome.GRASS:
case Biome.TALL_GRASS:
case Biome.FOREST:
case Biome.JUNGLE:
case Biome.MEADOW:
secondaryEffect = new StatusEffectAttr(StatusEffect.SLEEP, false);
break;
case Biome.SWAMP:
case Biome.MOUNTAIN:
case Biome.TEMPLE:
case Biome.RUINS:
secondaryEffect = new StatStageChangeAttr([ Stat.SPD ], -1, false);
break;
case Biome.ICE_CAVE:
case Biome.SNOWY_FOREST:
secondaryEffect = new StatusEffectAttr(StatusEffect.FREEZE, false);
break;
case Biome.VOLCANO:
secondaryEffect = new StatusEffectAttr(StatusEffect.BURN, false);
break;
case Biome.FAIRY_CAVE:
secondaryEffect = new StatStageChangeAttr([ Stat.SPATK ], -1, false);
break;
case Biome.DESERT:
case Biome.CONSTRUCTION_SITE:
case Biome.BEACH:
case Biome.ISLAND:
case Biome.BADLANDS:
secondaryEffect = new StatStageChangeAttr([ Stat.ACC ], -1, false);
break;
case Biome.SEA:
case Biome.LAKE:
case Biome.SEABED:
secondaryEffect = new StatStageChangeAttr([ Stat.ATK ], -1, false);
break;
case Biome.CAVE:
case Biome.WASTELAND:
case Biome.GRAVEYARD:
case Biome.ABYSS:
case Biome.SPACE:
secondaryEffect = new AddBattlerTagAttr(BattlerTagType.FLINCHED, false, true);
break;
case Biome.END:
secondaryEffect = new StatStageChangeAttr([ Stat.DEF ], -1, false);
break;
case Biome.TOWN:
case Biome.METROPOLIS:
case Biome.SLUM:
case Biome.DOJO:
case Biome.FACTORY:
case Biome.LABORATORY:
case Biome.POWER_PLANT:
default:
secondaryEffect = new StatusEffectAttr(StatusEffect.PARALYSIS, false);
break;
}
return secondaryEffect;
}
}
export class PostVictoryStatStageChangeAttr extends MoveAttr { export class PostVictoryStatStageChangeAttr extends MoveAttr {
private stats: BattleStat[]; private stats: BattleStat[];
private stages: number; private stages: number;
@ -3834,8 +3990,8 @@ export class LastMoveDoublePowerAttr extends VariablePowerAttr {
for (const p of pokemonActed) { for (const p of pokemonActed) {
const [ lastMove ] = p.getLastXMoves(1); const [ lastMove ] = p.getLastXMoves(1);
if (lastMove.result !== MoveResult.FAIL) { if (lastMove?.result !== MoveResult.FAIL) {
if ((lastMove.result === MoveResult.SUCCESS) && (lastMove.move === this.move)) { if ((lastMove?.result === MoveResult.SUCCESS) && (lastMove?.move === this.move)) {
power.value *= 2; power.value *= 2;
return true; return true;
} else { } else {
@ -4736,7 +4892,7 @@ export class AddBattlerTagAttr extends MoveEffectAttr {
} }
canApply(user: Pokemon, target: Pokemon, move: Move, args: any[]): boolean { canApply(user: Pokemon, target: Pokemon, move: Move, args: any[]): boolean {
if (!super.canApply(user, target, move, args) || (this.cancelOnFail === true && user.getLastXMoves(1)[0].result === MoveResult.FAIL)) { if (!super.canApply(user, target, move, args) || (this.cancelOnFail === true && user.getLastXMoves(1)[0]?.result === MoveResult.FAIL)) {
return false; return false;
} else { } else {
return true; return true;
@ -5174,7 +5330,7 @@ export class AddArenaTagAttr extends MoveEffectAttr {
return false; return false;
} }
if ((move.chance < 0 || move.chance === 100 || user.randSeedInt(100) < move.chance) && user.getLastXMoves(1)[0].result === MoveResult.SUCCESS) { if ((move.chance < 0 || move.chance === 100 || user.randSeedInt(100) < move.chance) && user.getLastXMoves(1)[0]?.result === MoveResult.SUCCESS) {
user.scene.arena.addTag(this.tagType, this.turnCount, move.id, user.id, (this.selfSideTarget ? user : target).isPlayer() ? ArenaTagSide.PLAYER : ArenaTagSide.ENEMY); user.scene.arena.addTag(this.tagType, this.turnCount, move.id, user.id, (this.selfSideTarget ? user : target).isPlayer() ? ArenaTagSide.PLAYER : ArenaTagSide.ENEMY);
return true; return true;
} }
@ -5249,7 +5405,7 @@ export class AddArenaTrapTagHitAttr extends AddArenaTagAttr {
const moveChance = this.getMoveChance(user, target, move, this.selfTarget, true); const moveChance = this.getMoveChance(user, target, move, this.selfTarget, true);
const side = (this.selfSideTarget ? user : target).isPlayer() ? ArenaTagSide.PLAYER : ArenaTagSide.ENEMY; const side = (this.selfSideTarget ? user : target).isPlayer() ? ArenaTagSide.PLAYER : ArenaTagSide.ENEMY;
const tag = user.scene.arena.getTagOnSide(this.tagType, side) as ArenaTrapTag; const tag = user.scene.arena.getTagOnSide(this.tagType, side) as ArenaTrapTag;
if ((moveChance < 0 || moveChance === 100 || user.randSeedInt(100) < moveChance) && user.getLastXMoves(1)[0].result === MoveResult.SUCCESS) { if ((moveChance < 0 || moveChance === 100 || user.randSeedInt(100) < moveChance) && user.getLastXMoves(1)[0]?.result === MoveResult.SUCCESS) {
user.scene.arena.addTag(this.tagType, 0, move.id, user.id, side); user.scene.arena.addTag(this.tagType, 0, move.id, user.id, side);
if (!tag) { if (!tag) {
return true; return true;
@ -5386,7 +5542,7 @@ export class AddPledgeEffectAttr extends AddArenaTagAttr {
override apply(user: Pokemon, target: Pokemon, move: Move, args: any[]): boolean { override apply(user: Pokemon, target: Pokemon, move: Move, args: any[]): boolean {
// TODO: add support for `HIT` effect triggering in AddArenaTagAttr to remove the need for this check // TODO: add support for `HIT` effect triggering in AddArenaTagAttr to remove the need for this check
if (user.getLastXMoves(1)[0].result !== MoveResult.SUCCESS) { if (user.getLastXMoves(1)[0]?.result !== MoveResult.SUCCESS) {
return false; return false;
} }
@ -7900,7 +8056,7 @@ export function initMoves() {
.unimplemented(), .unimplemented(),
new AttackMove(Moves.SECRET_POWER, Type.NORMAL, MoveCategory.PHYSICAL, 70, 100, 20, 30, 0, 3) new AttackMove(Moves.SECRET_POWER, Type.NORMAL, MoveCategory.PHYSICAL, 70, 100, 20, 30, 0, 3)
.makesContact(false) .makesContact(false)
.partial(), // No effect implemented .attr(SecretPowerAttr),
new AttackMove(Moves.DIVE, Type.WATER, MoveCategory.PHYSICAL, 80, 100, 10, -1, 0, 3) new AttackMove(Moves.DIVE, Type.WATER, MoveCategory.PHYSICAL, 80, 100, 10, -1, 0, 3)
.attr(ChargeAttr, ChargeAnim.DIVE_CHARGING, i18next.t("moveTriggers:hidUnderwater", { pokemonName: "{USER}" }), BattlerTagType.UNDERWATER, true) .attr(ChargeAttr, ChargeAnim.DIVE_CHARGING, i18next.t("moveTriggers:hidUnderwater", { pokemonName: "{USER}" }), BattlerTagType.UNDERWATER, true)
.attr(GulpMissileTagAttr) .attr(GulpMissileTagAttr)

View File

@ -154,7 +154,7 @@ export const ATrainersTestEncounter: MysteryEncounter =
}; };
encounter.setDialogueToken("eggType", i18next.t(`${namespace}:eggTypes.epic`)); encounter.setDialogueToken("eggType", i18next.t(`${namespace}:eggTypes.epic`));
setEncounterRewards(scene, { guaranteedModifierTypeFuncs: [ modifierTypes.SACRED_ASH ], guaranteedModifierTiers: [ ModifierTier.ROGUE, ModifierTier.ULTRA ], fillRemaining: true }, [ eggOptions ]); setEncounterRewards(scene, { guaranteedModifierTypeFuncs: [ modifierTypes.SACRED_ASH ], guaranteedModifierTiers: [ ModifierTier.ROGUE, ModifierTier.ULTRA ], fillRemaining: true }, [ eggOptions ]);
return initBattleWithEnemyConfig(scene, config); await initBattleWithEnemyConfig(scene, config);
} }
) )
.withSimpleOption( .withSimpleOption(

View File

@ -286,7 +286,7 @@ export const AbsoluteAvariceEncounter: MysteryEncounter =
ignorePp: true ignorePp: true
}); });
transitionMysteryEncounterIntroVisuals(scene, true, true, 500); await transitionMysteryEncounterIntroVisuals(scene, true, true, 500);
await initBattleWithEnemyConfig(scene, encounter.enemyPartyConfigs[0]); await initBattleWithEnemyConfig(scene, encounter.enemyPartyConfigs[0]);
}) })
.build() .build()
@ -328,7 +328,7 @@ export const AbsoluteAvariceEncounter: MysteryEncounter =
}); });
await scene.updateModifiers(true); await scene.updateModifiers(true);
transitionMysteryEncounterIntroVisuals(scene, true, true, 500); await transitionMysteryEncounterIntroVisuals(scene, true, true, 500);
leaveEncounterWithoutBattle(scene, true); leaveEncounterWithoutBattle(scene, true);
}) })
.build() .build()
@ -359,7 +359,7 @@ export const AbsoluteAvariceEncounter: MysteryEncounter =
greedent.moveset = [ new PokemonMove(Moves.THRASH), new PokemonMove(Moves.BODY_PRESS), new PokemonMove(Moves.STUFF_CHEEKS), new PokemonMove(Moves.SLACK_OFF) ]; greedent.moveset = [ new PokemonMove(Moves.THRASH), new PokemonMove(Moves.BODY_PRESS), new PokemonMove(Moves.STUFF_CHEEKS), new PokemonMove(Moves.SLACK_OFF) ];
greedent.passive = true; greedent.passive = true;
transitionMysteryEncounterIntroVisuals(scene, true, true, 500); await transitionMysteryEncounterIntroVisuals(scene, true, true, 500);
await catchPokemon(scene, greedent, null, PokeballType.POKEBALL, false); await catchPokemon(scene, greedent, null, PokeballType.POKEBALL, false);
leaveEncounterWithoutBattle(scene, true); leaveEncounterWithoutBattle(scene, true);
}) })

View File

@ -228,7 +228,7 @@ export const DancingLessonsEncounter: MysteryEncounter =
}) })
.withOptionPhase(async (scene: BattleScene) => { .withOptionPhase(async (scene: BattleScene) => {
// Learn its Dance // Learn its Dance
hideOricorioPokemon(scene); await hideOricorioPokemon(scene);
leaveEncounterWithoutBattle(scene, true); leaveEncounterWithoutBattle(scene, true);
}) })
.build() .build()
@ -303,7 +303,7 @@ export const DancingLessonsEncounter: MysteryEncounter =
} }
} }
hideOricorioPokemon(scene); await hideOricorioPokemon(scene);
await catchPokemon(scene, oricorio, null, PokeballType.POKEBALL, false); await catchPokemon(scene, oricorio, null, PokeballType.POKEBALL, false);
leaveEncounterWithoutBattle(scene, true); leaveEncounterWithoutBattle(scene, true);
}) })

View File

@ -182,7 +182,7 @@ export const DarkDealEncounter: MysteryEncounter =
const config: EnemyPartyConfig = { const config: EnemyPartyConfig = {
pokemonConfigs: [ pokemonConfig ], pokemonConfigs: [ pokemonConfig ],
}; };
return initBattleWithEnemyConfig(scene, config); await initBattleWithEnemyConfig(scene, config);
}) })
.build() .build()
) )

View File

@ -222,12 +222,13 @@ export const FieryFalloutEncounter: MysteryEncounter =
], ],
}) })
.withPreOptionPhase(async (scene: BattleScene) => { .withPreOptionPhase(async (scene: BattleScene) => {
// Do NOT await this, to prevent player from repeatedly pressing options
transitionMysteryEncounterIntroVisuals(scene, false, false, 2000); transitionMysteryEncounterIntroVisuals(scene, false, false, 2000);
}) })
.withOptionPhase(async (scene: BattleScene) => { .withOptionPhase(async (scene: BattleScene) => {
// Fire types help calm the Volcarona // Fire types help calm the Volcarona
const encounter = scene.currentBattle.mysteryEncounter!; const encounter = scene.currentBattle.mysteryEncounter!;
transitionMysteryEncounterIntroVisuals(scene); await transitionMysteryEncounterIntroVisuals(scene);
setEncounterRewards(scene, setEncounterRewards(scene,
{ fillRemaining: true }, { fillRemaining: true },
undefined, undefined,

View File

@ -152,7 +152,7 @@ export const FunAndGamesEncounter: MysteryEncounter =
}, },
async (scene: BattleScene) => { async (scene: BattleScene) => {
// Leave encounter with no rewards or exp // Leave encounter with no rewards or exp
transitionMysteryEncounterIntroVisuals(scene, true, true); await transitionMysteryEncounterIntroVisuals(scene, true, true);
leaveEncounterWithoutBattle(scene, true); leaveEncounterWithoutBattle(scene, true);
return true; return true;
} }

View File

@ -399,7 +399,7 @@ export const GlobalTradeSystemEncounter: MysteryEncounter =
if (modifier.stackCount === 0) { if (modifier.stackCount === 0) {
scene.removeModifier(modifier); scene.removeModifier(modifier);
} }
scene.updateModifiers(true, true); await scene.updateModifiers(true, true);
// Generate a trainer name // Generate a trainer name
const traderName = generateRandomTraderName(); const traderName = generateRandomTraderName();

View File

@ -129,7 +129,7 @@ export const LostAtSeaEncounter: MysteryEncounter = MysteryEncounterBuilder.with
* *
* @param scene Battle scene * @param scene Battle scene
*/ */
async function handlePokemonGuidingYouPhase(scene: BattleScene) { function handlePokemonGuidingYouPhase(scene: BattleScene) {
const laprasSpecies = getPokemonSpecies(Species.LAPRAS); const laprasSpecies = getPokemonSpecies(Species.LAPRAS);
const { mysteryEncounter } = scene.currentBattle; const { mysteryEncounter } = scene.currentBattle;

View File

@ -147,11 +147,11 @@ export const MysteriousChallengersEncounter: MysteryEncounter =
setEncounterRewards(scene, { guaranteedModifierTypeFuncs: [ modifierTypes.TM_COMMON, modifierTypes.TM_GREAT, modifierTypes.MEMORY_MUSHROOM ], fillRemaining: true }); setEncounterRewards(scene, { guaranteedModifierTypeFuncs: [ modifierTypes.TM_COMMON, modifierTypes.TM_GREAT, modifierTypes.MEMORY_MUSHROOM ], fillRemaining: true });
// Seed offsets to remove possibility of different trainers having exact same teams // Seed offsets to remove possibility of different trainers having exact same teams
let ret; let initBattlePromise: Promise<void>;
scene.executeWithSeedOffset(() => { scene.executeWithSeedOffset(() => {
ret = initBattleWithEnemyConfig(scene, config); initBattlePromise = initBattleWithEnemyConfig(scene, config);
}, scene.currentBattle.waveIndex * 10); }, scene.currentBattle.waveIndex * 10);
return ret; await initBattlePromise!;
} }
) )
.withSimpleOption( .withSimpleOption(
@ -172,11 +172,11 @@ export const MysteriousChallengersEncounter: MysteryEncounter =
setEncounterRewards(scene, { guaranteedModifierTiers: [ ModifierTier.ULTRA, ModifierTier.ULTRA, ModifierTier.GREAT, ModifierTier.GREAT ], fillRemaining: true }); setEncounterRewards(scene, { guaranteedModifierTiers: [ ModifierTier.ULTRA, ModifierTier.ULTRA, ModifierTier.GREAT, ModifierTier.GREAT ], fillRemaining: true });
// Seed offsets to remove possibility of different trainers having exact same teams // Seed offsets to remove possibility of different trainers having exact same teams
let ret; let initBattlePromise: Promise<void>;
scene.executeWithSeedOffset(() => { scene.executeWithSeedOffset(() => {
ret = initBattleWithEnemyConfig(scene, config); initBattlePromise = initBattleWithEnemyConfig(scene, config);
}, scene.currentBattle.waveIndex * 100); }, scene.currentBattle.waveIndex * 100);
return ret; await initBattlePromise!;
} }
) )
.withSimpleOption( .withSimpleOption(
@ -200,11 +200,11 @@ export const MysteriousChallengersEncounter: MysteryEncounter =
setEncounterRewards(scene, { guaranteedModifierTiers: [ ModifierTier.ROGUE, ModifierTier.ROGUE, ModifierTier.ULTRA, ModifierTier.GREAT ], fillRemaining: true }); setEncounterRewards(scene, { guaranteedModifierTiers: [ ModifierTier.ROGUE, ModifierTier.ROGUE, ModifierTier.ULTRA, ModifierTier.GREAT ], fillRemaining: true });
// Seed offsets to remove possibility of different trainers having exact same teams // Seed offsets to remove possibility of different trainers having exact same teams
let ret; let initBattlePromise: Promise<void>;
scene.executeWithSeedOffset(() => { scene.executeWithSeedOffset(() => {
ret = initBattleWithEnemyConfig(scene, config); initBattlePromise = initBattleWithEnemyConfig(scene, config);
}, scene.currentBattle.waveIndex * 1000); }, scene.currentBattle.waveIndex * 1000);
return ret; await initBattlePromise!;
} }
) )
.withOutroDialogue([ .withOutroDialogue([

View File

@ -184,7 +184,7 @@ export const MysteriousChestEncounter: MysteryEncounter =
scene.unshiftPhase(new GameOverPhase(scene)); scene.unshiftPhase(new GameOverPhase(scene));
} else { } else {
// Show which Pokemon was KOed, then start battle against Gimmighoul // Show which Pokemon was KOed, then start battle against Gimmighoul
transitionMysteryEncounterIntroVisuals(scene, true, true, 500); await transitionMysteryEncounterIntroVisuals(scene, true, true, 500);
setEncounterRewards(scene, { fillRemaining: true }); setEncounterRewards(scene, { fillRemaining: true });
await initBattleWithEnemyConfig(scene, encounter.enemyPartyConfigs[0]); await initBattleWithEnemyConfig(scene, encounter.enemyPartyConfigs[0]);
} }

View File

@ -303,13 +303,16 @@ async function summonSafariPokemon(scene: BattleScene) {
scene.unshiftPhase(new SummonPhase(scene, 0, false)); scene.unshiftPhase(new SummonPhase(scene, 0, false));
encounter.setDialogueToken("pokemonName", getPokemonNameWithAffix(pokemon)); encounter.setDialogueToken("pokemonName", getPokemonNameWithAffix(pokemon));
showEncounterText(scene, getEncounterText(scene, "battle:singleWildAppeared") ?? "", null, 1500, false)
.then(() => { // TODO: If we await showEncounterText here, then the text will display without
const ivScannerModifier = scene.findModifier(m => m instanceof IvScannerModifier); // the wild Pokemon on screen, but if we don't await it, then the text never
if (ivScannerModifier) { // shows up and the IV scanner breaks. For now, we place the IV scanner code
scene.pushPhase(new ScanIvsPhase(scene, pokemon.getBattlerIndex(), Math.min(ivScannerModifier.getStackCount() * 2, 6))); // separately so that at least the IV scanner works.
}
}); const ivScannerModifier = scene.findModifier(m => m instanceof IvScannerModifier);
if (ivScannerModifier) {
scene.pushPhase(new ScanIvsPhase(scene, pokemon.getBattlerIndex(), Math.min(ivScannerModifier.getStackCount() * 2, 6)));
}
} }
function throwPokeball(scene: BattleScene, pokemon: EnemyPokemon): Promise<boolean> { function throwPokeball(scene: BattleScene, pokemon: EnemyPokemon): Promise<boolean> {

View File

@ -142,7 +142,7 @@ export const ShadyVitaminDealerEncounter: MysteryEncounter =
encounter.setDialogueToken("newNature", getNatureName(newNature)); encounter.setDialogueToken("newNature", getNatureName(newNature));
queueEncounterMessage(scene, `${namespace}:cheap_side_effects`); queueEncounterMessage(scene, `${namespace}:cheap_side_effects`);
setEncounterExp(scene, [ chosenPokemon.id ], 100); setEncounterExp(scene, [ chosenPokemon.id ], 100);
chosenPokemon.updateInfo(); await chosenPokemon.updateInfo();
}) })
.build() .build()
) )
@ -204,7 +204,7 @@ export const ShadyVitaminDealerEncounter: MysteryEncounter =
queueEncounterMessage(scene, `${namespace}:no_bad_effects`); queueEncounterMessage(scene, `${namespace}:no_bad_effects`);
setEncounterExp(scene, [ chosenPokemon.id ], 100); setEncounterExp(scene, [ chosenPokemon.id ], 100);
chosenPokemon.updateInfo(); await chosenPokemon.updateInfo();
}) })
.build() .build()
) )

View File

@ -149,7 +149,7 @@ export const TeleportingHijinksEncounter: MysteryEncounter =
const magnet = generateModifierTypeOption(scene, modifierTypes.ATTACK_TYPE_BOOSTER, [ Type.STEEL ])!; const magnet = generateModifierTypeOption(scene, modifierTypes.ATTACK_TYPE_BOOSTER, [ Type.STEEL ])!;
const metalCoat = generateModifierTypeOption(scene, modifierTypes.ATTACK_TYPE_BOOSTER, [ Type.ELECTRIC ])!; const metalCoat = generateModifierTypeOption(scene, modifierTypes.ATTACK_TYPE_BOOSTER, [ Type.ELECTRIC ])!;
setEncounterRewards(scene, { guaranteedModifierTypeOptions: [ magnet, metalCoat ], fillRemaining: true }); setEncounterRewards(scene, { guaranteedModifierTypeOptions: [ magnet, metalCoat ], fillRemaining: true });
transitionMysteryEncounterIntroVisuals(scene, true, true); await transitionMysteryEncounterIntroVisuals(scene, true, true);
await initBattleWithEnemyConfig(scene, config); await initBattleWithEnemyConfig(scene, config);
} }
) )

View File

@ -61,7 +61,7 @@ const POOL_1_POKEMON: (Species | BreederSpeciesEvolution)[][] = [
const POOL_2_POKEMON: (Species | BreederSpeciesEvolution)[][] = [ const POOL_2_POKEMON: (Species | BreederSpeciesEvolution)[][] = [
[ Species.PICHU, new BreederSpeciesEvolution(Species.PIKACHU, FIRST_STAGE_EVOLUTION_WAVE), new BreederSpeciesEvolution(Species.RAICHU, FINAL_STAGE_EVOLUTION_WAVE) ], [ Species.PICHU, new BreederSpeciesEvolution(Species.PIKACHU, FIRST_STAGE_EVOLUTION_WAVE), new BreederSpeciesEvolution(Species.RAICHU, FINAL_STAGE_EVOLUTION_WAVE) ],
[ Species.PICHU, new BreederSpeciesEvolution(Species.PIKACHU, FIRST_STAGE_EVOLUTION_WAVE), new BreederSpeciesEvolution(Species.ALOLA_RAICHU, FINAL_STAGE_EVOLUTION_WAVE) ], [ Species.PICHU, new BreederSpeciesEvolution(Species.PIKACHU, FIRST_STAGE_EVOLUTION_WAVE), new BreederSpeciesEvolution(Species.ALOLA_RAICHU, FINAL_STAGE_EVOLUTION_WAVE) ],
[ Species.JYNX ], [ Species.SMOOCHUM, new BreederSpeciesEvolution(Species.JYNX, SECOND_STAGE_EVOLUTION_WAVE) ],
[ Species.TYROGUE, new BreederSpeciesEvolution(Species.HITMONLEE, SECOND_STAGE_EVOLUTION_WAVE) ], [ Species.TYROGUE, new BreederSpeciesEvolution(Species.HITMONLEE, SECOND_STAGE_EVOLUTION_WAVE) ],
[ Species.TYROGUE, new BreederSpeciesEvolution(Species.HITMONCHAN, SECOND_STAGE_EVOLUTION_WAVE) ], [ Species.TYROGUE, new BreederSpeciesEvolution(Species.HITMONCHAN, SECOND_STAGE_EVOLUTION_WAVE) ],
[ Species.TYROGUE, new BreederSpeciesEvolution(Species.HITMONTOP, SECOND_STAGE_EVOLUTION_WAVE) ], [ Species.TYROGUE, new BreederSpeciesEvolution(Species.HITMONTOP, SECOND_STAGE_EVOLUTION_WAVE) ],
@ -245,7 +245,7 @@ export const TheExpertPokemonBreederEncounter: MysteryEncounter =
} }
encounter.onGameOver = onGameOver; encounter.onGameOver = onGameOver;
initBattleWithEnemyConfig(scene, config); await initBattleWithEnemyConfig(scene, config);
}) })
.withPostOptionPhase(async (scene: BattleScene) => { .withPostOptionPhase(async (scene: BattleScene) => {
await doPostEncounterCleanup(scene); await doPostEncounterCleanup(scene);
@ -297,7 +297,7 @@ export const TheExpertPokemonBreederEncounter: MysteryEncounter =
} }
encounter.onGameOver = onGameOver; encounter.onGameOver = onGameOver;
initBattleWithEnemyConfig(scene, config); await initBattleWithEnemyConfig(scene, config);
}) })
.withPostOptionPhase(async (scene: BattleScene) => { .withPostOptionPhase(async (scene: BattleScene) => {
await doPostEncounterCleanup(scene); await doPostEncounterCleanup(scene);
@ -349,7 +349,7 @@ export const TheExpertPokemonBreederEncounter: MysteryEncounter =
} }
encounter.onGameOver = onGameOver; encounter.onGameOver = onGameOver;
initBattleWithEnemyConfig(scene, config); await initBattleWithEnemyConfig(scene, config);
}) })
.withPostOptionPhase(async (scene: BattleScene) => { .withPostOptionPhase(async (scene: BattleScene) => {
await doPostEncounterCleanup(scene); await doPostEncounterCleanup(scene);

View File

@ -201,7 +201,7 @@ export const TheStrongStuffEncounter: MysteryEncounter =
}); });
encounter.dialogue.outro = []; encounter.dialogue.outro = [];
transitionMysteryEncounterIntroVisuals(scene, true, true, 500); await transitionMysteryEncounterIntroVisuals(scene, true, true, 500);
await initBattleWithEnemyConfig(scene, encounter.enemyPartyConfigs[0]); await initBattleWithEnemyConfig(scene, encounter.enemyPartyConfigs[0]);
} }
) )

View File

@ -111,8 +111,8 @@ export const TheWinstrateChallengeEncounter: MysteryEncounter =
}, },
async (scene: BattleScene) => { async (scene: BattleScene) => {
// Spawn 5 trainer battles back to back with Macho Brace in rewards // Spawn 5 trainer battles back to back with Macho Brace in rewards
scene.currentBattle.mysteryEncounter!.doContinueEncounter = (scene: BattleScene) => { scene.currentBattle.mysteryEncounter!.doContinueEncounter = async (scene: BattleScene) => {
return endTrainerBattleAndShowDialogue(scene); await endTrainerBattleAndShowDialogue(scene);
}; };
await transitionMysteryEncounterIntroVisuals(scene, true, false); await transitionMysteryEncounterIntroVisuals(scene, true, false);
await spawnNextTrainerOrEndEncounter(scene); await spawnNextTrainerOrEndEncounter(scene);

View File

@ -162,7 +162,7 @@ export const TrainingSessionEncounter: MysteryEncounter =
setEncounterRewards(scene, { fillRemaining: true }, undefined, onBeforeRewardsPhase); setEncounterRewards(scene, { fillRemaining: true }, undefined, onBeforeRewardsPhase);
return initBattleWithEnemyConfig(scene, config); await initBattleWithEnemyConfig(scene, config);
}) })
.build() .build()
) )
@ -238,7 +238,7 @@ export const TrainingSessionEncounter: MysteryEncounter =
setEncounterRewards(scene, { fillRemaining: true }, undefined, onBeforeRewardsPhase); setEncounterRewards(scene, { fillRemaining: true }, undefined, onBeforeRewardsPhase);
return initBattleWithEnemyConfig(scene, config); await initBattleWithEnemyConfig(scene, config);
}) })
.build() .build()
) )
@ -351,7 +351,7 @@ export const TrainingSessionEncounter: MysteryEncounter =
setEncounterRewards(scene, { fillRemaining: true }, undefined, onBeforeRewardsPhase); setEncounterRewards(scene, { fillRemaining: true }, undefined, onBeforeRewardsPhase);
return initBattleWithEnemyConfig(scene, config); await initBattleWithEnemyConfig(scene, config);
}) })
.build() .build()
) )

View File

@ -105,7 +105,7 @@ export const TrashToTreasureEncounter: MysteryEncounter =
}) })
.withOptionPhase(async (scene: BattleScene) => { .withOptionPhase(async (scene: BattleScene) => {
// Gain 2 Leftovers and 2 Shell Bell // Gain 2 Leftovers and 2 Shell Bell
transitionMysteryEncounterIntroVisuals(scene); await transitionMysteryEncounterIntroVisuals(scene);
await tryApplyDigRewardItems(scene); await tryApplyDigRewardItems(scene);
const blackSludge = generateModifierType(scene, modifierTypes.MYSTERY_ENCOUNTER_BLACK_SLUDGE, [ SHOP_ITEM_COST_MULTIPLIER ]); const blackSludge = generateModifierType(scene, modifierTypes.MYSTERY_ENCOUNTER_BLACK_SLUDGE, [ SHOP_ITEM_COST_MULTIPLIER ]);
@ -136,7 +136,7 @@ export const TrashToTreasureEncounter: MysteryEncounter =
// Investigate garbage, battle Gmax Garbodor // Investigate garbage, battle Gmax Garbodor
scene.setFieldScale(0.75); scene.setFieldScale(0.75);
await showEncounterText(scene, `${namespace}:option.2.selected_2`); await showEncounterText(scene, `${namespace}:option.2.selected_2`);
transitionMysteryEncounterIntroVisuals(scene); await transitionMysteryEncounterIntroVisuals(scene);
const encounter = scene.currentBattle.mysteryEncounter!; const encounter = scene.currentBattle.mysteryEncounter!;
@ -222,7 +222,7 @@ async function tryApplyDigRewardItems(scene: BattleScene) {
await showEncounterText(scene, i18next.t("battle:rewardGainCount", { modifierName: shellBell.name, count: 2 }), null, undefined, true); await showEncounterText(scene, i18next.t("battle:rewardGainCount", { modifierName: shellBell.name, count: 2 }), null, undefined, true);
} }
async function doGarbageDig(scene: BattleScene) { function doGarbageDig(scene: BattleScene) {
scene.playSound("battle_anims/PRSFX- Dig2"); scene.playSound("battle_anims/PRSFX- Dig2");
scene.time.delayedCall(SOUND_EFFECT_WAIT_TIME, () => { scene.time.delayedCall(SOUND_EFFECT_WAIT_TIME, () => {
scene.playSound("battle_anims/PRSFX- Dig2"); scene.playSound("battle_anims/PRSFX- Dig2");

View File

@ -5,7 +5,7 @@ import { variantData } from "#app/data/variant";
import BattleInfo, { PlayerBattleInfo, EnemyBattleInfo } from "#app/ui/battle-info"; import BattleInfo, { PlayerBattleInfo, EnemyBattleInfo } from "#app/ui/battle-info";
import Move, { HighCritAttr, HitsTagAttr, applyMoveAttrs, FixedDamageAttr, VariableAtkAttr, allMoves, MoveCategory, TypelessAttr, CritOnlyAttr, getMoveTargets, OneHitKOAttr, VariableMoveTypeAttr, VariableDefAttr, AttackMove, ModifiedDamageAttr, VariableMoveTypeMultiplierAttr, IgnoreOpponentStatStagesAttr, SacrificialAttr, VariableMoveCategoryAttr, CounterDamageAttr, StatStageChangeAttr, RechargeAttr, ChargeAttr, IgnoreWeatherTypeDebuffAttr, BypassBurnDamageReductionAttr, SacrificialAttrOnHit, OneHitKOAccuracyAttr, RespectAttackTypeImmunityAttr, MoveTarget, CombinedPledgeStabBoostAttr } from "#app/data/move"; import Move, { HighCritAttr, HitsTagAttr, applyMoveAttrs, FixedDamageAttr, VariableAtkAttr, allMoves, MoveCategory, TypelessAttr, CritOnlyAttr, getMoveTargets, OneHitKOAttr, VariableMoveTypeAttr, VariableDefAttr, AttackMove, ModifiedDamageAttr, VariableMoveTypeMultiplierAttr, IgnoreOpponentStatStagesAttr, SacrificialAttr, VariableMoveCategoryAttr, CounterDamageAttr, StatStageChangeAttr, RechargeAttr, ChargeAttr, IgnoreWeatherTypeDebuffAttr, BypassBurnDamageReductionAttr, SacrificialAttrOnHit, OneHitKOAccuracyAttr, RespectAttackTypeImmunityAttr, MoveTarget, CombinedPledgeStabBoostAttr } from "#app/data/move";
import { default as PokemonSpecies, PokemonSpeciesForm, getFusedSpeciesName, getPokemonSpecies, getPokemonSpeciesForm } from "#app/data/pokemon-species"; import { default as PokemonSpecies, PokemonSpeciesForm, getFusedSpeciesName, getPokemonSpecies, getPokemonSpeciesForm } from "#app/data/pokemon-species";
import { getStarterValueFriendshipCap, speciesStarterCosts } from "#app/data/balance/starters"; import { CLASSIC_CANDY_FRIENDSHIP_MULTIPLIER, getStarterValueFriendshipCap, speciesStarterCosts } from "#app/data/balance/starters";
import { starterPassiveAbilities } from "#app/data/balance/passives"; import { starterPassiveAbilities } from "#app/data/balance/passives";
import { Constructor, isNullOrUndefined, randSeedInt } from "#app/utils"; import { Constructor, isNullOrUndefined, randSeedInt } from "#app/utils";
import * as Utils from "#app/utils"; import * as Utils from "#app/utils";
@ -3005,15 +3005,16 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container {
if (this.isFainted()) { if (this.isFainted()) {
// set splice index here, so future scene queues happen before FaintedPhase // set splice index here, so future scene queues happen before FaintedPhase
this.scene.setPhaseQueueSplice(); this.scene.setPhaseQueueSplice();
this.scene.unshiftPhase(new FaintPhase(this.scene, this.getBattlerIndex(), isOneHitKo)); if (!isNullOrUndefined(destinyTag) && dmg) {
// Destiny Bond will activate during FaintPhase
this.scene.unshiftPhase(new FaintPhase(this.scene, this.getBattlerIndex(), isOneHitKo, destinyTag, source));
} else {
this.scene.unshiftPhase(new FaintPhase(this.scene, this.getBattlerIndex(), isOneHitKo));
}
this.destroySubstitute(); this.destroySubstitute();
this.resetSummonData(); this.resetSummonData();
} }
if (dmg) {
destinyTag?.lapse(source, BattlerTagLapseType.CUSTOM);
}
return result; return result;
} }
} }
@ -4279,7 +4280,7 @@ export class PlayerPokemon extends Pokemon {
fusionStarterSpeciesId ? this.scene.gameData.starterData[fusionStarterSpeciesId] : null fusionStarterSpeciesId ? this.scene.gameData.starterData[fusionStarterSpeciesId] : null
].filter(d => !!d); ].filter(d => !!d);
const amount = new Utils.IntegerHolder(friendship); const amount = new Utils.IntegerHolder(friendship);
const starterAmount = new Utils.IntegerHolder(Math.floor(friendship * (this.scene.gameMode.isClassic && friendship > 0 ? 2 : 1) / (fusionStarterSpeciesId ? 2 : 1))); const starterAmount = new Utils.IntegerHolder(Math.floor(friendship * (this.scene.gameMode.isClassic && friendship > 0 ? CLASSIC_CANDY_FRIENDSHIP_MULTIPLIER : 1) / (fusionStarterSpeciesId ? 2 : 1)));
if (amount.value > 0) { if (amount.value > 0) {
this.scene.applyModifier(PokemonFriendshipBoosterModifier, true, this, amount); this.scene.applyModifier(PokemonFriendshipBoosterModifier, true, this, amount);
this.scene.applyModifier(PokemonFriendshipBoosterModifier, true, this, starterAmount); this.scene.applyModifier(PokemonFriendshipBoosterModifier, true, this, starterAmount);

View File

@ -30,6 +30,7 @@ import { StatusEffect } from "#enums/status-effect";
import i18next from "i18next"; import i18next from "i18next";
import { type DoubleBattleChanceBoosterModifierType, type EvolutionItemModifierType, type FormChangeItemModifierType, type ModifierOverride, type ModifierType, type PokemonBaseStatTotalModifierType, type PokemonExpBoosterModifierType, type PokemonFriendshipBoosterModifierType, type PokemonMoveAccuracyBoosterModifierType, type PokemonMultiHitModifierType, type TerastallizeModifierType, type TmModifierType, getModifierType, ModifierPoolType, ModifierTypeGenerator, modifierTypes, PokemonHeldItemModifierType } from "./modifier-type"; import { type DoubleBattleChanceBoosterModifierType, type EvolutionItemModifierType, type FormChangeItemModifierType, type ModifierOverride, type ModifierType, type PokemonBaseStatTotalModifierType, type PokemonExpBoosterModifierType, type PokemonFriendshipBoosterModifierType, type PokemonMoveAccuracyBoosterModifierType, type PokemonMultiHitModifierType, type TerastallizeModifierType, type TmModifierType, getModifierType, ModifierPoolType, ModifierTypeGenerator, modifierTypes, PokemonHeldItemModifierType } from "./modifier-type";
import { Color, ShadowColor } from "#enums/color"; import { Color, ShadowColor } from "#enums/color";
import { FRIENDSHIP_GAIN_FROM_RARE_CANDY } from "#app/data/balance/starters";
export type ModifierPredicate = (modifier: Modifier) => boolean; export type ModifierPredicate = (modifier: Modifier) => boolean;
@ -2213,7 +2214,7 @@ export class PokemonLevelIncrementModifier extends ConsumablePokemonModifier {
playerPokemon.levelExp = 0; playerPokemon.levelExp = 0;
} }
playerPokemon.addFriendship(5); playerPokemon.addFriendship(FRIENDSHIP_GAIN_FROM_RARE_CANDY);
playerPokemon.scene.unshiftPhase(new LevelUpPhase(playerPokemon.scene, playerPokemon.scene.getParty().indexOf(playerPokemon), playerPokemon.level - levelCount.value, playerPokemon.level)); playerPokemon.scene.unshiftPhase(new LevelUpPhase(playerPokemon.scene, playerPokemon.scene.getParty().indexOf(playerPokemon), playerPokemon.level - levelCount.value, playerPokemon.level));

View File

@ -1,12 +1,12 @@
import BattleScene from "#app/battle-scene"; import BattleScene from "#app/battle-scene";
import { BattlerIndex, BattleType } from "#app/battle"; import { BattlerIndex, BattleType } from "#app/battle";
import { applyPostFaintAbAttrs, PostFaintAbAttr, applyPostKnockOutAbAttrs, PostKnockOutAbAttr, applyPostVictoryAbAttrs, PostVictoryAbAttr } from "#app/data/ability"; import { applyPostFaintAbAttrs, PostFaintAbAttr, applyPostKnockOutAbAttrs, PostKnockOutAbAttr, applyPostVictoryAbAttrs, PostVictoryAbAttr } from "#app/data/ability";
import { BattlerTagLapseType } from "#app/data/battler-tags"; import { BattlerTagLapseType, DestinyBondTag } from "#app/data/battler-tags";
import { battleSpecDialogue } from "#app/data/dialogue"; import { battleSpecDialogue } from "#app/data/dialogue";
import { allMoves, PostVictoryStatStageChangeAttr } from "#app/data/move"; import { allMoves, PostVictoryStatStageChangeAttr } from "#app/data/move";
import { BattleSpec } from "#app/enums/battle-spec"; import { BattleSpec } from "#app/enums/battle-spec";
import { StatusEffect } from "#app/enums/status-effect"; import { StatusEffect } from "#app/enums/status-effect";
import { PokemonMove, EnemyPokemon, PlayerPokemon, HitResult } from "#app/field/pokemon"; import Pokemon, { PokemonMove, EnemyPokemon, PlayerPokemon, HitResult } from "#app/field/pokemon";
import { getPokemonNameWithAffix } from "#app/messages"; import { getPokemonNameWithAffix } from "#app/messages";
import { PokemonInstantReviveModifier } from "#app/modifier/modifier"; import { PokemonInstantReviveModifier } from "#app/modifier/modifier";
import i18next from "i18next"; import i18next from "i18next";
@ -19,19 +19,40 @@ import { SwitchPhase } from "./switch-phase";
import { VictoryPhase } from "./victory-phase"; import { VictoryPhase } from "./victory-phase";
import { SpeciesFormChangeActiveTrigger } from "#app/data/pokemon-forms"; import { SpeciesFormChangeActiveTrigger } from "#app/data/pokemon-forms";
import { SwitchType } from "#enums/switch-type"; import { SwitchType } from "#enums/switch-type";
import { isNullOrUndefined } from "#app/utils";
import { FRIENDSHIP_LOSS_FROM_FAINT } from "#app/data/balance/starters";
export class FaintPhase extends PokemonPhase { export class FaintPhase extends PokemonPhase {
/**
* Whether or not enduring (for this phase's purposes, Reviver Seed) should be prevented
*/
private preventEndure: boolean; private preventEndure: boolean;
constructor(scene: BattleScene, battlerIndex: BattlerIndex, preventEndure?: boolean) { /**
* Destiny Bond tag belonging to the currently fainting Pokemon, if applicable
*/
private destinyTag?: DestinyBondTag;
/**
* The source Pokemon that dealt fatal damage and should get KO'd by Destiny Bond, if applicable
*/
private source?: Pokemon;
constructor(scene: BattleScene, battlerIndex: BattlerIndex, preventEndure: boolean = false, destinyTag?: DestinyBondTag, source?: Pokemon) {
super(scene, battlerIndex); super(scene, battlerIndex);
this.preventEndure = preventEndure!; // TODO: is this bang correct? this.preventEndure = preventEndure;
this.destinyTag = destinyTag;
this.source = source;
} }
start() { start() {
super.start(); super.start();
if (!isNullOrUndefined(this.destinyTag) && !isNullOrUndefined(this.source)) {
this.destinyTag.lapse(this.source, BattlerTagLapseType.CUSTOM);
}
if (!this.preventEndure) { if (!this.preventEndure) {
const instantReviveModifier = this.scene.applyModifier(PokemonInstantReviveModifier, this.player, this.getPokemon()) as PokemonInstantReviveModifier; const instantReviveModifier = this.scene.applyModifier(PokemonInstantReviveModifier, this.player, this.getPokemon()) as PokemonInstantReviveModifier;
@ -127,7 +148,7 @@ export class FaintPhase extends PokemonPhase {
pokemon.faintCry(() => { pokemon.faintCry(() => {
if (pokemon instanceof PlayerPokemon) { if (pokemon instanceof PlayerPokemon) {
pokemon.addFriendship(-10); pokemon.addFriendship(-FRIENDSHIP_LOSS_FROM_FAINT);
} }
pokemon.hideInfo(); pokemon.hideInfo();
this.scene.playSound("se/faint"); this.scene.playSound("se/faint");

View File

@ -1,13 +0,0 @@
import BattleScene from "#app/battle-scene";
import { Phase } from "#app/phase";
import { Mode } from "#app/ui/ui";
export class OutdatedPhase extends Phase {
constructor(scene: BattleScene) {
super(scene);
}
start(): void {
this.scene.ui.setMode(Mode.OUTDATED);
}
}

View File

@ -43,10 +43,9 @@ import { Species } from "#enums/species";
import { applyChallenges, ChallengeType } from "#app/data/challenge"; import { applyChallenges, ChallengeType } from "#app/data/challenge";
import { WeatherType } from "#enums/weather-type"; import { WeatherType } from "#enums/weather-type";
import { TerrainType } from "#app/data/terrain"; import { TerrainType } from "#app/data/terrain";
import { OutdatedPhase } from "#app/phases/outdated-phase";
import { ReloadSessionPhase } from "#app/phases/reload-session-phase"; import { ReloadSessionPhase } from "#app/phases/reload-session-phase";
import { RUN_HISTORY_LIMIT } from "#app/ui/run-history-ui-handler"; import { RUN_HISTORY_LIMIT } from "#app/ui/run-history-ui-handler";
import { applySessionDataPatches, applySettingsDataPatches, applySystemDataPatches } from "#app/system/version-converter"; import { applySessionVersionMigration, applySystemVersionMigration, applySettingsVersionMigration } from "./version_migration/version_converter";
import { MysteryEncounterSaveData } from "#app/data/mystery-encounters/mystery-encounter-save-data"; import { MysteryEncounterSaveData } from "#app/data/mystery-encounters/mystery-encounter-save-data";
import { MysteryEncounterType } from "#enums/mystery-encounter-type"; import { MysteryEncounterType } from "#enums/mystery-encounter-type";
import { PokerogueApiClearSessionData } from "#app/@types/pokerogue-api"; import { PokerogueApiClearSessionData } from "#app/@types/pokerogue-api";
@ -403,10 +402,7 @@ export class GameData {
.then(error => { .then(error => {
this.scene.ui.savingIcon.hide(); this.scene.ui.savingIcon.hide();
if (error) { if (error) {
if (error.startsWith("client version out of date")) { if (error.startsWith("session out of date")) {
this.scene.clearPhaseQueue();
this.scene.unshiftPhase(new OutdatedPhase(this.scene));
} else if (error.startsWith("session out of date")) {
this.scene.clearPhaseQueue(); this.scene.clearPhaseQueue();
this.scene.unshiftPhase(new ReloadSessionPhase(this.scene)); this.scene.unshiftPhase(new ReloadSessionPhase(this.scene));
} }
@ -482,7 +478,7 @@ export class GameData {
localStorage.setItem(lsItemKey, ""); localStorage.setItem(lsItemKey, "");
} }
applySystemDataPatches(systemData); applySystemVersionMigration(systemData);
this.trainerId = systemData.trainerId; this.trainerId = systemData.trainerId;
this.secretId = systemData.secretId; this.secretId = systemData.secretId;
@ -857,7 +853,7 @@ export class GameData {
const settings = JSON.parse(localStorage.getItem("settings")!); // TODO: is this bang correct? const settings = JSON.parse(localStorage.getItem("settings")!); // TODO: is this bang correct?
applySettingsDataPatches(settings); applySettingsVersionMigration(settings);
for (const setting of Object.keys(settings)) { for (const setting of Object.keys(settings)) {
setSetting(this.scene, setting, settings[setting]); setSetting(this.scene, setting, settings[setting]);
@ -1313,7 +1309,7 @@ export class GameData {
return v; return v;
}) as SessionSaveData; }) as SessionSaveData;
applySessionDataPatches(sessionData); applySessionVersionMigration(sessionData);
return sessionData; return sessionData;
} }
@ -1354,10 +1350,7 @@ export class GameData {
this.scene.ui.savingIcon.hide(); this.scene.ui.savingIcon.hide();
} }
if (error) { if (error) {
if (error.startsWith("client version out of date")) { if (error.startsWith("session out of date")) {
this.scene.clearPhaseQueue();
this.scene.unshiftPhase(new OutdatedPhase(this.scene));
} else if (error.startsWith("session out of date")) {
this.scene.clearPhaseQueue(); this.scene.clearPhaseQueue();
this.scene.unshiftPhase(new ReloadSessionPhase(this.scene)); this.scene.unshiftPhase(new ReloadSessionPhase(this.scene));
} }

View File

@ -1,157 +0,0 @@
import { allSpecies } from "#app/data/pokemon-species";
import { AbilityAttr, defaultStarterSpecies, DexAttr, SessionSaveData, SystemSaveData } from "./game-data";
import { SettingKeys } from "./settings/settings";
const LATEST_VERSION = "1.0.5";
export function applySessionDataPatches(data: SessionSaveData) {
const curVersion = data.gameVersion;
// Always sanitize money as a safeguard
data.money = Math.floor(data.money);
if (curVersion !== LATEST_VERSION) {
switch (curVersion) {
case "1.0.0":
case "1.0.1":
case "1.0.2":
case "1.0.3":
case "1.0.4":
// --- PATCHES ---
// Fix Battle Items, Vitamins, and Lures
data.modifiers.forEach((m) => {
if (m.className === "PokemonBaseStatModifier") {
m.className = "BaseStatModifier";
} else if (m.className === "PokemonResetNegativeStatStageModifier") {
m.className = "ResetNegativeStatStageModifier";
} else if (m.className === "TempBattleStatBoosterModifier") {
// Dire Hit no longer a part of the TempBattleStatBoosterModifierTypeGenerator
if (m.typeId !== "DIRE_HIT") {
m.className = "TempStatStageBoosterModifier";
m.typeId = "TEMP_STAT_STAGE_BOOSTER";
// Migration from TempBattleStat to Stat
const newStat = m.typePregenArgs[0] + 1;
m.typePregenArgs[0] = newStat;
// From [ stat, battlesLeft ] to [ stat, maxBattles, battleCount ]
m.args = [ newStat, 5, m.args[1] ];
} else {
m.className = "TempCritBoosterModifier";
m.typePregenArgs = [];
// From [ stat, battlesLeft ] to [ maxBattles, battleCount ]
m.args = [ 5, m.args[1] ];
}
} else if (m.className === "DoubleBattleChanceBoosterModifier" && m.args.length === 1) {
let maxBattles: number;
switch (m.typeId) {
case "MAX_LURE":
maxBattles = 30;
break;
case "SUPER_LURE":
maxBattles = 15;
break;
default:
maxBattles = 10;
break;
}
// From [ battlesLeft ] to [ maxBattles, battleCount ]
m.args = [ maxBattles, m.args[0] ];
}
});
data.enemyModifiers.forEach((m) => {
if (m.className === "PokemonBaseStatModifier") {
m.className = "BaseStatModifier";
} else if (m.className === "PokemonResetNegativeStatStageModifier") {
m.className = "ResetNegativeStatStageModifier";
}
});
}
data.gameVersion = LATEST_VERSION;
}
}
export function applySystemDataPatches(data: SystemSaveData) {
const curVersion = data.gameVersion;
if (curVersion !== LATEST_VERSION) {
switch (curVersion) {
case "1.0.0":
case "1.0.1":
case "1.0.2":
case "1.0.3":
case "1.0.4":
// --- LEGACY PATCHES ---
if (data.starterData && data.dexData) {
// Migrate ability starter data if empty for caught species
Object.keys(data.starterData).forEach(sd => {
if (data.dexData[sd]?.caughtAttr && (data.starterData[sd] && !data.starterData[sd].abilityAttr)) {
data.starterData[sd].abilityAttr = 1;
}
});
}
// Fix Legendary Stats
if (data.gameStats && (data.gameStats.legendaryPokemonCaught !== undefined && data.gameStats.subLegendaryPokemonCaught === undefined)) {
data.gameStats.subLegendaryPokemonSeen = 0;
data.gameStats.subLegendaryPokemonCaught = 0;
data.gameStats.subLegendaryPokemonHatched = 0;
allSpecies.filter(s => s.subLegendary).forEach(s => {
const dexEntry = data.dexData[s.speciesId];
data.gameStats.subLegendaryPokemonSeen += dexEntry.seenCount;
data.gameStats.legendaryPokemonSeen = Math.max(data.gameStats.legendaryPokemonSeen - dexEntry.seenCount, 0);
data.gameStats.subLegendaryPokemonCaught += dexEntry.caughtCount;
data.gameStats.legendaryPokemonCaught = Math.max(data.gameStats.legendaryPokemonCaught - dexEntry.caughtCount, 0);
data.gameStats.subLegendaryPokemonHatched += dexEntry.hatchedCount;
data.gameStats.legendaryPokemonHatched = Math.max(data.gameStats.legendaryPokemonHatched - dexEntry.hatchedCount, 0);
});
data.gameStats.subLegendaryPokemonSeen = Math.max(data.gameStats.subLegendaryPokemonSeen, data.gameStats.subLegendaryPokemonCaught);
data.gameStats.legendaryPokemonSeen = Math.max(data.gameStats.legendaryPokemonSeen, data.gameStats.legendaryPokemonCaught);
data.gameStats.mythicalPokemonSeen = Math.max(data.gameStats.mythicalPokemonSeen, data.gameStats.mythicalPokemonCaught);
}
// --- PATCHES ---
// Fix Starter Data
if (data.starterData && data.dexData) {
for (const starterId of defaultStarterSpecies) {
if (data.starterData[starterId]?.abilityAttr) {
data.starterData[starterId].abilityAttr |= AbilityAttr.ABILITY_1;
}
if (data.dexData[starterId]?.caughtAttr) {
data.dexData[starterId].caughtAttr |= DexAttr.FEMALE;
}
}
}
}
data.gameVersion = LATEST_VERSION;
}
}
export function applySettingsDataPatches(settings: Object) {
const curVersion = settings.hasOwnProperty("gameVersion") ? settings["gameVersion"] : "1.0.0";
if (curVersion !== LATEST_VERSION) {
switch (curVersion) {
case "1.0.0":
case "1.0.1":
case "1.0.2":
case "1.0.3":
case "1.0.4":
// --- PATCHES ---
// Fix Reward Cursor Target
if (settings.hasOwnProperty("REROLL_TARGET") && !settings.hasOwnProperty(SettingKeys.Shop_Cursor_Target)) {
settings[SettingKeys.Shop_Cursor_Target] = settings["REROLL_TARGET"];
delete settings["REROLL_TARGET"];
localStorage.setItem("settings", JSON.stringify(settings));
}
}
// Note that the current game version will be written at `saveSettings`
}
}

View File

@ -0,0 +1,182 @@
import { SessionSaveData, SystemSaveData } from "../game-data";
import { version } from "../../../package.json";
// --- v1.0.4 (and below) PATCHES --- //
import * as v1_0_4 from "./versions/v1_0_4";
const LATEST_VERSION = version.split(".").map(value => parseInt(value));
/**
* Converts incoming {@linkcode SystemSaveData} that has a version below the
* current version number listed in `package.json`.
*
* Note that no transforms act on the {@linkcode data} if its version matches
* the current version or if there are no migrations made between its version up
* to the current version.
* @param data {@linkcode SystemSaveData}
* @see {@link SystemVersionConverter}
*/
export function applySystemVersionMigration(data: SystemSaveData) {
const curVersion = data.gameVersion.split(".").map(value => parseInt(value));
if (!curVersion.every((value, index) => value === LATEST_VERSION[index])) {
const converter = new SystemVersionConverter();
converter.applyStaticPreprocessors(data);
converter.applyMigration(data, curVersion);
}
}
/**
* Converts incoming {@linkcode SessionSavaData} that has a version below the
* current version number listed in `package.json`.
*
* Note that no transforms act on the {@linkcode data} if its version matches
* the current version or if there are no migrations made between its version up
* to the current version.
* @param data {@linkcode SessionSaveData}
* @see {@link SessionVersionConverter}
*/
export function applySessionVersionMigration(data: SessionSaveData) {
const curVersion = data.gameVersion.split(".").map(value => parseInt(value));
if (!curVersion.every((value, index) => value === LATEST_VERSION[index])) {
const converter = new SessionVersionConverter();
converter.applyStaticPreprocessors(data);
converter.applyMigration(data, curVersion);
}
}
/**
* Converts incoming settings data that has a version below the
* current version number listed in `package.json`.
*
* Note that no transforms act on the {@linkcode data} if its version matches
* the current version or if there are no migrations made between its version up
* to the current version.
* @param data Settings data object
* @see {@link SettingsVersionConverter}
*/
export function applySettingsVersionMigration(data: Object) {
const gameVersion: string = data.hasOwnProperty("gameVersion") ? data["gameVersion"] : "1.0.0";
const curVersion = gameVersion.split(".").map(value => parseInt(value));
if (!curVersion.every((value, index) => value === LATEST_VERSION[index])) {
const converter = new SettingsVersionConverter();
converter.applyStaticPreprocessors(data);
converter.applyMigration(data, curVersion);
}
}
/**
* Abstract class encapsulating the logic for migrating data from a given version up to
* the current version listed in `package.json`.
*
* Note that, for any version converter, the corresponding `applyMigration`
* function would only need to be changed once when the first migration for a
* given version is introduced. Similarly, a version file (within the `versions`
* folder) would only need to be created for a version once with the appropriate
* array nomenclature.
*/
abstract class VersionConverter {
/**
* Iterates through an array of designated migration functions that are each
* called one by one to transform the data.
* @param data The data to be operated on
* @param migrationArr An array of functions that will transform the incoming data
*/
callMigrators(data: any, migrationArr: readonly any[]) {
for (const migrate of migrationArr) {
migrate(data);
}
}
/**
* Applies any version-agnostic data sanitation as defined within the function
* body.
* @param data The data to be operated on
*/
applyStaticPreprocessors(_data: any): void {
}
/**
* Uses the current version the incoming data to determine the starting point
* of the migration which will cascade up to the latest version, calling the
* necessary migration functions in the process.
* @param data The data to be operated on
* @param curVersion [0] Current major version
* [1] Current minor version
* [2] Current patch version
*/
abstract applyMigration(data: any, curVersion: number[]): void;
}
/**
* Class encapsulating the logic for migrating {@linkcode SessionSaveData} from
* a given version up to the current version listed in `package.json`.
* @extends VersionConverter
*/
class SessionVersionConverter extends VersionConverter {
override applyStaticPreprocessors(data: SessionSaveData): void {
// Always sanitize money as a safeguard
data.money = Math.floor(data.money);
}
override applyMigration(data: SessionSaveData, curVersion: number[]): void {
const [ curMajor, curMinor, curPatch ] = curVersion;
if (curMajor === 1) {
if (curMinor === 0) {
if (curPatch <= 4) {
console.log("Applying v1.0.4 session data migration!");
this.callMigrators(data, v1_0_4.sessionMigrators);
}
}
}
console.log(`Session data successfully migrated to v${version}!`);
}
}
/**
* Class encapsulating the logic for migrating {@linkcode SystemSaveData} from
* a given version up to the current version listed in `package.json`.
* @extends VersionConverter
*/
class SystemVersionConverter extends VersionConverter {
override applyMigration(data: SystemSaveData, curVersion: number[]): void {
const [ curMajor, curMinor, curPatch ] = curVersion;
if (curMajor === 1) {
if (curMinor === 0) {
if (curPatch <= 4) {
console.log("Applying v1.0.4 system data migraton!");
this.callMigrators(data, v1_0_4.systemMigrators);
}
}
}
console.log(`System data successfully migrated to v${version}!`);
}
}
/**
* Class encapsulating the logic for migrating settings data from
* a given version up to the current version listed in `package.json`.
* @extends VersionConverter
*/
class SettingsVersionConverter extends VersionConverter {
override applyMigration(data: Object, curVersion: number[]): void {
const [ curMajor, curMinor, curPatch ] = curVersion;
if (curMajor === 1) {
if (curMinor === 0) {
if (curPatch <= 4) {
console.log("Applying v1.0.4 settings data migraton!");
this.callMigrators(data, v1_0_4.settingsMigrators);
}
}
}
console.log(`System data successfully migrated to v${version}!`);
}
}

View File

@ -0,0 +1,135 @@
import { SettingKeys } from "../../settings/settings";
import { AbilityAttr, defaultStarterSpecies, DexAttr, SystemSaveData, SessionSaveData } from "../../game-data";
import { allSpecies } from "../../../data/pokemon-species";
export const systemMigrators = [
/**
* Migrate ability starter data if empty for caught species.
* @param data {@linkcode SystemSaveData}
*/
function migrateAbilityData(data: SystemSaveData) {
if (data.starterData && data.dexData) {
Object.keys(data.starterData).forEach(sd => {
if (data.dexData[sd]?.caughtAttr && (data.starterData[sd] && !data.starterData[sd].abilityAttr)) {
data.starterData[sd].abilityAttr = 1;
}
});
}
},
/**
* Populate legendary Pokémon statistics if they are missing.
* @param data {@linkcode SystemSaveData}
*/
function fixLegendaryStats(data: SystemSaveData) {
if (data.gameStats && (data.gameStats.legendaryPokemonCaught !== undefined && data.gameStats.subLegendaryPokemonCaught === undefined)) {
data.gameStats.subLegendaryPokemonSeen = 0;
data.gameStats.subLegendaryPokemonCaught = 0;
data.gameStats.subLegendaryPokemonHatched = 0;
allSpecies.filter(s => s.subLegendary).forEach(s => {
const dexEntry = data.dexData[s.speciesId];
data.gameStats.subLegendaryPokemonSeen += dexEntry.seenCount;
data.gameStats.legendaryPokemonSeen = Math.max(data.gameStats.legendaryPokemonSeen - dexEntry.seenCount, 0);
data.gameStats.subLegendaryPokemonCaught += dexEntry.caughtCount;
data.gameStats.legendaryPokemonCaught = Math.max(data.gameStats.legendaryPokemonCaught - dexEntry.caughtCount, 0);
data.gameStats.subLegendaryPokemonHatched += dexEntry.hatchedCount;
data.gameStats.legendaryPokemonHatched = Math.max(data.gameStats.legendaryPokemonHatched - dexEntry.hatchedCount, 0);
});
data.gameStats.subLegendaryPokemonSeen = Math.max(data.gameStats.subLegendaryPokemonSeen, data.gameStats.subLegendaryPokemonCaught);
data.gameStats.legendaryPokemonSeen = Math.max(data.gameStats.legendaryPokemonSeen, data.gameStats.legendaryPokemonCaught);
data.gameStats.mythicalPokemonSeen = Math.max(data.gameStats.mythicalPokemonSeen, data.gameStats.mythicalPokemonCaught);
}
},
/**
* Unlock all starters' first ability and female gender option.
* @param data {@linkcode SystemSaveData}
*/
function fixStarterData(data: SystemSaveData) {
for (const starterId of defaultStarterSpecies) {
if (data.starterData[starterId]?.abilityAttr) {
data.starterData[starterId].abilityAttr |= AbilityAttr.ABILITY_1;
}
if (data.dexData[starterId]?.caughtAttr) {
data.dexData[starterId].caughtAttr |= DexAttr.FEMALE;
}
}
}
] as const;
export const settingsMigrators = [
/**
* Migrate from "REROLL_TARGET" property to {@linkcode
* SettingKeys.Shop_Cursor_Target}.
* @param data the `settings` object
*/
function fixRerollTarget(data: Object) {
if (data.hasOwnProperty("REROLL_TARGET") && !data.hasOwnProperty(SettingKeys.Shop_Cursor_Target)) {
data[SettingKeys.Shop_Cursor_Target] = data["REROLL_TARGET"];
delete data["REROLL_TARGET"];
localStorage.setItem("settings", JSON.stringify(data));
}
}
] as const;
export const sessionMigrators = [
/**
* Converts old lapsing modifiers (battle items, lures, and Dire Hit) and
* other miscellaneous modifiers (vitamins, White Herb) to any new class
* names and/or change in reload arguments.
* @param data {@linkcode SessionSaveData}
*/
function migrateModifiers(data: SessionSaveData) {
data.modifiers.forEach((m) => {
if (m.className === "PokemonBaseStatModifier") {
m.className = "BaseStatModifier";
} else if (m.className === "PokemonResetNegativeStatStageModifier") {
m.className = "ResetNegativeStatStageModifier";
} else if (m.className === "TempBattleStatBoosterModifier") {
const maxBattles = 5;
// Dire Hit no longer a part of the TempBattleStatBoosterModifierTypeGenerator
if (m.typeId !== "DIRE_HIT") {
m.className = "TempStatStageBoosterModifier";
m.typeId = "TEMP_STAT_STAGE_BOOSTER";
// Migration from TempBattleStat to Stat
const newStat = m.typePregenArgs[0] + 1;
m.typePregenArgs[0] = newStat;
// From [ stat, battlesLeft ] to [ stat, maxBattles, battleCount ]
m.args = [ newStat, maxBattles, Math.min(m.args[1], maxBattles) ];
} else {
m.className = "TempCritBoosterModifier";
m.typePregenArgs = [];
// From [ stat, battlesLeft ] to [ maxBattles, battleCount ]
m.args = [ maxBattles, Math.min(m.args[1], maxBattles) ];
}
} else if (m.className === "DoubleBattleChanceBoosterModifier" && m.args.length === 1) {
let maxBattles: number;
switch (m.typeId) {
case "MAX_LURE":
maxBattles = 30;
break;
case "SUPER_LURE":
maxBattles = 15;
break;
default:
maxBattles = 10;
break;
}
// From [ battlesLeft ] to [ maxBattles, battleCount ]
m.args = [ maxBattles, Math.min(m.args[0], maxBattles) ];
}
});
data.enemyModifiers.forEach((m) => {
if (m.className === "PokemonBaseStatModifier") {
m.className = "BaseStatModifier";
} else if (m.className === "PokemonResetNegativeStatStageModifier") {
m.className = "ResetNegativeStatStageModifier";
}
});
}
] as const;

View File

@ -0,0 +1,255 @@
import { ArenaTagSide, ArenaTrapTag } from "#app/data/arena-tag";
import { allMoves } from "#app/data/move";
import { Abilities } from "#enums/abilities";
import { ArenaTagType } from "#enums/arena-tag-type";
import { Moves } from "#enums/moves";
import { Species } from "#enums/species";
import GameManager from "#test/utils/gameManager";
import Phaser from "phaser";
import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest";
import { BattlerIndex } from "#app/battle";
import { StatusEffect } from "#enums/status-effect";
import { PokemonInstantReviveModifier } from "#app/modifier/modifier";
describe("Moves - Destiny Bond", () => {
let phaserGame: Phaser.Game;
let game: GameManager;
const defaultParty = [ Species.BULBASAUR, Species.SQUIRTLE ];
const enemyFirst = [ BattlerIndex.ENEMY, BattlerIndex.PLAYER ];
const playerFirst = [ BattlerIndex.PLAYER, BattlerIndex.ENEMY ];
beforeAll(() => {
phaserGame = new Phaser.Game({
type: Phaser.HEADLESS,
});
});
afterEach(() => {
game.phaseInterceptor.restoreOg();
});
beforeEach(() => {
game = new GameManager(phaserGame);
game.override.battleType("single")
.ability(Abilities.UNNERVE) // Pre-emptively prevent flakiness from opponent berries
.enemySpecies(Species.RATTATA)
.enemyAbility(Abilities.RUN_AWAY)
.startingLevel(100) // Make sure tested moves KO
.enemyLevel(5)
.enemyMoveset(Moves.DESTINY_BOND);
});
it("should KO the opponent on the same turn", async () => {
const moveToUse = Moves.TACKLE;
game.override.moveset(moveToUse);
await game.classicMode.startBattle(defaultParty);
const enemyPokemon = game.scene.getEnemyPokemon();
const playerPokemon = game.scene.getPlayerPokemon();
game.move.select(moveToUse);
await game.setTurnOrder(enemyFirst);
await game.phaseInterceptor.to("BerryPhase");
expect(enemyPokemon?.isFainted()).toBe(true);
expect(playerPokemon?.isFainted()).toBe(true);
});
it("should KO the opponent on the next turn", async () => {
const moveToUse = Moves.TACKLE;
game.override.moveset([ Moves.SPLASH, moveToUse ]);
await game.classicMode.startBattle(defaultParty);
const enemyPokemon = game.scene.getEnemyPokemon();
const playerPokemon = game.scene.getPlayerPokemon();
// Turn 1: Enemy uses Destiny Bond and doesn't faint
game.move.select(Moves.SPLASH);
await game.setTurnOrder(playerFirst);
await game.toNextTurn();
expect(enemyPokemon?.isFainted()).toBe(false);
expect(playerPokemon?.isFainted()).toBe(false);
// Turn 2: Player KO's the enemy before the enemy's turn
game.move.select(moveToUse);
await game.setTurnOrder(playerFirst);
await game.phaseInterceptor.to("BerryPhase");
expect(enemyPokemon?.isFainted()).toBe(true);
expect(playerPokemon?.isFainted()).toBe(true);
});
it("should fail if used twice in a row", async () => {
const moveToUse = Moves.TACKLE;
game.override.moveset([ Moves.SPLASH, moveToUse ]);
await game.classicMode.startBattle(defaultParty);
const enemyPokemon = game.scene.getEnemyPokemon();
const playerPokemon = game.scene.getPlayerPokemon();
// Turn 1: Enemy uses Destiny Bond and doesn't faint
game.move.select(Moves.SPLASH);
await game.setTurnOrder(enemyFirst);
await game.toNextTurn();
expect(enemyPokemon?.isFainted()).toBe(false);
expect(playerPokemon?.isFainted()).toBe(false);
// Turn 2: Enemy should fail Destiny Bond then get KO'd
game.move.select(moveToUse);
await game.setTurnOrder(enemyFirst);
await game.phaseInterceptor.to("BerryPhase");
expect(enemyPokemon?.isFainted()).toBe(true);
expect(playerPokemon?.isFainted()).toBe(false);
});
it("should not KO the opponent if the user dies to weather", async () => {
// Opponent will be reduced to 1 HP by False Swipe, then faint to Sandstorm
const moveToUse = Moves.FALSE_SWIPE;
game.override.moveset(moveToUse)
.ability(Abilities.SAND_STREAM);
await game.classicMode.startBattle(defaultParty);
const enemyPokemon = game.scene.getEnemyPokemon();
const playerPokemon = game.scene.getPlayerPokemon();
game.move.select(moveToUse);
await game.setTurnOrder(enemyFirst);
await game.phaseInterceptor.to("BerryPhase");
expect(enemyPokemon?.isFainted()).toBe(true);
expect(playerPokemon?.isFainted()).toBe(false);
});
it("should not KO the opponent if the user had another turn", async () => {
const moveToUse = Moves.TACKLE;
game.override.moveset([ Moves.SPORE, moveToUse ]);
await game.classicMode.startBattle(defaultParty);
const enemyPokemon = game.scene.getEnemyPokemon();
const playerPokemon = game.scene.getPlayerPokemon();
// Turn 1: Enemy uses Destiny Bond and doesn't faint
game.move.select(Moves.SPORE);
await game.setTurnOrder(enemyFirst);
await game.toNextTurn();
expect(enemyPokemon?.isFainted()).toBe(false);
expect(playerPokemon?.isFainted()).toBe(false);
expect(enemyPokemon?.status?.effect).toBe(StatusEffect.SLEEP);
// Turn 2: Enemy should skip a turn due to sleep, then get KO'd
game.move.select(moveToUse);
await game.setTurnOrder(enemyFirst);
await game.phaseInterceptor.to("BerryPhase");
expect(enemyPokemon?.isFainted()).toBe(true);
expect(playerPokemon?.isFainted()).toBe(false);
});
it("should not KO an ally", async () => {
game.override.moveset([ Moves.DESTINY_BOND, Moves.CRUNCH ])
.battleType("double");
await game.classicMode.startBattle([ Species.SHEDINJA, Species.BULBASAUR, Species.SQUIRTLE ]);
const enemyPokemon0 = game.scene.getEnemyField()[0];
const enemyPokemon1 = game.scene.getEnemyField()[1];
const playerPokemon0 = game.scene.getPlayerField()[0];
const playerPokemon1 = game.scene.getPlayerField()[1];
// Shedinja uses Destiny Bond, then ally Bulbasaur KO's Shedinja with Crunch
game.move.select(Moves.DESTINY_BOND, 0);
game.move.select(Moves.CRUNCH, 1, BattlerIndex.PLAYER);
await game.setTurnOrder([ BattlerIndex.PLAYER, BattlerIndex.PLAYER_2, BattlerIndex.ENEMY, BattlerIndex.ENEMY_2 ]);
await game.phaseInterceptor.to("BerryPhase");
expect(enemyPokemon0?.isFainted()).toBe(false);
expect(enemyPokemon1?.isFainted()).toBe(false);
expect(playerPokemon0?.isFainted()).toBe(true);
expect(playerPokemon1?.isFainted()).toBe(false);
});
it("should not cause a crash if the user is KO'd by Ceaseless Edge", async () => {
const moveToUse = Moves.CEASELESS_EDGE;
vi.spyOn(allMoves[moveToUse], "accuracy", "get").mockReturnValue(100);
game.override.moveset(moveToUse);
await game.classicMode.startBattle(defaultParty);
const enemyPokemon = game.scene.getEnemyPokemon();
const playerPokemon = game.scene.getPlayerPokemon();
game.move.select(moveToUse);
await game.setTurnOrder(enemyFirst);
await game.phaseInterceptor.to("BerryPhase");
expect(enemyPokemon?.isFainted()).toBe(true);
expect(playerPokemon?.isFainted()).toBe(true);
// Ceaseless Edge spikes effect should still activate
const tagAfter = game.scene.arena.getTagOnSide(ArenaTagType.SPIKES, ArenaTagSide.ENEMY) as ArenaTrapTag;
expect(tagAfter.tagType).toBe(ArenaTagType.SPIKES);
expect(tagAfter.layers).toBe(1);
});
it("should not cause a crash if the user is KO'd by Pledge moves", async () => {
game.override.moveset([ Moves.GRASS_PLEDGE, Moves.WATER_PLEDGE ])
.battleType("double");
await game.classicMode.startBattle(defaultParty);
const enemyPokemon0 = game.scene.getEnemyField()[0];
const enemyPokemon1 = game.scene.getEnemyField()[1];
const playerPokemon0 = game.scene.getPlayerField()[0];
const playerPokemon1 = game.scene.getPlayerField()[1];
game.move.select(Moves.GRASS_PLEDGE, 0, BattlerIndex.ENEMY);
game.move.select(Moves.WATER_PLEDGE, 1, BattlerIndex.ENEMY);
await game.setTurnOrder([ BattlerIndex.ENEMY, BattlerIndex.ENEMY_2, BattlerIndex.PLAYER, BattlerIndex.PLAYER_2 ]);
await game.phaseInterceptor.to("BerryPhase");
expect(enemyPokemon0?.isFainted()).toBe(true);
expect(enemyPokemon1?.isFainted()).toBe(false);
expect(playerPokemon0?.isFainted()).toBe(false);
expect(playerPokemon1?.isFainted()).toBe(true);
// Pledge secondary effect should still activate
const tagAfter = game.scene.arena.getTagOnSide(ArenaTagType.GRASS_WATER_PLEDGE, ArenaTagSide.ENEMY) as ArenaTrapTag;
expect(tagAfter.tagType).toBe(ArenaTagType.GRASS_WATER_PLEDGE);
});
/**
* In particular, this should prevent something like
* {@link https://github.com/pagefaultgames/pokerogue/issues/4219}
* from occurring with fainting by KO'ing a Destiny Bond user with U-Turn.
*/
it("should not allow the opponent to revive via Reviver Seed", async () => {
const moveToUse = Moves.TACKLE;
game.override.moveset(moveToUse)
.startingHeldItems([{ name: "REVIVER_SEED" }]);
await game.classicMode.startBattle(defaultParty);
const enemyPokemon = game.scene.getEnemyPokemon();
const playerPokemon = game.scene.getPlayerPokemon();
game.move.select(moveToUse);
await game.setTurnOrder(enemyFirst);
await game.phaseInterceptor.to("BerryPhase");
expect(enemyPokemon?.isFainted()).toBe(true);
expect(playerPokemon?.isFainted()).toBe(true);
// Check that the Tackle user's Reviver Seed did not activate
const revSeeds = game.scene.getModifiers(PokemonInstantReviveModifier).filter(m => m.pokemonId === playerPokemon?.id);
expect(revSeeds.length).toBe(1);
});
});

View File

@ -0,0 +1,89 @@
import { Abilities } from "#enums/abilities";
import { Biome } from "#enums/biome";
import { Moves } from "#enums/moves";
import { Stat } from "#enums/stat";
import { allMoves, SecretPowerAttr } from "#app/data/move";
import { Species } from "#enums/species";
import GameManager from "#test/utils/gameManager";
import Phaser from "phaser";
import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest";
import { StatusEffect } from "#enums/status-effect";
import { BattlerIndex } from "#app/battle";
import { ArenaTagType } from "#enums/arena-tag-type";
import { ArenaTagSide } from "#app/data/arena-tag";
describe("Moves - Secret Power", () => {
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.SECRET_POWER ])
.ability(Abilities.BALL_FETCH)
.battleType("single")
.disableCrits()
.enemySpecies(Species.MAGIKARP)
.enemyLevel(60)
.enemyAbility(Abilities.BALL_FETCH);
});
it("Secret Power checks for an active terrain first then looks at the biome for its secondary effect", async () => {
game.override
.startingBiome(Biome.VOLCANO)
.enemyMoveset([ Moves.SPLASH, Moves.MISTY_TERRAIN ]);
vi.spyOn(allMoves[Moves.SECRET_POWER], "chance", "get").mockReturnValue(100);
await game.classicMode.startBattle([ Species.FEEBAS ]);
const enemyPokemon = game.scene.getEnemyPokemon()!;
// No Terrain + Biome.VOLCANO --> Burn
game.move.select(Moves.SECRET_POWER);
await game.forceEnemyMove(Moves.SPLASH);
await game.phaseInterceptor.to("TurnEndPhase");
expect(enemyPokemon.status?.effect).toBe(StatusEffect.BURN);
// Misty Terrain --> SpAtk -1
game.move.select(Moves.SECRET_POWER);
await game.forceEnemyMove(Moves.MISTY_TERRAIN);
await game.phaseInterceptor.to("TurnEndPhase");
expect(enemyPokemon.getStatStage(Stat.SPATK)).toBe(-1);
});
it("the 'rainbow' effect of fire+water pledge does not double the chance of secret power's secondary effect",
async () => {
game.override
.moveset([ Moves.FIRE_PLEDGE, Moves.WATER_PLEDGE, Moves.SECRET_POWER, Moves.SPLASH ])
.enemyMoveset([ Moves.SPLASH ])
.battleType("double");
await game.classicMode.startBattle([ Species.BLASTOISE, Species.CHARIZARD ]);
const secretPowerAttr = allMoves[Moves.SECRET_POWER].getAttrs(SecretPowerAttr)[0];
vi.spyOn(secretPowerAttr, "getMoveChance");
game.move.select(Moves.WATER_PLEDGE, 0, BattlerIndex.ENEMY);
game.move.select(Moves.FIRE_PLEDGE, 1, BattlerIndex.ENEMY_2);
await game.phaseInterceptor.to("TurnEndPhase");
expect(game.scene.arena.getTagOnSide(ArenaTagType.WATER_FIRE_PLEDGE, ArenaTagSide.PLAYER)).toBeDefined();
game.move.select(Moves.SECRET_POWER, 0, BattlerIndex.ENEMY);
game.move.select(Moves.SPLASH, 1);
await game.phaseInterceptor.to("BerryPhase", false);
expect(secretPowerAttr.getMoveChance).toHaveLastReturnedWith(30);
}
);
});

View File

@ -124,10 +124,31 @@ describe("The Expert Pokémon Breeder - Mystery Encounter", () => {
}); });
}); });
it("should start battle against the trainer", async () => { it("should start battle against the trainer with correctly loaded assets", async () => {
await game.runToMysteryEncounter(MysteryEncounterType.THE_EXPERT_POKEMON_BREEDER, defaultParty); await game.runToMysteryEncounter(MysteryEncounterType.THE_EXPERT_POKEMON_BREEDER, defaultParty);
let successfullyLoaded = false;
vi.spyOn(scene, "getEnemyParty").mockImplementation(() => {
const ace = scene.currentBattle?.enemyParty[0];
if (ace) {
// Pretend that loading assets takes an extra 500ms
vi.spyOn(ace, "loadAssets").mockImplementation(() => new Promise(resolve => {
setTimeout(() => {
successfullyLoaded = true;
resolve();
}, 500);
}));
}
return scene.currentBattle?.enemyParty ?? [];
});
await runMysteryEncounterToEnd(game, 1, undefined, true); await runMysteryEncounterToEnd(game, 1, undefined, true);
// Check that assets are successfully loaded
expect(successfullyLoaded).toBe(true);
// Check usual battle stuff
expect(scene.getCurrentPhase()?.constructor.name).toBe(CommandPhase.name); expect(scene.getCurrentPhase()?.constructor.name).toBe(CommandPhase.name);
expect(scene.currentBattle.trainer).toBeDefined(); expect(scene.currentBattle.trainer).toBeDefined();
expect(scene.currentBattle.mysteryEncounter?.encounterMode).toBe(MysteryEncounterMode.TRAINER_BATTLE); expect(scene.currentBattle.mysteryEncounter?.encounterMode).toBe(MysteryEncounterMode.TRAINER_BATTLE);
@ -182,10 +203,31 @@ describe("The Expert Pokémon Breeder - Mystery Encounter", () => {
}); });
}); });
it("should start battle against the trainer", async () => { it("should start battle against the trainer with correctly loaded assets", async () => {
await game.runToMysteryEncounter(MysteryEncounterType.THE_EXPERT_POKEMON_BREEDER, defaultParty); await game.runToMysteryEncounter(MysteryEncounterType.THE_EXPERT_POKEMON_BREEDER, defaultParty);
let successfullyLoaded = false;
vi.spyOn(scene, "getEnemyParty").mockImplementation(() => {
const ace = scene.currentBattle?.enemyParty[0];
if (ace) {
// Pretend that loading assets takes an extra 500ms
vi.spyOn(ace, "loadAssets").mockImplementation(() => new Promise(resolve => {
setTimeout(() => {
successfullyLoaded = true;
resolve();
}, 500);
}));
}
return scene.currentBattle?.enemyParty ?? [];
});
await runMysteryEncounterToEnd(game, 2, undefined, true); await runMysteryEncounterToEnd(game, 2, undefined, true);
// Check that assets are successfully loaded
expect(successfullyLoaded).toBe(true);
// Check usual battle stuff
expect(scene.getCurrentPhase()?.constructor.name).toBe(CommandPhase.name); expect(scene.getCurrentPhase()?.constructor.name).toBe(CommandPhase.name);
expect(scene.currentBattle.trainer).toBeDefined(); expect(scene.currentBattle.trainer).toBeDefined();
expect(scene.currentBattle.mysteryEncounter?.encounterMode).toBe(MysteryEncounterMode.TRAINER_BATTLE); expect(scene.currentBattle.mysteryEncounter?.encounterMode).toBe(MysteryEncounterMode.TRAINER_BATTLE);
@ -240,10 +282,31 @@ describe("The Expert Pokémon Breeder - Mystery Encounter", () => {
}); });
}); });
it("should start battle against the trainer", async () => { it("should start battle against the trainer with correctly loaded assets", async () => {
await game.runToMysteryEncounter(MysteryEncounterType.THE_EXPERT_POKEMON_BREEDER, defaultParty); await game.runToMysteryEncounter(MysteryEncounterType.THE_EXPERT_POKEMON_BREEDER, defaultParty);
let successfullyLoaded = false;
vi.spyOn(scene, "getEnemyParty").mockImplementation(() => {
const ace = scene.currentBattle?.enemyParty[0];
if (ace) {
// Pretend that loading assets takes an extra 500ms
vi.spyOn(ace, "loadAssets").mockImplementation(() => new Promise(resolve => {
setTimeout(() => {
successfullyLoaded = true;
resolve();
}, 500);
}));
}
return scene.currentBattle?.enemyParty ?? [];
});
await runMysteryEncounterToEnd(game, 3, undefined, true); await runMysteryEncounterToEnd(game, 3, undefined, true);
// Check that assets are successfully loaded
expect(successfullyLoaded).toBe(true);
// Check usual battle stuff
expect(scene.getCurrentPhase()?.constructor.name).toBe(CommandPhase.name); expect(scene.getCurrentPhase()?.constructor.name).toBe(CommandPhase.name);
expect(scene.currentBattle.trainer).toBeDefined(); expect(scene.currentBattle.trainer).toBeDefined();
expect(scene.currentBattle.mysteryEncounter?.encounterMode).toBe(MysteryEncounterMode.TRAINER_BATTLE); expect(scene.currentBattle.mysteryEncounter?.encounterMode).toBe(MysteryEncounterMode.TRAINER_BATTLE);

View File

@ -23,6 +23,7 @@ import KeyboardPlugin = Phaser.Input.Keyboard.KeyboardPlugin;
import GamepadPlugin = Phaser.Input.Gamepad.GamepadPlugin; import GamepadPlugin = Phaser.Input.Gamepad.GamepadPlugin;
import EventEmitter = Phaser.Events.EventEmitter; import EventEmitter = Phaser.Events.EventEmitter;
import UpdateList = Phaser.GameObjects.UpdateList; import UpdateList = Phaser.GameObjects.UpdateList;
import { version } from "../../../package.json";
Object.defineProperty(window, "localStorage", { Object.defineProperty(window, "localStorage", {
value: mockLocalStorage(), value: mockLocalStorage(),
@ -101,6 +102,7 @@ export default class GameWrapper {
injectMandatory() { injectMandatory() {
this.game.config = { this.game.config = {
seed: ["test"], seed: ["test"],
gameVersion: version
}; };
this.scene.game = this.game; this.scene.game = this.game;
this.game.renderer = { this.game.renderer = {

View File

@ -1,47 +0,0 @@
import BattleScene from "../battle-scene";
import { ModalConfig, ModalUiHandler } from "./modal-ui-handler";
import { addTextObject, TextStyle } from "./text";
import { Mode } from "./ui";
export default class OutdatedModalUiHandler extends ModalUiHandler {
constructor(scene: BattleScene, mode: Mode | null = null) {
super(scene, mode);
}
getModalTitle(): string {
return "";
}
getWidth(): number {
return 160;
}
getHeight(): number {
return 64;
}
getMargin(): [number, number, number, number] {
return [ 0, 0, 48, 0 ];
}
getButtonLabels(): string[] {
return [ ];
}
setup(): void {
super.setup();
const label = addTextObject(this.scene, this.getWidth() / 2, this.getHeight() / 2, "Your client is currently outdated.\nPlease reload to update the game.\n\nIf this error persists, please clear your browser cache.", TextStyle.WINDOW, { fontSize: "48px", align: "center" });
label.setOrigin(0.5, 0.5);
this.modalContainer.add(label);
}
show(args: any[]): boolean {
const config: ModalConfig = {
buttonActions: []
};
return super.show([ config ]);
}
}

View File

@ -671,6 +671,9 @@ export default class PartyUiHandler extends MessageUiHandler {
} else if (this.cursor === 6) { } else if (this.cursor === 6) {
this.partyCancelButton.select(); this.partyCancelButton.select();
} }
if (this.lastCursor < 6 && this.lastCursor >= party.length) {
this.lastCursor = party.length - 1;
}
for (const p in party) { for (const p in party) {
const slotIndex = parseInt(p); const slotIndex = parseInt(p);

View File

@ -34,7 +34,6 @@ import SaveSlotSelectUiHandler from "./save-slot-select-ui-handler";
import TitleUiHandler from "./title-ui-handler"; import TitleUiHandler from "./title-ui-handler";
import SavingIconHandler from "./saving-icon-handler"; import SavingIconHandler from "./saving-icon-handler";
import UnavailableModalUiHandler from "./unavailable-modal-ui-handler"; import UnavailableModalUiHandler from "./unavailable-modal-ui-handler";
import OutdatedModalUiHandler from "./outdated-modal-ui-handler";
import SessionReloadModalUiHandler from "./session-reload-modal-ui-handler"; import SessionReloadModalUiHandler from "./session-reload-modal-ui-handler";
import { Button } from "#enums/buttons"; import { Button } from "#enums/buttons";
import i18next from "i18next"; import i18next from "i18next";
@ -90,7 +89,6 @@ export enum Mode {
LOADING, LOADING,
SESSION_RELOAD, SESSION_RELOAD,
UNAVAILABLE, UNAVAILABLE,
OUTDATED,
CHALLENGE_SELECT, CHALLENGE_SELECT,
RENAME_POKEMON, RENAME_POKEMON,
RUN_HISTORY, RUN_HISTORY,
@ -134,7 +132,6 @@ const noTransitionModes = [
Mode.LOADING, Mode.LOADING,
Mode.SESSION_RELOAD, Mode.SESSION_RELOAD,
Mode.UNAVAILABLE, Mode.UNAVAILABLE,
Mode.OUTDATED,
Mode.RENAME_POKEMON, Mode.RENAME_POKEMON,
Mode.TEST_DIALOGUE, Mode.TEST_DIALOGUE,
Mode.AUTO_COMPLETE, Mode.AUTO_COMPLETE,
@ -200,7 +197,6 @@ export default class UI extends Phaser.GameObjects.Container {
new LoadingModalUiHandler(scene), new LoadingModalUiHandler(scene),
new SessionReloadModalUiHandler(scene), new SessionReloadModalUiHandler(scene),
new UnavailableModalUiHandler(scene), new UnavailableModalUiHandler(scene),
new OutdatedModalUiHandler(scene),
new GameChallengesUiHandler(scene), new GameChallengesUiHandler(scene),
new RenameFormUiHandler(scene), new RenameFormUiHandler(scene),
new RunHistoryUiHandler(scene), new RunHistoryUiHandler(scene),