This commit is contained in:
muscode13 2024-10-09 16:14:57 -06:00
commit 8a1e82e168
67 changed files with 639 additions and 250 deletions

14
global.d.ts vendored Normal file
View File

@ -0,0 +1,14 @@
import type { SetupServerApi } from "msw/node";
export {};
declare global {
/**
* Only used in testing.
* Can technically be undefined/null but for ease of use we are going to assume it is always defined.
* Used to load i18n files exclusively.
*
* To set up your own server in a test see `game_data.test.ts`
*/
var i18nServer: SetupServerApi;
}

View File

@ -399,13 +399,13 @@
"x": 0, "x": 0,
"y": 6, "y": 6,
"w": 36, "w": 36,
"h": 55 "h": 54
}, },
"frame": { "frame": {
"x": 72, "x": 72,
"y": 55, "y": 55,
"w": 36, "w": 36,
"h": 55 "h": 54
} }
}, },
{ {
@ -420,13 +420,13 @@
"x": 0, "x": 0,
"y": 6, "y": 6,
"w": 36, "w": 36,
"h": 55 "h": 54
}, },
"frame": { "frame": {
"x": 72, "x": 72,
"y": 55, "y": 55,
"w": 36, "w": 36,
"h": 55 "h": 54
} }
}, },
{ {

View File

@ -399,13 +399,13 @@
"x": 0, "x": 0,
"y": 6, "y": 6,
"w": 36, "w": 36,
"h": 55 "h": 54
}, },
"frame": { "frame": {
"x": 72, "x": 72,
"y": 55, "y": 55,
"w": 36, "w": 36,
"h": 55 "h": 54
} }
}, },
{ {
@ -420,13 +420,13 @@
"x": 0, "x": 0,
"y": 6, "y": 6,
"w": 36, "w": 36,
"h": 55 "h": 54
}, },
"frame": { "frame": {
"x": 72, "x": 72,
"y": 55, "y": 55,
"w": 36, "w": 36,
"h": 55 "h": 54
} }
}, },
{ {

@ -1 +1 @@
Subproject commit 3ccef8472dd7cc7c362538489954cb8fdad27e5f Subproject commit fc4a1effd5170def3c8314208a52cd0d8e6913ef

View File

@ -3161,13 +3161,17 @@ export default class BattleScene extends SceneBase {
/** /**
* Loads or generates a mystery encounter * Loads or generates a mystery encounter
* @param encounterType used to load session encounter when restarting game, etc. * @param encounterType used to load session encounter when restarting game, etc.
* @param canBypass optional boolean to indicate that the request is coming from a function that needs to access a Mystery Encounter outside of gameplay requirements
* @returns * @returns
*/ */
getMysteryEncounter(encounterType?: MysteryEncounterType): MysteryEncounter { getMysteryEncounter(encounterType?: MysteryEncounterType, canBypass?: boolean): MysteryEncounter {
// Loading override or session encounter // Loading override or session encounter
let encounter: MysteryEncounter | null; let encounter: MysteryEncounter | null;
if (!isNullOrUndefined(Overrides.MYSTERY_ENCOUNTER_OVERRIDE) && allMysteryEncounters.hasOwnProperty(Overrides.MYSTERY_ENCOUNTER_OVERRIDE)) { if (!isNullOrUndefined(Overrides.MYSTERY_ENCOUNTER_OVERRIDE) && allMysteryEncounters.hasOwnProperty(Overrides.MYSTERY_ENCOUNTER_OVERRIDE)) {
encounter = allMysteryEncounters[Overrides.MYSTERY_ENCOUNTER_OVERRIDE]; encounter = allMysteryEncounters[Overrides.MYSTERY_ENCOUNTER_OVERRIDE];
} else if (canBypass) {
encounter = allMysteryEncounters[encounterType ?? -1];
return encounter;
} else { } else {
encounter = !isNullOrUndefined(encounterType) ? allMysteryEncounters[encounterType] : null; encounter = !isNullOrUndefined(encounterType) ? allMysteryEncounters[encounterType] : null;
} }

View File

@ -970,13 +970,16 @@ export class MoveEffectAttr extends MoveAttr {
public lastHitOnly: boolean; public lastHitOnly: boolean;
/** Should this effect only apply on the first target hit? */ /** Should this effect only apply on the first target hit? */
public firstTargetOnly: boolean; public firstTargetOnly: boolean;
/** Overrides the secondary effect chance for this attr if set. */
public effectChanceOverride?: number;
constructor(selfTarget?: boolean, trigger?: MoveEffectTrigger, firstHitOnly: boolean = false, lastHitOnly: boolean = false, firstTargetOnly: boolean = false) { constructor(selfTarget?: boolean, trigger?: MoveEffectTrigger, firstHitOnly: boolean = false, lastHitOnly: boolean = false, firstTargetOnly: boolean = false, effectChanceOverride?: number) {
super(selfTarget); super(selfTarget);
this.trigger = trigger !== undefined ? trigger : MoveEffectTrigger.POST_APPLY; this.trigger = trigger ?? MoveEffectTrigger.POST_APPLY;
this.firstHitOnly = firstHitOnly; this.firstHitOnly = firstHitOnly;
this.lastHitOnly = lastHitOnly; this.lastHitOnly = lastHitOnly;
this.firstTargetOnly = firstTargetOnly; this.firstTargetOnly = firstTargetOnly;
this.effectChanceOverride = effectChanceOverride;
} }
/** /**
@ -1001,15 +1004,15 @@ export class MoveEffectAttr extends MoveAttr {
/** /**
* Gets the used move's additional effect chance. * Gets the used move's additional effect chance.
* If user's ability has MoveEffectChanceMultiplierAbAttr or IgnoreMoveEffectsAbAttr modifies the base chance. * Chance is modified by {@linkcode MoveEffectChanceMultiplierAbAttr} and {@linkcode IgnoreMoveEffectsAbAttr}.
* @param user {@linkcode Pokemon} using this move * @param user {@linkcode Pokemon} using this move
* @param target {@linkcode Pokemon} target of this move * @param target {@linkcode Pokemon | Target} of this move
* @param move {@linkcode Move} being used * @param move {@linkcode Move} being used
* @param selfEffect {@linkcode Boolean} if move targets user. * @param selfEffect `true` if move targets user.
* @returns Move chance value. * @returns Move effect chance value.
*/ */
getMoveChance(user: Pokemon, target: Pokemon, move: Move, selfEffect?: Boolean, showAbility?: Boolean): integer { getMoveChance(user: Pokemon, target: Pokemon, move: Move, selfEffect?: Boolean, showAbility?: Boolean): integer {
const moveChance = new Utils.NumberHolder(move.chance); const moveChance = new Utils.NumberHolder(this.effectChanceOverride ?? move.chance);
applyAbAttrs(MoveEffectChanceMultiplierAbAttr, user, null, false, moveChance, move, target, selfEffect, showAbility); applyAbAttrs(MoveEffectChanceMultiplierAbAttr, user, null, false, moveChance, move, target, selfEffect, showAbility);
@ -2755,14 +2758,17 @@ export class AwaitCombinedPledgeAttr extends OverrideMoveEffectAttr {
/** /**
* Attribute used for moves that change stat stages * Attribute used for moves that change stat stages
* @param stats {@linkcode BattleStat} array of stats to be changed *
* @param stages stages by which to change the stats, from -6 to 6 * @param stats {@linkcode BattleStat} Array of stat(s) to change
* @param selfTarget whether the changes are applied to the user (true) or the target (false) * @param stages How many stages to change the stat(s) by, [-6, 6]
* @param condition {@linkcode MoveConditionFunc} optional condition to trigger the stat change * @param selfTarget `true` if the move is self-targetting
* @param firstHitOnly whether the stat change only applies on the first hit of a multi hit move * @param condition {@linkcode MoveConditionFunc} Optional condition to be checked in order to apply the changes
* @param moveEffectTrigger {@linkcode MoveEffectTrigger} the trigger for the effect to take place * @param showMessage `true` to display a message; default `true`
* @param firstTargetOnly whether, if this is a multi target move, to only apply the effect after the first target is hit, rather than once for each target * @param firstHitOnly `true` if only the first hit of a multi hit move should cause a stat stage change; default `false`
* @param lastHitOnly whether the effect should only apply after the last hit of a multi hit move * @param moveEffectTrigger {@linkcode MoveEffectTrigger} When the stat change should trigger; default {@linkcode MoveEffectTrigger.HIT}
* @param firstTargetOnly `true` if a move that hits multiple pokemon should only trigger the stat change if it hits at least one pokemon, rather than once per hit pokemon; default `false`
* @param lastHitOnly `true` if the effect should only apply after the last hit of a multi hit move; default `false`
* @param effectChanceOverride Will override the move's normal secondary effect chance if specified
* *
* @extends MoveEffectAttr * @extends MoveEffectAttr
* @see {@linkcode apply} * @see {@linkcode apply}
@ -2770,14 +2776,14 @@ export class AwaitCombinedPledgeAttr extends OverrideMoveEffectAttr {
export class StatStageChangeAttr extends MoveEffectAttr { export class StatStageChangeAttr extends MoveEffectAttr {
public stats: BattleStat[]; public stats: BattleStat[];
public stages: integer; public stages: integer;
private condition: MoveConditionFunc | null; private condition?: MoveConditionFunc | null;
private showMessage: boolean; private showMessage: boolean;
constructor(stats: BattleStat[], stages: integer, selfTarget?: boolean, condition?: MoveConditionFunc | null, showMessage: boolean = true, firstHitOnly: boolean = false, moveEffectTrigger: MoveEffectTrigger = MoveEffectTrigger.HIT, firstTargetOnly: boolean = false, lastHitOnly: boolean = false) { constructor(stats: BattleStat[], stages: integer, selfTarget?: boolean, condition?: MoveConditionFunc | null, showMessage: boolean = true, firstHitOnly: boolean = false, moveEffectTrigger: MoveEffectTrigger = MoveEffectTrigger.HIT, firstTargetOnly: boolean = false, lastHitOnly: boolean = false, effectChanceOverride?: number) {
super(selfTarget, moveEffectTrigger, firstHitOnly, lastHitOnly, firstTargetOnly); super(selfTarget, moveEffectTrigger, firstHitOnly, lastHitOnly, firstTargetOnly, effectChanceOverride);
this.stats = stats; this.stats = stats;
this.stages = stages; this.stages = stages;
this.condition = condition!; // TODO: is this bang correct? this.condition = condition;
this.showMessage = showMessage; this.showMessage = showMessage;
} }
@ -9559,9 +9565,8 @@ export function initMoves() {
new AttackMove(Moves.TRIPLE_ARROWS, Type.FIGHTING, MoveCategory.PHYSICAL, 90, 100, 10, 30, 0, 8) new AttackMove(Moves.TRIPLE_ARROWS, Type.FIGHTING, MoveCategory.PHYSICAL, 90, 100, 10, 30, 0, 8)
.makesContact(false) .makesContact(false)
.attr(HighCritAttr) .attr(HighCritAttr)
.attr(StatStageChangeAttr, [ Stat.DEF ], -1) .attr(StatStageChangeAttr, [ Stat.DEF ], -1, undefined, undefined, undefined, undefined, undefined, undefined, undefined, 50)
.attr(FlinchAttr) .attr(FlinchAttr),
.partial(),
new AttackMove(Moves.INFERNAL_PARADE, Type.GHOST, MoveCategory.SPECIAL, 60, 100, 15, 30, 0, 8) new AttackMove(Moves.INFERNAL_PARADE, Type.GHOST, MoveCategory.SPECIAL, 60, 100, 15, 30, 0, 8)
.attr(StatusEffectAttr, StatusEffect.BURN) .attr(StatusEffectAttr, StatusEffect.BURN)
.attr(MovePowerMultiplierAttr, (user, target, move) => target.status ? 2 : 1), .attr(MovePowerMultiplierAttr, (user, target, move) => target.status ? 2 : 1),

View File

@ -128,6 +128,7 @@ export const ATrainersTestEncounter: MysteryEncounter =
return true; return true;
}) })
.setLocalizationKey(`${namespace}`)
.withTitle(`${namespace}:title`) .withTitle(`${namespace}:title`)
.withDescription(`${namespace}:description`) .withDescription(`${namespace}:description`)
.withQuery(`${namespace}:query`) .withQuery(`${namespace}:query`)

View File

@ -166,6 +166,7 @@ export const AbsoluteAvariceEncounter: MysteryEncounter =
text: `${namespace}:intro`, text: `${namespace}:intro`,
} }
]) ])
.setLocalizationKey(`${namespace}`)
.withTitle(`${namespace}:title`) .withTitle(`${namespace}:title`)
.withDescription(`${namespace}:description`) .withDescription(`${namespace}:description`)
.withQuery(`${namespace}:query`) .withQuery(`${namespace}:query`)

View File

@ -64,6 +64,7 @@ export const AnOfferYouCantRefuseEncounter: MysteryEncounter =
speaker: `${namespace}:speaker`, speaker: `${namespace}:speaker`,
}, },
]) ])
.setLocalizationKey(`${namespace}`)
.withTitle(`${namespace}:title`) .withTitle(`${namespace}:title`)
.withDescription(`${namespace}:description`) .withDescription(`${namespace}:description`)
.withQuery(`${namespace}:query`) .withQuery(`${namespace}:query`)

View File

@ -110,6 +110,7 @@ export const BerriesAboundEncounter: MysteryEncounter =
return true; return true;
}) })
.setLocalizationKey(`${namespace}`)
.withTitle(`${namespace}:title`) .withTitle(`${namespace}:title`)
.withDescription(`${namespace}:description`) .withDescription(`${namespace}:description`)
.withQuery(`${namespace}:query`) .withQuery(`${namespace}:query`)

View File

@ -276,6 +276,7 @@ export const BugTypeSuperfanEncounter: MysteryEncounter =
return true; return true;
}) })
.setLocalizationKey(`${namespace}`)
.withTitle(`${namespace}:title`) .withTitle(`${namespace}:title`)
.withDescription(`${namespace}:description`) .withDescription(`${namespace}:description`)
.withQuery(`${namespace}:query`) .withQuery(`${namespace}:query`)

View File

@ -148,6 +148,7 @@ export const ClowningAroundEncounter: MysteryEncounter =
return true; return true;
}) })
.setLocalizationKey(`${namespace}`)
.withTitle(`${namespace}:title`) .withTitle(`${namespace}:title`)
.withDescription(`${namespace}:description`) .withDescription(`${namespace}:description`)
.withQuery(`${namespace}:query`) .withQuery(`${namespace}:query`)

View File

@ -102,6 +102,7 @@ export const DancingLessonsEncounter: MysteryEncounter =
text: `${namespace}:intro`, text: `${namespace}:intro`,
} }
]) ])
.setLocalizationKey(`${namespace}`)
.withTitle(`${namespace}:title`) .withTitle(`${namespace}:title`)
.withDescription(`${namespace}:description`) .withDescription(`${namespace}:description`)
.withQuery(`${namespace}:query`) .withQuery(`${namespace}:query`)

View File

@ -117,6 +117,7 @@ export const DarkDealEncounter: MysteryEncounter =
.withSceneWaveRangeRequirement(30, CLASSIC_MODE_MYSTERY_ENCOUNTER_WAVES[1]) .withSceneWaveRangeRequirement(30, CLASSIC_MODE_MYSTERY_ENCOUNTER_WAVES[1])
.withScenePartySizeRequirement(2, 6, true) // Must have at least 2 pokemon in party .withScenePartySizeRequirement(2, 6, true) // Must have at least 2 pokemon in party
.withCatchAllowed(true) .withCatchAllowed(true)
.setLocalizationKey(`${namespace}`)
.withTitle(`${namespace}:title`) .withTitle(`${namespace}:title`)
.withDescription(`${namespace}:description`) .withDescription(`${namespace}:description`)
.withQuery(`${namespace}:query`) .withQuery(`${namespace}:query`)

View File

@ -84,6 +84,7 @@ export const DelibirdyEncounter: MysteryEncounter =
text: `${namespace}:intro`, text: `${namespace}:intro`,
} }
]) ])
.setLocalizationKey(`${namespace}`)
.withTitle(`${namespace}:title`) .withTitle(`${namespace}:title`)
.withDescription(`${namespace}:description`) .withDescription(`${namespace}:description`)
.withQuery(`${namespace}:query`) .withQuery(`${namespace}:query`)

View File

@ -51,6 +51,7 @@ export const DepartmentStoreSaleEncounter: MysteryEncounter =
}, },
]) ])
.withAutoHideIntroVisuals(false) .withAutoHideIntroVisuals(false)
.setLocalizationKey(`${namespace}`)
.withTitle(`${namespace}:title`) .withTitle(`${namespace}:title`)
.withDescription(`${namespace}:description`) .withDescription(`${namespace}:description`)
.withQuery(`${namespace}:query`) .withQuery(`${namespace}:query`)

View File

@ -52,6 +52,7 @@ export const FieldTripEncounter: MysteryEncounter =
}, },
]) ])
.withAutoHideIntroVisuals(false) .withAutoHideIntroVisuals(false)
.setLocalizationKey(`${namespace}`)
.withTitle(`${namespace}:title`) .withTitle(`${namespace}:title`)
.withDescription(`${namespace}:description`) .withDescription(`${namespace}:description`)
.withQuery(`${namespace}:query`) .withQuery(`${namespace}:query`)

View File

@ -122,6 +122,7 @@ export const FieryFalloutEncounter: MysteryEncounter =
return true; return true;
}) })
.setLocalizationKey(`${namespace}`)
.withTitle(`${namespace}:title`) .withTitle(`${namespace}:title`)
.withDescription(`${namespace}:description`) .withDescription(`${namespace}:description`)
.withQuery(`${namespace}:query`) .withQuery(`${namespace}:query`)

View File

@ -120,6 +120,7 @@ export const FightOrFlightEncounter: MysteryEncounter =
return true; return true;
}) })
.setLocalizationKey(`${namespace}`)
.withTitle(`${namespace}:title`) .withTitle(`${namespace}:title`)
.withDescription(`${namespace}:description`) .withDescription(`${namespace}:description`)
.withQuery(`${namespace}:query`) .withQuery(`${namespace}:query`)

View File

@ -76,6 +76,7 @@ export const FunAndGamesEncounter: MysteryEncounter =
text: `${namespace}:intro_dialogue`, text: `${namespace}:intro_dialogue`,
}, },
]) ])
.setLocalizationKey(`${namespace}`)
.withTitle(`${namespace}:title`) .withTitle(`${namespace}:title`)
.withDescription(`${namespace}:description`) .withDescription(`${namespace}:description`)
.withQuery(`${namespace}:query`) .withQuery(`${namespace}:query`)

View File

@ -96,6 +96,7 @@ export const GlobalTradeSystemEncounter: MysteryEncounter =
text: `${namespace}:intro`, text: `${namespace}:intro`,
} }
]) ])
.setLocalizationKey(`${namespace}`)
.withTitle(`${namespace}:title`) .withTitle(`${namespace}:title`)
.withDescription(`${namespace}:description`) .withDescription(`${namespace}:description`)
.withQuery(`${namespace}:query`) .withQuery(`${namespace}:query`)

View File

@ -50,6 +50,7 @@ export const LostAtSeaEncounter: MysteryEncounter = MysteryEncounterBuilder.with
return true; return true;
}) })
.setLocalizationKey(`${namespace}`)
.withTitle(`${namespace}:title`) .withTitle(`${namespace}:title`)
.withDescription(`${namespace}:description`) .withDescription(`${namespace}:description`)
.withQuery(`${namespace}:query`) .withQuery(`${namespace}:query`)

View File

@ -125,6 +125,7 @@ export const MysteriousChallengersEncounter: MysteryEncounter =
return true; return true;
}) })
.setLocalizationKey(`${namespace}`)
.withTitle(`${namespace}:title`) .withTitle(`${namespace}:title`)
.withDescription(`${namespace}:description`) .withDescription(`${namespace}:description`)
.withQuery(`${namespace}:query`) .withQuery(`${namespace}:query`)

View File

@ -61,6 +61,7 @@ export const MysteriousChestEncounter: MysteryEncounter =
text: `${namespace}:intro`, text: `${namespace}:intro`,
} }
]) ])
.setLocalizationKey(`${namespace}`)
.withTitle(`${namespace}:title`) .withTitle(`${namespace}:title`)
.withDescription(`${namespace}:description`) .withDescription(`${namespace}:description`)
.withQuery(`${namespace}:query`) .withQuery(`${namespace}:query`)

View File

@ -69,6 +69,7 @@ export const PartTimerEncounter: MysteryEncounter =
return true; return true;
}) })
.setLocalizationKey(`${namespace}`)
.withTitle(`${namespace}:title`) .withTitle(`${namespace}:title`)
.withDescription(`${namespace}:description`) .withDescription(`${namespace}:description`)
.withQuery(`${namespace}:query`) .withQuery(`${namespace}:query`)

View File

@ -54,6 +54,7 @@ export const SafariZoneEncounter: MysteryEncounter =
text: `${namespace}:intro`, text: `${namespace}:intro`,
}, },
]) ])
.setLocalizationKey(`${namespace}`)
.withTitle(`${namespace}:title`) .withTitle(`${namespace}:title`)
.withDescription(`${namespace}:description`) .withDescription(`${namespace}:description`)
.withQuery(`${namespace}:query`) .withQuery(`${namespace}:query`)

View File

@ -62,6 +62,7 @@ export const ShadyVitaminDealerEncounter: MysteryEncounter =
speaker: `${namespace}:speaker`, speaker: `${namespace}:speaker`,
}, },
]) ])
.setLocalizationKey(`${namespace}`)
.withTitle(`${namespace}:title`) .withTitle(`${namespace}:title`)
.withDescription(`${namespace}:description`) .withDescription(`${namespace}:description`)
.withQuery(`${namespace}:query`) .withQuery(`${namespace}:query`)

View File

@ -88,6 +88,7 @@ export const SlumberingSnorlaxEncounter: MysteryEncounter =
return true; return true;
}) })
.setLocalizationKey(`${namespace}`)
.withTitle(`${namespace}:title`) .withTitle(`${namespace}:title`)
.withDescription(`${namespace}:description`) .withDescription(`${namespace}:description`)
.withQuery(`${namespace}:query`) .withQuery(`${namespace}:query`)

View File

@ -58,6 +58,7 @@ export const TeleportingHijinksEncounter: MysteryEncounter =
text: `${namespace}:intro`, text: `${namespace}:intro`,
} }
]) ])
.setLocalizationKey(`${namespace}`)
.withTitle(`${namespace}:title`) .withTitle(`${namespace}:title`)
.withDescription(`${namespace}:description`) .withDescription(`${namespace}:description`)
.withQuery(`${namespace}:query`) .withQuery(`${namespace}:query`)

View File

@ -196,6 +196,7 @@ export const TheExpertPokemonBreederEncounter: MysteryEncounter =
return true; return true;
}) })
.setLocalizationKey(`${namespace}`)
.withTitle(`${namespace}:title`) .withTitle(`${namespace}:title`)
.withDescription(`${namespace}:description`) .withDescription(`${namespace}:description`)
.withQuery(`${namespace}:query`) .withQuery(`${namespace}:query`)

View File

@ -53,6 +53,7 @@ export const ThePokemonSalesmanEncounter: MysteryEncounter =
speaker: `${namespace}:speaker`, speaker: `${namespace}:speaker`,
}, },
]) ])
.setLocalizationKey(`${namespace}`)
.withTitle(`${namespace}:title`) .withTitle(`${namespace}:title`)
.withDescription(`${namespace}:description`) .withDescription(`${namespace}:description`)
.withQuery(`${namespace}:query`) .withQuery(`${namespace}:query`)

View File

@ -117,6 +117,7 @@ export const TheStrongStuffEncounter: MysteryEncounter =
return true; return true;
}) })
.setLocalizationKey(`${namespace}`)
.withTitle(`${namespace}:title`) .withTitle(`${namespace}:title`)
.withDescription(`${namespace}:description`) .withDescription(`${namespace}:description`)
.withQuery(`${namespace}:query`) .withQuery(`${namespace}:query`)

View File

@ -94,6 +94,7 @@ export const TheWinstrateChallengeEncounter: MysteryEncounter =
return true; return true;
}) })
.setLocalizationKey(`${namespace}`)
.withTitle(`${namespace}:title`) .withTitle(`${namespace}:title`)
.withDescription(`${namespace}:description`) .withDescription(`${namespace}:description`)
.withQuery(`${namespace}:query`) .withQuery(`${namespace}:query`)

View File

@ -52,6 +52,7 @@ export const TrainingSessionEncounter: MysteryEncounter =
text: `${namespace}:intro`, text: `${namespace}:intro`,
} }
]) ])
.setLocalizationKey(`${namespace}`)
.withTitle(`${namespace}:title`) .withTitle(`${namespace}:title`)
.withDescription(`${namespace}:description`) .withDescription(`${namespace}:description`)
.withQuery(`${namespace}:query`) .withQuery(`${namespace}:query`)

View File

@ -54,6 +54,7 @@ export const TrashToTreasureEncounter: MysteryEncounter =
text: `${namespace}:intro`, text: `${namespace}:intro`,
}, },
]) ])
.setLocalizationKey(`${namespace}`)
.withTitle(`${namespace}:title`) .withTitle(`${namespace}:title`)
.withDescription(`${namespace}:description`) .withDescription(`${namespace}:description`)
.withQuery(`${namespace}:query`) .withQuery(`${namespace}:query`)

View File

@ -125,6 +125,7 @@ export const UncommonBreedEncounter: MysteryEncounter =
scene.time.delayedCall(500, () => scene.playSound("battle_anims/PRSFX- Spotlight2")); scene.time.delayedCall(500, () => scene.playSound("battle_anims/PRSFX- Spotlight2"));
return true; return true;
}) })
.setLocalizationKey(`${namespace}`)
.withTitle(`${namespace}:title`) .withTitle(`${namespace}:title`)
.withDescription(`${namespace}:description`) .withDescription(`${namespace}:description`)
.withQuery(`${namespace}:query`) .withQuery(`${namespace}:query`)

View File

@ -125,6 +125,7 @@ export const WeirdDreamEncounter: MysteryEncounter =
text: `${namespace}:intro_dialogue`, text: `${namespace}:intro_dialogue`,
}, },
]) ])
.setLocalizationKey(`${namespace}`)
.withTitle(`${namespace}:title`) .withTitle(`${namespace}:title`)
.withDescription(`${namespace}:description`) .withDescription(`${namespace}:description`)
.withQuery(`${namespace}:query`) .withQuery(`${namespace}:query`)

View File

@ -190,7 +190,7 @@ export default class MysteryEncounter implements IMysteryEncounter {
secondaryPokemon?: PlayerPokemon[]; secondaryPokemon?: PlayerPokemon[];
// #region Post-construct / Auto-populated params // #region Post-construct / Auto-populated params
localizationKey: string;
/** /**
* Dialogue object containing all the dialogue, messages, tooltips, etc. for an encounter * Dialogue object containing all the dialogue, messages, tooltips, etc. for an encounter
*/ */
@ -264,6 +264,7 @@ export default class MysteryEncounter implements IMysteryEncounter {
Object.assign(this, encounter); Object.assign(this, encounter);
} }
this.encounterTier = this.encounterTier ?? MysteryEncounterTier.COMMON; this.encounterTier = this.encounterTier ?? MysteryEncounterTier.COMMON;
this.localizationKey = this.localizationKey ?? "";
this.dialogue = this.dialogue ?? {}; this.dialogue = this.dialogue ?? {};
this.spriteConfigs = this.spriteConfigs ? [ ...this.spriteConfigs ] : []; this.spriteConfigs = this.spriteConfigs ? [ ...this.spriteConfigs ] : [];
// Default max is 1 for ROGUE encounters, 2 for others // Default max is 1 for ROGUE encounters, 2 for others
@ -528,6 +529,7 @@ export class MysteryEncounterBuilder implements Partial<IMysteryEncounter> {
options: [MysteryEncounterOption, MysteryEncounterOption, ...MysteryEncounterOption[]]; options: [MysteryEncounterOption, MysteryEncounterOption, ...MysteryEncounterOption[]];
enemyPartyConfigs: EnemyPartyConfig[] = []; enemyPartyConfigs: EnemyPartyConfig[] = [];
localizationKey: string = "";
dialogue: MysteryEncounterDialogue = {}; dialogue: MysteryEncounterDialogue = {};
requirements: EncounterSceneRequirement[] = []; requirements: EncounterSceneRequirement[] = [];
primaryPokemonRequirements: EncounterPokemonRequirement[] = []; primaryPokemonRequirements: EncounterPokemonRequirement[] = [];
@ -632,6 +634,16 @@ export class MysteryEncounterBuilder implements Partial<IMysteryEncounter> {
return this.withIntroSpriteConfigs(spriteConfigs).withIntroDialogue(dialogue); return this.withIntroSpriteConfigs(spriteConfigs).withIntroDialogue(dialogue);
} }
/**
* Sets the localization key used by the encounter
* @param localizationKey the string used as the key
* @returns `this`
*/
setLocalizationKey(localizationKey: string): this {
this.localizationKey = localizationKey;
return this;
}
/** /**
* OPTIONAL * OPTIONAL
*/ */

View File

@ -2761,7 +2761,7 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container {
if (damage > 0) { if (damage > 0) {
if (source.isPlayer()) { if (source.isPlayer()) {
this.scene.validateAchvs(DamageAchv, damage); this.scene.validateAchvs(DamageAchv, new Utils.NumberHolder(damage));
if (damage > this.scene.gameData.gameStats.highestDamage) { if (damage > this.scene.gameData.gameStats.highestDamage) {
this.scene.gameData.gameStats.highestDamage = damage; this.scene.gameData.gameStats.highestDamage = damage;
} }

View File

@ -3084,11 +3084,12 @@ export abstract class HeldItemTransferModifier extends PokemonHeldItemModifier {
* Steals an item from a set of target Pokemon. * Steals an item from a set of target Pokemon.
* This prioritizes high-tier held items when selecting the item to steal. * This prioritizes high-tier held items when selecting the item to steal.
* @param pokemon The {@linkcode Pokemon} holding this item * @param pokemon The {@linkcode Pokemon} holding this item
* @param target The {@linkcode Pokemon} to steal from (optional)
* @param _args N/A * @param _args N/A
* @returns `true` if an item was stolen; false otherwise. * @returns `true` if an item was stolen; false otherwise.
*/ */
override apply(pokemon: Pokemon, ..._args: unknown[]): boolean { override apply(pokemon: Pokemon, target?: Pokemon, ..._args: unknown[]): boolean {
const opponents = this.getTargets(pokemon); const opponents = this.getTargets(pokemon, target);
if (!opponents.length) { if (!opponents.length) {
return false; return false;
@ -3188,7 +3189,7 @@ export class TurnHeldItemTransferModifier extends HeldItemTransferModifier {
* @see {@linkcode HeldItemTransferModifier} * @see {@linkcode HeldItemTransferModifier}
*/ */
export class ContactHeldItemTransferChanceModifier extends HeldItemTransferModifier { export class ContactHeldItemTransferChanceModifier extends HeldItemTransferModifier {
private chance: number; public readonly chance: number;
constructor(type: ModifierType, pokemonId: number, chancePercent: number, stackCount?: number) { constructor(type: ModifierType, pokemonId: number, chancePercent: number, stackCount?: number) {
super(type, pokemonId, stackCount); super(type, pokemonId, stackCount);

View File

@ -156,6 +156,7 @@ class DefaultOverrides {
readonly EGG_VARIANT_OVERRIDE: VariantTier | null = null; readonly EGG_VARIANT_OVERRIDE: VariantTier | null = null;
readonly EGG_FREE_GACHA_PULLS_OVERRIDE: boolean = false; readonly EGG_FREE_GACHA_PULLS_OVERRIDE: boolean = false;
readonly EGG_GACHA_PULL_COUNT_OVERRIDE: number = 0; readonly EGG_GACHA_PULL_COUNT_OVERRIDE: number = 0;
readonly UNLIMITED_EGG_COUNT_OVERRIDE: boolean = false;
// ------------------------- // -------------------------
// MYSTERY ENCOUNTER OVERRIDES // MYSTERY ENCOUNTER OVERRIDES

View File

@ -28,7 +28,7 @@ export class LevelUpPhase extends PlayerPartyMemberPokemonPhase {
this.scene.gameData.gameStats.highestLevel = this.level; this.scene.gameData.gameStats.highestLevel = this.level;
} }
this.scene.validateAchvs(LevelAchv, new Utils.IntegerHolder(this.level)); this.scene.validateAchvs(LevelAchv, new Utils.NumberHolder(this.level));
const pokemon = this.getPokemon(); const pokemon = this.getPokemon();
const prevStats = pokemon.stats.slice(0); const prevStats = pokemon.stats.slice(0);

View File

@ -196,7 +196,7 @@ export class TitlePhase extends Phase {
this.scene.gameMode = getGameMode(GameModes.DAILY); this.scene.gameMode = getGameMode(GameModes.DAILY);
this.scene.setSeed(seed); this.scene.setSeed(seed);
this.scene.resetSeed(1); this.scene.resetSeed(0);
this.scene.money = this.scene.gameMode.getStartingMoney(); this.scene.money = this.scene.gameMode.getStartingMoney();

View File

@ -81,6 +81,8 @@ const namespaceMap = {
miscDialogue: "dialogue-misc", miscDialogue: "dialogue-misc",
battleSpecDialogue: "dialogue-final-boss", battleSpecDialogue: "dialogue-final-boss",
doubleBattleDialogue: "dialogue-double-battle", doubleBattleDialogue: "dialogue-double-battle",
splashMessages: "splash-texts",
mysteryEncounterMessages: "mystery-encounter-texts",
}; };
//#region Functions //#region Functions

View File

@ -109,7 +109,7 @@ export class DamageAchv extends Achv {
damageAmount: integer; damageAmount: integer;
constructor(localizationKey: string, name: string, damageAmount: integer, iconImage: string, score: integer) { constructor(localizationKey: string, name: string, damageAmount: integer, iconImage: string, score: integer) {
super(localizationKey, name, "", iconImage, score, (_scene: BattleScene, args: any[]) => (args[0] as Utils.NumberHolder).value >= this.damageAmount); super(localizationKey, name, "", iconImage, score, (_scene: BattleScene, args: any[]) => (args[0] instanceof Utils.NumberHolder ? args[0].value : args[0]) >= this.damageAmount);
this.damageAmount = damageAmount; this.damageAmount = damageAmount;
} }
} }
@ -118,7 +118,7 @@ export class HealAchv extends Achv {
healAmount: integer; healAmount: integer;
constructor(localizationKey: string, name: string, healAmount: integer, iconImage: string, score: integer) { constructor(localizationKey: string, name: string, healAmount: integer, iconImage: string, score: integer) {
super(localizationKey, name, "", iconImage, score, (_scene: BattleScene, args: any[]) => (args[0] as Utils.NumberHolder).value >= this.healAmount); super(localizationKey, name, "", iconImage, score, (_scene: BattleScene, args: any[]) => (args[0] instanceof Utils.NumberHolder ? args[0].value : args[0]) >= this.healAmount);
this.healAmount = healAmount; this.healAmount = healAmount;
} }
} }
@ -127,7 +127,7 @@ export class LevelAchv extends Achv {
level: integer; level: integer;
constructor(localizationKey: string, name: string, level: integer, iconImage: string, score: integer) { constructor(localizationKey: string, name: string, level: integer, iconImage: string, score: integer) {
super(localizationKey, name, "", iconImage, score, (scene: BattleScene, args: any[]) => (args[0] as Utils.IntegerHolder).value >= this.level); super(localizationKey, name, "", iconImage, score, (scene: BattleScene, args: any[]) => (args[0] instanceof Utils.NumberHolder ? args[0].value : args[0]) >= this.level);
this.level = level; this.level = level;
} }
} }

View File

@ -1125,10 +1125,16 @@ export class GameData {
}); });
} }
/**
* Delete the session data at the given slot when overwriting a save file
* For deleting the session of a finished run, use {@linkcode tryClearSession}
* @param slotId the slot to clear
* @returns Promise with result `true` if the session was deleted successfully, `false` otherwise
*/
deleteSession(slotId: integer): Promise<boolean> { deleteSession(slotId: integer): Promise<boolean> {
return new Promise<boolean>(resolve => { return new Promise<boolean>(resolve => {
if (bypassLogin) { if (bypassLogin) {
localStorage.removeItem(`sessionData${this.scene.sessionSlotId ? this.scene.sessionSlotId : ""}_${loggedInUser?.username}`); localStorage.removeItem(`sessionData${slotId ? slotId : ""}_${loggedInUser?.username}`);
return resolve(true); return resolve(true);
} }
@ -1139,7 +1145,7 @@ export class GameData {
Utils.apiFetch(`savedata/session/delete?slot=${slotId}&clientSessionId=${clientSessionId}`, true).then(response => { Utils.apiFetch(`savedata/session/delete?slot=${slotId}&clientSessionId=${clientSessionId}`, true).then(response => {
if (response.ok) { if (response.ok) {
loggedInUser!.lastSessionSlot = -1; // TODO: is the bang correct? loggedInUser!.lastSessionSlot = -1; // TODO: is the bang correct?
localStorage.removeItem(`sessionData${this.scene.sessionSlotId ? this.scene.sessionSlotId : ""}_${loggedInUser?.username}`); localStorage.removeItem(`sessionData${slotId ? slotId : ""}_${loggedInUser?.username}`);
resolve(true); resolve(true);
} }
return response.text(); return response.text();
@ -1190,7 +1196,9 @@ export class GameData {
/** /**
* Attempt to clear session data. After session data is removed, attempt to update user info so the menu updates * Attempt to clear session data after the end of a run
* After session data is removed, attempt to update user info so the menu updates
* To delete an unfinished run instead, use {@linkcode deleteSession}
*/ */
async tryClearSession(scene: BattleScene, slotId: integer): Promise<[success: boolean, newClear: boolean]> { async tryClearSession(scene: BattleScene, slotId: integer): Promise<[success: boolean, newClear: boolean]> {
let result: [boolean, boolean] = [ false, false ]; let result: [boolean, boolean] = [ false, false ];
@ -1204,7 +1212,7 @@ export class GameData {
if (response.ok) { if (response.ok) {
loggedInUser!.lastSessionSlot = -1; // TODO: is the bang correct? loggedInUser!.lastSessionSlot = -1; // TODO: is the bang correct?
localStorage.removeItem(`sessionData${this.scene.sessionSlotId ? this.scene.sessionSlotId : ""}_${loggedInUser?.username}`); localStorage.removeItem(`sessionData${slotId ? slotId : ""}_${loggedInUser?.username}`);
} }
const jsonResponse: PokerogueApiClearSessionData = await response.json(); const jsonResponse: PokerogueApiClearSessionData = await response.json();

View File

@ -1,13 +1,13 @@
import { BattleStyle } from "#app/enums/battle-style"; import { BattleStyle } from "#app/enums/battle-style";
import { CommandPhase } from "#app/phases/command-phase"; import { CommandPhase } from "#app/phases/command-phase";
import { TurnInitPhase } from "#app/phases/turn-init-phase"; import { TurnInitPhase } from "#app/phases/turn-init-phase";
import i18next, { initI18n } from "#app/plugins/i18n"; import i18next from "#app/plugins/i18n";
import { Mode } from "#app/ui/ui"; import { Mode } from "#app/ui/ui";
import { Abilities } from "#enums/abilities"; import { Abilities } from "#enums/abilities";
import { Species } from "#enums/species"; import { Species } from "#enums/species";
import GameManager from "#test/utils/gameManager"; import GameManager from "#test/utils/gameManager";
import Phaser from "phaser"; import Phaser from "phaser";
import { afterEach, beforeAll, beforeEach, describe, expect, it } from "vitest"; import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest";
describe("Ability Timing", () => { describe("Ability Timing", () => {
@ -32,11 +32,10 @@ describe("Ability Timing", () => {
.enemySpecies(Species.MAGIKARP) .enemySpecies(Species.MAGIKARP)
.enemyAbility(Abilities.INTIMIDATE) .enemyAbility(Abilities.INTIMIDATE)
.ability(Abilities.BALL_FETCH); .ability(Abilities.BALL_FETCH);
vi.spyOn(i18next, "t");
}); });
it("should trigger after switch check", async () => { it("should trigger after switch check", async () => {
initI18n();
i18next.changeLanguage("en");
game.settings.battleStyle = BattleStyle.SWITCH; game.settings.battleStyle = BattleStyle.SWITCH;
await game.classicMode.runToSummon([ Species.EEVEE, Species.FEEBAS ]); await game.classicMode.runToSummon([ Species.EEVEE, Species.FEEBAS ]);
@ -46,7 +45,6 @@ describe("Ability Timing", () => {
}, () => game.isCurrentPhase(CommandPhase) || game.isCurrentPhase(TurnInitPhase)); }, () => game.isCurrentPhase(CommandPhase) || game.isCurrentPhase(TurnInitPhase));
await game.phaseInterceptor.to("MessagePhase"); await game.phaseInterceptor.to("MessagePhase");
const message = game.textInterceptor.getLatestMessage(); expect(i18next.t).toHaveBeenCalledWith("battle:statFell", expect.objectContaining({ count: 1 }));
expect(message).toContain("battle:statFell");
}, 5000); }, 5000);
}); });

View File

@ -1,16 +1,14 @@
import { BattlerIndex } from "#app/battle"; import { BattlerIndex } from "#app/battle";
import { allMoves } from "#app/data/move"; import Pokemon from "#app/field/pokemon";
import { Abilities } from "#app/enums/abilities"; import { ContactHeldItemTransferChanceModifier } from "#app/modifier/modifier";
import { BerryType } from "#app/enums/berry-type"; import { Abilities } from "#enums/abilities";
import { Moves } from "#app/enums/moves"; import { BerryType } from "#enums/berry-type";
import { Species } from "#app/enums/species"; import { Moves } from "#enums/moves";
import { MoveEndPhase } from "#app/phases/move-end-phase"; import { Species } from "#enums/species";
import GameManager from "#test/utils/gameManager"; import GameManager from "#test/utils/gameManager";
import Phase from "phaser"; import Phase from "phaser";
import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest"; import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest";
// 20 seconds
describe("Items - Grip Claw", () => { describe("Items - Grip Claw", () => {
let phaserGame: Phaser.Game; let phaserGame: Phaser.Game;
let game: GameManager; let game: GameManager;
@ -30,39 +28,85 @@ describe("Items - Grip Claw", () => {
game.override game.override
.battleType("double") .battleType("double")
.moveset([ Moves.POPULATION_BOMB, Moves.SPLASH ]) .moveset([ Moves.TACKLE, Moves.SPLASH, Moves.ATTRACT ])
.startingHeldItems([ .startingHeldItems([
{ name: "GRIP_CLAW", count: 5 }, // TODO: Find a way to mock the steal chance of grip claw { name: "GRIP_CLAW", count: 1 },
{ name: "MULTI_LENS", count: 3 },
]) ])
.enemySpecies(Species.SNORLAX) .enemySpecies(Species.SNORLAX)
.ability(Abilities.KLUTZ) .enemyAbility(Abilities.UNNERVE)
.ability(Abilities.UNNERVE)
.enemyMoveset(Moves.SPLASH) .enemyMoveset(Moves.SPLASH)
.enemyHeldItems([ .enemyHeldItems([
{ name: "BERRY", type: BerryType.SITRUS, count: 2 }, { name: "BERRY", type: BerryType.SITRUS, count: 2 },
{ name: "BERRY", type: BerryType.LUM, count: 2 }, { name: "BERRY", type: BerryType.LUM, count: 2 },
]) ])
.startingLevel(100)
.enemyLevel(100); .enemyLevel(100);
vi.spyOn(allMoves[Moves.POPULATION_BOMB], "accuracy", "get").mockReturnValue(100);
}); });
it( it("should steal items on contact and only from the attack target", async () => {
"should only steal items from the attack target", await game.classicMode.startBattle([ Species.FEEBAS, Species.MILOTIC ]);
async () => {
await game.startBattle([ Species.PANSEAR, Species.ROWLET ]); const [ playerPokemon, ] = game.scene.getPlayerField();
const gripClaw = playerPokemon.getHeldItems()[0] as ContactHeldItemTransferChanceModifier;
vi.spyOn(gripClaw, "chance", "get").mockReturnValue(100);
const enemyPokemon = game.scene.getEnemyField(); const enemyPokemon = game.scene.getEnemyField();
const enemyHeldItemCt = enemyPokemon.map(p => p.getHeldItems.length); const playerHeldItemCount = getHeldItemCount(playerPokemon);
const enemy1HeldItemCount = getHeldItemCount(enemyPokemon[0]);
const enemy2HeldItemCount = getHeldItemCount(enemyPokemon[1]);
expect(enemy2HeldItemCount).toBeGreaterThan(0);
game.move.select(Moves.POPULATION_BOMB, 0, BattlerIndex.ENEMY); game.move.select(Moves.TACKLE, 0, BattlerIndex.ENEMY_2);
game.move.select(Moves.SPLASH, 1); game.move.select(Moves.SPLASH, 1);
await game.phaseInterceptor.to(MoveEndPhase, false); await game.phaseInterceptor.to("BerryPhase", false);
expect(enemyPokemon[1].getHeldItems.length).toBe(enemyHeldItemCt[1]); const playerHeldItemCountAfter = getHeldItemCount(playerPokemon);
} const enemy1HeldItemCountsAfter = getHeldItemCount(enemyPokemon[0]);
); const enemy2HeldItemCountsAfter = getHeldItemCount(enemyPokemon[1]);
expect(playerHeldItemCountAfter).toBe(playerHeldItemCount + 1);
expect(enemy1HeldItemCountsAfter).toBe(enemy1HeldItemCount);
expect(enemy2HeldItemCountsAfter).toBe(enemy2HeldItemCount - 1);
});
it("should not steal items when using a targetted, non attack move", async () => {
await game.classicMode.startBattle([ Species.FEEBAS, Species.MILOTIC ]);
const [ playerPokemon, ] = game.scene.getPlayerField();
const gripClaw = playerPokemon.getHeldItems()[0] as ContactHeldItemTransferChanceModifier;
vi.spyOn(gripClaw, "chance", "get").mockReturnValue(100);
const enemyPokemon = game.scene.getEnemyField();
const playerHeldItemCount = getHeldItemCount(playerPokemon);
const enemy1HeldItemCount = getHeldItemCount(enemyPokemon[0]);
const enemy2HeldItemCount = getHeldItemCount(enemyPokemon[1]);
expect(enemy2HeldItemCount).toBeGreaterThan(0);
game.move.select(Moves.ATTRACT, 0, BattlerIndex.ENEMY_2);
game.move.select(Moves.SPLASH, 1);
await game.phaseInterceptor.to("BerryPhase", false);
const playerHeldItemCountAfter = getHeldItemCount(playerPokemon);
const enemy1HeldItemCountsAfter = getHeldItemCount(enemyPokemon[0]);
const enemy2HeldItemCountsAfter = getHeldItemCount(enemyPokemon[1]);
expect(playerHeldItemCountAfter).toBe(playerHeldItemCount);
expect(enemy1HeldItemCountsAfter).toBe(enemy1HeldItemCount);
expect(enemy2HeldItemCountsAfter).toBe(enemy2HeldItemCount);
});
}); });
/*
* Gets the total number of items a Pokemon holds
*/
function getHeldItemCount(pokemon: Pokemon) {
return pokemon.getHeldItems().reduce((currentTotal, item) => currentTotal + item.getStackCount(), 0);
}

View File

@ -2,13 +2,13 @@ import { StatusEffect } from "#app/data/status-effect";
import { EnemyCommandPhase } from "#app/phases/enemy-command-phase"; import { EnemyCommandPhase } from "#app/phases/enemy-command-phase";
import { MessagePhase } from "#app/phases/message-phase"; import { MessagePhase } from "#app/phases/message-phase";
import { TurnEndPhase } from "#app/phases/turn-end-phase"; import { TurnEndPhase } from "#app/phases/turn-end-phase";
import i18next, { initI18n } from "#app/plugins/i18n"; import i18next from "#app/plugins/i18n";
import { Abilities } from "#enums/abilities"; import { Abilities } from "#enums/abilities";
import { Moves } from "#enums/moves"; import { Moves } from "#enums/moves";
import { Species } from "#enums/species"; import { Species } from "#enums/species";
import GameManager from "#test/utils/gameManager"; import GameManager from "#test/utils/gameManager";
import Phaser from "phaser"; import Phaser from "phaser";
import { afterEach, beforeAll, beforeEach, describe, expect, it } from "vitest"; import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest";
describe("Items - Toxic orb", () => { describe("Items - Toxic orb", () => {
@ -39,11 +39,11 @@ describe("Items - Toxic orb", () => {
game.override.startingHeldItems([{ game.override.startingHeldItems([{
name: "TOXIC_ORB", name: "TOXIC_ORB",
}]); }]);
vi.spyOn(i18next, "t");
}); });
it("TOXIC ORB", async () => { it("TOXIC ORB", async () => {
initI18n();
i18next.changeLanguage("en");
const moveToUse = Moves.GROWTH; const moveToUse = Moves.GROWTH;
await game.startBattle([ await game.startBattle([
Species.MIGHTYENA, Species.MIGHTYENA,
@ -57,11 +57,10 @@ describe("Items - Toxic orb", () => {
await game.phaseInterceptor.runFrom(EnemyCommandPhase).to(TurnEndPhase); await game.phaseInterceptor.runFrom(EnemyCommandPhase).to(TurnEndPhase);
// Toxic orb should trigger here // Toxic orb should trigger here
await game.phaseInterceptor.run(MessagePhase); await game.phaseInterceptor.run(MessagePhase);
const message = game.textInterceptor.getLatestMessage(); expect(i18next.t).toHaveBeenCalledWith("statusEffect:toxic.obtainSource", expect.anything());
expect(message).toContain("statusEffect:toxic.obtainSource");
await game.phaseInterceptor.run(MessagePhase); await game.phaseInterceptor.run(MessagePhase);
const message2 = game.textInterceptor.getLatestMessage(); expect(i18next.t).toHaveBeenCalledWith("statusEffect:toxic.activation", expect.anything());
expect(message2).toBe("statusEffect:toxic.activation");
expect(game.scene.getParty()[0].status!.effect).toBe(StatusEffect.TOXIC); expect(game.scene.getParty()[0].status!.effect).toBe(StatusEffect.TOXIC);
}, 20000); }, 20000);
}); });

View File

@ -0,0 +1,60 @@
import { allMoves, FlinchAttr, StatStageChangeAttr } from "#app/data/move";
import { Abilities } from "#enums/abilities";
import { Moves } from "#enums/moves";
import { Species } from "#enums/species";
import GameManager from "#test/utils/gameManager";
import Phaser from "phaser";
import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest";
describe("Moves - Triple Arrows", () => {
let phaserGame: Phaser.Game;
let game: GameManager;
const tripleArrows = allMoves[Moves.TRIPLE_ARROWS];
const flinchAttr = tripleArrows.getAttrs(FlinchAttr)[0];
const defDropAttr = tripleArrows.getAttrs(StatStageChangeAttr)[0];
beforeAll(() => {
phaserGame = new Phaser.Game({
type: Phaser.HEADLESS,
});
});
afterEach(() => {
game.phaseInterceptor.restoreOg();
});
beforeEach(() => {
game = new GameManager(phaserGame);
game.override
.ability(Abilities.BALL_FETCH)
.moveset([ Moves.TRIPLE_ARROWS ])
.battleType("single")
.enemySpecies(Species.MAGIKARP)
.enemyAbility(Abilities.STURDY)
.enemyMoveset(Moves.SPLASH);
vi.spyOn(flinchAttr, "getMoveChance");
vi.spyOn(defDropAttr, "getMoveChance");
});
it("has a 30% flinch chance and 50% defense drop chance", async () => {
await game.classicMode.startBattle([ Species.FEEBAS ]);
game.move.select(Moves.TRIPLE_ARROWS);
await game.phaseInterceptor.to("BerryPhase");
expect(flinchAttr.getMoveChance).toHaveReturnedWith(30);
expect(defDropAttr.getMoveChance).toHaveReturnedWith(50);
});
it("is affected normally by Serene Grace", async () => {
game.override.ability(Abilities.SERENE_GRACE);
await game.classicMode.startBattle([ Species.FEEBAS ]);
game.move.select(Moves.TRIPLE_ARROWS);
await game.phaseInterceptor.to("BerryPhase");
expect(flinchAttr.getMoveChance).toHaveReturnedWith(60);
expect(defDropAttr.getMoveChance).toHaveReturnedWith(100);
});
});

View File

@ -16,6 +16,7 @@ import { EggTier } from "#enums/egg-type";
import { CommandPhase } from "#app/phases/command-phase"; import { CommandPhase } from "#app/phases/command-phase";
import { SelectModifierPhase } from "#app/phases/select-modifier-phase"; import { SelectModifierPhase } from "#app/phases/select-modifier-phase";
import { PartyHealPhase } from "#app/phases/party-heal-phase"; import { PartyHealPhase } from "#app/phases/party-heal-phase";
import i18next from "i18next";
const namespace = "mysteryEncounters/aTrainersTest"; const namespace = "mysteryEncounters/aTrainersTest";
const defaultParty = [ Species.LAPRAS, Species.GENGAR, Species.ABRA ]; const defaultParty = [ Species.LAPRAS, Species.GENGAR, Species.ABRA ];
@ -106,7 +107,8 @@ describe("A Trainer's Test - 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(scene.currentBattle.trainer).toBeDefined(); expect(scene.currentBattle.trainer).toBeDefined();
expect([ "trainerNames:buck", "trainerNames:cheryl", "trainerNames:marley", "trainerNames:mira", "trainerNames:riley" ].includes(scene.currentBattle.trainer!.config.name)).toBeTruthy(); expect([ i18next.t("trainerNames:buck"), i18next.t("trainerNames:cheryl"), i18next.t("trainerNames:marley"), i18next.t("trainerNames:mira"), i18next.t("trainerNames:riley") ]
.map(name => name.toLowerCase()).includes(scene.currentBattle.trainer!.config.name)).toBeTruthy();
expect(enemyField[0]).toBeDefined(); expect(enemyField[0]).toBeDefined();
}); });

View File

@ -16,6 +16,7 @@ import { Moves } from "#enums/moves";
import { CommandPhase } from "#app/phases/command-phase"; import { CommandPhase } from "#app/phases/command-phase";
import { MovePhase } from "#app/phases/move-phase"; import { MovePhase } from "#app/phases/move-phase";
import { SelectModifierPhase } from "#app/phases/select-modifier-phase"; import { SelectModifierPhase } from "#app/phases/select-modifier-phase";
import i18next from "i18next";
const namespace = "mysteryEncounters/absoluteAvarice"; const namespace = "mysteryEncounters/absoluteAvarice";
const defaultParty = [ Species.LAPRAS, Species.GENGAR, Species.ABRA ]; const defaultParty = [ Species.LAPRAS, Species.GENGAR, Species.ABRA ];
@ -146,7 +147,7 @@ describe("Absolute Avarice - Mystery Encounter", () => {
const pokemonId = partyPokemon.id; const pokemonId = partyPokemon.id;
const pokemonItems = scene.findModifiers(m => m instanceof PokemonHeldItemModifier const pokemonItems = scene.findModifiers(m => m instanceof PokemonHeldItemModifier
&& (m as PokemonHeldItemModifier).pokemonId === pokemonId, true) as PokemonHeldItemModifier[]; && (m as PokemonHeldItemModifier).pokemonId === pokemonId, true) as PokemonHeldItemModifier[];
const revSeed = pokemonItems.find(i => i.type.name === "modifierType:ModifierType.REVIVER_SEED.name"); const revSeed = pokemonItems.find(i => i.type.name === i18next.t("modifierType:ModifierType.REVIVER_SEED.name"));
expect(revSeed).toBeDefined; expect(revSeed).toBeDefined;
expect(revSeed?.stackCount).toBe(1); expect(revSeed?.stackCount).toBe(1);
} }

View File

@ -17,6 +17,7 @@ import { getPokemonSpecies } from "#app/data/pokemon-species";
import { Moves } from "#enums/moves"; import { Moves } from "#enums/moves";
import { ShinyRateBoosterModifier } from "#app/modifier/modifier"; import { ShinyRateBoosterModifier } from "#app/modifier/modifier";
import { SelectModifierPhase } from "#app/phases/select-modifier-phase"; import { SelectModifierPhase } from "#app/phases/select-modifier-phase";
import i18next from "i18next";
const namespace = "mysteryEncounters/anOfferYouCantRefuse"; const namespace = "mysteryEncounters/anOfferYouCantRefuse";
/** Gyarados for Indimidate */ /** Gyarados for Indimidate */
@ -93,8 +94,8 @@ describe("An Offer You Can't Refuse - Mystery Encounter", () => {
expect(AnOfferYouCantRefuseEncounter.dialogueTokens?.strongestPokemon).toBeDefined(); expect(AnOfferYouCantRefuseEncounter.dialogueTokens?.strongestPokemon).toBeDefined();
expect(AnOfferYouCantRefuseEncounter.dialogueTokens?.price).toBeDefined(); expect(AnOfferYouCantRefuseEncounter.dialogueTokens?.price).toBeDefined();
expect(AnOfferYouCantRefuseEncounter.dialogueTokens?.option2PrimaryAbility).toBe("ability:intimidate.name"); expect(AnOfferYouCantRefuseEncounter.dialogueTokens?.option2PrimaryAbility).toBe(i18next.t("ability:intimidate.name"));
expect(AnOfferYouCantRefuseEncounter.dialogueTokens?.moveOrAbility).toBe("ability:intimidate.name"); expect(AnOfferYouCantRefuseEncounter.dialogueTokens?.moveOrAbility).toBe(i18next.t("ability:intimidate.name"));
expect(AnOfferYouCantRefuseEncounter.misc.pokemon instanceof PlayerPokemon).toBeTruthy(); expect(AnOfferYouCantRefuseEncounter.misc.pokemon instanceof PlayerPokemon).toBeTruthy();
expect(AnOfferYouCantRefuseEncounter.misc?.price?.toString()).toBe(AnOfferYouCantRefuseEncounter.dialogueTokens?.price); expect(AnOfferYouCantRefuseEncounter.misc?.price?.toString()).toBe(AnOfferYouCantRefuseEncounter.dialogueTokens?.price);
expect(onInitResult).toBe(true); expect(onInitResult).toBe(true);

View File

@ -103,11 +103,11 @@ describe("Field Trip - Mystery Encounter", () => {
expect(scene.ui.getMode()).to.equal(Mode.MODIFIER_SELECT); expect(scene.ui.getMode()).to.equal(Mode.MODIFIER_SELECT);
const modifierSelectHandler = scene.ui.handlers.find(h => h instanceof ModifierSelectUiHandler) as ModifierSelectUiHandler; const modifierSelectHandler = scene.ui.handlers.find(h => h instanceof ModifierSelectUiHandler) as ModifierSelectUiHandler;
expect(modifierSelectHandler.options.length).toEqual(5); expect(modifierSelectHandler.options.length).toEqual(5);
expect(modifierSelectHandler.options[0].modifierTypeOption.type.name).toBe("modifierType:TempStatStageBoosterItem.x_attack"); expect(modifierSelectHandler.options[0].modifierTypeOption.type.name).toBe(i18next.t("modifierType:TempStatStageBoosterItem.x_attack"));
expect(modifierSelectHandler.options[1].modifierTypeOption.type.name).toBe("modifierType:TempStatStageBoosterItem.x_defense"); expect(modifierSelectHandler.options[1].modifierTypeOption.type.name).toBe(i18next.t("modifierType:TempStatStageBoosterItem.x_defense"));
expect(modifierSelectHandler.options[2].modifierTypeOption.type.name).toBe("modifierType:TempStatStageBoosterItem.x_speed"); expect(modifierSelectHandler.options[2].modifierTypeOption.type.name).toBe(i18next.t("modifierType:TempStatStageBoosterItem.x_speed"));
expect(modifierSelectHandler.options[3].modifierTypeOption.type.name).toBe("modifierType:ModifierType.DIRE_HIT.name"); expect(modifierSelectHandler.options[3].modifierTypeOption.type.name).toBe(i18next.t("modifierType:ModifierType.DIRE_HIT.name"));
expect(modifierSelectHandler.options[4].modifierTypeOption.type.name).toBe("modifierType:ModifierType.RARER_CANDY.name"); expect(modifierSelectHandler.options[4].modifierTypeOption.type.name).toBe(i18next.t("modifierType:ModifierType.RARER_CANDY.name"));
}); });
it("should leave encounter without battle", async () => { it("should leave encounter without battle", async () => {
@ -150,11 +150,11 @@ describe("Field Trip - Mystery Encounter", () => {
expect(scene.ui.getMode()).to.equal(Mode.MODIFIER_SELECT); expect(scene.ui.getMode()).to.equal(Mode.MODIFIER_SELECT);
const modifierSelectHandler = scene.ui.handlers.find(h => h instanceof ModifierSelectUiHandler) as ModifierSelectUiHandler; const modifierSelectHandler = scene.ui.handlers.find(h => h instanceof ModifierSelectUiHandler) as ModifierSelectUiHandler;
expect(modifierSelectHandler.options.length).toEqual(5); expect(modifierSelectHandler.options.length).toEqual(5);
expect(modifierSelectHandler.options[0].modifierTypeOption.type.name).toBe("modifierType:TempStatStageBoosterItem.x_sp_atk"); expect(modifierSelectHandler.options[0].modifierTypeOption.type.name).toBe(i18next.t("modifierType:TempStatStageBoosterItem.x_sp_atk"));
expect(modifierSelectHandler.options[1].modifierTypeOption.type.name).toBe("modifierType:TempStatStageBoosterItem.x_sp_def"); expect(modifierSelectHandler.options[1].modifierTypeOption.type.name).toBe(i18next.t("modifierType:TempStatStageBoosterItem.x_sp_def"));
expect(modifierSelectHandler.options[2].modifierTypeOption.type.name).toBe("modifierType:TempStatStageBoosterItem.x_speed"); expect(modifierSelectHandler.options[2].modifierTypeOption.type.name).toBe(i18next.t("modifierType:TempStatStageBoosterItem.x_speed"));
expect(modifierSelectHandler.options[3].modifierTypeOption.type.name).toBe("modifierType:ModifierType.DIRE_HIT.name"); expect(modifierSelectHandler.options[3].modifierTypeOption.type.name).toBe(i18next.t("modifierType:ModifierType.DIRE_HIT.name"));
expect(modifierSelectHandler.options[4].modifierTypeOption.type.name).toBe("modifierType:ModifierType.RARER_CANDY.name"); expect(modifierSelectHandler.options[4].modifierTypeOption.type.name).toBe(i18next.t("modifierType:ModifierType.RARER_CANDY.name"));
}); });
it("should leave encounter without battle", async () => { it("should leave encounter without battle", async () => {
@ -198,12 +198,12 @@ describe("Field Trip - Mystery Encounter", () => {
expect(scene.ui.getMode()).to.equal(Mode.MODIFIER_SELECT); expect(scene.ui.getMode()).to.equal(Mode.MODIFIER_SELECT);
const modifierSelectHandler = scene.ui.handlers.find(h => h instanceof ModifierSelectUiHandler) as ModifierSelectUiHandler; const modifierSelectHandler = scene.ui.handlers.find(h => h instanceof ModifierSelectUiHandler) as ModifierSelectUiHandler;
expect(modifierSelectHandler.options.length).toEqual(5); expect(modifierSelectHandler.options.length).toEqual(5);
expect(modifierSelectHandler.options[0].modifierTypeOption.type.name).toBe("modifierType:TempStatStageBoosterItem.x_accuracy"); expect(modifierSelectHandler.options[0].modifierTypeOption.type.name).toBe(i18next.t("modifierType:TempStatStageBoosterItem.x_accuracy"));
expect(modifierSelectHandler.options[1].modifierTypeOption.type.name).toBe("modifierType:TempStatStageBoosterItem.x_speed"); expect(modifierSelectHandler.options[1].modifierTypeOption.type.name).toBe(i18next.t("modifierType:TempStatStageBoosterItem.x_speed"));
expect(modifierSelectHandler.options[2].modifierTypeOption.type.name).toBe("modifierType:ModifierType.AddPokeballModifierType.name"); expect(modifierSelectHandler.options[2].modifierTypeOption.type.name).toBe(i18next.t("modifierType:ModifierType.AddPokeballModifierType.name", { modifierCount: 5, pokeballName: i18next.t("pokeball:greatBall") }));
expect(i18next.t).toHaveBeenCalledWith("modifierType:ModifierType.AddPokeballModifierType.name", expect.objectContaining({ modifierCount: 5 })); expect(i18next.t).toHaveBeenCalledWith(("modifierType:ModifierType.AddPokeballModifierType.name"), expect.objectContaining({ modifierCount: 5 }));
expect(modifierSelectHandler.options[3].modifierTypeOption.type.name).toBe("modifierType:ModifierType.IV_SCANNER.name"); expect(modifierSelectHandler.options[3].modifierTypeOption.type.name).toBe(i18next.t("modifierType:ModifierType.IV_SCANNER.name"));
expect(modifierSelectHandler.options[4].modifierTypeOption.type.name).toBe("modifierType:ModifierType.RARER_CANDY.name"); expect(modifierSelectHandler.options[4].modifierTypeOption.type.name).toBe(i18next.t("modifierType:ModifierType.RARER_CANDY.name"));
}); });
it("should leave encounter without battle", async () => { it("should leave encounter without battle", async () => {

View File

@ -22,6 +22,7 @@ import { initSceneWithoutEncounterPhase } from "#test/utils/gameManagerUtils";
import { CommandPhase } from "#app/phases/command-phase"; import { CommandPhase } from "#app/phases/command-phase";
import { MovePhase } from "#app/phases/move-phase"; import { MovePhase } from "#app/phases/move-phase";
import { SelectModifierPhase } from "#app/phases/select-modifier-phase"; import { SelectModifierPhase } from "#app/phases/select-modifier-phase";
import i18next from "i18next";
const namespace = "mysteryEncounters/fieryFallout"; const namespace = "mysteryEncounters/fieryFallout";
/** Arcanine and Ninetails for 2 Fire types. Lapras, Gengar, Abra for burnable mon. */ /** Arcanine and Ninetails for 2 Fire types. Lapras, Gengar, Abra for burnable mon. */
@ -205,7 +206,7 @@ describe("Fiery Fallout - Mystery Encounter", () => {
const burnablePokemon = party.filter((pkm) => pkm.isAllowedInBattle() && !pkm.getTypes().includes(Type.FIRE)); const burnablePokemon = party.filter((pkm) => pkm.isAllowedInBattle() && !pkm.getTypes().includes(Type.FIRE));
const notBurnablePokemon = party.filter((pkm) => !pkm.isAllowedInBattle() || pkm.getTypes().includes(Type.FIRE)); const notBurnablePokemon = party.filter((pkm) => !pkm.isAllowedInBattle() || pkm.getTypes().includes(Type.FIRE));
expect(scene.currentBattle.mysteryEncounter?.dialogueTokens["burnedPokemon"]).toBe("pokemon:gengar"); expect(scene.currentBattle.mysteryEncounter?.dialogueTokens["burnedPokemon"]).toBe(i18next.t("pokemon:gengar"));
burnablePokemon.forEach((pkm) => { burnablePokemon.forEach((pkm) => {
expect(pkm.hp, `${pkm.name} should have received 20% damage: ${pkm.hp} / ${pkm.getMaxHp()} HP`).toBe(pkm.getMaxHp() - Math.floor(pkm.getMaxHp() * 0.2)); expect(pkm.hp, `${pkm.name} should have received 20% damage: ${pkm.hp} / ${pkm.getMaxHp()} HP`).toBe(pkm.getMaxHp() - Math.floor(pkm.getMaxHp() * 0.2));
}); });

View File

@ -14,6 +14,7 @@ import { initSceneWithoutEncounterPhase } from "#test/utils/gameManagerUtils";
import BattleScene from "#app/battle-scene"; import BattleScene from "#app/battle-scene";
import { MysteryEncounterPhase } from "#app/phases/mystery-encounter-phases"; import { MysteryEncounterPhase } from "#app/phases/mystery-encounter-phases";
import { PartyExpPhase } from "#app/phases/party-exp-phase"; import { PartyExpPhase } from "#app/phases/party-exp-phase";
import i18next from "i18next";
const namespace = "mysteryEncounters/lostAtSea"; const namespace = "mysteryEncounters/lostAtSea";
@ -86,8 +87,8 @@ describe("Lost at Sea - Mystery Encounter", () => {
const onInitResult = onInit!(scene); const onInitResult = onInit!(scene);
expect(LostAtSeaEncounter.dialogueTokens?.damagePercentage).toBe("25"); expect(LostAtSeaEncounter.dialogueTokens?.damagePercentage).toBe("25");
expect(LostAtSeaEncounter.dialogueTokens?.option1RequiredMove).toBe("move:surf.name"); expect(LostAtSeaEncounter.dialogueTokens?.option1RequiredMove).toBe(i18next.t("move:surf.name"));
expect(LostAtSeaEncounter.dialogueTokens?.option2RequiredMove).toBe("move:fly.name"); expect(LostAtSeaEncounter.dialogueTokens?.option2RequiredMove).toBe(i18next.t("move:fly.name"));
expect(onInitResult).toBe(true); expect(onInitResult).toBe(true);
}); });

View File

@ -1,21 +1,22 @@
import * as MysteryEncounters from "#app/data/mystery-encounters/mystery-encounters";
import { Biome } from "#app/enums/biome";
import { MysteryEncounterType } from "#app/enums/mystery-encounter-type";
import { Species } from "#app/enums/species";
import GameManager from "#app/test/utils/gameManager";
import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest";
import { runMysteryEncounterToEnd, runSelectMysteryEncounterOption, skipBattleRunMysteryEncounterRewardsPhase } from "#test/mystery-encounter/encounter-test-utils";
import BattleScene from "#app/battle-scene"; import BattleScene from "#app/battle-scene";
import { TeleportingHijinksEncounter } from "#app/data/mystery-encounters/encounters/teleporting-hijinks-encounter";
import * as MysteryEncounters from "#app/data/mystery-encounters/mystery-encounters";
import { Abilities } from "#enums/abilities";
import { Biome } from "#enums/biome";
import { MysteryEncounterType } from "#enums/mystery-encounter-type";
import { Species } from "#enums/species";
import { CommandPhase } from "#app/phases/command-phase";
import { MysteryEncounterPhase } from "#app/phases/mystery-encounter-phases";
import { SelectModifierPhase } from "#app/phases/select-modifier-phase";
import GameManager from "#test/utils/gameManager";
import ModifierSelectUiHandler from "#app/ui/modifier-select-ui-handler";
import { Mode } from "#app/ui/ui";
import { MysteryEncounterOptionMode } from "#enums/mystery-encounter-option-mode"; import { MysteryEncounterOptionMode } from "#enums/mystery-encounter-option-mode";
import { MysteryEncounterTier } from "#enums/mystery-encounter-tier"; import { MysteryEncounterTier } from "#enums/mystery-encounter-tier";
import { runMysteryEncounterToEnd, runSelectMysteryEncounterOption, skipBattleRunMysteryEncounterRewardsPhase } from "#test/mystery-encounter/encounter-test-utils";
import { initSceneWithoutEncounterPhase } from "#test/utils/gameManagerUtils"; import { initSceneWithoutEncounterPhase } from "#test/utils/gameManagerUtils";
import { MysteryEncounterPhase } from "#app/phases/mystery-encounter-phases"; import i18next from "i18next";
import { CommandPhase } from "#app/phases/command-phase"; import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest";
import { TeleportingHijinksEncounter } from "#app/data/mystery-encounters/encounters/teleporting-hijinks-encounter";
import { SelectModifierPhase } from "#app/phases/select-modifier-phase";
import { Mode } from "#app/ui/ui";
import ModifierSelectUiHandler from "#app/ui/modifier-select-ui-handler";
import { Abilities } from "#app/enums/abilities";
const namespace = "mysteryEncounters/teleportingHijinks"; const namespace = "mysteryEncounters/teleportingHijinks";
const defaultParty = [ Species.LAPRAS, Species.GENGAR, Species.ABRA ]; const defaultParty = [ Species.LAPRAS, Species.GENGAR, Species.ABRA ];
@ -300,8 +301,8 @@ describe("Teleporting Hijinks - Mystery Encounter", () => {
expect(scene.ui.getMode()).to.equal(Mode.MODIFIER_SELECT); expect(scene.ui.getMode()).to.equal(Mode.MODIFIER_SELECT);
const modifierSelectHandler = scene.ui.handlers.find(h => h instanceof ModifierSelectUiHandler) as ModifierSelectUiHandler; const modifierSelectHandler = scene.ui.handlers.find(h => h instanceof ModifierSelectUiHandler) as ModifierSelectUiHandler;
expect(modifierSelectHandler.options.some(opt => opt.modifierTypeOption.type.name === "modifierType:AttackTypeBoosterItem.metal_coat")).toBe(true); expect(modifierSelectHandler.options.some(opt => opt.modifierTypeOption.type.name === i18next.t("modifierType:AttackTypeBoosterItem.metal_coat"))).toBe(true);
expect(modifierSelectHandler.options.some(opt => opt.modifierTypeOption.type.name === "modifierType:AttackTypeBoosterItem.magnet")).toBe(true); expect(modifierSelectHandler.options.some(opt => opt.modifierTypeOption.type.name === i18next.t("modifierType:AttackTypeBoosterItem.magnet"))).toBe(true);
}); });
}); });
}); });

View File

@ -9,6 +9,7 @@ import MysteryEncounterUiHandler from "#app/ui/mystery-encounter-ui-handler";
import { MysteryEncounterType } from "#enums/mystery-encounter-type"; import { MysteryEncounterType } from "#enums/mystery-encounter-type";
import MessageUiHandler from "#app/ui/message-ui-handler"; import MessageUiHandler from "#app/ui/message-ui-handler";
import { MysteryEncounterTier } from "#enums/mystery-encounter-tier"; import { MysteryEncounterTier } from "#enums/mystery-encounter-tier";
import i18next from "i18next";
describe("Mystery Encounter Phases", () => { describe("Mystery Encounter Phases", () => {
let phaserGame: Phaser.Game; let phaserGame: Phaser.Game;
@ -78,9 +79,9 @@ describe("Mystery Encounter Phases", () => {
expect(ui.getMode()).toBe(Mode.MESSAGE); expect(ui.getMode()).toBe(Mode.MESSAGE);
expect(ui.showDialogue).toHaveBeenCalledTimes(1); expect(ui.showDialogue).toHaveBeenCalledTimes(1);
expect(ui.showText).toHaveBeenCalledTimes(2); expect(ui.showText).toHaveBeenCalledTimes(2);
expect(ui.showDialogue).toHaveBeenCalledWith("battle:mysteryEncounterAppeared", "???", null, expect.any(Function)); expect(ui.showDialogue).toHaveBeenCalledWith(i18next.t("battle:mysteryEncounterAppeared"), "???", null, expect.any(Function));
expect(ui.showText).toHaveBeenCalledWith("mysteryEncounters/mysteriousChallengers:intro", null, expect.any(Function), 750, true); expect(ui.showText).toHaveBeenCalledWith(i18next.t("mysteryEncounters/mysteriousChallengers:intro"), null, expect.any(Function), 750, true);
expect(ui.showText).toHaveBeenCalledWith("mysteryEncounters/mysteriousChallengers:option.selected", null, expect.any(Function), 300, true); expect(ui.showText).toHaveBeenCalledWith(i18next.t("mysteryEncounters/mysteriousChallengers:option.selected"), null, expect.any(Function), 300, true);
}); });
}); });

View File

@ -11,13 +11,15 @@ import * as account from "../../account";
const apiBase = import.meta.env.VITE_API_BASE_URL ?? "http://localhost:8001"; const apiBase = import.meta.env.VITE_API_BASE_URL ?? "http://localhost:8001";
export const server = setupServer(); /** We need a custom server. For some reasons I can't extend the listeners of {@linkcode global.i18nServer} with {@linkcode global.i18nServer.use} */
const server = setupServer();
describe("System - Game Data", () => { describe("System - Game Data", () => {
let phaserGame: Phaser.Game; let phaserGame: Phaser.Game;
let game: GameManager; let game: GameManager;
beforeAll(() => { beforeAll(() => {
global.i18nServer.close();
server.listen(); server.listen();
phaserGame = new Phaser.Game({ phaserGame = new Phaser.Game({
type: Phaser.HEADLESS, type: Phaser.HEADLESS,
@ -26,6 +28,7 @@ describe("System - Game Data", () => {
afterAll(() => { afterAll(() => {
server.close(); server.close();
global.i18nServer.listen();
}); });
beforeEach(() => { beforeEach(() => {

View File

@ -14,6 +14,7 @@ import { Abilities } from "#enums/abilities";
import { Button } from "#enums/buttons"; import { Button } from "#enums/buttons";
import { Species } from "#enums/species"; import { Species } from "#enums/species";
import GameManager from "#test/utils/gameManager"; import GameManager from "#test/utils/gameManager";
import i18next from "i18next";
import Phaser from "phaser"; import Phaser from "phaser";
import { afterEach, beforeAll, beforeEach, describe, expect, it } from "vitest"; import { afterEach, beforeAll, beforeEach, describe, expect, it } from "vitest";
@ -66,11 +67,11 @@ describe("UI - Starter select", () => {
resolve(); resolve();
}); });
}); });
expect(options.some(option => option.label === "starterSelectUiHandler:addToParty")).toBe(true); expect(options.some(option => option.label === i18next.t("starterSelectUiHandler:addToParty"))).toBe(true);
expect(options.some(option => option.label === "starterSelectUiHandler:toggleIVs")).toBe(true); expect(options.some(option => option.label === i18next.t("starterSelectUiHandler:toggleIVs"))).toBe(true);
expect(options.some(option => option.label === "starterSelectUiHandler:manageMoves")).toBe(true); expect(options.some(option => option.label === i18next.t("starterSelectUiHandler:manageMoves"))).toBe(true);
expect(options.some(option => option.label === "starterSelectUiHandler:useCandies")).toBe(true); expect(options.some(option => option.label === i18next.t("starterSelectUiHandler:useCandies"))).toBe(true);
expect(options.some(option => option.label === "menu:cancel")).toBe(true); expect(options.some(option => option.label === i18next.t("menu:cancel"))).toBe(true);
optionSelectUiHandler?.processInput(Button.ACTION); optionSelectUiHandler?.processInput(Button.ACTION);
await new Promise<void>((resolve) => { await new Promise<void>((resolve) => {
@ -127,11 +128,11 @@ describe("UI - Starter select", () => {
resolve(); resolve();
}); });
}); });
expect(options.some(option => option.label === "starterSelectUiHandler:addToParty")).toBe(true); expect(options.some(option => option.label === i18next.t("starterSelectUiHandler:addToParty"))).toBe(true);
expect(options.some(option => option.label === "starterSelectUiHandler:toggleIVs")).toBe(true); expect(options.some(option => option.label === i18next.t("starterSelectUiHandler:toggleIVs"))).toBe(true);
expect(options.some(option => option.label === "starterSelectUiHandler:manageMoves")).toBe(true); expect(options.some(option => option.label === i18next.t("starterSelectUiHandler:manageMoves"))).toBe(true);
expect(options.some(option => option.label === "starterSelectUiHandler:useCandies")).toBe(true); expect(options.some(option => option.label === i18next.t("starterSelectUiHandler:useCandies"))).toBe(true);
expect(options.some(option => option.label === "menu:cancel")).toBe(true); expect(options.some(option => option.label === i18next.t("menu:cancel"))).toBe(true);
optionSelectUiHandler?.processInput(Button.ACTION); optionSelectUiHandler?.processInput(Button.ACTION);
await new Promise<void>((resolve) => { await new Promise<void>((resolve) => {
@ -191,11 +192,11 @@ describe("UI - Starter select", () => {
resolve(); resolve();
}); });
}); });
expect(options.some(option => option.label === "starterSelectUiHandler:addToParty")).toBe(true); expect(options.some(option => option.label === i18next.t("starterSelectUiHandler:addToParty"))).toBe(true);
expect(options.some(option => option.label === "starterSelectUiHandler:toggleIVs")).toBe(true); expect(options.some(option => option.label === i18next.t("starterSelectUiHandler:toggleIVs"))).toBe(true);
expect(options.some(option => option.label === "starterSelectUiHandler:manageMoves")).toBe(true); expect(options.some(option => option.label === i18next.t("starterSelectUiHandler:manageMoves"))).toBe(true);
expect(options.some(option => option.label === "starterSelectUiHandler:useCandies")).toBe(true); expect(options.some(option => option.label === i18next.t("starterSelectUiHandler:useCandies"))).toBe(true);
expect(options.some(option => option.label === "menu:cancel")).toBe(true); expect(options.some(option => option.label === i18next.t("menu:cancel"))).toBe(true);
optionSelectUiHandler?.processInput(Button.ACTION); optionSelectUiHandler?.processInput(Button.ACTION);
await new Promise<void>((resolve) => { await new Promise<void>((resolve) => {
@ -254,11 +255,11 @@ describe("UI - Starter select", () => {
resolve(); resolve();
}); });
}); });
expect(options.some(option => option.label === "starterSelectUiHandler:addToParty")).toBe(true); expect(options.some(option => option.label === i18next.t("starterSelectUiHandler:addToParty"))).toBe(true);
expect(options.some(option => option.label === "starterSelectUiHandler:toggleIVs")).toBe(true); expect(options.some(option => option.label === i18next.t("starterSelectUiHandler:toggleIVs"))).toBe(true);
expect(options.some(option => option.label === "starterSelectUiHandler:manageMoves")).toBe(true); expect(options.some(option => option.label === i18next.t("starterSelectUiHandler:manageMoves"))).toBe(true);
expect(options.some(option => option.label === "starterSelectUiHandler:useCandies")).toBe(true); expect(options.some(option => option.label === i18next.t("starterSelectUiHandler:useCandies"))).toBe(true);
expect(options.some(option => option.label === "menu:cancel")).toBe(true); expect(options.some(option => option.label === i18next.t("menu:cancel"))).toBe(true);
optionSelectUiHandler?.processInput(Button.ACTION); optionSelectUiHandler?.processInput(Button.ACTION);
await new Promise<void>((resolve) => { await new Promise<void>((resolve) => {
@ -315,11 +316,11 @@ describe("UI - Starter select", () => {
resolve(); resolve();
}); });
}); });
expect(options.some(option => option.label === "starterSelectUiHandler:addToParty")).toBe(true); expect(options.some(option => option.label === i18next.t("starterSelectUiHandler:addToParty"))).toBe(true);
expect(options.some(option => option.label === "starterSelectUiHandler:toggleIVs")).toBe(true); expect(options.some(option => option.label === i18next.t("starterSelectUiHandler:toggleIVs"))).toBe(true);
expect(options.some(option => option.label === "starterSelectUiHandler:manageMoves")).toBe(true); expect(options.some(option => option.label === i18next.t("starterSelectUiHandler:manageMoves"))).toBe(true);
expect(options.some(option => option.label === "starterSelectUiHandler:useCandies")).toBe(true); expect(options.some(option => option.label === i18next.t("starterSelectUiHandler:useCandies"))).toBe(true);
expect(options.some(option => option.label === "menu:cancel")).toBe(true); expect(options.some(option => option.label === i18next.t("menu:cancel"))).toBe(true);
optionSelectUiHandler?.processInput(Button.ACTION); optionSelectUiHandler?.processInput(Button.ACTION);
await new Promise<void>((resolve) => { await new Promise<void>((resolve) => {
@ -376,11 +377,11 @@ describe("UI - Starter select", () => {
resolve(); resolve();
}); });
}); });
expect(options.some(option => option.label === "starterSelectUiHandler:addToParty")).toBe(true); expect(options.some(option => option.label === i18next.t("starterSelectUiHandler:addToParty"))).toBe(true);
expect(options.some(option => option.label === "starterSelectUiHandler:toggleIVs")).toBe(true); expect(options.some(option => option.label === i18next.t("starterSelectUiHandler:toggleIVs"))).toBe(true);
expect(options.some(option => option.label === "starterSelectUiHandler:manageMoves")).toBe(true); expect(options.some(option => option.label === i18next.t("starterSelectUiHandler:manageMoves"))).toBe(true);
expect(options.some(option => option.label === "starterSelectUiHandler:useCandies")).toBe(true); expect(options.some(option => option.label === i18next.t("starterSelectUiHandler:useCandies"))).toBe(true);
expect(options.some(option => option.label === "menu:cancel")).toBe(true); expect(options.some(option => option.label === i18next.t("menu:cancel"))).toBe(true);
optionSelectUiHandler?.processInput(Button.ACTION); optionSelectUiHandler?.processInput(Button.ACTION);
await new Promise<void>((resolve) => { await new Promise<void>((resolve) => {
@ -436,11 +437,11 @@ describe("UI - Starter select", () => {
resolve(); resolve();
}); });
}); });
expect(options.some(option => option.label === "starterSelectUiHandler:addToParty")).toBe(true); expect(options.some(option => option.label === i18next.t("starterSelectUiHandler:addToParty"))).toBe(true);
expect(options.some(option => option.label === "starterSelectUiHandler:toggleIVs")).toBe(true); expect(options.some(option => option.label === i18next.t("starterSelectUiHandler:toggleIVs"))).toBe(true);
expect(options.some(option => option.label === "starterSelectUiHandler:manageMoves")).toBe(true); expect(options.some(option => option.label === i18next.t("starterSelectUiHandler:manageMoves"))).toBe(true);
expect(options.some(option => option.label === "starterSelectUiHandler:useCandies")).toBe(true); expect(options.some(option => option.label === i18next.t("starterSelectUiHandler:useCandies"))).toBe(true);
expect(options.some(option => option.label === "menu:cancel")).toBe(true); expect(options.some(option => option.label === i18next.t("menu:cancel"))).toBe(true);
optionSelectUiHandler?.processInput(Button.ACTION); optionSelectUiHandler?.processInput(Button.ACTION);
await new Promise<void>((resolve) => { await new Promise<void>((resolve) => {
@ -496,11 +497,11 @@ describe("UI - Starter select", () => {
resolve(); resolve();
}); });
}); });
expect(options.some(option => option.label === "starterSelectUiHandler:addToParty")).toBe(true); expect(options.some(option => option.label === i18next.t("starterSelectUiHandler:addToParty"))).toBe(true);
expect(options.some(option => option.label === "starterSelectUiHandler:toggleIVs")).toBe(true); expect(options.some(option => option.label === i18next.t("starterSelectUiHandler:toggleIVs"))).toBe(true);
expect(options.some(option => option.label === "starterSelectUiHandler:manageMoves")).toBe(true); expect(options.some(option => option.label === i18next.t("starterSelectUiHandler:manageMoves"))).toBe(true);
expect(options.some(option => option.label === "starterSelectUiHandler:useCandies")).toBe(true); expect(options.some(option => option.label === i18next.t("starterSelectUiHandler:useCandies"))).toBe(true);
expect(options.some(option => option.label === "menu:cancel")).toBe(true); expect(options.some(option => option.label === i18next.t("menu:cancel"))).toBe(true);
optionSelectUiHandler?.processInput(Button.ACTION); optionSelectUiHandler?.processInput(Button.ACTION);
let starterSelectUiHandler: StarterSelectUiHandler; let starterSelectUiHandler: StarterSelectUiHandler;
@ -561,11 +562,11 @@ describe("UI - Starter select", () => {
resolve(); resolve();
}); });
}); });
expect(options.some(option => option.label === "starterSelectUiHandler:addToParty")).toBe(true); expect(options.some(option => option.label === i18next.t("starterSelectUiHandler:addToParty"))).toBe(true);
expect(options.some(option => option.label === "starterSelectUiHandler:toggleIVs")).toBe(true); expect(options.some(option => option.label === i18next.t("starterSelectUiHandler:toggleIVs"))).toBe(true);
expect(options.some(option => option.label === "starterSelectUiHandler:manageMoves")).toBe(true); expect(options.some(option => option.label === i18next.t("starterSelectUiHandler:manageMoves"))).toBe(true);
expect(options.some(option => option.label === "starterSelectUiHandler:useCandies")).toBe(true); expect(options.some(option => option.label === i18next.t("starterSelectUiHandler:useCandies"))).toBe(true);
expect(options.some(option => option.label === "menu:cancel")).toBe(true); expect(options.some(option => option.label === i18next.t("menu:cancel"))).toBe(true);
optionSelectUiHandler?.processInput(Button.ACTION); optionSelectUiHandler?.processInput(Button.ACTION);
let starterSelectUiHandler: StarterSelectUiHandler | undefined; let starterSelectUiHandler: StarterSelectUiHandler | undefined;

View File

@ -7,7 +7,8 @@ import { Mode } from "#app/ui/ui";
import GameManager from "#test/utils/gameManager"; import GameManager from "#test/utils/gameManager";
import Phaser from "phaser"; import Phaser from "phaser";
import { afterEach, beforeAll, beforeEach, describe, expect, it } from "vitest"; import { afterEach, beforeAll, beforeEach, describe, expect, it } from "vitest";
import MockText from "../utils/mocks/mocksContainer/mockText"; import MockText from "#test/utils/mocks/mocksContainer/mockText";
import i18next from "i18next";
describe("UI - Type Hints", () => { describe("UI - Type Hints", () => {
let phaserGame: Phaser.Game; let phaserGame: Phaser.Game;
@ -53,7 +54,7 @@ describe("UI - Type Hints", () => {
const movesContainer = ui.getByName<Phaser.GameObjects.Container>(FightUiHandler.MOVES_CONTAINER_NAME); const movesContainer = ui.getByName<Phaser.GameObjects.Container>(FightUiHandler.MOVES_CONTAINER_NAME);
const dragonClawText = movesContainer const dragonClawText = movesContainer
.getAll<Phaser.GameObjects.Text>() .getAll<Phaser.GameObjects.Text>()
.find((text) => text.text === "move:dragonClaw.name")! as unknown as MockText; .find((text) => text.text === i18next.t("move:dragonClaw.name"))! as unknown as MockText;
expect.soft(dragonClawText.color).toBe("#929292"); expect.soft(dragonClawText.color).toBe("#929292");
ui.getHandler().processInput(Button.ACTION); ui.getHandler().processInput(Button.ACTION);
@ -78,7 +79,7 @@ describe("UI - Type Hints", () => {
const movesContainer = ui.getByName<Phaser.GameObjects.Container>(FightUiHandler.MOVES_CONTAINER_NAME); const movesContainer = ui.getByName<Phaser.GameObjects.Container>(FightUiHandler.MOVES_CONTAINER_NAME);
const growlText = movesContainer const growlText = movesContainer
.getAll<Phaser.GameObjects.Text>() .getAll<Phaser.GameObjects.Text>()
.find((text) => text.text === "move:growl.name")! as unknown as MockText; .find((text) => text.text === i18next.t("move:growl.name"))! as unknown as MockText;
expect.soft(growlText.color).toBe(undefined); expect.soft(growlText.color).toBe(undefined);
ui.getHandler().processInput(Button.ACTION); ui.getHandler().processInput(Button.ACTION);

View File

@ -4,16 +4,17 @@ import { initLoggedInUser } from "#app/account";
import { initAbilities } from "#app/data/ability"; import { initAbilities } from "#app/data/ability";
import { initBiomes } from "#app/data/balance/biomes"; import { initBiomes } from "#app/data/balance/biomes";
import { initEggMoves } from "#app/data/balance/egg-moves"; import { initEggMoves } from "#app/data/balance/egg-moves";
import { initPokemonPrevolutions } from "#app/data/balance/pokemon-evolutions";
import { initMoves } from "#app/data/move"; import { initMoves } from "#app/data/move";
import { initMysteryEncounters } from "#app/data/mystery-encounters/mystery-encounters"; import { initMysteryEncounters } from "#app/data/mystery-encounters/mystery-encounters";
import { initPokemonPrevolutions } from "#app/data/balance/pokemon-evolutions";
import { initPokemonForms } from "#app/data/pokemon-forms"; import { initPokemonForms } from "#app/data/pokemon-forms";
import { initSpecies } from "#app/data/pokemon-species"; import { initSpecies } from "#app/data/pokemon-species";
import { initAchievements } from "#app/system/achv"; import { initAchievements } from "#app/system/achv";
import { initVouchers } from "#app/system/voucher"; import { initVouchers } from "#app/system/voucher";
import { initStatsKeys } from "#app/ui/game-stats-ui-handler"; import { initStatsKeys } from "#app/ui/game-stats-ui-handler";
import { beforeAll, vi } from "vitest"; import { afterAll, beforeAll, vi } from "vitest";
/** Set the timezone to UTC for tests. */
process.env.TZ = "UTC"; process.env.TZ = "UTC";
/** Mock the override import to always return default values, ignoring any custom overrides. */ /** Mock the override import to always return default values, ignoring any custom overrides. */
@ -26,26 +27,36 @@ vi.mock("#app/overrides", async (importOriginal) => {
} satisfies typeof import("#app/overrides"); } satisfies typeof import("#app/overrides");
}); });
vi.mock("i18next", () => ({ /**
default: { * This is a hacky way to mock the i18n backend requests (with the help of {@link https://mswjs.io/ | msw}).
use: () => {}, * The reason to put it inside of a mock is to elevate it.
t: (key: string) => key, * This is necessary because how our code is structured.
changeLanguage: () => Promise.resolve(), * Do NOT try to put any of this code into external functions, it won't work as it's elevated during runtime.
init: () => Promise.resolve(), */
resolvedLanguage: "en", vi.mock("i18next", async (importOriginal) => {
exists: () => true, console.log("Mocking i18next");
getDataByLanguage:() => ({ const { setupServer } = await import("msw/node");
en: { const { http, HttpResponse } = await import("msw");
keys: [ "foo" ]
}, global.i18nServer = setupServer(
}), http.get("/locales/en/*", async (req) => {
services: { const filename = req.params[0];
formatter: {
add: () => {}, try {
const json = await import(`../../public/locales/en/${req.params[0]}`);
console.log("Loaded locale", filename);
return HttpResponse.json(json);
} catch (err) {
console.log(`Failed to load locale ${filename}!`, err);
return HttpResponse.json({});
} }
}, })
}, );
})); global.i18nServer.listen({ onUnhandledRequest: "error" });
console.log("i18n MSW server listening!");
return await importOriginal();
});
initVouchers(); initVouchers();
initAchievements(); initAchievements();
@ -70,3 +81,8 @@ beforeAll(() => {
}, },
}); });
}); });
afterAll(() => {
global.i18nServer.close();
console.log("Closing i18n MSW server!");
});

View File

@ -34,6 +34,7 @@ export default class EggGachaUiHandler extends MessageUiHandler {
private cursorObj: Phaser.GameObjects.Image; private cursorObj: Phaser.GameObjects.Image;
private transitioning: boolean; private transitioning: boolean;
private transitionCancelled: boolean; private transitionCancelled: boolean;
private summaryFinished: boolean;
private defaultText: string; private defaultText: string;
private scale: number = 0.1666666667; private scale: number = 0.1666666667;
@ -479,7 +480,12 @@ export default class EggGachaUiHandler extends MessageUiHandler {
} }
showSummary(eggs: Egg[]): void { showSummary(eggs: Egg[]): void {
this.transitioning = false; // the overlay will appear faster if the egg pulling animation was skipped
const overlayEaseInDuration = this.getDelayValue(750);
this.summaryFinished = false;
this.transitionCancelled = false;
this.setTransitioning(true);
this.eggGachaSummaryContainer.setVisible(true); this.eggGachaSummaryContainer.setVisible(true);
const eggScale = eggs.length < 20 ? 1 : 0.5; const eggScale = eggs.length < 20 ? 1 : 0.5;
@ -488,12 +494,14 @@ export default class EggGachaUiHandler extends MessageUiHandler {
targets: this.eggGachaOverlay, targets: this.eggGachaOverlay,
alpha: 0.5, alpha: 0.5,
ease: "Sine.easeOut", ease: "Sine.easeOut",
duration: 750, duration: overlayEaseInDuration,
onComplete: () => { onComplete: () => {
const rowItems = 5; const rowItems = 5;
const rows = Math.ceil(eggs.length / rowItems); const rows = Math.ceil(eggs.length / rowItems);
const cols = Math.min(eggs.length, rowItems); const cols = Math.min(eggs.length, rowItems);
const height = this.eggGachaOverlay.displayHeight - this.eggGachaMessageBox.displayHeight; const height = this.eggGachaOverlay.displayHeight - this.eggGachaMessageBox.displayHeight;
// Create sprites for each egg
const eggContainers = eggs.map((egg, t) => { const eggContainers = eggs.map((egg, t) => {
const col = t % rowItems; const col = t % rowItems;
const row = Math.floor(t / rowItems); const row = Math.floor(t / rowItems);
@ -515,14 +523,24 @@ export default class EggGachaUiHandler extends MessageUiHandler {
return ret; return ret;
}); });
eggContainers.forEach((eggContainer, e) => { // If action/cancel was pressed when the overlay was easing in, show all eggs at once
// Otherwise show the eggs one by one with a small delay between each
eggContainers.forEach((eggContainer, index) => {
const delay = !this.transitionCancelled ? this.getDelayValue(index * 100) : 0;
this.scene.time.delayedCall(delay, () =>
this.scene.tweens.add({ this.scene.tweens.add({
targets: eggContainer, targets: eggContainer,
delay: this.getDelayValue(e * 100),
duration: this.getDelayValue(350), duration: this.getDelayValue(350),
scale: eggScale, scale: eggScale,
ease: "Sine.easeOut" ease: "Sine.easeOut",
}); onComplete: () => {
if (index === eggs.length - 1) {
this.setTransitioning(false);
this.summaryFinished = true;
}
}
}));
}); });
} }
}); });
@ -540,6 +558,7 @@ export default class EggGachaUiHandler extends MessageUiHandler {
this.eggGachaSummaryContainer.setAlpha(1); this.eggGachaSummaryContainer.setAlpha(1);
this.eggGachaSummaryContainer.removeAll(true); this.eggGachaSummaryContainer.removeAll(true);
this.setTransitioning(false); this.setTransitioning(false);
this.summaryFinished = false;
this.eggGachaOptionsContainer.setVisible(true); this.eggGachaOptionsContainer.setVisible(true);
} }
}); });
@ -613,7 +632,7 @@ export default class EggGachaUiHandler extends MessageUiHandler {
} else { } else {
if (this.eggGachaSummaryContainer.visible) { if (this.eggGachaSummaryContainer.visible) {
if (button === Button.ACTION || button === Button.CANCEL) { if (this.summaryFinished && (button === Button.ACTION || button === Button.CANCEL)) {
this.hideSummary(); this.hideSummary();
success = true; success = true;
} }
@ -625,7 +644,7 @@ export default class EggGachaUiHandler extends MessageUiHandler {
if (!this.scene.gameData.voucherCounts[VoucherType.REGULAR] && !Overrides.EGG_FREE_GACHA_PULLS_OVERRIDE) { if (!this.scene.gameData.voucherCounts[VoucherType.REGULAR] && !Overrides.EGG_FREE_GACHA_PULLS_OVERRIDE) {
error = true; error = true;
this.showError(i18next.t("egg:notEnoughVouchers")); this.showError(i18next.t("egg:notEnoughVouchers"));
} else if (this.scene.gameData.eggs.length < 99) { } else if (this.scene.gameData.eggs.length < 99 || Overrides.UNLIMITED_EGG_COUNT_OVERRIDE) {
if (!Overrides.EGG_FREE_GACHA_PULLS_OVERRIDE) { if (!Overrides.EGG_FREE_GACHA_PULLS_OVERRIDE) {
this.consumeVouchers(VoucherType.REGULAR, 1); this.consumeVouchers(VoucherType.REGULAR, 1);
} }
@ -640,7 +659,7 @@ export default class EggGachaUiHandler extends MessageUiHandler {
if (!this.scene.gameData.voucherCounts[VoucherType.PLUS] && !Overrides.EGG_FREE_GACHA_PULLS_OVERRIDE) { if (!this.scene.gameData.voucherCounts[VoucherType.PLUS] && !Overrides.EGG_FREE_GACHA_PULLS_OVERRIDE) {
error = true; error = true;
this.showError(i18next.t("egg:notEnoughVouchers")); this.showError(i18next.t("egg:notEnoughVouchers"));
} else if (this.scene.gameData.eggs.length < 95) { } else if (this.scene.gameData.eggs.length < 95 || Overrides.UNLIMITED_EGG_COUNT_OVERRIDE) {
if (!Overrides.EGG_FREE_GACHA_PULLS_OVERRIDE) { if (!Overrides.EGG_FREE_GACHA_PULLS_OVERRIDE) {
this.consumeVouchers(VoucherType.PLUS, 1); this.consumeVouchers(VoucherType.PLUS, 1);
} }
@ -657,7 +676,7 @@ export default class EggGachaUiHandler extends MessageUiHandler {
|| (this.cursor === 3 && !this.scene.gameData.voucherCounts[VoucherType.PREMIUM] && !Overrides.EGG_FREE_GACHA_PULLS_OVERRIDE)) { || (this.cursor === 3 && !this.scene.gameData.voucherCounts[VoucherType.PREMIUM] && !Overrides.EGG_FREE_GACHA_PULLS_OVERRIDE)) {
error = true; error = true;
this.showError(i18next.t("egg:notEnoughVouchers")); this.showError(i18next.t("egg:notEnoughVouchers"));
} else if (this.scene.gameData.eggs.length < 90) { } else if (this.scene.gameData.eggs.length < 90 || Overrides.UNLIMITED_EGG_COUNT_OVERRIDE) {
if (this.cursor === 3) { if (this.cursor === 3) {
if (!Overrides.EGG_FREE_GACHA_PULLS_OVERRIDE) { if (!Overrides.EGG_FREE_GACHA_PULLS_OVERRIDE) {
this.consumeVouchers(VoucherType.PREMIUM, 1); this.consumeVouchers(VoucherType.PREMIUM, 1);
@ -678,7 +697,7 @@ export default class EggGachaUiHandler extends MessageUiHandler {
if (!this.scene.gameData.voucherCounts[VoucherType.GOLDEN] && !Overrides.EGG_FREE_GACHA_PULLS_OVERRIDE) { if (!this.scene.gameData.voucherCounts[VoucherType.GOLDEN] && !Overrides.EGG_FREE_GACHA_PULLS_OVERRIDE) {
error = true; error = true;
this.showError(i18next.t("egg:notEnoughVouchers")); this.showError(i18next.t("egg:notEnoughVouchers"));
} else if (this.scene.gameData.eggs.length < 75) { } else if (this.scene.gameData.eggs.length < 75 || Overrides.UNLIMITED_EGG_COUNT_OVERRIDE) {
if (!Overrides.EGG_FREE_GACHA_PULLS_OVERRIDE) { if (!Overrides.EGG_FREE_GACHA_PULLS_OVERRIDE) {
this.consumeVouchers(VoucherType.GOLDEN, 1); this.consumeVouchers(VoucherType.GOLDEN, 1);
} }

View File

@ -12,6 +12,7 @@ import { BattleType } from "../battle";
import { RunEntry } from "../system/game-data"; import { RunEntry } from "../system/game-data";
import { PlayerGender } from "#enums/player-gender"; import { PlayerGender } from "#enums/player-gender";
import { TrainerVariant } from "../field/trainer"; import { TrainerVariant } from "../field/trainer";
import { RunDisplayMode } from "#app/ui/run-info-ui-handler";
export type RunSelectCallback = (cursor: number) => void; export type RunSelectCallback = (cursor: number) => void;
@ -104,7 +105,7 @@ export default class RunHistoryUiHandler extends MessageUiHandler {
if (button === Button.ACTION) { if (button === Button.ACTION) {
const cursor = this.cursor + this.scrollCursor; const cursor = this.cursor + this.scrollCursor;
if (this.runs[cursor]) { if (this.runs[cursor]) {
this.scene.ui.setOverlayMode(Mode.RUN_INFO, this.runs[cursor].entryData, true); this.scene.ui.setOverlayMode(Mode.RUN_INFO, this.runs[cursor].entryData, RunDisplayMode.RUN_HISTORY, true);
} else { } else {
return false; return false;
} }

View File

@ -5,6 +5,7 @@ import { SessionSaveData } from "../system/game-data";
import { TextStyle, addTextObject, addBBCodeTextObject, getTextColor } from "./text"; import { TextStyle, addTextObject, addBBCodeTextObject, getTextColor } from "./text";
import { Mode } from "./ui"; import { Mode } from "./ui";
import { addWindow } from "./ui-theme"; import { addWindow } from "./ui-theme";
import { getPokeballAtlasKey } from "#app/data/pokeball";
import * as Utils from "../utils"; import * as Utils from "../utils";
import PokemonData from "../system/pokemon-data"; import PokemonData from "../system/pokemon-data";
import i18next from "i18next"; import i18next from "i18next";
@ -22,6 +23,8 @@ import * as Modifier from "../modifier/modifier";
import { Species } from "#enums/species"; import { Species } from "#enums/species";
import { PlayerGender } from "#enums/player-gender"; import { PlayerGender } from "#enums/player-gender";
import { SettingKeyboard } from "#app/system/settings/settings-keyboard"; import { SettingKeyboard } from "#app/system/settings/settings-keyboard";
import { getBiomeName } from "#app/data/balance/biomes";
import { MysteryEncounterType } from "#enums/mystery-encounter-type";
/** /**
* RunInfoUiMode indicates possible overlays of RunInfoUiHandler. * RunInfoUiMode indicates possible overlays of RunInfoUiHandler.
@ -34,6 +37,11 @@ enum RunInfoUiMode {
ENDING_ART ENDING_ART
} }
export enum RunDisplayMode {
RUN_HISTORY,
SESSION_PREVIEW
}
/** /**
* Some variables are protected because this UI class will most likely be extended in the future to display more information. * Some variables are protected because this UI class will most likely be extended in the future to display more information.
* These variables will most likely be shared across 'classes' aka pages. * These variables will most likely be shared across 'classes' aka pages.
@ -41,6 +49,7 @@ enum RunInfoUiMode {
* For now, I leave as is. * For now, I leave as is.
*/ */
export default class RunInfoUiHandler extends UiHandler { export default class RunInfoUiHandler extends UiHandler {
protected runDisplayMode: RunDisplayMode;
protected runInfo: SessionSaveData; protected runInfo: SessionSaveData;
protected isVictory: boolean; protected isVictory: boolean;
protected pageMode: RunInfoUiMode; protected pageMode: RunInfoUiMode;
@ -66,6 +75,7 @@ export default class RunInfoUiHandler extends UiHandler {
// The import of the modifiersModule is loaded here to sidestep async/await issues. // The import of the modifiersModule is loaded here to sidestep async/await issues.
this.modifiersModule = Modifier; this.modifiersModule = Modifier;
this.runContainer.setVisible(false); this.runContainer.setVisible(false);
this.scene.loadImage("encounter_exclaim", "mystery-encounters");
} }
/** /**
@ -87,9 +97,15 @@ export default class RunInfoUiHandler extends UiHandler {
this.runContainer.add(gameStatsBg); this.runContainer.add(gameStatsBg);
const run = args[0]; const run = args[0];
// Assigning information necessary for the UI's creation this.runDisplayMode = args[1];
if (this.runDisplayMode === RunDisplayMode.RUN_HISTORY) {
this.runInfo = this.scene.gameData.parseSessionData(JSON.stringify(run.entry)); this.runInfo = this.scene.gameData.parseSessionData(JSON.stringify(run.entry));
this.isVictory = run.isVictory; this.isVictory = run.isVictory ?? false;
} else if (this.runDisplayMode === RunDisplayMode.SESSION_PREVIEW) {
this.runInfo = args[0];
}
// Assigning information necessary for the UI's creation
this.pageMode = RunInfoUiMode.MAIN; this.pageMode = RunInfoUiMode.MAIN;
// Creates Header and adds to this.runContainer // Creates Header and adds to this.runContainer
@ -102,7 +118,11 @@ export default class RunInfoUiHandler extends UiHandler {
const runResultWindow = addWindow(this.scene, 0, 0, this.statsBgWidth - 11, 65); const runResultWindow = addWindow(this.scene, 0, 0, this.statsBgWidth - 11, 65);
runResultWindow.setOrigin(0, 0); runResultWindow.setOrigin(0, 0);
this.runResultContainer.add(runResultWindow); this.runResultContainer.add(runResultWindow);
if (this.runDisplayMode === RunDisplayMode.RUN_HISTORY) {
this.parseRunResult(); this.parseRunResult();
} else if (this.runDisplayMode === RunDisplayMode.SESSION_PREVIEW) {
this.parseRunStatus();
}
// Creates Run Info Container // Creates Run Info Container
this.runInfoContainer = this.scene.add.container(0, 89); this.runInfoContainer = this.scene.add.container(0, 89);
@ -226,6 +246,66 @@ export default class RunInfoUiHandler extends UiHandler {
this.runContainer.add(this.runResultContainer); this.runContainer.add(this.runResultContainer);
} }
/**
* This function is used when the Run Info UI is used to preview a Session.
* It edits {@linkcode runResultContainer}, but most importantly - does not display the negative results of a Mystery Encounter or any details of a trainer's party.
* Trainer Parties are replaced with their sprites, names, and their party size.
* Mystery Encounters contain sprites associated with MEs + the title of the specific ME.
*/
private parseRunStatus() {
const runStatusText = addTextObject(this.scene, 6, 5, `${i18next.t("saveSlotSelectUiHandler:wave")} ${this.runInfo.waveIndex} - ${getBiomeName(this.runInfo.arena.biome)}`, TextStyle.WINDOW, { fontSize : "65px", lineSpacing: 0.1 });
const enemyContainer = this.scene.add.container(0, 0);
this.runResultContainer.add(enemyContainer);
if (this.runInfo.battleType === BattleType.WILD) {
if (this.runInfo.enemyParty.length === 1) {
this.parseWildSingleDefeat(enemyContainer);
} else if (this.runInfo.enemyParty.length === 2) {
this.parseWildDoubleDefeat(enemyContainer);
}
} else if (this.runInfo.battleType === BattleType.TRAINER) {
this.showTrainerSprites(enemyContainer);
const row_limit = 3;
this.runInfo.enemyParty.forEach((p, i) => {
const pokeball = this.scene.add.sprite(0, 0, "pb");
pokeball.setFrame(getPokeballAtlasKey(p.pokeball));
pokeball.setScale(0.5);
pokeball.setPosition(52 + ((i % row_limit) * 8), (i <= 2) ? 18 : 25);
enemyContainer.add(pokeball);
});
const trainerObj = this.runInfo.trainer.toTrainer(this.scene);
const RIVAL_TRAINER_ID_THRESHOLD = 375;
let trainerName = "";
if (this.runInfo.trainer.trainerType >= RIVAL_TRAINER_ID_THRESHOLD) {
trainerName = (trainerObj.variant === TrainerVariant.FEMALE) ? i18next.t("trainerNames:rival_female") : i18next.t("trainerNames:rival");
} else {
trainerName = trainerObj.getName(0, true);
}
const boxString = i18next.t(trainerObj.variant !== TrainerVariant.DOUBLE ? "battle:trainerAppeared" : "battle:trainerAppearedDouble", { trainerName: trainerName }).replace(/\n/g, " ");
const descContainer = this.scene.add.container(0, 0);
const textBox = addTextObject(this.scene, 0, 0, boxString, TextStyle.WINDOW, { fontSize : "35px", wordWrap: { width: 200 }});
descContainer.add(textBox);
descContainer.setPosition(52, 29);
this.runResultContainer.add(descContainer);
} else if (this.runInfo.battleType === BattleType.MYSTERY_ENCOUNTER) {
const encounterExclaim = this.scene.add.sprite(0, 0, "encounter_exclaim");
encounterExclaim.setPosition(34, 26);
encounterExclaim.setScale(0.65);
const subSprite = this.scene.add.sprite(56, -106, "pkmn__sub");
subSprite.setScale(0.65);
subSprite.setPosition(34, 46);
const mysteryEncounterTitle = i18next.t(this.scene.getMysteryEncounter(this.runInfo.mysteryEncounterType as MysteryEncounterType, true).localizationKey + ":title");
const descContainer = this.scene.add.container(0, 0);
const textBox = addTextObject(this.scene, 0, 0, mysteryEncounterTitle, TextStyle.WINDOW, { fontSize : "45px", wordWrap: { width: 160 }});
descContainer.add(textBox);
descContainer.setPosition(47, 37);
this.runResultContainer.add([ encounterExclaim, subSprite, descContainer ]);
}
this.runResultContainer.add(runStatusText);
this.runContainer.add(this.runResultContainer);
}
/** /**
* This function is called to edit an enemyContainer to represent a loss from a defeat by a wild single Pokemon battle. * This function is called to edit an enemyContainer to represent a loss from a defeat by a wild single Pokemon battle.
* @param enemyContainer - container holding enemy visual and level information * @param enemyContainer - container holding enemy visual and level information
@ -278,40 +358,58 @@ export default class RunInfoUiHandler extends UiHandler {
} }
/** /**
* This edits a container to represent a loss from a defeat by a trainer battle. * This loads the enemy sprites, positions, and scales them according to the current display mode of the RunInfo UI and then adds them to the container parameter.
* @param enemyContainer - container holding enemy visuals and level information * Used by {@linkcode parseRunStatus} and {@linkcode parseTrainerDefeat}
* The trainers are placed to the left of their party. * @param enemyContainer a Phaser Container that should hold enemy sprites
* Depending on the trainer icon, there may be overlap between the edges of the box or their party. (Capes...)
*
* Party Pokemon have their icons, terastalization status, and level shown.
*/ */
private parseTrainerDefeat(enemyContainer: Phaser.GameObjects.Container) { private showTrainerSprites(enemyContainer: Phaser.GameObjects.Container) {
// Creating the trainer sprite and adding it to enemyContainer // Creating the trainer sprite and adding it to enemyContainer
const tObj = this.runInfo.trainer.toTrainer(this.scene); const tObj = this.runInfo.trainer.toTrainer(this.scene);
// Loads trainer assets on demand, as they are not loaded by default in the scene // Loads trainer assets on demand, as they are not loaded by default in the scene
tObj.config.loadAssets(this.scene, this.runInfo.trainer.variant).then(() => { tObj.config.loadAssets(this.scene, this.runInfo.trainer.variant).then(() => {
const tObjSpriteKey = tObj.config.getSpriteKey(this.runInfo.trainer.variant === TrainerVariant.FEMALE, false); const tObjSpriteKey = tObj.config.getSpriteKey(this.runInfo.trainer.variant === TrainerVariant.FEMALE, false);
const tObjSprite = this.scene.add.sprite(0, 5, tObjSpriteKey); const tObjSprite = this.scene.add.sprite(0, 5, tObjSpriteKey);
if (this.runInfo.trainer.variant === TrainerVariant.DOUBLE) { if (this.runInfo.trainer.variant === TrainerVariant.DOUBLE && !tObj.config.doubleOnly) {
const doubleContainer = this.scene.add.container(5, 8); const doubleContainer = this.scene.add.container(5, 8);
tObjSprite.setPosition(-3, -3); tObjSprite.setPosition(-3, -3);
const tObjPartnerSpriteKey = tObj.config.getSpriteKey(true, true); const tObjPartnerSpriteKey = tObj.config.getSpriteKey(true, true);
const tObjPartnerSprite = this.scene.add.sprite(5, -3, tObjPartnerSpriteKey); const tObjPartnerSprite = this.scene.add.sprite(5, -3, tObjPartnerSpriteKey);
// Double Trainers have smaller sprites than Single Trainers // Double Trainers have smaller sprites than Single Trainers
if (this.runDisplayMode === RunDisplayMode.RUN_HISTORY) {
tObjPartnerSprite.setScale(0.20); tObjPartnerSprite.setScale(0.20);
tObjSprite.setScale(0.20); tObjSprite.setScale(0.20);
doubleContainer.add(tObjSprite); doubleContainer.add(tObjSprite);
doubleContainer.add(tObjPartnerSprite); doubleContainer.add(tObjPartnerSprite);
doubleContainer.setPosition(12, 38); doubleContainer.setPosition(12, 38);
} else {
tObjSprite.setScale(0.55);
tObjSprite.setPosition(-9, -3);
tObjPartnerSprite.setScale(0.55);
doubleContainer.add([ tObjSprite, tObjPartnerSprite ]);
doubleContainer.setPosition(28, 40);
}
enemyContainer.add(doubleContainer); enemyContainer.add(doubleContainer);
} else { } else {
tObjSprite.setScale(0.35, 0.35); const scale = (this.runDisplayMode === RunDisplayMode.RUN_HISTORY) ? 0.35 : 0.65;
tObjSprite.setPosition(12, 28); const position = (this.runDisplayMode === RunDisplayMode.RUN_HISTORY) ? [ 12, 28 ] : [ 32, 36 ];
tObjSprite.setScale(scale, scale);
tObjSprite.setPosition(position[0], position[1]);
enemyContainer.add(tObjSprite); enemyContainer.add(tObjSprite);
} }
}); });
}
/**
* This edits a container to represent a loss from a defeat by a trainer battle.
* The trainers are placed to the left of their party.
* Depending on the trainer icon, there may be overlap between the edges of the box or their party. (Capes...)
*
* Party Pokemon have their icons, terastalization status, and level shown.
* @param enemyContainer - container holding enemy visuals and level information
*/
private parseTrainerDefeat(enemyContainer: Phaser.GameObjects.Container) {
// Loads and adds trainer sprites to the UI
this.showTrainerSprites(enemyContainer);
// Determining which Terastallize Modifier belongs to which Pokemon // Determining which Terastallize Modifier belongs to which Pokemon
// Creates a dictionary {PokemonId: TeraShardType} // Creates a dictionary {PokemonId: TeraShardType}
const teraPokemon = {}; const teraPokemon = {};

View File

@ -10,6 +10,7 @@ import MessageUiHandler from "./message-ui-handler";
import { TextStyle, addTextObject } from "./text"; import { TextStyle, addTextObject } from "./text";
import { Mode } from "./ui"; import { Mode } from "./ui";
import { addWindow } from "./ui-theme"; import { addWindow } from "./ui-theme";
import { RunDisplayMode } from "#app/ui/run-info-ui-handler";
const sessionSlotCount = 5; const sessionSlotCount = 5;
@ -33,7 +34,7 @@ export default class SaveSlotSelectUiHandler extends MessageUiHandler {
private scrollCursor: integer = 0; private scrollCursor: integer = 0;
private cursorObj: Phaser.GameObjects.NineSlice | null; private cursorObj: Phaser.GameObjects.Container | null;
private sessionSlotsContainerInitialY: number; private sessionSlotsContainerInitialY: number;
@ -83,9 +84,11 @@ export default class SaveSlotSelectUiHandler extends MessageUiHandler {
this.saveSlotSelectCallback = args[1] as SaveSlotSelectCallback; this.saveSlotSelectCallback = args[1] as SaveSlotSelectCallback;
this.saveSlotSelectContainer.setVisible(true); this.saveSlotSelectContainer.setVisible(true);
this.populateSessionSlots(); this.populateSessionSlots()
.then(() => {
this.setScrollCursor(0); this.setScrollCursor(0);
this.setCursor(0); this.setCursor(0);
});
return true; return true;
} }
@ -147,21 +150,28 @@ export default class SaveSlotSelectUiHandler extends MessageUiHandler {
success = true; success = true;
} }
} else { } else {
const cursorPosition = this.cursor + this.scrollCursor;
switch (button) { switch (button) {
case Button.UP: case Button.UP:
if (this.cursor) { if (this.cursor) {
success = this.setCursor(this.cursor - 1); // Check to prevent cursor from accessing a negative index
success = (this.cursor === 0) ? this.setCursor(this.cursor) : this.setCursor(this.cursor - 1, cursorPosition);
} else if (this.scrollCursor) { } else if (this.scrollCursor) {
success = this.setScrollCursor(this.scrollCursor - 1); success = this.setScrollCursor(this.scrollCursor - 1, cursorPosition);
} }
break; break;
case Button.DOWN: case Button.DOWN:
if (this.cursor < 2) { if (this.cursor < 2) {
success = this.setCursor(this.cursor + 1); success = this.setCursor(this.cursor + 1, this.cursor);
} else if (this.scrollCursor < sessionSlotCount - 3) { } else if (this.scrollCursor < sessionSlotCount - 3) {
success = this.setScrollCursor(this.scrollCursor + 1); success = this.setScrollCursor(this.scrollCursor + 1, cursorPosition);
} }
break; break;
case Button.RIGHT:
if (this.sessionSlots[cursorPosition].hasData && this.sessionSlots[cursorPosition].saveData) {
this.scene.ui.setOverlayMode(Mode.RUN_INFO, this.sessionSlots[cursorPosition].saveData, RunDisplayMode.SESSION_PREVIEW);
success = true;
}
} }
} }
@ -174,10 +184,10 @@ export default class SaveSlotSelectUiHandler extends MessageUiHandler {
return success || error; return success || error;
} }
populateSessionSlots() { async populateSessionSlots() {
for (let s = 0; s < sessionSlotCount; s++) { for (let s = 0; s < sessionSlotCount; s++) {
const sessionSlot = new SessionSlot(this.scene, s); const sessionSlot = new SessionSlot(this.scene, s);
sessionSlot.load(); await sessionSlot.load();
this.scene.add.existing(sessionSlot); this.scene.add.existing(sessionSlot);
this.sessionSlotsContainer.add(sessionSlot); this.sessionSlotsContainer.add(sessionSlot);
this.sessionSlots.push(sessionSlot); this.sessionSlots.push(sessionSlot);
@ -198,25 +208,74 @@ export default class SaveSlotSelectUiHandler extends MessageUiHandler {
this.saveSlotSelectMessageBoxContainer.setVisible(!!text?.length); this.saveSlotSelectMessageBoxContainer.setVisible(!!text?.length);
} }
setCursor(cursor: integer): boolean { /**
* setCursor takes user navigation as an input and positions the cursor accordingly
* @param cursor the index provided to the cursor
* @param prevCursor the previous index occupied by the cursor - optional
* @returns `true` if the cursor position has changed | `false` if it has not
*/
override setCursor(cursor: integer, prevCursor?: integer): boolean {
const changed = super.setCursor(cursor); const changed = super.setCursor(cursor);
if (!this.cursorObj) { if (!this.cursorObj) {
this.cursorObj = this.scene.add.nineslice(0, 0, "select_cursor_highlight_thick", undefined, 296, 44, 6, 6, 6, 6); this.cursorObj = this.scene.add.container(0, 0);
this.cursorObj.setOrigin(0, 0); const cursorBox = this.scene.add.nineslice(0, 0, "select_cursor_highlight_thick", undefined, 296, 44, 6, 6, 6, 6);
const rightArrow = this.scene.add.image(0, 0, "cursor");
rightArrow.setPosition(160, 0);
rightArrow.setName("rightArrow");
this.cursorObj.add([ cursorBox, rightArrow ]);
this.sessionSlotsContainer.add(this.cursorObj); this.sessionSlotsContainer.add(this.cursorObj);
} }
this.cursorObj.setPosition(4, 4 + (cursor + this.scrollCursor) * 56); const cursorPosition = cursor + this.scrollCursor;
const cursorIncrement = cursorPosition * 56;
if (this.sessionSlots[cursorPosition] && this.cursorObj) {
const hasData = this.sessionSlots[cursorPosition].hasData;
// If the session slot lacks session data, it does not move from its default, central position.
// Only session slots with session data will move leftwards and have a visible arrow.
if (!hasData) {
this.cursorObj.setPosition(151, 26 + cursorIncrement);
this.sessionSlots[cursorPosition].setPosition(0, cursorIncrement);
} else {
this.cursorObj.setPosition(145, 26 + cursorIncrement);
this.sessionSlots[cursorPosition].setPosition(-6, cursorIncrement);
}
this.setArrowVisibility(hasData);
}
if (!Utils.isNullOrUndefined(prevCursor)) {
this.revertSessionSlot(prevCursor);
}
return changed; return changed;
} }
setScrollCursor(scrollCursor: integer): boolean { /**
* Helper function that resets the session slot position to its default central position
* @param prevCursor the previous location of the cursor
*/
revertSessionSlot(prevCursor: integer): void {
const sessionSlot = this.sessionSlots[prevCursor];
if (sessionSlot) {
sessionSlot.setPosition(0, prevCursor * 56);
}
}
/**
* Helper function that checks if the session slot involved holds data or not
* @param hasData `true` if session slot contains data | 'false' if not
*/
setArrowVisibility(hasData: boolean): void {
if (this.cursorObj) {
const rightArrow = this.cursorObj?.getByName("rightArrow") as Phaser.GameObjects.Image;
rightArrow.setVisible(hasData);
}
}
setScrollCursor(scrollCursor: integer, priorCursor?: integer): boolean {
const changed = scrollCursor !== this.scrollCursor; const changed = scrollCursor !== this.scrollCursor;
if (changed) { if (changed) {
this.scrollCursor = scrollCursor; this.scrollCursor = scrollCursor;
this.setCursor(this.cursor); this.setCursor(this.cursor, priorCursor);
this.scene.tweens.add({ this.scene.tweens.add({
targets: this.sessionSlotsContainer, targets: this.sessionSlotsContainer,
y: this.sessionSlotsContainerInitialY - 56 * scrollCursor, y: this.sessionSlotsContainerInitialY - 56 * scrollCursor,
@ -254,6 +313,8 @@ class SessionSlot extends Phaser.GameObjects.Container {
public hasData: boolean; public hasData: boolean;
private loadingLabel: Phaser.GameObjects.Text; private loadingLabel: Phaser.GameObjects.Text;
public saveData: SessionSaveData;
constructor(scene: BattleScene, slotId: integer) { constructor(scene: BattleScene, slotId: integer) {
super(scene, 0, slotId * 56); super(scene, 0, slotId * 56);
@ -337,6 +398,7 @@ class SessionSlot extends Phaser.GameObjects.Container {
return; return;
} }
this.hasData = true; this.hasData = true;
this.saveData = sessionData;
await this.setupWithData(sessionData); await this.setupWithData(sessionData);
resolve(true); resolve(true);
}); });

View File

@ -1789,7 +1789,8 @@ export default class StarterSelectUiHandler extends MessageUiHandler {
options.push({ options.push({
label: `x${sameSpeciesEggCost} ${i18next.t("starterSelectUiHandler:sameSpeciesEgg")}`, label: `x${sameSpeciesEggCost} ${i18next.t("starterSelectUiHandler:sameSpeciesEgg")}`,
handler: () => { handler: () => {
if (this.scene.gameData.eggs.length < 99 && (Overrides.FREE_CANDY_UPGRADE_OVERRIDE || candyCount >= sameSpeciesEggCost)) { if ((this.scene.gameData.eggs.length < 99 || Overrides.UNLIMITED_EGG_COUNT_OVERRIDE)
&& (Overrides.FREE_CANDY_UPGRADE_OVERRIDE || candyCount >= sameSpeciesEggCost)) {
if (!Overrides.FREE_CANDY_UPGRADE_OVERRIDE) { if (!Overrides.FREE_CANDY_UPGRADE_OVERRIDE) {
starterData.candyCount -= sameSpeciesEggCost; starterData.candyCount -= sameSpeciesEggCost;
} }