Create Nuzlocke-related challenges

- "No Free Heal": Disables the automatic healing
that occurs after every 10th wave,
replacing it with a normal shop phase

- "Hardcore": Fainted Pokémon can't be revived

- "Limited Catch": Only the first wild Pokémon encounter
of every biome can be added to your current party
This commit is contained in:
NightKev 2024-09-18 02:26:30 -07:00
parent 414e0a5447
commit df73978bb4
16 changed files with 924 additions and 411 deletions

File diff suppressed because it is too large Load Diff

View File

@ -5,4 +5,7 @@ export enum Challenges {
LOWER_STARTER_POINTS, LOWER_STARTER_POINTS,
FRESH_START, FRESH_START,
INVERSE_BATTLE, INVERSE_BATTLE,
NO_AUTO_HEAL,
HARDCORE,
LIMITED_CATCH,
} }

View File

@ -2,6 +2,7 @@ import BattleScene from "#app/battle-scene";
import { EvolutionItem, pokemonEvolutions } from "#app/data/balance/pokemon-evolutions"; import { EvolutionItem, pokemonEvolutions } from "#app/data/balance/pokemon-evolutions";
import { tmPoolTiers, tmSpecies } from "#app/data/balance/tms"; import { tmPoolTiers, tmSpecies } from "#app/data/balance/tms";
import { getBerryEffectDescription, getBerryName } from "#app/data/berry"; import { getBerryEffectDescription, getBerryName } from "#app/data/berry";
import { applyChallenges, ChallengeType } from "#app/data/challenge";
import { allMoves, AttackMove, selfStatLowerMoves } from "#app/data/move"; import { allMoves, AttackMove, selfStatLowerMoves } from "#app/data/move";
import { getNatureName, getNatureStatMultiplier, Nature } from "#app/data/nature"; import { getNatureName, getNatureStatMultiplier, Nature } from "#app/data/nature";
import { getPokeballCatchMultiplier, getPokeballName, MAX_PER_TYPE_POKEBALLS, PokeballType } from "#app/data/pokeball"; import { getPokeballCatchMultiplier, getPokeballName, MAX_PER_TYPE_POKEBALLS, PokeballType } from "#app/data/pokeball";
@ -9,6 +10,7 @@ import { FormChangeItem, pokemonFormChanges, SpeciesFormChangeCondition, Species
import { getStatusEffectDescriptor, StatusEffect } from "#app/data/status-effect"; import { getStatusEffectDescriptor, StatusEffect } from "#app/data/status-effect";
import { Type } from "#app/data/type"; import { Type } from "#app/data/type";
import Pokemon, { EnemyPokemon, PlayerPokemon, PokemonMove } from "#app/field/pokemon"; import Pokemon, { EnemyPokemon, PlayerPokemon, PokemonMove } from "#app/field/pokemon";
import { GameMode } from "#app/game-mode";
import { getPokemonNameWithAffix } from "#app/messages"; import { getPokemonNameWithAffix } from "#app/messages";
import { import {
AddPokeballModifier, AddVoucherModifier, AttackTypeBoosterModifier, BaseStatModifier, BerryModifier, BoostBugSpawnModifier, BypassSpeedChanceModifier, ContactHeldItemTransferChanceModifier, CritBoosterModifier, DamageMoneyRewardModifier, DoubleBattleChanceBoosterModifier, EnemyAttackStatusEffectChanceModifier, EnemyDamageBoosterModifier, EnemyDamageReducerModifier, EnemyEndureChanceModifier, EnemyFusionChanceModifier, EnemyStatusEffectHealChanceModifier, EnemyTurnHealModifier, EvolutionItemModifier, EvolutionStatBoosterModifier, EvoTrackerModifier, ExpBalanceModifier, ExpBoosterModifier, ExpShareModifier, ExtraModifierModifier, FlinchChanceModifier, FusePokemonModifier, GigantamaxAccessModifier, HealingBoosterModifier, HealShopCostModifier, HiddenAbilityRateBoosterModifier, HitHealModifier, IvScannerModifier, LevelIncrementBoosterModifier, LockModifierTiersModifier, MapModifier, MegaEvolutionAccessModifier, MoneyInterestModifier, MoneyMultiplierModifier, MoneyRewardModifier, MultipleParticipantExpBonusModifier, PokemonAllMovePpRestoreModifier, PokemonBaseStatFlatModifier, PokemonBaseStatTotalModifier, PokemonExpBoosterModifier, PokemonFormChangeItemModifier, PokemonFriendshipBoosterModifier, PokemonHeldItemModifier, PokemonHpRestoreModifier, PokemonIncrementingStatModifier, PokemonInstantReviveModifier, PokemonLevelIncrementModifier, PokemonMoveAccuracyBoosterModifier, PokemonMultiHitModifier, PokemonNatureChangeModifier, PokemonNatureWeightModifier, PokemonPpRestoreModifier, PokemonPpUpModifier, PokemonStatusHealModifier, PreserveBerryModifier, RememberMoveModifier, ResetNegativeStatStageModifier, ShinyRateBoosterModifier, SpeciesCritBoosterModifier, SpeciesStatBoosterModifier, SurviveDamageModifier, SwitchEffectTransferModifier, TempCritBoosterModifier, TempStatStageBoosterModifier, TerastallizeAccessModifier, TerastallizeModifier, TmModifier, TurnHealModifier, TurnHeldItemTransferModifier, TurnStatusEffectModifier, type EnemyPersistentModifier, type Modifier, type PersistentModifier, TempExtraModifierModifier AddPokeballModifier, AddVoucherModifier, AttackTypeBoosterModifier, BaseStatModifier, BerryModifier, BoostBugSpawnModifier, BypassSpeedChanceModifier, ContactHeldItemTransferChanceModifier, CritBoosterModifier, DamageMoneyRewardModifier, DoubleBattleChanceBoosterModifier, EnemyAttackStatusEffectChanceModifier, EnemyDamageBoosterModifier, EnemyDamageReducerModifier, EnemyEndureChanceModifier, EnemyFusionChanceModifier, EnemyStatusEffectHealChanceModifier, EnemyTurnHealModifier, EvolutionItemModifier, EvolutionStatBoosterModifier, EvoTrackerModifier, ExpBalanceModifier, ExpBoosterModifier, ExpShareModifier, ExtraModifierModifier, FlinchChanceModifier, FusePokemonModifier, GigantamaxAccessModifier, HealingBoosterModifier, HealShopCostModifier, HiddenAbilityRateBoosterModifier, HitHealModifier, IvScannerModifier, LevelIncrementBoosterModifier, LockModifierTiersModifier, MapModifier, MegaEvolutionAccessModifier, MoneyInterestModifier, MoneyMultiplierModifier, MoneyRewardModifier, MultipleParticipantExpBonusModifier, PokemonAllMovePpRestoreModifier, PokemonBaseStatFlatModifier, PokemonBaseStatTotalModifier, PokemonExpBoosterModifier, PokemonFormChangeItemModifier, PokemonFriendshipBoosterModifier, PokemonHeldItemModifier, PokemonHpRestoreModifier, PokemonIncrementingStatModifier, PokemonInstantReviveModifier, PokemonLevelIncrementModifier, PokemonMoveAccuracyBoosterModifier, PokemonMultiHitModifier, PokemonNatureChangeModifier, PokemonNatureWeightModifier, PokemonPpRestoreModifier, PokemonPpUpModifier, PokemonStatusHealModifier, PreserveBerryModifier, RememberMoveModifier, ResetNegativeStatStageModifier, ShinyRateBoosterModifier, SpeciesCritBoosterModifier, SpeciesStatBoosterModifier, SurviveDamageModifier, SwitchEffectTransferModifier, TempCritBoosterModifier, TempStatStageBoosterModifier, TerastallizeAccessModifier, TerastallizeModifier, TmModifier, TurnHealModifier, TurnHeldItemTransferModifier, TurnStatusEffectModifier, type EnemyPersistentModifier, type Modifier, type PersistentModifier, TempExtraModifierModifier
@ -19,7 +21,7 @@ import { Unlockables } from "#app/system/unlockables";
import { getVoucherTypeIcon, getVoucherTypeName, VoucherType } from "#app/system/voucher"; import { getVoucherTypeIcon, getVoucherTypeName, VoucherType } from "#app/system/voucher";
import PartyUiHandler, { PokemonMoveSelectFilter, PokemonSelectFilter } from "#app/ui/party-ui-handler"; import PartyUiHandler, { PokemonMoveSelectFilter, PokemonSelectFilter } from "#app/ui/party-ui-handler";
import { getModifierTierTextTint } from "#app/ui/text"; import { getModifierTierTextTint } from "#app/ui/text";
import { formatMoney, getEnumKeys, getEnumValues, IntegerHolder, NumberHolder, padInt, randSeedInt, randSeedItem } from "#app/utils"; import { BooleanHolder, formatMoney, getEnumKeys, getEnumValues, IntegerHolder, NumberHolder, padInt, randSeedInt, randSeedItem } from "#app/utils";
import { Abilities } from "#enums/abilities"; import { Abilities } from "#enums/abilities";
import { BattlerTagType } from "#enums/battler-tag-type"; import { BattlerTagType } from "#enums/battler-tag-type";
import { BerryType } from "#enums/berry-type"; import { BerryType } from "#enums/berry-type";
@ -144,7 +146,7 @@ export class ModifierType {
type ModifierTypeGeneratorFunc = (party: Pokemon[], pregenArgs?: any[]) => ModifierType | null; type ModifierTypeGeneratorFunc = (party: Pokemon[], pregenArgs?: any[]) => ModifierType | null;
export class ModifierTypeGenerator extends ModifierType { export class ModifierTypeGenerator extends ModifierType {
private genTypeFunc: ModifierTypeGeneratorFunc; private genTypeFunc: ModifierTypeGeneratorFunc;
constructor(genTypeFunc: ModifierTypeGeneratorFunc) { constructor(genTypeFunc: ModifierTypeGeneratorFunc) {
super(null, null, null); super(null, null, null);
@ -873,7 +875,7 @@ export class FormChangeItemModifierType extends PokemonModifierType implements G
if (pokemonFormChanges.hasOwnProperty(pokemon.species.speciesId) if (pokemonFormChanges.hasOwnProperty(pokemon.species.speciesId)
// Get all form changes for this species with an item trigger, including any compound triggers // Get all form changes for this species with an item trigger, including any compound triggers
&& pokemonFormChanges[pokemon.species.speciesId].filter(fc => fc.trigger.hasTriggerType(SpeciesFormChangeItemTrigger) && (fc.preFormKey === pokemon.getFormKey())) && pokemonFormChanges[pokemon.species.speciesId].filter(fc => fc.trigger.hasTriggerType(SpeciesFormChangeItemTrigger) && (fc.preFormKey === pokemon.getFormKey()))
// Returns true if any form changes match this item // Returns true if any form changes match this item
.map(fc => fc.findTrigger(SpeciesFormChangeItemTrigger) as SpeciesFormChangeItemTrigger) .map(fc => fc.findTrigger(SpeciesFormChangeItemTrigger) as SpeciesFormChangeItemTrigger)
.flat().flatMap(fc => fc.item).includes(this.formChangeItem) .flat().flatMap(fc => fc.item).includes(this.formChangeItem)
) { ) {
@ -1272,7 +1274,7 @@ type WeightedModifierTypeWeightFunc = (party: Pokemon[], rerollCount?: integer)
*/ */
function skipInClassicAfterWave(wave: integer, defaultWeight: integer): WeightedModifierTypeWeightFunc { function skipInClassicAfterWave(wave: integer, defaultWeight: integer): WeightedModifierTypeWeightFunc {
return (party: Pokemon[]) => { return (party: Pokemon[]) => {
const gameMode = party[0].scene.gameMode; const gameMode = party[0].scene.gameMode;
const currentWave = party[0].scene.currentBattle.waveIndex; const currentWave = party[0].scene.currentBattle.waveIndex;
return gameMode.isClassic && currentWave >= wave ? 0 : defaultWeight; return gameMode.isClassic && currentWave >= wave ? 0 : defaultWeight;
}; };
@ -1284,7 +1286,7 @@ function skipInClassicAfterWave(wave: integer, defaultWeight: integer): Weighted
* @param defaultWeight ModifierType default weight * @param defaultWeight ModifierType default weight
* @returns A WeightedModifierTypeWeightFunc * @returns A WeightedModifierTypeWeightFunc
*/ */
function skipInLastClassicWaveOrDefault(defaultWeight: integer) : WeightedModifierTypeWeightFunc { function skipInLastClassicWaveOrDefault(defaultWeight: number): WeightedModifierTypeWeightFunc {
return skipInClassicAfterWave(199, defaultWeight); return skipInClassicAfterWave(199, defaultWeight);
} }
@ -1302,7 +1304,7 @@ function lureWeightFunc(maxBattles: number, weight: number): WeightedModifierTyp
}; };
} }
class WeightedModifierType { export class WeightedModifierType {
public modifierType: ModifierType; public modifierType: ModifierType;
public weight: integer | WeightedModifierTypeWeightFunc; public weight: integer | WeightedModifierTypeWeightFunc;
public maxWeight: integer; public maxWeight: integer;
@ -1598,7 +1600,7 @@ export const modifierTypes = {
MYSTERY_ENCOUNTER_GOLDEN_BUG_NET: () => new ModifierType("modifierType:ModifierType.MYSTERY_ENCOUNTER_GOLDEN_BUG_NET", "golden_net", (type, _args) => new BoostBugSpawnModifier(type)), MYSTERY_ENCOUNTER_GOLDEN_BUG_NET: () => new ModifierType("modifierType:ModifierType.MYSTERY_ENCOUNTER_GOLDEN_BUG_NET", "golden_net", (type, _args) => new BoostBugSpawnModifier(type)),
}; };
interface ModifierPool { export interface ModifierPool {
[tier: string]: WeightedModifierType[] [tier: string]: WeightedModifierType[]
} }
@ -1910,10 +1912,10 @@ const enemyBuffModifierPool: ModifierPool = {
].map(m => { ].map(m => {
m.setTier(ModifierTier.ULTRA); return m; m.setTier(ModifierTier.ULTRA); return m;
}), }),
[ModifierTier.ROGUE]: [ ].map((m: WeightedModifierType) => { [ModifierTier.ROGUE]: [].map((m: WeightedModifierType) => {
m.setTier(ModifierTier.ROGUE); return m; m.setTier(ModifierTier.ROGUE); return m;
}), }),
[ModifierTier.MASTER]: [ ].map((m: WeightedModifierType) => { [ModifierTier.MASTER]: [].map((m: WeightedModifierType) => {
m.setTier(ModifierTier.MASTER); return m; m.setTier(ModifierTier.MASTER); return m;
}) })
}; };
@ -2018,7 +2020,7 @@ export function regenerateModifierPoolThresholds(party: Pokemon[], poolType: Mod
let tierMaxWeight = 0; let tierMaxWeight = 0;
let i = 0; let i = 0;
pool[t].reduce((total: integer, modifierType: WeightedModifierType) => { pool[t].reduce((total: integer, modifierType: WeightedModifierType) => {
const weightedModifierType = modifierType as WeightedModifierType; const weightedModifierType = modifierType;
const existingModifiers = party[0].scene.findModifiers(m => m.type.id === weightedModifierType.modifierType.id, poolType === ModifierPoolType.PLAYER); const existingModifiers = party[0].scene.findModifiers(m => m.type.id === weightedModifierType.modifierType.id, poolType === ModifierPoolType.PLAYER);
const itemModifierType = weightedModifierType.modifierType instanceof ModifierTypeGenerator const itemModifierType = weightedModifierType.modifierType instanceof ModifierTypeGenerator
? weightedModifierType.modifierType.generateType(party) ? weightedModifierType.modifierType.generateType(party)
@ -2028,8 +2030,8 @@ export function regenerateModifierPoolThresholds(party: Pokemon[], poolType: Mod
|| itemModifierType instanceof FormChangeItemModifierType || itemModifierType instanceof FormChangeItemModifierType
|| existingModifiers.find(m => m.stackCount < m.getMaxStackCount(party[0].scene, true)) || existingModifiers.find(m => m.stackCount < m.getMaxStackCount(party[0].scene, true))
? weightedModifierType.weight instanceof Function ? weightedModifierType.weight instanceof Function
? (weightedModifierType.weight as Function)(party, rerollCount) ? weightedModifierType.weight(party, rerollCount)
: weightedModifierType.weight as integer : weightedModifierType.weight
: 0; : 0;
if (weightedModifierType.maxWeight) { if (weightedModifierType.maxWeight) {
const modifierId = weightedModifierType.modifierType.id; const modifierId = weightedModifierType.modifierType.id;
@ -2174,12 +2176,23 @@ export function getPlayerModifierTypeOptions(count: integer, party: PlayerPokemo
* @param tier If specified will generate item of tier * @param tier If specified will generate item of tier
* @param allowLuckUpgrades `true` to allow items to upgrade tiers (the little animation that plays and is affected by luck) * @param allowLuckUpgrades `true` to allow items to upgrade tiers (the little animation that plays and is affected by luck)
*/ */
function getModifierTypeOptionWithRetry(existingOptions: ModifierTypeOption[], retryCount: integer, party: PlayerPokemon[], tier?: ModifierTier, allowLuckUpgrades?: boolean): ModifierTypeOption { function getModifierTypeOptionWithRetry(existingOptions: ModifierTypeOption[], retryCount: number, party: PlayerPokemon[], tier?: ModifierTier, allowLuckUpgrades?: boolean): ModifierTypeOption {
allowLuckUpgrades = allowLuckUpgrades ?? true; allowLuckUpgrades = allowLuckUpgrades ?? true;
let candidate = getNewModifierTypeOption(party, ModifierPoolType.PLAYER, tier, undefined, 0, allowLuckUpgrades); let candidate = getNewModifierTypeOption(party, ModifierPoolType.PLAYER, tier, undefined, 0, allowLuckUpgrades);
let r = 0; let r = 0;
while (existingOptions.length && ++r < retryCount && existingOptions.filter(o => o.type.name === candidate?.type.name || o.type.group === candidate?.type.group).length) { let isValidForChallenge = new BooleanHolder(true);
applyChallenges(party[0].scene.gameMode, ChallengeType.RANDOM_ITEM_BLACKLIST, candidate, isValidForChallenge);
while (
(
existingOptions.length
&& ++r < retryCount
&& existingOptions.filter(o => o.type.name === candidate?.type.name || o.type.group === candidate?.type.group).length
)
|| !isValidForChallenge.value
) {
candidate = getNewModifierTypeOption(party, ModifierPoolType.PLAYER, candidate?.type.tier ?? tier, candidate?.upgradeCount, 0, allowLuckUpgrades); candidate = getNewModifierTypeOption(party, ModifierPoolType.PLAYER, candidate?.type.tier ?? tier, candidate?.upgradeCount, 0, allowLuckUpgrades);
isValidForChallenge = new BooleanHolder(true);
applyChallenges(party[0].scene.gameMode, ChallengeType.RANDOM_ITEM_BLACKLIST, candidate, isValidForChallenge);
} }
return candidate!; return candidate!;
} }
@ -2209,7 +2222,7 @@ export function overridePlayerModifierTypeOptions(options: ModifierTypeOption[],
} }
} }
export function getPlayerShopModifierTypeOptionsForWave(waveIndex: integer, baseCost: integer): ModifierTypeOption[] { export function getPlayerShopModifierTypeOptionsForWave(waveIndex: number, baseCost: number, gameMode: GameMode): ModifierTypeOption[] {
if (!(waveIndex % 10)) { if (!(waveIndex % 10)) {
return []; return [];
} }
@ -2244,7 +2257,11 @@ export function getPlayerShopModifierTypeOptionsForWave(waveIndex: integer, base
new ModifierTypeOption(modifierTypes.SACRED_ASH(), 0, baseCost * 10) new ModifierTypeOption(modifierTypes.SACRED_ASH(), 0, baseCost * 10)
] ]
]; ];
return options.slice(0, Math.ceil(Math.max(waveIndex + 10, 0) / 30)).flat(); return options.slice(0, Math.ceil(Math.max(waveIndex + 10, 0) / 30)).flat().filter(item => {
const isValidForChallenge = new BooleanHolder(true);
applyChallenges(gameMode, ChallengeType.SHOP_ITEM_BLACKLIST, item, isValidForChallenge);
return isValidForChallenge.value;
});
} }
export function getEnemyBuffModifierForWave(tier: ModifierTier, enemyModifiers: PersistentModifier[], scene: BattleScene): EnemyPersistentModifier { export function getEnemyBuffModifierForWave(tier: ModifierTier, enemyModifiers: PersistentModifier[], scene: BattleScene): EnemyPersistentModifier {
@ -2319,8 +2336,8 @@ export function getDailyRunStarterModifiers(party: PlayerPokemon[]): PokemonHeld
* @param retryCount Max allowed tries before the next tier down is checked for a valid ModifierType * @param retryCount Max allowed tries before the next tier down is checked for a valid ModifierType
* @param allowLuckUpgrades Default true. If false, will not allow ModifierType to randomly upgrade to next tier * @param allowLuckUpgrades Default true. If false, will not allow ModifierType to randomly upgrade to next tier
*/ */
function getNewModifierTypeOption(party: Pokemon[], poolType: ModifierPoolType, tier?: ModifierTier, upgradeCount?: integer, retryCount: integer = 0, allowLuckUpgrades: boolean = true): ModifierTypeOption | null { function getNewModifierTypeOption(party: Pokemon[], poolType: ModifierPoolType, tier?: ModifierTier, upgradeCount?: number, retryCount: number = 0, allowLuckUpgrades: boolean = true): ModifierTypeOption | null {
const player = !poolType; const player = poolType === ModifierPoolType.PLAYER;
const pool = getModifierPoolForType(poolType); const pool = getModifierPoolForType(poolType);
let thresholds: object; let thresholds: object;
switch (poolType) { switch (poolType) {
@ -2370,7 +2387,7 @@ function getNewModifierTypeOption(party: Pokemon[], poolType: ModifierPoolType,
} }
tier += upgradeCount; tier += upgradeCount;
while (tier && (!modifierPool.hasOwnProperty(tier) || !modifierPool[tier].length)) { while (tier && (!pool.hasOwnProperty(tier) || !pool[tier].length)) {
tier--; tier--;
if (upgradeCount) { if (upgradeCount) {
upgradeCount--; upgradeCount--;
@ -2381,7 +2398,7 @@ function getNewModifierTypeOption(party: Pokemon[], poolType: ModifierPoolType,
if (tier < ModifierTier.MASTER && allowLuckUpgrades) { if (tier < ModifierTier.MASTER && allowLuckUpgrades) {
const partyShinyCount = party.filter(p => p.isShiny() && !p.isFainted()).length; const partyShinyCount = party.filter(p => p.isShiny() && !p.isFainted()).length;
const upgradeOdds = Math.floor(32 / ((partyShinyCount + 2) / 2)); const upgradeOdds = Math.floor(32 / ((partyShinyCount + 2) / 2));
while (modifierPool.hasOwnProperty(tier + upgradeCount + 1) && modifierPool[tier + upgradeCount + 1].length) { while (pool.hasOwnProperty(tier + upgradeCount + 1) && pool[tier + upgradeCount + 1].length) {
if (!randSeedInt(upgradeOdds)) { if (!randSeedInt(upgradeOdds)) {
upgradeCount++; upgradeCount++;
} else { } else {

View File

@ -1,21 +1,23 @@
import BattleScene from "#app/battle-scene";
import { BattlerIndex } from "#app/battle"; import { BattlerIndex } from "#app/battle";
import { getPokeballCatchMultiplier, getPokeballAtlasKey, getPokeballTintColor, doPokeballBounceAnim } from "#app/data/pokeball"; import BattleScene from "#app/battle-scene";
import { SubstituteTag } from "#app/data/battler-tags";
import { ChallengeType, applyChallenges } from "#app/data/challenge";
import { doPokeballBounceAnim, getPokeballAtlasKey, getPokeballCatchMultiplier, getPokeballTintColor } from "#app/data/pokeball";
import { getStatusEffectCatchRateMultiplier } from "#app/data/status-effect"; import { getStatusEffectCatchRateMultiplier } from "#app/data/status-effect";
import { PokeballType } from "#app/enums/pokeball"; import { PokeballType } from "#app/enums/pokeball";
import { StatusEffect } from "#app/enums/status-effect"; import { StatusEffect } from "#app/enums/status-effect";
import { addPokeballOpenParticles, addPokeballCaptureStars } from "#app/field/anims"; import { addPokeballCaptureStars, addPokeballOpenParticles } from "#app/field/anims";
import { EnemyPokemon } from "#app/field/pokemon"; import { EnemyPokemon } from "#app/field/pokemon";
import { getPokemonNameWithAffix } from "#app/messages"; import { getPokemonNameWithAffix } from "#app/messages";
import { PokemonHeldItemModifier } from "#app/modifier/modifier"; import { PokemonHeldItemModifier } from "#app/modifier/modifier";
import { PokemonPhase } from "#app/phases/pokemon-phase";
import { VictoryPhase } from "#app/phases/victory-phase";
import { achvs } from "#app/system/achv"; import { achvs } from "#app/system/achv";
import { PartyUiMode, PartyOption } from "#app/ui/party-ui-handler"; import { PartyOption, PartyUiMode } from "#app/ui/party-ui-handler";
import { SummaryUiMode } from "#app/ui/summary-ui-handler"; import { SummaryUiMode } from "#app/ui/summary-ui-handler";
import { Mode } from "#app/ui/ui"; import { Mode } from "#app/ui/ui";
import { BooleanHolder } from "#app/utils";
import i18next from "i18next"; import i18next from "i18next";
import { PokemonPhase } from "./pokemon-phase";
import { VictoryPhase } from "./victory-phase";
import { SubstituteTag } from "#app/data/battler-tags";
export class AttemptCapturePhase extends PokemonPhase { export class AttemptCapturePhase extends PokemonPhase {
private pokeballType: PokeballType; private pokeballType: PokeballType;
@ -249,6 +251,13 @@ export class AttemptCapturePhase extends PokemonPhase {
}); });
}; };
Promise.all([ pokemon.hideInfo(), this.scene.gameData.setPokemonCaught(pokemon) ]).then(() => { Promise.all([ pokemon.hideInfo(), this.scene.gameData.setPokemonCaught(pokemon) ]).then(() => {
const challengeCanAddToParty = new BooleanHolder(true);
applyChallenges(this.scene.gameMode, ChallengeType.ADD_POKEMON_TO_PARTY, pokemon, this.scene.currentBattle.waveIndex, challengeCanAddToParty);
if (!challengeCanAddToParty.value) {
removePokemon();
end();
return;
}
if (this.scene.getParty().length === 6) { if (this.scene.getParty().length === 6) {
const promptRelease = () => { const promptRelease = () => {
this.scene.ui.showText(i18next.t("battle:partyFull", { pokemonName: pokemon.getNameToRender() }), null, () => { this.scene.ui.showText(i18next.t("battle:partyFull", { pokemonName: pokemon.getNameToRender() }), null, () => {

View File

@ -1,22 +1,23 @@
import { BattleType, TurnCommand } from "#app/battle";
import BattleScene from "#app/battle-scene"; import BattleScene from "#app/battle-scene";
import { TurnCommand, BattleType } from "#app/battle";
import { TrappedTag, EncoreTag } from "#app/data/battler-tags";
import { MoveTargetSet, getMoveTargets } from "#app/data/move";
import { speciesStarterCosts } from "#app/data/balance/starters"; import { speciesStarterCosts } from "#app/data/balance/starters";
import { Abilities } from "#app/enums/abilities"; import { EncoreTag, TrappedTag } from "#app/data/battler-tags";
import { BattlerTagType } from "#app/enums/battler-tag-type"; import { applyChallenges, ChallengeType } from "#app/data/challenge";
import { Biome } from "#app/enums/biome"; import { getMoveTargets, MoveTargetSet } from "#app/data/move";
import { Moves } from "#app/enums/moves";
import { PokeballType } from "#app/enums/pokeball"; import { PokeballType } from "#app/enums/pokeball";
import { FieldPosition, PlayerPokemon } from "#app/field/pokemon"; import { FieldPosition, PlayerPokemon } from "#app/field/pokemon";
import { getPokemonNameWithAffix } from "#app/messages"; import { getPokemonNameWithAffix } from "#app/messages";
import { FieldPhase } from "#app/phases/field-phase";
import { SelectTargetPhase } from "#app/phases/select-target-phase";
import { Command } from "#app/ui/command-ui-handler"; import { Command } from "#app/ui/command-ui-handler";
import { Mode } from "#app/ui/ui"; import { Mode } from "#app/ui/ui";
import i18next from "i18next"; import { BooleanHolder, isNullOrUndefined } from "#app/utils";
import { FieldPhase } from "./field-phase"; import { Abilities } from "#enums/abilities";
import { SelectTargetPhase } from "./select-target-phase"; import { BattlerTagType } from "#enums/battler-tag-type";
import { Biome } from "#enums/biome";
import { Moves } from "#enums/moves";
import { MysteryEncounterMode } from "#enums/mystery-encounter-mode"; import { MysteryEncounterMode } from "#enums/mystery-encounter-mode";
import { isNullOrUndefined } from "#app/utils"; import i18next from "i18next";
export class CommandPhase extends FieldPhase { export class CommandPhase extends FieldPhase {
protected fieldIndex: integer; protected fieldIndex: integer;
@ -94,6 +95,18 @@ export class CommandPhase extends FieldPhase {
switch (command) { switch (command) {
case Command.FIGHT: case Command.FIGHT:
// Check if move can be used in challenge
const isValidForChallenge = new BooleanHolder(true);
applyChallenges(this.scene.gameMode, ChallengeType.MOVE_BLACKLIST, playerPokemon.getMoveset()[cursor]!, isValidForChallenge);
if (!isValidForChallenge.value) {
const moveName = playerPokemon.getMoveset()[cursor]?.getName();
this.scene.ui.setMode(Mode.MESSAGE);
this.scene.ui.showText(i18next.t("challenges:illegalMove", { moveName: moveName }), null, () => {
this.scene.ui.clearText();
this.scene.ui.setMode(Mode.FIGHT, this.fieldIndex);
}, null, true);
break;
}
let useStruggle = false; let useStruggle = false;
if (cursor === -1 || if (cursor === -1 ||
playerPokemon.trySelectMove(cursor, args[0] as boolean) || playerPokemon.trySelectMove(cursor, args[0] as boolean) ||

View File

@ -1,6 +1,7 @@
import BattleScene from "#app/battle-scene"; import BattleScene from "#app/battle-scene";
import * as Utils from "#app/utils"; import { applyChallenges, ChallengeType } from "#app/data/challenge";
import { BattlePhase } from "./battle-phase"; import { BattlePhase } from "#app/phases/battle-phase";
import { BooleanHolder, fixedInt } from "#app/utils";
export class PartyHealPhase extends BattlePhase { export class PartyHealPhase extends BattlePhase {
private resumeBgm: boolean; private resumeBgm: boolean;
@ -14,21 +15,34 @@ export class PartyHealPhase extends BattlePhase {
start() { start() {
super.start(); super.start();
const isHealPhaseActive = new BooleanHolder(true);
applyChallenges(this.scene.gameMode, ChallengeType.NO_HEAL_PHASE, isHealPhaseActive);
if (!isHealPhaseActive.value) {
return this.end();
}
const bgmPlaying = this.scene.isBgmPlaying(); const bgmPlaying = this.scene.isBgmPlaying();
if (bgmPlaying) { if (bgmPlaying) {
this.scene.fadeOutBgm(1000, false); this.scene.fadeOutBgm(1000, false);
} }
const canBeRevived = new BooleanHolder(true);
this.scene.ui.fadeOut(1000).then(() => { this.scene.ui.fadeOut(1000).then(() => {
for (const pokemon of this.scene.getParty()) { for (const pokemon of this.scene.getParty()) {
pokemon.hp = pokemon.getMaxHp(); applyChallenges(this.scene.gameMode, ChallengeType.PREVENT_REVIVE, pokemon, canBeRevived);
pokemon.resetStatus(); if (canBeRevived.value || !pokemon.isFainted()) {
for (const move of pokemon.moveset) { pokemon.hp = pokemon.getMaxHp();
move!.ppUsed = 0; // TODO: is this bang correct? pokemon.resetStatus();
for (const move of pokemon.moveset) {
if (move) {
move.ppUsed = 0;
}
}
pokemon.updateInfo(true);
} }
pokemon.updateInfo(true);
} }
const healSong = this.scene.playSoundWithoutBgm("heal"); const healSong = this.scene.playSoundWithoutBgm("heal");
this.scene.time.delayedCall(Utils.fixedInt(healSong.totalDuration * 1000), () => { this.scene.time.delayedCall(fixedInt(healSong.totalDuration * 1000), () => {
healSong.destroy(); healSong.destroy();
if (this.resumeBgm && bgmPlaying) { if (this.resumeBgm && bgmPlaying) {
this.scene.playBgm(); this.scene.playBgm();

View File

@ -1,16 +1,35 @@
import BattleScene from "#app/battle-scene"; import BattleScene from "#app/battle-scene";
import {
ExtraModifierModifier,
HealShopCostModifier,
Modifier,
PokemonHeldItemModifier,
TempExtraModifierModifier
} from "#app/modifier/modifier";
import { ModifierTier } from "#app/modifier/modifier-tier"; import { ModifierTier } from "#app/modifier/modifier-tier";
import { regenerateModifierPoolThresholds, ModifierTypeOption, ModifierType, getPlayerShopModifierTypeOptionsForWave, PokemonModifierType, FusePokemonModifierType, PokemonMoveModifierType, TmModifierType, RememberMoveModifierType, PokemonPpRestoreModifierType, PokemonPpUpModifierType, ModifierPoolType, getPlayerModifierTypeOptions } from "#app/modifier/modifier-type"; import {
import { ExtraModifierModifier, HealShopCostModifier, Modifier, PokemonHeldItemModifier, TempExtraModifierModifier } from "#app/modifier/modifier"; CustomModifierSettings,
import ModifierSelectUiHandler, { SHOP_OPTIONS_ROW_LIMIT } from "#app/ui/modifier-select-ui-handler"; FusePokemonModifierType,
import PartyUiHandler, { PartyUiMode, PartyOption } from "#app/ui/party-ui-handler"; getPlayerModifierTypeOptions,
import { Mode } from "#app/ui/ui"; getPlayerShopModifierTypeOptionsForWave,
import i18next from "i18next"; ModifierPoolType,
import * as Utils from "#app/utils"; ModifierType,
import { BattlePhase } from "./battle-phase"; ModifierTypeOption,
PokemonModifierType,
PokemonMoveModifierType,
PokemonPpRestoreModifierType,
PokemonPpUpModifierType,
regenerateModifierPoolThresholds,
RememberMoveModifierType,
TmModifierType
} from "#app/modifier/modifier-type";
import Overrides from "#app/overrides"; import Overrides from "#app/overrides";
import { CustomModifierSettings } from "#app/modifier/modifier-type"; import { BattlePhase } from "#app/phases/battle-phase";
import ModifierSelectUiHandler, { SHOP_OPTIONS_ROW_LIMIT } from "#app/ui/modifier-select-ui-handler";
import PartyUiHandler, { PartyOption, PartyUiMode } from "#app/ui/party-ui-handler";
import { Mode } from "#app/ui/ui";
import { isNullOrUndefined, NumberHolder } from "#app/utils"; import { isNullOrUndefined, NumberHolder } from "#app/utils";
import i18next from "i18next";
export class SelectModifierPhase extends BattlePhase { export class SelectModifierPhase extends BattlePhase {
private rerollCount: integer; private rerollCount: integer;
@ -42,7 +61,7 @@ export class SelectModifierPhase extends BattlePhase {
if (!this.isCopy) { if (!this.isCopy) {
regenerateModifierPoolThresholds(party, this.getPoolType(), this.rerollCount); regenerateModifierPoolThresholds(party, this.getPoolType(), this.rerollCount);
} }
const modifierCount = new Utils.IntegerHolder(3); const modifierCount = new NumberHolder(3);
if (this.isPlayer()) { if (this.isPlayer()) {
this.scene.applyModifiers(ExtraModifierModifier, true, modifierCount); this.scene.applyModifiers(ExtraModifierModifier, true, modifierCount);
this.scene.applyModifiers(TempExtraModifierModifier, true, modifierCount); this.scene.applyModifiers(TempExtraModifierModifier, true, modifierCount);
@ -140,7 +159,7 @@ export class SelectModifierPhase extends BattlePhase {
} }
break; break;
default: default:
const shopOptions = getPlayerShopModifierTypeOptionsForWave(this.scene.currentBattle.waveIndex, this.scene.getWaveMoneyAmount(1)); const shopOptions = getPlayerShopModifierTypeOptionsForWave(this.scene.currentBattle.waveIndex, this.scene.getWaveMoneyAmount(1), this.scene.gameMode);
const shopOption = shopOptions[rowCursor > 2 || shopOptions.length <= SHOP_OPTIONS_ROW_LIMIT ? cursor : cursor + SHOP_OPTIONS_ROW_LIMIT]; const shopOption = shopOptions[rowCursor > 2 || shopOptions.length <= SHOP_OPTIONS_ROW_LIMIT ? cursor : cursor + SHOP_OPTIONS_ROW_LIMIT];
if (shopOption.type) { if (shopOption.type) {
modifierType = shopOption.type; modifierType = shopOption.type;

View File

@ -1,16 +1,18 @@
import BattleScene from "#app/battle-scene";
import { BattlerIndex, BattleType, ClassicFixedBossWaves } from "#app/battle"; import { BattlerIndex, BattleType, ClassicFixedBossWaves } from "#app/battle";
import { CustomModifierSettings, modifierTypes } from "#app/modifier/modifier-type"; import BattleScene from "#app/battle-scene";
import { BattleEndPhase } from "./battle-end-phase"; import { applyChallenges, ChallengeType } from "#app/data/challenge";
import { NewBattlePhase } from "./new-battle-phase";
import { PokemonPhase } from "./pokemon-phase";
import { AddEnemyBuffModifierPhase } from "./add-enemy-buff-modifier-phase";
import { EggLapsePhase } from "./egg-lapse-phase";
import { GameOverPhase } from "./game-over-phase";
import { ModifierRewardPhase } from "./modifier-reward-phase";
import { SelectModifierPhase } from "./select-modifier-phase";
import { TrainerVictoryPhase } from "./trainer-victory-phase";
import { handleMysteryEncounterVictory } from "#app/data/mystery-encounters/utils/encounter-phase-utils"; import { handleMysteryEncounterVictory } from "#app/data/mystery-encounters/utils/encounter-phase-utils";
import { CustomModifierSettings, modifierTypes } from "#app/modifier/modifier-type";
import { AddEnemyBuffModifierPhase } from "#app/phases/add-enemy-buff-modifier-phase";
import { BattleEndPhase } from "#app/phases/battle-end-phase";
import { EggLapsePhase } from "#app/phases/egg-lapse-phase";
import { GameOverPhase } from "#app/phases/game-over-phase";
import { ModifierRewardPhase } from "#app/phases/modifier-reward-phase";
import { NewBattlePhase } from "#app/phases/new-battle-phase";
import { PokemonPhase } from "#app/phases/pokemon-phase";
import { SelectModifierPhase } from "#app/phases/select-modifier-phase";
import { TrainerVictoryPhase } from "#app/phases/trainer-victory-phase";
import { BooleanHolder } from "#app/utils";
export class VictoryPhase extends PokemonPhase { export class VictoryPhase extends PokemonPhase {
/** If true, indicates that the phase is intended for EXP purposes only, and not to continue a battle to next phase */ /** If true, indicates that the phase is intended for EXP purposes only, and not to continue a battle to next phase */
@ -40,6 +42,9 @@ export class VictoryPhase extends PokemonPhase {
return this.end(); return this.end();
} }
const isHealPhaseActive = new BooleanHolder(true);
applyChallenges(this.scene.gameMode, ChallengeType.NO_HEAL_PHASE, isHealPhaseActive);
if (!this.scene.getEnemyParty().find(p => this.scene.currentBattle.battleType === BattleType.WILD ? p.isOnField() : !p?.isFainted(true))) { if (!this.scene.getEnemyParty().find(p => this.scene.currentBattle.battleType === BattleType.WILD ? p.isOnField() : !p?.isFainted(true))) {
this.scene.pushPhase(new BattleEndPhase(this.scene)); this.scene.pushPhase(new BattleEndPhase(this.scene));
if (this.scene.currentBattle.battleType === BattleType.TRAINER) { if (this.scene.currentBattle.battleType === BattleType.TRAINER) {
@ -51,7 +56,7 @@ export class VictoryPhase extends PokemonPhase {
// Should get Lock Capsule on 165 before shop phase so it can be used in the rewards shop // Should get Lock Capsule on 165 before shop phase so it can be used in the rewards shop
this.scene.pushPhase(new ModifierRewardPhase(this.scene, modifierTypes.LOCK_CAPSULE)); this.scene.pushPhase(new ModifierRewardPhase(this.scene, modifierTypes.LOCK_CAPSULE));
} }
if (this.scene.currentBattle.waveIndex % 10) { if (this.scene.currentBattle.waveIndex % 10 || (this.scene.currentBattle.waveIndex === 0 && !isHealPhaseActive.value)) {
this.scene.pushPhase(new SelectModifierPhase(this.scene, undefined, undefined, this.getFixedBattleCustomModifiers())); this.scene.pushPhase(new SelectModifierPhase(this.scene, undefined, undefined, this.getFixedBattleCustomModifiers()));
} else if (this.scene.gameMode.isDaily) { } else if (this.scene.gameMode.isDaily) {
this.scene.pushPhase(new ModifierRewardPhase(this.scene, modifierTypes.EXP_CHARM)); this.scene.pushPhase(new ModifierRewardPhase(this.scene, modifierTypes.EXP_CHARM));

View File

@ -0,0 +1,62 @@
import { Abilities } from "#enums/abilities";
import { Challenges } from "#enums/challenges";
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 } from "vitest";
describe("Challenge - Hardcore", () => {
let phaserGame: Phaser.Game;
let game: GameManager;
beforeAll(() => {
phaserGame = new Phaser.Game({
type: Phaser.HEADLESS,
});
});
afterEach(() => {
game.phaseInterceptor.restoreOg();
});
beforeEach(() => {
game = new GameManager(phaserGame);
game.challengeMode.addChallenge(Challenges.HARDCORE);
game.override
.battleType("single")
.ability(Abilities.BALL_FETCH)
.moveset(Moves.THUNDERBOLT)
.startingLevel(2000)
.enemySpecies(Species.MAGIKARP)
.enemyAbility(Abilities.BALL_FETCH)
.enemyMoveset(Moves.SPLASH);
});
it.todo("prevents revival items from showing up in the shop", async () => {
game.override.startingWave(191);
await game.challengeMode.startBattle();
game.move.select(Moves.THUNDERBOLT);
await game.phaseInterceptor.to("SelectModifierPhase");
expect(1);
});
it.todo("prevents revival items from showing up in rewards", async () => {
game.modifiers
.addCheck("REVIVE")
.addCheck("MAX_REVIVE")
.addCheck("REVIVER_SEED");
await game.challengeMode.startBattle();
game.move.select(Moves.THUNDERBOLT);
await game.phaseInterceptor.to("SelectModifierPhase");
game.modifiers
.testCheck("REVIVE", false)
.testCheck("MAX_REVIVE", false)
.testCheck("REVIVER_SEED", false);
});
});

View File

@ -0,0 +1,43 @@
import { CommandPhase } from "#app/phases/command-phase";
import { Command } from "#app/ui/command-ui-handler";
import { Challenges } from "#enums/challenges";
import { Species } from "#enums/species";
import GameManager from "#test/utils/gameManager";
import Phaser from "phaser";
import { afterEach, beforeAll, beforeEach, describe, expect, it } from "vitest";
describe("Challenge - Limited Catch", () => {
let phaserGame: Phaser.Game;
let game: GameManager;
beforeAll(() => {
phaserGame = new Phaser.Game({
type: Phaser.HEADLESS,
});
});
afterEach(() => {
game.phaseInterceptor.restoreOg();
});
beforeEach(() => {
game = new GameManager(phaserGame);
game.challengeMode.addChallenge(Challenges.LIMITED_CATCH);
game.override.battleType("single");
});
it("prevents catching pokemon outside of the first wave of the biome", async () => {
game.override
.startingWave(3)
.pokeballs([ 0, 0, 0, 0, 100 ]);
await game.challengeMode.startBattle([ Species.FEEBAS ]);
const phase = game.scene.getCurrentPhase() as CommandPhase;
phase.handleCommand(Command.BALL, 4);
await game.phaseInterceptor.to("BattleEndPhase");
expect(game.scene.getParty().length).toBe(1);
});
});

View File

@ -0,0 +1,55 @@
import { Abilities } from "#enums/abilities";
import { Challenges } from "#enums/challenges";
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 } from "vitest";
describe("Challenge - No Auto Heal", () => {
let phaserGame: Phaser.Game;
let game: GameManager;
beforeAll(() => {
phaserGame = new Phaser.Game({
type: Phaser.HEADLESS,
});
});
afterEach(() => {
game.phaseInterceptor.restoreOg();
});
beforeEach(() => {
game = new GameManager(phaserGame);
game.challengeMode.addChallenge(Challenges.NO_AUTO_HEAL);
game.override
.battleType("single")
.starterSpecies(Species.FEEBAS)
.ability(Abilities.BALL_FETCH)
.moveset(Moves.THUNDERBOLT)
.enemySpecies(Species.MAGIKARP)
.enemyAbility(Abilities.BALL_FETCH)
.enemyMoveset(Moves.SPLASH);
});
it("prevents PartyHealPhase from healing the player's pokemon", async () => {
game.override
.startingWave(10)
.startingLevel(100);
await game.challengeMode.startBattle();
const player = game.scene.getPlayerField()[0];
player.damageAndUpdate(1);
game.move.select(Moves.THUNDERBOLT);
await game.phaseInterceptor.to("SelectModifierPhase", false);
game.doSelectModifier();
await game.toNextTurn();
expect(player.hp).toBe(player.getMaxHp() - 1);
expect(player.moveset[0]?.ppUsed).toBe(1);
});
});

View File

@ -1,13 +1,11 @@
import { MapModifier } from "#app/modifier/modifier";
import { afterEach, beforeAll, beforeEach, describe, expect, it } from "vitest";
import GameManager from "./utils/gameManager";
import { Moves } from "#app/enums/moves";
import { Biome } from "#app/enums/biome"; import { Biome } from "#app/enums/biome";
import { Mode } from "#app/ui/ui"; import { Moves } from "#app/enums/moves";
import { MapModifier } from "#app/modifier/modifier";
import ModifierSelectUiHandler from "#app/ui/modifier-select-ui-handler"; import ModifierSelectUiHandler from "#app/ui/modifier-select-ui-handler";
import { Mode } from "#app/ui/ui";
import { Species } from "#enums/species"; import { Species } from "#enums/species";
import GameManager from "#test/utils/gameManager";
//const TIMEOUT = 20 * 1000; import { afterEach, beforeAll, beforeEach, describe, expect, it } from "vitest";
describe("Daily Mode", () => { describe("Daily Mode", () => {
let phaserGame: Phaser.Game; let phaserGame: Phaser.Game;

View File

@ -1,16 +1,15 @@
import { BattleStyle } from "#app/enums/battle-style"; import { Challenge, copyChallenge } from "#app/data/challenge";
import { Species } from "#app/enums/species";
import overrides from "#app/overrides"; import overrides from "#app/overrides";
import { CommandPhase } from "#app/phases/command-phase";
import { EncounterPhase } from "#app/phases/encounter-phase"; import { EncounterPhase } from "#app/phases/encounter-phase";
import { SelectStarterPhase } from "#app/phases/select-starter-phase"; import { SelectStarterPhase } from "#app/phases/select-starter-phase";
import { Mode } from "#app/ui/ui";
import { generateStarter } from "../gameManagerUtils";
import { GameManagerHelper } from "./gameManagerHelper";
import { Challenge } from "#app/data/challenge";
import { CommandPhase } from "#app/phases/command-phase";
import { TurnInitPhase } from "#app/phases/turn-init-phase"; import { TurnInitPhase } from "#app/phases/turn-init-phase";
import { Mode } from "#app/ui/ui";
import { BattleStyle } from "#enums/battle-style";
import { Challenges } from "#enums/challenges"; import { Challenges } from "#enums/challenges";
import { copyChallenge } from "data/challenge"; import { Species } from "#enums/species";
import { generateStarter } from "#test/utils/gameManagerUtils";
import { GameManagerHelper } from "#test/utils/helpers/gameManagerHelper";
/** /**
* Helper to handle Challenge mode specifics * Helper to handle Challenge mode specifics
@ -25,7 +24,7 @@ export class ChallengeModeHelper extends GameManagerHelper {
* @param value - The challenge value. * @param value - The challenge value.
* @param severity - The challenge severity. * @param severity - The challenge severity.
*/ */
addChallenge(id: Challenges, value: number, severity: number) { addChallenge(id: Challenges, value: number = 1, severity: number = 1) {
const challenge = copyChallenge({ id, value, severity }); const challenge = copyChallenge({ id, value, severity });
this.challenges.push(challenge); this.challenges.push(challenge);
} }

View File

@ -1,19 +1,20 @@
import { StatusEffect } from "#app/data/status-effect"; import { StatusEffect } from "#app/data/status-effect";
import { Variant } from "#app/data/variant";
import { Weather, WeatherType } from "#app/data/weather"; import { Weather, WeatherType } from "#app/data/weather";
import { Abilities } from "#app/enums/abilities";
import { Biome } from "#app/enums/biome";
import { Moves } from "#app/enums/moves";
import { Species } from "#app/enums/species";
import * as GameMode from "#app/game-mode"; import * as GameMode from "#app/game-mode";
import { GameModes, getGameMode } from "#app/game-mode"; import { GameModes, getGameMode } from "#app/game-mode";
import { ModifierOverride } from "#app/modifier/modifier-type"; import { ModifierOverride } from "#app/modifier/modifier-type";
import Overrides from "#app/overrides"; import Overrides from "#app/overrides";
import { vi } from "vitest";
import { GameManagerHelper } from "./gameManagerHelper";
import { Unlockables } from "#app/system/unlockables"; import { Unlockables } from "#app/system/unlockables";
import { Variant } from "#app/data/variant"; import { Abilities } from "#enums/abilities";
import { MysteryEncounterType } from "#enums/mystery-encounter-type"; import { Biome } from "#enums/biome";
import { Moves } from "#enums/moves";
import { MysteryEncounterTier } from "#enums/mystery-encounter-tier"; import { MysteryEncounterTier } from "#enums/mystery-encounter-tier";
import { MysteryEncounterType } from "#enums/mystery-encounter-type";
import { PokeballType } from "#enums/pokeball";
import { Species } from "#enums/species";
import { GameManagerHelper } from "#test/utils/helpers/gameManagerHelper";
import { vi } from "vitest";
/** /**
* Helper to handle overrides in tests * Helper to handle overrides in tests
@ -27,14 +28,14 @@ export class OverridesHelper extends GameManagerHelper {
/** /**
* Override the starting biome * Override the starting biome
* @warning Any event listeners that are attached to [NewArenaEvent](events\battle-scene.ts) may need to be handled down the line * @warning Any event listeners that are attached to [NewArenaEvent](events\battle-scene.ts) may need to be handled down the line
* @param biome the biome to set * @param biome The {@linkcode Biome} to set
* @returns `this`
*/ */
public startingBiome(biome: Biome): this { public startingBiome(biome: Biome): this {
this.game.scene.newArena(biome); this.game.scene.newArena(biome);
this.log(`Starting biome set to ${Biome[biome]} (=${biome})!`); this.log(`Starting biome set to ${Biome[biome]} (=${biome})!`);
return this; return this;
} }
/** /**
* Override the starting wave (index) * Override the starting wave (index)
* @param wave the wave (index) to set. Classic: `1`-`200` * @param wave the wave (index) to set. Classic: `1`-`200`
@ -47,8 +48,8 @@ export class OverridesHelper extends GameManagerHelper {
} }
/** /**
* Override the player (pokemon) starting level * Override the player pokemon's starting level
* @param level the (pokemon) level to set * @param level the level to set
* @returns `this` * @returns `this`
*/ */
public startingLevel(level: Species | number): this { public startingLevel(level: Species | number): this {
@ -69,8 +70,8 @@ export class OverridesHelper extends GameManagerHelper {
} }
/** /**
* Override the player (pokemon) starting held items * Override the player pokemon's starting held items
* @param items the items to hold * @param items the {@linkcode ModifierOverride | items} to hold
* @returns `this` * @returns `this`
*/ */
public startingHeldItems(items: ModifierOverride[]): this { public startingHeldItems(items: ModifierOverride[]): this {
@ -80,8 +81,35 @@ export class OverridesHelper extends GameManagerHelper {
} }
/** /**
* Override the player (pokemon) {@linkcode Species | species} * Override the player's pokeball inventory
* @param species the (pokemon) {@linkcode Species | species} to set * @param pokeballs Array specifying the amount of each pokeball to set, or `null` if disabling the override
* @param enable Whether to enable or disable the override, default `true`
* @returns `this`
*/
pokeballs(pokeballs: [number, number, number, number, number] | null, enable: boolean = true): this {
if (!pokeballs) {
pokeballs = [ 5, 0, 0, 0, 0 ];
}
pokeballs = pokeballs!;
const pokeballOverride = {
active: enable,
pokeballs: {
[PokeballType.POKEBALL]: pokeballs[0],
[PokeballType.GREAT_BALL]: pokeballs[1],
[PokeballType.ULTRA_BALL]: pokeballs[2],
[PokeballType.ROGUE_BALL]: pokeballs[3],
[PokeballType.MASTER_BALL]: pokeballs[4],
},
};
vi.spyOn(Overrides, "POKEBALL_OVERRIDE", "get").mockReturnValue(pokeballOverride);
this.log(`Pokeball override ${enable ? `set to [${pokeballs}]!` : "disabled!"}`);
return this;
}
/**
* Override the player pokemon's species.
* It's preferred to use `startBattle([Species.PKMN1, Species.PKMN2, ...])` if possible.
* @param species the {@linkcode Species} to set
* @returns `this` * @returns `this`
*/ */
public starterSpecies(species: Species | number): this { public starterSpecies(species: Species | number): this {
@ -112,8 +140,15 @@ export class OverridesHelper extends GameManagerHelper {
} }
/** /**
* Override the player (pokemons) forms * Override the player pokemons' forms
* @param forms the (pokemon) forms to set * @param forms the forms to set
* @example
* ```ts
* game.override.starterForms({
* [Species.KYOGRE]: 1,
* [Species.PIKACHU]: 3,
* });
* ```
* @returns `this` * @returns `this`
*/ */
public starterForms(forms: Partial<Record<Species, number>>): this { public starterForms(forms: Partial<Record<Species, number>>): this {
@ -127,7 +162,7 @@ export class OverridesHelper extends GameManagerHelper {
/** /**
* Override the player's starting modifiers * Override the player's starting modifiers
* @param modifiers the modifiers to set * @param modifiers the {@linkcode ModifierOverride | modifiers} to set
* @returns `this` * @returns `this`
*/ */
public startingModifier(modifiers: ModifierOverride[]): this { public startingModifier(modifiers: ModifierOverride[]): this {
@ -137,8 +172,8 @@ export class OverridesHelper extends GameManagerHelper {
} }
/** /**
* Override the player (pokemon) {@linkcode Abilities | ability} * Override the player pokemon's ability
* @param ability the (pokemon) {@linkcode Abilities | ability} to set * @param ability the {@linkcode Abilities | ability} to set
* @returns `this` * @returns `this`
*/ */
public ability(ability: Abilities): this { public ability(ability: Abilities): this {
@ -148,8 +183,8 @@ export class OverridesHelper extends GameManagerHelper {
} }
/** /**
* Override the player (pokemon) **passive** {@linkcode Abilities | ability} * Override the player pokemon's **passive** ability
* @param passiveAbility the (pokemon) **passive** {@linkcode Abilities | ability} to set * @param passiveAbility the **passive** {@linkcode Abilities | ability} to set
* @returns `this` * @returns `this`
*/ */
public passiveAbility(passiveAbility: Abilities): this { public passiveAbility(passiveAbility: Abilities): this {
@ -159,8 +194,8 @@ export class OverridesHelper extends GameManagerHelper {
} }
/** /**
* Override the player (pokemon) {@linkcode Moves | moves}set * Override the player pokemon's moveset
* @param moveset the {@linkcode Moves | moves}set to set * @param moveset the {@linkcode Moves | moveset} to set
* @returns `this` * @returns `this`
*/ */
public moveset(moveset: Moves | Moves[]): this { public moveset(moveset: Moves | Moves[]): this {
@ -174,9 +209,9 @@ export class OverridesHelper extends GameManagerHelper {
} }
/** /**
* Override the player (pokemon) {@linkcode StatusEffect | status-effect} * Override the player pokemon's status effect
* @param statusEffect the {@linkcode StatusEffect | status-effect} to set * @param statusEffect the {@linkcode StatusEffect | status effect} to set
* @returns * @returns `this`
*/ */
public statusEffect(statusEffect: StatusEffect): this { public statusEffect(statusEffect: StatusEffect): this {
vi.spyOn(Overrides, "STATUS_OVERRIDE", "get").mockReturnValue(statusEffect); vi.spyOn(Overrides, "STATUS_OVERRIDE", "get").mockReturnValue(statusEffect);
@ -210,8 +245,8 @@ export class OverridesHelper extends GameManagerHelper {
} }
/** /**
* Override the {@linkcode WeatherType | weather (type)} * Override the weather
* @param type {@linkcode WeatherType | weather type} to set * @param type The {@linkcode WeatherType} to set
* @returns `this` * @returns `this`
*/ */
public weather(type: WeatherType): this { public weather(type: WeatherType): this {
@ -237,8 +272,8 @@ export class OverridesHelper extends GameManagerHelper {
} }
/** /**
* Override the battle type (single or double) * Override the battle type
* @param battleType battle type to set * @param battleType - The battle type to set ("single" or "double"), `null` to disable
* @returns `this` * @returns `this`
*/ */
public battleType(battleType: "single" | "double" | null): this { public battleType(battleType: "single" | "double" | null): this {
@ -248,8 +283,8 @@ export class OverridesHelper extends GameManagerHelper {
} }
/** /**
* Override the enemy (pokemon) {@linkcode Species | species} * Override the enemy pokemon's species
* @param species the (pokemon) {@linkcode Species | species} to set * @param species the {@linkcode Species} to set
* @returns `this` * @returns `this`
*/ */
public enemySpecies(species: Species | number): this { public enemySpecies(species: Species | number): this {
@ -280,7 +315,7 @@ export class OverridesHelper extends GameManagerHelper {
} }
/** /**
* Override the enemy (pokemon) {@linkcode Abilities | ability} * Override the enemy pokemon's ability
* @param ability the (pokemon) {@linkcode Abilities | ability} to set * @param ability the (pokemon) {@linkcode Abilities | ability} to set
* @returns `this` * @returns `this`
*/ */
@ -291,8 +326,8 @@ export class OverridesHelper extends GameManagerHelper {
} }
/** /**
* Override the enemy (pokemon) **passive** {@linkcode Abilities | ability} * Override the enemy pokemon's **passive** ability
* @param passiveAbility the (pokemon) **passive** {@linkcode Abilities | ability} to set * @param passiveAbility the **passive** {@linkcode Abilities | ability} to set
* @returns `this` * @returns `this`
*/ */
public enemyPassiveAbility(passiveAbility: Abilities): this { public enemyPassiveAbility(passiveAbility: Abilities): this {
@ -302,8 +337,8 @@ export class OverridesHelper extends GameManagerHelper {
} }
/** /**
* Override the enemy (pokemon) {@linkcode Moves | moves}set * Override the enemy pokemon's moveset
* @param moveset the {@linkcode Moves | moves}set to set * @param moveset the {@linkcode Moves | moveset} to set
* @returns `this` * @returns `this`
*/ */
public enemyMoveset(moveset: Moves | Moves[]): this { public enemyMoveset(moveset: Moves | Moves[]): this {
@ -317,7 +352,7 @@ export class OverridesHelper extends GameManagerHelper {
} }
/** /**
* Override the enemy (pokemon) level * Override the enemy pokemon's level
* @param level the level to set * @param level the level to set
* @returns `this` * @returns `this`
*/ */
@ -328,8 +363,8 @@ export class OverridesHelper extends GameManagerHelper {
} }
/** /**
* Override the enemy (pokemon) {@linkcode StatusEffect | status-effect} * Override the enemy pokemon's status effect
* @param statusEffect the {@linkcode StatusEffect | status-effect} to set * @param statusEffect the {@linkcode StatusEffect} to set
* @returns * @returns
*/ */
public enemyStatusEffect(statusEffect: StatusEffect): this { public enemyStatusEffect(statusEffect: StatusEffect): this {
@ -339,8 +374,8 @@ export class OverridesHelper extends GameManagerHelper {
} }
/** /**
* Override the enemy (pokemon) held items * Override the enemy pokemon's held items
* @param items the items to hold * @param items the {@linkcode ModifierOverride | items} to hold
* @returns `this` * @returns `this`
*/ */
public enemyHeldItems(items: ModifierOverride[]): this { public enemyHeldItems(items: ModifierOverride[]): this {
@ -362,7 +397,7 @@ export class OverridesHelper extends GameManagerHelper {
/** /**
* Override the items rolled at the end of a battle * Override the items rolled at the end of a battle
* @param items the items to be rolled * @param items the {@linkcode ModifierOverride | items} to be rolled
* @returns `this` * @returns `this`
*/ */
public itemRewards(items: ModifierOverride[]): this { public itemRewards(items: ModifierOverride[]): this {
@ -420,7 +455,7 @@ export class OverridesHelper extends GameManagerHelper {
} }
/** /**
* Override the enemy (Pokemon) to have the given amount of health segments * Override the enemy Pokemon to have the given amount of health segments
* @param healthSegments the number of segments to give * @param healthSegments the number of segments to give
* - `0` (default): the health segments will be handled like in the game based on wave, level and species * - `0` (default): the health segments will be handled like in the game based on wave, level and species
* - `1`: the Pokemon will not be a boss * - `1`: the Pokemon will not be a boss
@ -462,8 +497,8 @@ export class OverridesHelper extends GameManagerHelper {
} }
/** /**
* Override the encounter chance for a mystery encounter. * Override the encounter tier for a mystery encounter.
* @param tier - The {@linkcode MysteryEncounterTier} to encounter * @param tier What {@linkcode MysteryEncounterTier | tier} of encounter to set
* @returns `this` * @returns `this`
*/ */
public mysteryEncounterTier(tier: MysteryEncounterTier): this { public mysteryEncounterTier(tier: MysteryEncounterTier): this {
@ -474,7 +509,7 @@ export class OverridesHelper extends GameManagerHelper {
/** /**
* Override the encounter that spawns for the scene * Override the encounter that spawns for the scene
* @param encounterType - The {@linkcode MysteryEncounterType} of the encounter * @param encounterType What {@linkcode MysteryEncounterType | type} of encounter to set
* @returns `this` * @returns `this`
*/ */
public mysteryEncounter(encounterType: MysteryEncounterType): this { public mysteryEncounter(encounterType: MysteryEncounterType): this {

View File

@ -1,20 +1,20 @@
import BattleScene from "../battle-scene"; import BattleScene from "#app/battle-scene";
import { getPlayerShopModifierTypeOptionsForWave, ModifierTypeOption, TmModifierType } from "../modifier/modifier-type"; import { applyChallenges, ChallengeType } from "#app/data/challenge";
import { getPokeballAtlasKey, PokeballType } from "../data/pokeball"; import { allMoves } from "#app/data/move";
import { addTextObject, getTextStyleOptions, getModifierTierTextTint, getTextColor, TextStyle } from "./text"; import { getPokeballAtlasKey, PokeballType } from "#app/data/pokeball";
import AwaitableUiHandler from "./awaitable-ui-handler"; import { HealShopCostModifier, LockModifierTiersModifier, PokemonHeldItemModifier } from "#app/modifier/modifier";
import { Mode } from "./ui"; import { getPlayerShopModifierTypeOptionsForWave, ModifierTypeOption, TmModifierType } from "#app/modifier/modifier-type";
import { LockModifierTiersModifier, PokemonHeldItemModifier, HealShopCostModifier } from "../modifier/modifier";
import { handleTutorial, Tutorial } from "../tutorial";
import { Button } from "#enums/buttons";
import MoveInfoOverlay from "./move-info-overlay";
import { allMoves } from "../data/move";
import * as Utils from "./../utils";
import Overrides from "#app/overrides"; import Overrides from "#app/overrides";
import { handleTutorial, Tutorial } from "#app/tutorial";
import { BooleanHolder, formatMoney, NumberHolder } from "#app/utils";
import { Button } from "#enums/buttons";
import { ShopCursorTarget } from "#enums/shop-cursor-target";
import i18next from "i18next"; import i18next from "i18next";
import { ShopCursorTarget } from "#app/enums/shop-cursor-target";
import { IntegerHolder } from "./../utils";
import Phaser from "phaser"; import Phaser from "phaser";
import AwaitableUiHandler from "./awaitable-ui-handler";
import MoveInfoOverlay from "./move-info-overlay";
import { addTextObject, getModifierTierTextTint, getTextColor, getTextStyleOptions, TextStyle } from "./text";
import { Mode } from "./ui";
export const SHOP_OPTIONS_ROW_LIMIT = 7; export const SHOP_OPTIONS_ROW_LIMIT = 7;
const SINGLE_SHOP_ROW_YOFFSET = 12; const SINGLE_SHOP_ROW_YOFFSET = 12;
@ -189,10 +189,16 @@ export default class ModifierSelectUiHandler extends AwaitableUiHandler {
const typeOptions = args[1] as ModifierTypeOption[]; const typeOptions = args[1] as ModifierTypeOption[];
const removeHealShop = this.scene.gameMode.hasNoShop; const removeHealShop = this.scene.gameMode.hasNoShop;
const baseShopCost = new IntegerHolder(this.scene.getWaveMoneyAmount(1)); const baseShopCost = new NumberHolder(this.scene.getWaveMoneyAmount(1));
this.scene.applyModifier(HealShopCostModifier, true, baseShopCost); this.scene.applyModifier(HealShopCostModifier, true, baseShopCost);
const shopTypeOptions = !removeHealShop const shopTypeOptions = !removeHealShop
? getPlayerShopModifierTypeOptionsForWave(this.scene.currentBattle.waveIndex, baseShopCost.value) ? getPlayerShopModifierTypeOptionsForWave(this.scene.currentBattle.waveIndex, baseShopCost.value, this.scene.gameMode).filter(shopItem => {
const isValidForChallenge = new BooleanHolder(true);
applyChallenges(this.scene.gameMode, ChallengeType.SHOP_ITEM_BLACKLIST, shopItem, isValidForChallenge);
return isValidForChallenge.value;
})
: []; : [];
const optionsYOffset = shopTypeOptions.length > SHOP_OPTIONS_ROW_LIMIT ? -SINGLE_SHOP_ROW_YOFFSET : -DOUBLE_SHOP_ROW_YOFFSET; const optionsYOffset = shopTypeOptions.length > SHOP_OPTIONS_ROW_LIMIT ? -SINGLE_SHOP_ROW_YOFFSET : -DOUBLE_SHOP_ROW_YOFFSET;
@ -559,7 +565,7 @@ export default class ModifierSelectUiHandler extends AwaitableUiHandler {
} }
const canReroll = this.scene.money >= this.rerollCost; const canReroll = this.scene.money >= this.rerollCost;
const formattedMoney = Utils.formatMoney(this.scene.moneyFormat, this.rerollCost); const formattedMoney = formatMoney(this.scene.moneyFormat, this.rerollCost);
this.rerollCostText.setText(i18next.t("modifierSelectUiHandler:rerollCost", { formattedMoney })); this.rerollCostText.setText(i18next.t("modifierSelectUiHandler:rerollCost", { formattedMoney }));
this.rerollCostText.setColor(this.getTextColor(canReroll ? TextStyle.MONEY : TextStyle.PARTY_RED)); this.rerollCostText.setColor(this.getTextColor(canReroll ? TextStyle.MONEY : TextStyle.PARTY_RED));
@ -828,7 +834,7 @@ class ModifierOption extends Phaser.GameObjects.Container {
const cost = Overrides.WAIVE_ROLL_FEE_OVERRIDE ? 0 : this.modifierTypeOption.cost; const cost = Overrides.WAIVE_ROLL_FEE_OVERRIDE ? 0 : this.modifierTypeOption.cost;
const textStyle = cost <= scene.money ? TextStyle.MONEY : TextStyle.PARTY_RED; const textStyle = cost <= scene.money ? TextStyle.MONEY : TextStyle.PARTY_RED;
const formattedMoney = Utils.formatMoney(scene.moneyFormat, cost); const formattedMoney = formatMoney(scene.moneyFormat, cost);
this.itemCostText.setText(i18next.t("modifierSelectUiHandler:itemCost", { formattedMoney })); this.itemCostText.setText(i18next.t("modifierSelectUiHandler:itemCost", { formattedMoney }));
this.itemCostText.setColor(getTextColor(textStyle, false, scene.uiTheme)); this.itemCostText.setColor(getTextColor(textStyle, false, scene.uiTheme));