Added type safety to reward generators

This commit is contained in:
Bertie690 2025-08-03 22:57:54 -04:00
parent dac9e202a0
commit 6077cfff17
9 changed files with 541 additions and 350 deletions

View File

@ -0,0 +1,127 @@
import type { RewardId } from "#enums/reward-id";
import type {
AddMoneyReward,
AddPokeballReward,
AddVoucherReward,
AllPokemonFullReviveReward,
AllPokemonLevelIncrementReward,
AttackTypeBoosterRewardGenerator,
BaseStatBoosterRewardGenerator,
BerryRewardGenerator,
EvolutionItemRewardGenerator,
FormChangeItemRewardGenerator,
FusePokemonReward,
LapsingTrainerItemReward,
MintRewardGenerator,
PokemonAllMovePpRestoreReward,
PokemonHpRestoreReward,
PokemonLevelIncrementReward,
PokemonPpRestoreReward,
PokemonPpUpReward,
PokemonReviveReward,
PokemonStatusHealReward,
RememberMoveReward,
SpeciesStatBoosterRewardGenerator,
TempStatStageBoosterRewardGenerator,
TeraTypeRewardGenerator,
TmRewardGenerator,
} from "#items/reward";
/**
* The type of the `allRewards` const object.
* @todo Make `allRewards` a const object and replace all references to this with `typeof allRewards`
*/
export type allRewardsType = {
// Pokeball rewards
[RewardId.POKEBALL]: () => AddPokeballReward;
[RewardId.GREAT_BALL]: () => AddPokeballReward;
[RewardId.ULTRA_BALL]: () => AddPokeballReward;
[RewardId.ROGUE_BALL]: () => AddPokeballReward;
[RewardId.MASTER_BALL]: () => AddPokeballReward;
// Voucher rewards
[RewardId.VOUCHER]: () => AddVoucherReward;
[RewardId.VOUCHER_PLUS]: () => AddVoucherReward;
[RewardId.VOUCHER_PREMIUM]: () => AddVoucherReward;
// Money rewards
[RewardId.NUGGET]: () => AddMoneyReward;
[RewardId.BIG_NUGGET]: () => AddMoneyReward;
[RewardId.RELIC_GOLD]: () => AddMoneyReward;
// Party-wide consumables
[RewardId.RARER_CANDY]: () => AllPokemonLevelIncrementReward;
[RewardId.SACRED_ASH]: () => AllPokemonFullReviveReward;
// Pokemon consumables
[RewardId.RARE_CANDY]: () => PokemonLevelIncrementReward;
[RewardId.EVOLUTION_ITEM]: () => EvolutionItemRewardGenerator;
[RewardId.RARE_EVOLUTION_ITEM]: () => EvolutionItemRewardGenerator;
[RewardId.POTION]: () => PokemonHpRestoreReward;
[RewardId.SUPER_POTION]: () => PokemonHpRestoreReward;
[RewardId.HYPER_POTION]: () => PokemonHpRestoreReward;
[RewardId.MAX_POTION]: () => PokemonHpRestoreReward;
[RewardId.FULL_RESTORE]: () => PokemonHpRestoreReward;
[RewardId.REVIVE]: () => PokemonReviveReward;
[RewardId.MAX_REVIVE]: () => PokemonReviveReward;
[RewardId.FULL_HEAL]: () => PokemonStatusHealReward;
[RewardId.ETHER]: () => PokemonPpRestoreReward;
[RewardId.MAX_ETHER]: () => PokemonPpRestoreReward;
[RewardId.ELIXIR]: () => PokemonAllMovePpRestoreReward;
[RewardId.MAX_ELIXIR]: () => PokemonAllMovePpRestoreReward;
[RewardId.PP_UP]: () => PokemonPpUpReward;
[RewardId.PP_MAX]: () => PokemonPpUpReward;
/*
[RewardId.REPEL]: () => DoubleBattleChanceBoosterReward,
[RewardId.SUPER_REPEL]: () => DoubleBattleChanceBoosterReward,
[RewardId.MAX_REPEL]: () => DoubleBattleChanceBoosterReward,
*/
[RewardId.MINT]: () => MintRewardGenerator;
[RewardId.TERA_SHARD]: () => TeraTypeRewardGenerator;
[RewardId.TM_COMMON]: () => TmRewardGenerator;
[RewardId.TM_GREAT]: () => TmRewardGenerator;
[RewardId.TM_ULTRA]: () => TmRewardGenerator;
[RewardId.MEMORY_MUSHROOM]: () => RememberMoveReward;
[RewardId.DNA_SPLICERS]: () => FusePokemonReward;
// Form change items
[RewardId.FORM_CHANGE_ITEM]: () => FormChangeItemRewardGenerator;
[RewardId.RARE_FORM_CHANGE_ITEM]: () => FormChangeItemRewardGenerator;
// Held items
[RewardId.SPECIES_STAT_BOOSTER]: () => SpeciesStatBoosterRewardGenerator;
[RewardId.RARE_SPECIES_STAT_BOOSTER]: () => SpeciesStatBoosterRewardGenerator;
[RewardId.BASE_STAT_BOOSTER]: () => BaseStatBoosterRewardGenerator;
[RewardId.ATTACK_TYPE_BOOSTER]: () => AttackTypeBoosterRewardGenerator;
[RewardId.BERRY]: () => BerryRewardGenerator;
// [RewardId.MINI_BLACK_HOLE]: () => HeldItemReward,
// Trainer items
[RewardId.LURE]: () => LapsingTrainerItemReward;
[RewardId.SUPER_LURE]: () => LapsingTrainerItemReward;
[RewardId.MAX_LURE]: () => LapsingTrainerItemReward;
[RewardId.TEMP_STAT_STAGE_BOOSTER]: () => TempStatStageBoosterRewardGenerator;
[RewardId.DIRE_HIT]: () => LapsingTrainerItemReward;
// [RewardId.GOLDEN_POKEBALL]: () => TrainerItemReward,
};

View File

@ -3,19 +3,36 @@ import type { RewardId } from "#enums/reward-id";
import type { TrainerItemId } from "#enums/trainer-item-id";
import type { Pokemon } from "#field/pokemon";
import type { Reward, RewardGenerator } from "#items/reward";
import type { allRewardsType } from "#types/all-reward-type";
export type RewardFunc = () => Reward | RewardGenerator;
// TODO: Remove party from arguments can be accessed from `globalScene`
export type WeightedRewardWeightFunc = (party: Pokemon[], rerollCount?: number) => number;
export type RewardPoolId = RewardId | HeldItemId | TrainerItemId;
export type RewardGeneratorSpecs = {
id: RewardId;
args: RewardGeneratorArgs;
type allRewardsInstanceMap = {
[k in keyof allRewardsType as ReturnType<allRewardsType[k]> extends RewardGenerator ? k : never]: ReturnType<
allRewardsType[k]
>;
};
// TODO: fix this with correctly typed args for different RewardIds
export type RewardSpecs = RewardPoolId | RewardGeneratorSpecs;
export type RewardGeneratorArgMap = {
[k in keyof allRewardsInstanceMap]: Exclude<Parameters<allRewardsInstanceMap[k]["generateReward"]>[0], undefined>;
};
/** Union type containing all `RewardId`s corresponding to valid {@linkcode RewardGenerator}s. */
export type RewardGeneratorId = keyof allRewardsInstanceMap;
// TODO: SOrt out which types can and cannot be exported
export type RewardGeneratorSpecs<T extends RewardGeneratorId = RewardGeneratorId> = {
id: T;
args: RewardGeneratorArgMap[T];
};
export type RewardSpecs<T extends RewardPoolId = RewardPoolId> = T extends RewardGeneratorId
? T | RewardGeneratorSpecs<T>
: T;
export type RewardPoolEntry = {
id: RewardPoolId;

View File

@ -1,12 +1,11 @@
import type { Ability } from "#abilities/ability";
import type { PokemonSpecies } from "#data/pokemon-species";
import type { HeldItemId } from "#enums/held-item-id";
import type { RewardId } from "#enums/reward-id";
import type { TrainerItemId } from "#enums/trainer-item-id";
import type { HeldItem } from "#items/held-item";
import type { TrainerItem } from "#items/trainer-item";
import type { Move } from "#moves/move";
import type { RewardFunc } from "#types/rewards";
import type { allRewardsType } from "#types/all-reward-type";
export const allAbilities: Ability[] = [];
export const allMoves: Move[] = [];
@ -14,4 +13,5 @@ export const allSpecies: PokemonSpecies[] = [];
export const allHeldItems: Record<HeldItemId, HeldItem> = {};
export const allTrainerItems: Record<TrainerItemId, TrainerItem> = {};
export const allRewards: Record<RewardId, RewardFunc> = {};
// TODO: Consider moving into `all-rewards.ts` as a const object - files should not be importing this
export const allRewards: allRewardsType = {};

View File

@ -132,7 +132,8 @@ export function initRewards() {
allRewards[RewardId.PP_MAX] = () =>
new PokemonPpUpReward("modifierType:ModifierType.PP_MAX", "pp_max", RewardId.PP_MAX, 3);
/*REPEL] = () => new DoubleBattleChanceBoosterReward('Repel', 5),
/*
REPEL] = () => new DoubleBattleChanceBoosterReward('Repel', 5),
SUPER_REPEL] = () => new DoubleBattleChanceBoosterReward('Super Repel', 10),
MAX_REPEL] = () => new DoubleBattleChanceBoosterReward('Max Repel', 25),*/

View File

@ -8,7 +8,7 @@ import { isNullOrUndefined, pickWeightedIndex, randSeedInt } from "#utils/common
import { getPartyLuckValue } from "#utils/party";
import type { RewardOption } from "./reward";
import { rewardPool, rewardPoolWeights } from "./reward-pools";
import { generateRewardOptionFromId, generateRewardOptionFromSpecs, isTrainerItemId } from "./reward-utils";
import { generateRewardOptionFromId, isTrainerItemId } from "./reward-utils";
/*
This file still contains several functions to generate rewards from pools. The hierarchy of these functions is explained here.
@ -160,7 +160,7 @@ export function generatePlayerRewardOptions(
if (customRewardSettings?.guaranteedRewardSpecs && customRewardSettings.guaranteedRewardSpecs.length > 0) {
for (const specs of customRewardSettings.guaranteedRewardSpecs) {
const rewardOption = generateRewardOptionFromSpecs(specs);
const rewardOption = generateRewardOptionFromId(specs);
if (rewardOption) {
options.push(rewardOption);
}
@ -287,13 +287,12 @@ function getNewRewardOption(
* Replaces the {@linkcode Reward} of the entries within {@linkcode options} with any
* up to the smallest amount of entries between {@linkcode options} and the override array.
* @param options Array of naturally rolled {@linkcode RewardOption}s
* @param party Array of the player's current party
*/
export function overridePlayerRewardOptions(options: RewardOption[]) {
const minLength = Math.min(options.length, Overrides.REWARD_OVERRIDE.length);
for (let i = 0; i < minLength; i++) {
const specs: RewardSpecs = Overrides.REWARD_OVERRIDE[i];
const rewardOption = generateRewardOptionFromSpecs(specs);
const rewardOption = generateRewardOptionFromId(specs);
if (rewardOption) {
options[i] = rewardOption;
}

View File

@ -1,10 +1,15 @@
import { globalScene } from "#app/global-scene";
import { allRewards } from "#data/data-lists";
import type { HeldItemId } from "#enums/held-item-id";
import { getRewardCategory, RewardCategoryId, RewardId } from "#enums/reward-id";
import type { RarityTier } from "#enums/reward-tier";
import type { TrainerItemId } from "#enums/trainer-item-id";
import type { RewardFunc, RewardPoolId, RewardSpecs } from "#types/rewards";
import type {
RewardFunc,
RewardGeneratorArgMap,
RewardGeneratorId,
RewardGeneratorSpecs,
RewardPoolId,
} from "#types/rewards";
import { heldItemRarities } from "./held-item-default-tiers";
import {
HeldItemReward,
@ -33,22 +38,73 @@ export function isRememberMoveReward(reward: Reward): reward is RememberMoveRewa
}
/**
* Generates a Reward from a given function
* Generates a Reward from a given function.
* @param rewardFunc
*/
function generateReward(rewardFunc: () => Reward): Reward | null;
/**
* Generates a Reward from a given function
* @param generator
* @param pregenArgs Can specify BerryType for berries, TM for TMs, AttackBoostType for item, etc.
*/
export function generateReward(rewardFunc: RewardFunc, pregenArgs?: any[]): Reward | null {
function generateReward<T extends RewardGenerator>(
generator: () => T,
pregenArgs?: Parameters<T["generateReward"]>[0],
): Reward | null;
function generateReward(rewardFunc: RewardFunc, pregenArgs?: any[]): Reward | null {
const reward = rewardFunc();
return reward instanceof RewardGenerator ? reward.generateReward(globalScene.getPlayerParty(), pregenArgs) : reward;
return reward instanceof RewardGenerator ? reward.generateReward(pregenArgs) : reward;
}
/**
* Dynamically generate a {@linkcode RewardOption} from a given ID.
* @param specs - The {@linkcode RewardGeneratorSpecs} used to generate the reward
* @param cost - The monetary cost of selecting the option; default `0`
* @param tierOverride - An optional {@linkcode RarityTier} to override the option's rarity
* @param upgradeCount - The number of tier upgrades having occurred; default `0`
* @returns The generated {@linkcode RewardOption}, or `null` if no reward could be generated
* @todo Remove `null` from signature eventually
*/
export function generateRewardOptionFromId<T extends RewardGeneratorId>(
specs: RewardGeneratorSpecs<T>,
cost?: number,
tierOverride?: RarityTier,
upgradeCount?: number,
): RewardOption | null;
/**
* Dynamically generate a {@linkcode RewardOption} from a given ID.
* @param id - The {@linkcode GeneratorRewardId} to generate a reward for
* @param cost - The monetary cost of selecting the option; default `0`
* @param tierOverride - An optional {@linkcode RarityTier} to override the option's rarity
* @param upgradeCount - The number of tier upgrades having occurred; default `0`
* @param pregenArgs - Optional arguments used to seed the generator.
* @returns The generated {@linkcode RewardOption}, or `null` if no reward could be generated
*/
export function generateRewardOptionFromId<T extends RewardGeneratorId>(
id: T,
cost?: number,
tierOverride?: RarityTier,
upgradeCount?: number,
pregenArgs?: RewardGeneratorArgMap[T],
): RewardOption | null;
export function generateRewardOptionFromId(
id: RewardPoolId,
id: Exclude<RewardPoolId, RewardGeneratorId>,
cost?: number,
tierOverride?: RarityTier,
upgradeCount?: number,
): RewardOption | null;
export function generateRewardOptionFromId(
id: RewardGeneratorSpecs | RewardPoolId,
cost = 0,
tierOverride?: RarityTier,
upgradeCount = 0,
pregenArgs?: any[],
pregenArgs?: unknown,
): RewardOption | null {
// Destructure specs into objects
if (typeof id === "object") {
({ id, args: pregenArgs } = id);
}
if (isHeldItemId(id)) {
const reward = new HeldItemReward(id);
const tier = tierOverride ?? heldItemRarities[id];
@ -61,6 +117,7 @@ export function generateRewardOptionFromId(
return new RewardOption(reward, upgradeCount, tier, cost);
}
// TODO: This narrows to `any`
const rewardFunc = allRewards[id];
const reward = generateReward(rewardFunc, pregenArgs);
if (reward) {
@ -70,17 +127,6 @@ export function generateRewardOptionFromId(
return null;
}
export function generateRewardOptionFromSpecs(
specs: RewardSpecs,
cost = 0,
overrideTier?: RarityTier,
): RewardOption | null {
if (typeof specs === "number") {
return generateRewardOptionFromId(specs, cost, overrideTier);
}
return generateRewardOptionFromId(specs.id, cost, overrideTier, 0, specs.args);
}
export function getPlayerShopRewardOptionsForWave(waveIndex: number, baseCost: number): RewardOption[] {
if (!(waveIndex % 10)) {
return [];

View File

@ -11,16 +11,16 @@ import { getNatureName, getNatureStatMultiplier } from "#data/nature";
import { getPokeballCatchMultiplier, getPokeballName, MAX_PER_TYPE_POKEBALLS } from "#data/pokeball";
import { pokemonFormChanges, SpeciesFormChangeCondition } from "#data/pokemon-forms";
import { BattlerTagType } from "#enums/battler-tag-type";
import { BerryType } from "#enums/berry-type";
import type { BerryType } from "#enums/berry-type";
import { FormChangeItem } from "#enums/form-change-item";
import { HeldItemId } from "#enums/held-item-id";
import { LearnMoveType } from "#enums/learn-move-type";
import { MoveId } from "#enums/move-id";
import type { MoveId } from "#enums/move-id";
import { Nature } from "#enums/nature";
import type { PokeballType } from "#enums/pokeball";
import { PokemonType } from "#enums/pokemon-type";
import { RewardId } from "#enums/reward-id";
import { RarityTier } from "#enums/reward-tier";
import type { RarityTier } from "#enums/reward-tier";
import { SpeciesFormKey } from "#enums/species-form-key";
import { SpeciesId } from "#enums/species-id";
import type { PermanentStat, TempBattleStat } from "#enums/stat";
@ -32,7 +32,7 @@ import { permanentStatToHeldItem, statBoostItems } from "#items/base-stat-booste
import { berryTypeToHeldItem } from "#items/berry";
import { getNewAttackTypeBoosterHeldItem, getNewBerryHeldItem, getNewVitaminHeldItem } from "#items/held-item-pool";
import { formChangeItemName } from "#items/item-utility";
import { SPECIES_STAT_BOOSTER_ITEMS, type SpeciesStatBoostHeldItem } from "#items/stat-booster";
import type { SpeciesStatBoosterItemId, SpeciesStatBoostHeldItem } from "#items/stat-booster";
import { TrainerItemEffect, tempStatToTrainerItem } from "#items/trainer-item";
import type { PokemonMove } from "#moves/pokemon-move";
import { getVoucherTypeIcon, getVoucherTypeName, type VoucherType } from "#system/voucher";
@ -43,51 +43,58 @@ import { formatMoney, NumberHolder, padInt, randSeedInt, randSeedItem } from "#u
import { getEnumKeys, getEnumValues } from "#utils/enums";
import i18next from "i18next";
/*
The term "Reward" refers to items the player can access in the post-battle screen (although
they may be used in other places of the code as well).
/**
* @module
* The term "Reward" refers to items the player can access in the post-battle screen (although
* they may be used in other places of the code as well).
Examples include (but are not limited to):
- Potions and other healing items
- Held items and trainer items
- Money items such as nugget and ancient relic
* Examples include (but are not limited to):
* - Potions and other healing items
* - Held items and trainer items
* - Money items such as nugget and ancient relic
Rewards have a basic structure with a name, description, and icon. These are used to display
the reward in the reward select screen. All rewards have an .apply() method, which applies the
effect, for example:
- Apply healing to a pokemon
- Assign a held item to a pokemon, or a trainer item to the player
- Add money
* Rewards have a basic structure with a name, description, and icon. These are used to display
* the reward in the reward select screen. All rewards have an .apply() method, which applies the
* effect, for example:
* - Apply healing to a pokemon
* - Assign a held item to a pokemon, or a trainer item to the player
* - Add money
Some rewards, once clicked, simply have their effect---these are Rewards that add money, pokéball,
vouchers, or global effect such as Sacred Ash.
Most rewards require extra parameters. They are divided into subclasses depending on the parameters
that they need, in particular:
- PokemonReward requires to pass a Pokemon (to apply healing, assign item...)
- PokemonMoveReward requires to pass a Pokemon and a move (for Elixir, or PP Up)
Plus some edge cases for Memory Mushroom and DNA Splicers.
* Some rewards, once clicked, simply have their effect---these are Rewards that add money, pokéball,
* vouchers, or global effect such as Sacred Ash.
* Most rewards require extra parameters. They are divided into subclasses depending on the parameters
* that they need, in particular:
* - PokemonReward requires to pass a Pokemon (to apply healing, assign item...)
* - PokemonMoveReward requires to pass a Pokemon and a move (for Elixir, or PP Up)
* Plus some edge cases for Memory Mushroom and DNA Splicers.
The parameters to be passed are generated by the .applyReward() function in SelectRewardPhase.
This function takes care of opening the party screen and letting the player select a party pokemon,
a move, etc. depending on what is required. Once the parameters are generated, instead of calling
.apply() directly, we call the .applyReward() method in BattleScene, which also plays the sound.
[This method could perhaps be removed].
* The parameters to be passed are generated by the .applyReward() function in {@linkcode SelectRewardPhase}.
* This function takes care of opening the party screen and letting the player select a party pokemon,
* a move, etc. depending on what is required. Once the parameters are generated, instead of calling
* .apply() directly, we call the .applyReward() method in BattleScene, which also plays the sound.
* [This method could perhaps be removed].
Rewards are assigned RewardId, and there are also RewardCategoryId. For example, TM is a RewardCategoryId,
while CommonTM, RareTM etc are RewardIds. There is _not_ a RewardId for _each_ move. Similarly,
some specific categories of held items are assigned their own RewardId, but they all fall under a single
RewardCategoryId.
* Rewards are assigned RewardId, and there are also RewardCategoryId. For example, TM is a RewardCategoryId,
* while CommonTM, RareTM etc are RewardIds. There is _not_ a RewardId for _each_ move. Similarly,
* some specific categories of held items are assigned their own RewardId, but they all fall under a single
* RewardCategoryId.
rewardInitObj plays a similar role to allHeldItems, except instead of containing all possible reward
instances, it instead contains functions that generate those rewards. Here, the keys used are strings
rather than RewardId, the difference exists because here we want to distinguish unique held items
for example. The entries of rewardInitObj are used in the RewardPool.
* rewardInitObj plays a similar role to allHeldItems, except instead of containing all possible reward
* instances, it instead contains functions that generate those rewards. Here, the keys used are strings
* rather than RewardId, the difference exists because here we want to distinguish unique held items
* for example. The entries of rewardInitObj are used in the RewardPool.
There are some more derived classes, in particular:
RewardGenerator, which creates Reward instances from a certain group (e.g. TMs, nature mints, or berries);
and RewardOption, which is displayed during the select reward phase at the end of each encounter.
* There are some more derived classes, in particular:
* RewardGenerator, which creates Reward instances from a certain group (e.g. TMs, nature mints, or berries);
* and RewardOption, which is displayed during the select reward phase at the end of each encounter.
*/
/**
* Type helper to exactly match objects and nothing else.
* @todo merge with `Exact` later on
*/
type MatchExact<T> = T extends object ? Exact<T> : T;
export abstract class Reward {
// TODO: If all we care about for categorization is the reward's ID's _category_, why not do it there?
// TODO: Make abstract and readonly
@ -121,8 +128,8 @@ export abstract class Reward {
/**
* Check whether this reward should be applied.
*/
// TODO: This is erroring on stuff of typ
shouldApply(_params: Exact<Parameters<this["apply"]>[0]>): boolean {
// TODO: This is erroring on stuff with `undefined`
shouldApply(_params: MatchExact<Parameters<this["apply"]>[0]>): boolean {
return true;
}
@ -131,25 +138,18 @@ export abstract class Reward {
abstract apply(_params?: unknown): void;
}
// TODO: Can this return null?
// TODO: Make this generic based on T
type RewardGeneratorFunc<T extends Reward> = (party: Pokemon[], pregenArgs?: any[]) => T | null;
export abstract class RewardGenerator<T extends Reward = Reward> {
private genRewardFunc: RewardGeneratorFunc<T>;
public id: RewardId;
constructor(genRewardFunc: RewardGeneratorFunc<T>) {
this.genRewardFunc = genRewardFunc;
}
generateReward(party: Pokemon[], pregenArgs?: any[]) {
const ret = this.genRewardFunc(party, pregenArgs);
if (ret && this.id) {
ret.id = this.id;
}
return ret;
}
/**
* A {@linkcode RewardGenerator} represents a dynamic generator for a given type of reward.
* These can be customized by lieu of {@linkcode generateReward} to alter the generation result.
*/
export abstract class RewardGenerator {
/**
* Dynamically generate a new reward.
* @param pregenArgs - An optional argument taken by super classes to customize the reward generated.
* @returns The generated reward, or `null` if none are able to be produced
*/
// TODO: Remove null from signature in favor of adding a condition or similar (reduces bangs needed)
abstract generateReward(pregenArgs?: unknown): Reward | null;
}
export class AddPokeballReward extends Reward {
@ -440,6 +440,7 @@ export class ChangeTeraTypeReward extends PokemonReward {
});
}
// TODO: What is this for?
getPregenArgs(): any[] {
return [this.teraType];
}
@ -467,6 +468,9 @@ export class ChangeTeraTypeReward extends PokemonReward {
}
}
// todo: denest
// TODO: Consider removing `revive` from the signature of PokemonHealPhase in the wake of this
// (was only used for revives)
function restorePokemonHp(
pokemon: Pokemon,
percentToRestore: number,
@ -858,59 +862,55 @@ export class RememberMoveReward extends PokemonReward {
}
export class BerryRewardGenerator extends RewardGenerator {
constructor() {
super((_party: Pokemon[], pregenArgs?: any[]) => {
if (pregenArgs && pregenArgs.length === 1 && pregenArgs[0] in BerryType) {
const item = berryTypeToHeldItem[pregenArgs[0] as BerryType];
return new HeldItemReward(item);
}
const item = getNewBerryHeldItem();
override generateReward(pregenArgs?: BerryType): HeldItemReward {
if (pregenArgs !== undefined) {
const item = berryTypeToHeldItem[pregenArgs];
return new HeldItemReward(item);
});
this.id = RewardId.BERRY;
}
const item = getNewBerryHeldItem();
return new HeldItemReward(item);
}
}
export class MintRewardGenerator extends RewardGenerator {
constructor() {
super((_party: Pokemon[], pregenArgs?: any[]) => {
if (pregenArgs && pregenArgs.length === 1 && pregenArgs[0] in Nature) {
return new PokemonNatureChangeReward(pregenArgs[0] as Nature);
}
return new PokemonNatureChangeReward(randSeedItem(getEnumValues(Nature)));
});
this.id = RewardId.MINT;
override generateReward(pregenArgs?: Nature) {
if (pregenArgs !== undefined) {
return new PokemonNatureChangeReward(pregenArgs);
}
return new PokemonNatureChangeReward(randSeedItem(getEnumValues(Nature)));
}
}
export class TeraTypeRewardGenerator extends RewardGenerator {
constructor() {
super((party: Pokemon[], pregenArgs?: any[]) => {
if (pregenArgs && pregenArgs.length === 1 && pregenArgs[0] in PokemonType) {
return new ChangeTeraTypeReward(pregenArgs[0] as PokemonType);
override generateReward(pregenArgs?: PokemonType) {
if (pregenArgs !== undefined) {
return new ChangeTeraTypeReward(pregenArgs[0]);
}
if (!globalScene.trainerItems.hasItem(TrainerItemId.TERA_ORB)) {
return null;
}
const shardType = this.getTeraType();
return new ChangeTeraTypeReward(shardType);
}
private getTeraType(): PokemonType {
// If all party members have a given Tera Type, omit it from the pool
const excludedType = globalScene.getPlayerParty().reduce((prevType, p) => {
if (
// Ignore Pokemon with fixed Tera Types
p.hasSpecies(SpeciesId.TERAPAGOS) ||
p.hasSpecies(SpeciesId.OGERPON) ||
p.hasSpecies(SpeciesId.SHEDINJA)
) {
return prevType;
}
if (!globalScene.trainerItems.hasItem(TrainerItemId.TERA_ORB)) {
return null;
}
const teraTypes: PokemonType[] = [];
for (const p of party) {
if (
!(p.hasSpecies(SpeciesId.TERAPAGOS) || p.hasSpecies(SpeciesId.OGERPON) || p.hasSpecies(SpeciesId.SHEDINJA))
) {
teraTypes.push(p.teraType);
}
}
let excludedType = PokemonType.UNKNOWN;
if (teraTypes.length > 0 && teraTypes.filter(t => t === teraTypes[0]).length === teraTypes.length) {
excludedType = teraTypes[0];
}
let shardType = randSeedInt(64) ? (randSeedInt(18) as PokemonType) : PokemonType.STELLAR;
while (shardType === excludedType) {
shardType = randSeedInt(64) ? (randSeedInt(18) as PokemonType) : PokemonType.STELLAR;
}
return new ChangeTeraTypeReward(shardType);
});
this.id = RewardId.TERA_SHARD;
return prevType === p.teraType ? prevType : PokemonType.UNKNOWN;
}, PokemonType.UNKNOWN);
const validTypes = getEnumValues(PokemonType).filter(t => t !== excludedType);
// 1/64 chance for tera stellar
return randSeedInt(64) ? randSeedItem(validTypes) : PokemonType.STELLAR;
}
}
@ -1220,29 +1220,23 @@ export class FusePokemonReward extends PokemonReward {
}
export class AttackTypeBoosterRewardGenerator extends RewardGenerator {
constructor() {
super((party: Pokemon[], pregenArgs?: any[]) => {
if (pregenArgs && pregenArgs.length === 1 && pregenArgs[0] in PokemonType) {
return new AttackTypeBoosterReward(pregenArgs[0] as PokemonType, TYPE_BOOST_ITEM_BOOST_PERCENT);
}
override generateReward(pregenArgs?: PokemonType) {
if (pregenArgs !== undefined) {
return new AttackTypeBoosterReward(pregenArgs, TYPE_BOOST_ITEM_BOOST_PERCENT);
}
const item = getNewAttackTypeBoosterHeldItem(party);
const item = getNewAttackTypeBoosterHeldItem(globalScene.getPlayerParty());
return item ? new HeldItemReward(item) : null;
});
this.id = RewardId.ATTACK_TYPE_BOOSTER;
return item ? new HeldItemReward(item) : null;
}
}
export class BaseStatBoosterRewardGenerator extends RewardGenerator {
constructor() {
super((_party: Pokemon[], pregenArgs?: any[]) => {
if (pregenArgs) {
return new BaseStatBoosterReward(pregenArgs[0]);
}
return new HeldItemReward(getNewVitaminHeldItem());
});
this.id = RewardId.BASE_STAT_BOOSTER;
override generateReward(pregenArgs?: PermanentStat) {
if (pregenArgs !== undefined) {
return new BaseStatBoosterReward(pregenArgs);
}
return new HeldItemReward(getNewVitaminHeldItem());
}
}
@ -1256,15 +1250,8 @@ export class TempStatStageBoosterRewardGenerator extends RewardGenerator {
[Stat.ACC]: "x_accuracy",
};
constructor() {
super((_party: Pokemon[], pregenArgs?: any[]) => {
if (pregenArgs && pregenArgs.length === 1 && TEMP_BATTLE_STATS.includes(pregenArgs[0])) {
return new LapsingTrainerItemReward(tempStatToTrainerItem[pregenArgs[0]]);
}
const randStat: TempBattleStat = randSeedInt(Stat.ACC, Stat.ATK);
return new LapsingTrainerItemReward(tempStatToTrainerItem[randStat]);
});
this.id = RewardId.TEMP_STAT_STAGE_BOOSTER;
override generateReward(pregenArgs?: TempBattleStat) {
return new LapsingTrainerItemReward(tempStatToTrainerItem[pregenArgs ?? randSeedItem(TEMP_BATTLE_STATS)]);
}
}
@ -1277,232 +1264,243 @@ export class TempStatStageBoosterRewardGenerator extends RewardGenerator {
export class SpeciesStatBoosterRewardGenerator extends RewardGenerator {
/** Object comprised of the currently available species-based stat boosting held items */
private rare: boolean;
constructor(rare: boolean) {
super((party: Pokemon[], pregenArgs?: any[]) => {
if (pregenArgs && pregenArgs.length === 1 && pregenArgs[0] in SPECIES_STAT_BOOSTER_ITEMS) {
return new HeldItemReward(pregenArgs[0] as HeldItemId);
}
super();
this.rare = rare;
}
override generateReward(pregenArgs?: SpeciesStatBoosterItemId) {
if (pregenArgs !== undefined) {
return new HeldItemReward(pregenArgs);
}
// Get a pool of items based on the rarity.
const tierItems = rare
? [HeldItemId.LIGHT_BALL, HeldItemId.THICK_CLUB, HeldItemId.METAL_POWDER, HeldItemId.QUICK_POWDER]
: [HeldItemId.DEEP_SEA_SCALE, HeldItemId.DEEP_SEA_TOOTH];
// Get a pool of items based on the rarity.
const tierItems = this.rare
? [HeldItemId.LIGHT_BALL, HeldItemId.THICK_CLUB, HeldItemId.METAL_POWDER, HeldItemId.QUICK_POWDER]
: [HeldItemId.DEEP_SEA_SCALE, HeldItemId.DEEP_SEA_TOOTH];
const weights = new Array(tierItems.length).fill(0);
const weights = new Array(tierItems.length).fill(0);
for (const p of party) {
const speciesId = p.getSpeciesForm(true).speciesId;
const fusionSpeciesId = p.isFusion() ? p.getFusionSpeciesForm(true).speciesId : null;
// TODO: Use commented boolean when Fling is implemented
const hasFling = false; /* p.getMoveset(true).some(m => m.moveId === MoveId.FLING) */
for (const p of globalScene.getPlayerParty()) {
const speciesId = p.getSpeciesForm(true).speciesId;
const fusionSpeciesId = p.isFusion() ? p.getFusionSpeciesForm(true).speciesId : null;
// TODO: Use commented boolean when Fling is implemented
const hasFling = false; /* p.getMoveset(true).some(m => m.moveId === MoveId.FLING) */
for (const i in tierItems) {
const checkedSpecies = (allHeldItems[tierItems[i]] as SpeciesStatBoostHeldItem).species;
for (const i in tierItems) {
const checkedSpecies = (allHeldItems[tierItems[i]] as SpeciesStatBoostHeldItem).species;
// If party member already has the item being weighted currently, skip to the next item
const hasItem = p.heldItemManager.hasItem(tierItems[i]);
// If party member already has the item being weighted currently, skip to the next item
const hasItem = p.heldItemManager.hasItem(tierItems[i]);
if (!hasItem) {
if (checkedSpecies.includes(speciesId) || (!!fusionSpeciesId && checkedSpecies.includes(fusionSpeciesId))) {
// Add weight if party member has a matching species or, if applicable, a matching fusion species
weights[i]++;
} else if (checkedSpecies.includes(SpeciesId.PIKACHU) && hasFling) {
// Add weight to Light Ball if party member has Fling
weights[i]++;
}
if (!hasItem) {
if (checkedSpecies.includes(speciesId) || (!!fusionSpeciesId && checkedSpecies.includes(fusionSpeciesId))) {
// Add weight if party member has a matching species or, if applicable, a matching fusion species
weights[i]++;
} else if (checkedSpecies.includes(SpeciesId.PIKACHU) && hasFling) {
// Add weight to Light Ball if party member has Fling
weights[i]++;
}
}
}
}
// TODO: Replace this with a helper function
let totalWeight = 0;
for (const weight of weights) {
totalWeight += weight;
}
// TODO: Replace this with a helper function
let totalWeight = 0;
for (const weight of weights) {
totalWeight += weight;
}
if (totalWeight !== 0) {
const randInt = randSeedInt(totalWeight, 1);
let weight = 0;
if (totalWeight !== 0) {
const randInt = randSeedInt(totalWeight, 1);
let weight = 0;
for (const i in weights) {
if (weights[i] !== 0) {
const curWeight = weight + weights[i];
if (randInt <= weight + weights[i]) {
return new HeldItemReward(tierItems[i]);
}
weight = curWeight;
for (const i in weights) {
if (weights[i] !== 0) {
const curWeight = weight + weights[i];
if (randInt <= weight + weights[i]) {
return new HeldItemReward(tierItems[i]);
}
weight = curWeight;
}
}
}
return null;
});
this.id = rare ? RewardId.SPECIES_STAT_BOOSTER : RewardId.RARE_SPECIES_STAT_BOOSTER;
return null;
}
}
export class TmRewardGenerator extends RewardGenerator {
private tier: RarityTier;
constructor(tier: RarityTier) {
super((party: Pokemon[], pregenArgs?: any[]) => {
if (pregenArgs && pregenArgs.length === 1 && pregenArgs[0] in MoveId) {
return new TmReward(pregenArgs[0] as MoveId);
}
const partyMemberCompatibleTms = party.map(p => {
const previousLevelMoves = p.getLearnableLevelMoves();
return (p as PlayerPokemon).compatibleTms.filter(
tm => !p.moveset.find(m => m.moveId === tm) && !previousLevelMoves.find(lm => lm === tm),
);
});
const tierUniqueCompatibleTms = partyMemberCompatibleTms
.flat()
.filter(tm => tmPoolTiers[tm] === tier)
.filter(tm => !allMoves[tm].name.endsWith(" (N)"))
.filter((tm, i, array) => array.indexOf(tm) === i);
if (!tierUniqueCompatibleTms.length) {
return null;
}
// TODO: should this use `randSeedItem`?
const randTmIndex = randSeedInt(tierUniqueCompatibleTms.length);
return new TmReward(tierUniqueCompatibleTms[randTmIndex]);
super();
this.tier = tier;
}
override generateReward(pregenArgs?: MoveId) {
if (pregenArgs !== undefined) {
return new TmReward(pregenArgs);
}
const party = globalScene.getPlayerParty();
const partyMemberCompatibleTms = party.map(p => {
const previousLevelMoves = p.getLearnableLevelMoves();
return (p as PlayerPokemon).compatibleTms.filter(
tm => !p.moveset.find(m => m.moveId === tm) && !previousLevelMoves.find(lm => lm === tm),
);
});
this.id =
tier === RarityTier.COMMON
? RewardId.TM_COMMON
: tier === RarityTier.GREAT
? RewardId.TM_GREAT
: RewardId.TM_ULTRA;
const tierUniqueCompatibleTms = partyMemberCompatibleTms
.flat()
.filter(tm => tmPoolTiers[tm] === this.tier)
.filter(tm => !allMoves[tm].name.endsWith(" (N)"))
.filter((tm, i, array) => array.indexOf(tm) === i);
if (!tierUniqueCompatibleTms.length) {
return null;
}
const randTmIndex = randSeedItem(tierUniqueCompatibleTms);
return new TmReward(randTmIndex);
}
}
export class EvolutionItemRewardGenerator extends RewardGenerator {
constructor(rare: boolean, id: RewardId) {
super((party: Pokemon[], pregenArgs?: any[]) => {
if (pregenArgs && pregenArgs.length === 1 && pregenArgs[0] in EvolutionItem) {
return new EvolutionItemReward(pregenArgs[0] as EvolutionItem);
}
private rare: boolean;
constructor(rare: boolean) {
super();
this.rare = rare;
}
const evolutionItemPool = [
party
.filter(
p =>
pokemonEvolutions.hasOwnProperty(p.species.speciesId) &&
(!p.pauseEvolutions ||
p.species.speciesId === SpeciesId.SLOWPOKE ||
p.species.speciesId === SpeciesId.EEVEE ||
p.species.speciesId === SpeciesId.KIRLIA ||
p.species.speciesId === SpeciesId.SNORUNT),
)
.flatMap(p => {
const evolutions = pokemonEvolutions[p.species.speciesId];
return evolutions.filter(e => e.isValidItemEvolution(p));
}),
party
.filter(
p =>
p.isFusion() &&
p.fusionSpecies &&
pokemonEvolutions.hasOwnProperty(p.fusionSpecies.speciesId) &&
(!p.pauseEvolutions ||
p.fusionSpecies.speciesId === SpeciesId.SLOWPOKE ||
p.fusionSpecies.speciesId === SpeciesId.EEVEE ||
p.fusionSpecies.speciesId === SpeciesId.KIRLIA ||
p.fusionSpecies.speciesId === SpeciesId.SNORUNT),
)
.flatMap(p => {
const evolutions = pokemonEvolutions[p.fusionSpecies!.speciesId];
return evolutions.filter(e => e.isValidItemEvolution(p, true));
}),
]
.flat()
.flatMap(e => e.evoItem)
.filter(i => !!i && i > 50 === rare);
override generateReward(pregenArgs?: EvolutionItem) {
if (pregenArgs !== undefined) {
return new EvolutionItemReward(pregenArgs);
}
if (!evolutionItemPool.length) {
return null;
}
const party = globalScene.getPlayerParty();
// TODO: should this use `randSeedItem`?
return new EvolutionItemReward(evolutionItemPool[randSeedInt(evolutionItemPool.length)]!); // TODO: is the bang correct?
});
this.id = id;
const evolutionItemPool = [
party
.filter(
p =>
pokemonEvolutions.hasOwnProperty(p.species.speciesId) &&
(!p.pauseEvolutions ||
p.species.speciesId === SpeciesId.SLOWPOKE ||
p.species.speciesId === SpeciesId.EEVEE ||
p.species.speciesId === SpeciesId.KIRLIA ||
p.species.speciesId === SpeciesId.SNORUNT),
)
.flatMap(p => {
const evolutions = pokemonEvolutions[p.species.speciesId];
return evolutions.filter(e => e.isValidItemEvolution(p));
}),
party
.filter(
p =>
p.isFusion() &&
p.fusionSpecies &&
pokemonEvolutions.hasOwnProperty(p.fusionSpecies.speciesId) &&
(!p.pauseEvolutions ||
p.fusionSpecies.speciesId === SpeciesId.SLOWPOKE ||
p.fusionSpecies.speciesId === SpeciesId.EEVEE ||
p.fusionSpecies.speciesId === SpeciesId.KIRLIA ||
p.fusionSpecies.speciesId === SpeciesId.SNORUNT),
)
.flatMap(p => {
const evolutions = pokemonEvolutions[p.fusionSpecies!.speciesId];
return evolutions.filter(e => e.isValidItemEvolution(p, true));
}),
]
.flat()
.flatMap(e => e.evoItem)
.filter(i => !!i && i > 50 === this.rare);
if (!evolutionItemPool.length) {
return null;
}
return new EvolutionItemReward(randSeedItem(evolutionItemPool));
}
}
export class FormChangeItemRewardGenerator extends RewardGenerator {
constructor(isRareFormChangeItem: boolean, id: RewardId) {
super((party: Pokemon[], pregenArgs?: any[]) => {
if (pregenArgs && pregenArgs.length === 1 && pregenArgs[0] in FormChangeItem) {
return new FormChangeItemReward(pregenArgs[0] as FormChangeItem);
}
private isRareFormChangeItem: boolean;
const formChangeItemPool = [
...new Set(
party
.filter(p => pokemonFormChanges.hasOwnProperty(p.species.speciesId))
.flatMap(p => {
const formChanges = pokemonFormChanges[p.species.speciesId];
let formChangeItemTriggers = formChanges
.filter(
fc =>
((fc.formKey.indexOf(SpeciesFormKey.MEGA) === -1 &&
fc.formKey.indexOf(SpeciesFormKey.PRIMAL) === -1) ||
globalScene.trainerItems.hasItem(TrainerItemId.MEGA_BRACELET)) &&
((fc.formKey.indexOf(SpeciesFormKey.GIGANTAMAX) === -1 &&
fc.formKey.indexOf(SpeciesFormKey.ETERNAMAX) === -1) ||
globalScene.trainerItems.hasItem(TrainerItemId.DYNAMAX_BAND)) &&
(!fc.conditions.length ||
fc.conditions.filter(cond => cond instanceof SpeciesFormChangeCondition && cond.predicate(p))
.length) &&
fc.preFormKey === p.getFormKey(),
)
.map(fc => fc.findTrigger(SpeciesFormChangeItemTrigger) as SpeciesFormChangeItemTrigger)
.filter(t => t?.active && !p.heldItemManager.hasFormChangeItem(t.item));
constructor(isRareFormChangeItem: boolean) {
super();
this.isRareFormChangeItem = isRareFormChangeItem;
}
if (p.species.speciesId === SpeciesId.NECROZMA) {
// technically we could use a simplified version and check for formChanges.length > 3, but in case any code changes later, this might break...
let foundULTRA_Z = false,
foundN_LUNA = false,
foundN_SOLAR = false;
formChangeItemTriggers.forEach((fc, _i) => {
console.log("Checking ", fc.item);
switch (fc.item) {
case FormChangeItem.ULTRANECROZIUM_Z:
foundULTRA_Z = true;
break;
case FormChangeItem.N_LUNARIZER:
foundN_LUNA = true;
break;
case FormChangeItem.N_SOLARIZER:
foundN_SOLAR = true;
break;
}
});
if (foundULTRA_Z && foundN_LUNA && foundN_SOLAR) {
// all three items are present -> user hasn't acquired any of the N_*ARIZERs -> block ULTRANECROZIUM_Z acquisition.
formChangeItemTriggers = formChangeItemTriggers.filter(
fc => fc.item !== FormChangeItem.ULTRANECROZIUM_Z,
);
} else {
console.log("DID NOT FIND ");
override generateReward(pregenArgs?: FormChangeItem) {
if (pregenArgs !== undefined) {
return new FormChangeItemReward(pregenArgs);
}
const party = globalScene.getPlayerParty();
// TODO: REFACTOR THIS FUCKERY PLEASE
const formChangeItemPool = [
...new Set(
party
.filter(p => pokemonFormChanges.hasOwnProperty(p.species.speciesId))
.flatMap(p => {
const formChanges = pokemonFormChanges[p.species.speciesId];
let formChangeItemTriggers = formChanges
.filter(
fc =>
((fc.formKey.indexOf(SpeciesFormKey.MEGA) === -1 &&
fc.formKey.indexOf(SpeciesFormKey.PRIMAL) === -1) ||
globalScene.trainerItems.hasItem(TrainerItemId.MEGA_BRACELET)) &&
((fc.formKey.indexOf(SpeciesFormKey.GIGANTAMAX) === -1 &&
fc.formKey.indexOf(SpeciesFormKey.ETERNAMAX) === -1) ||
globalScene.trainerItems.hasItem(TrainerItemId.DYNAMAX_BAND)) &&
(!fc.conditions.length ||
fc.conditions.filter(cond => cond instanceof SpeciesFormChangeCondition && cond.predicate(p))
.length) &&
fc.preFormKey === p.getFormKey(),
)
.map(fc => fc.findTrigger(SpeciesFormChangeItemTrigger) as SpeciesFormChangeItemTrigger)
.filter(t => t?.active && !p.heldItemManager.hasFormChangeItem(t.item));
if (p.species.speciesId === SpeciesId.NECROZMA) {
// technically we could use a simplified version and check for formChanges.length > 3, but in case any code changes later, this might break...
let foundULTRA_Z = false,
foundN_LUNA = false,
foundN_SOLAR = false;
formChangeItemTriggers.forEach((fc, _i) => {
console.log("Checking ", fc.item);
switch (fc.item) {
case FormChangeItem.ULTRANECROZIUM_Z:
foundULTRA_Z = true;
break;
case FormChangeItem.N_LUNARIZER:
foundN_LUNA = true;
break;
case FormChangeItem.N_SOLARIZER:
foundN_SOLAR = true;
break;
}
});
if (foundULTRA_Z && foundN_LUNA && foundN_SOLAR) {
// all three items are present -> user hasn't acquired any of the N_*ARIZERs -> block ULTRANECROZIUM_Z acquisition.
formChangeItemTriggers = formChangeItemTriggers.filter(
fc => fc.item !== FormChangeItem.ULTRANECROZIUM_Z,
);
} else {
console.log("DID NOT FIND ");
}
return formChangeItemTriggers;
}),
),
]
.flat()
.flatMap(fc => fc.item)
.filter(i => (i && i < 100) === isRareFormChangeItem);
// convert it into a set to remove duplicate values, which can appear when the same species with a potential form change is in the party.
}
return formChangeItemTriggers;
}),
),
]
.flat()
.flatMap(fc => fc.item)
.filter(i => (i && i < 100) === this.isRareFormChangeItem);
// convert it into a set to remove duplicate values, which can appear when the same species with a potential form change is in the party.
if (!formChangeItemPool.length) {
return null;
}
if (!formChangeItemPool.length) {
return null;
}
// TODO: should this use `randSeedItem`?
return new FormChangeItemReward(formChangeItemPool[randSeedInt(formChangeItemPool.length)]);
});
this.id = id;
return new FormChangeItemReward(randSeedItem(formChangeItemPool));
}
}

View File

@ -1,5 +1,5 @@
import { globalScene } from "#app/global-scene";
import type { Reward } from "#items/reward";
import { type Reward, RewardGenerator } from "#items/reward";
import { BattlePhase } from "#phases/battle-phase";
import type { RewardFunc } from "#types/rewards";
import i18next from "i18next";
@ -13,7 +13,8 @@ export class RewardPhase extends BattlePhase {
constructor(rewardFunc: RewardFunc) {
super();
this.reward = rewardFunc();
const reward = rewardFunc();
this.reward = reward instanceof RewardGenerator ? reward.generateReward() : reward;
}
start() {

View File

@ -125,7 +125,9 @@ export function randItem<T>(items: T[]): T {
return items.length === 1 ? items[0] : items[randInt(items.length)];
}
export function randSeedItem<T>(items: T[]): T {
export function randSeedItem<T>(items: T[] | readonly T[]): T {
// TODO: Resolve this later
// @ts-expect-error - phaser is dumb af
return items.length === 1 ? items[0] : Phaser.Math.RND.pick(items);
}