From 4afb9b695d9bcad90c12afd1cd3119779cc7cc42 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Matilde=20Sim=C3=B5es?= Date: Wed, 4 Jun 2025 20:33:03 +0100 Subject: [PATCH 1/5] =?UTF-8?q?Implement=20NuzLocke=20related=20challenges?= =?UTF-8?q?=20and=20AI=20changes=20The=20Nuzlocke=20challenges=20that=20we?= =?UTF-8?q?re=20implemented=20were=20the=20following:=20-=20"No=20Free=20H?= =?UTF-8?q?eal"=20which=20consists=20in=20disabling=20the=20auto=20heal=20?= =?UTF-8?q?that=20occurs=20every=2010th=20wave,=20replacing=20it=20with=20?= =?UTF-8?q?a=20normal=20shop=20phase.=20Changes=20made:=20=20=20=20-=20Add?= =?UTF-8?q?ed=20a=20new=20challenge=20"NO=5FAUTO=5FHEAL"=20with=20a=20chal?= =?UTF-8?q?lenge=20type=20"NO=5FHEAL=5FPHASE"=20and=20created=20a=20new=20?= =?UTF-8?q?function=20"applyNoHealPhase"=20to=20determine=20whether=20the?= =?UTF-8?q?=20Pok=C3=A9mon=20can=20heal=20if=20the=20challenge=20is=20acti?= =?UTF-8?q?ve=20("challenge.ts").=20=20=20=20-=20Added=20a=20confirmation?= =?UTF-8?q?=20on=20the=20healing=20phase=20to=20check=20if=20the=20challen?= =?UTF-8?q?ge=20is=20active=20or=20not,=20and=20if=20it=20is,=20it=20shoul?= =?UTF-8?q?d=20skip=20the=20phase=20("party-heal-phase.ts").=20-=20Changed?= =?UTF-8?q?=20the=20logistic=20of=20when=20the=20shop=20should=20be=20disp?= =?UTF-8?q?layed,=20so=20when=20the=20challenge=20is=20active=20the=20shop?= =?UTF-8?q?=20appears=20every=2010th=20wave=20("modifier-type.ts")=20and?= =?UTF-8?q?=20actually=20push=20the=20shop=20phase=20("victory-phase.ts").?= =?UTF-8?q?=20-=20"Hardcore":=20Challenge=20divided=20into=20two=20modes,?= =?UTF-8?q?=20normal=20and=20hard,=20where=20fainted=20Pok=C3=A9mon=20can'?= =?UTF-8?q?t=20be=20revived,=20in=20addition,=20the=20hard=20mode=20delete?= =?UTF-8?q?s=20the=20fainted=20Pok=C3=A9mon=20so=20the=20player=20can't=20?= =?UTF-8?q?switch=20it's=20items=20after=20death.=20Changes=20made:=20-=20?= =?UTF-8?q?Added=20a=20new=20challenge=20"HARDCORE"=20with=20several=20cha?= =?UTF-8?q?llenge=20types=20with=20the=20correspondent=20apply=20functions?= =?UTF-8?q?=20("challenge.ts"),=20each=20one=20is=20used=20as=20follows:?= =?UTF-8?q?=20=09-=20RANDOM=5FITEM=5FBLACKLIST:=20filter=20the=20reward=20?= =?UTF-8?q?items=20with=20only=20the=20valid=20one's=20("modifier-type.ts"?= =?UTF-8?q?).=20=09-=20SHOP=5FITEM=5FBLACKLIST:=20filter=20the=20shop=20it?= =?UTF-8?q?ems=20with=20only=20the=20valid=20one's=20("modifier-select-ui-?= =?UTF-8?q?handler.ts").=20=09-=20MOVE=5FBLACKLIST:=20checks=20if=20the=20?= =?UTF-8?q?move=20selected=20is=20allowed=20and=20if=20not=20sends=20a=20m?= =?UTF-8?q?essage=20of=20no=20apply=20("pokemon.ts").=20=09-=20DELETE=5FPO?= =?UTF-8?q?KEMON:=20if=20hard=20mode=20was=20selected,=20automatically=20d?= =?UTF-8?q?elete=20the=20fainted=20Pok=C3=A9mon=20from=20the=20party=20("b?= =?UTF-8?q?attle-end-pahse.ts").=20=09-=20SHOULD=5FFUSE:=20changed=20the?= =?UTF-8?q?=20logic=20of=20should=20apply=20function=20to=20prohibit=20the?= =?UTF-8?q?=20fusion=20with=20dead=20Pok=C3=A9mon=20("modifier.ts").=20=09?= =?UTF-8?q?-=20PREVENT=5FREVIVE:=20prevent=20the=20gain=20of=20hp=20of=20f?= =?UTF-8?q?ainted=20Pok=C3=A9mon=20("party-heal-phase.ts").=20-=20"Limited?= =?UTF-8?q?=20Catch":=20Only=20the=20first=20wild=20Pok=C3=A9mon=20encount?= =?UTF-8?q?er=20of=20every=20biome=20can=20be=20added=20to=20the=20player'?= =?UTF-8?q?s=20current=20party.=20Changes=20made:=20-=20Added=20a=20new=20?= =?UTF-8?q?challenge=20LIMITED=5FCATCH=20with=20a=20challenge=20type=20=20?= =?UTF-8?q?ADD=5FPOKEMON=5FTO=5FPARTY=20and=20created=20a=20new=20function?= =?UTF-8?q?=20"applyAddPokemonToParty"=20to=20determine=20whether=20the=20?= =?UTF-8?q?Pok=C3=A9mon=20can=20be=20added=20to=20the=20party,=20which=20s?= =?UTF-8?q?hould=20only=20occur=20every=2011th=20wave=20if=20it=20isn't=20?= =?UTF-8?q?a=20catchable=20mystery=20encounter=20or=20every=2012th=20wave?= =?UTF-8?q?=20if=20the=2011th=20wave=20was=20a=20catchable=20mystery=20enc?= =?UTF-8?q?ounter=20("challenge.ts").=20-=20Changed=20the=20logistic=20of?= =?UTF-8?q?=20adding=20a=20Pok=C3=A9mon=20where=20it=20can=20be=20caught?= =?UTF-8?q?=20so=20that=20the=20"pokedex"=20is=20updated=20but=20the=20Pok?= =?UTF-8?q?=C3=A9mon=20isn't=20added=20to=20the=20party=20of=20the=20playe?= =?UTF-8?q?r=20affecting=20specifically=20mystery=20encounters=20("encount?= =?UTF-8?q?er-pokemon-utils.ts")=20and=20added=20the=20same=20logic=20to?= =?UTF-8?q?=20normal=20encounters.=20("attempt-capture-phase.ts")?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The changes in the game AI were as follows ("pokemon.ts"): - More accurately accounts for the Pokémon's actual moves and their effectiveness against the player instead of only the pokemon type - Introduced logic to decide when a Pokémon should be sacrificed or switched based on its HP and speed. Signed-off-by: Matilde Simões Co-authored-by: Fuad Ali --- src/data/challenge.ts | 314 ++++++++++++++++++ .../encounters/a-trainers-test-encounter.ts | 2 + .../slumbering-snorlax-encounter.ts | 2 + .../the-winstrate-challenge-encounter.ts | 2 + .../utils/encounter-pokemon-utils.ts | 13 + src/enums/challenges.ts | 3 + src/field/pokemon.ts | 35 +- src/modifier/modifier-type.ts | 17 +- src/modifier/modifier.ts | 8 +- src/phases/attempt-capture-phase.ts | 13 + src/phases/battle-end-phase.ts | 12 + src/phases/command-phase.ts | 4 +- src/phases/party-heal-phase.ts | 22 +- src/phases/victory-phase.ts | 9 + src/ui/modifier-select-ui-handler.ts | 15 +- 15 files changed, 453 insertions(+), 18 deletions(-) diff --git a/src/data/challenge.ts b/src/data/challenge.ts index 683fb48a9ba..a82ae3a1432 100644 --- a/src/data/challenge.ts +++ b/src/data/challenge.ts @@ -25,6 +25,8 @@ import { ModifierTier } from "#enums/modifier-tier"; import { globalScene } from "#app/global-scene"; import { pokemonFormChanges } from "./pokemon-forms"; import { pokemonEvolutions } from "./balance/pokemon-evolutions"; +import type { ModifierTypeOption } from "#app/modifier/modifier-type"; +import { MysteryEncounterType } from "#enums/mystery-encounter-type"; import { ChallengeType } from "#enums/challenge-type"; import type { MoveSourceType } from "#enums/move-source-type"; @@ -344,6 +346,84 @@ export abstract class Challenge { applyFlipStat(_pokemon: Pokemon, _baseStats: number[]) { return false; } + + /** + * An apply function for NO_AUTO_HEAL challenges. Derived classes should alter this. + * @param _applyHealPhase {@link BooleanHolder} Whether it should apply the heal phase. + * @returns {@link boolean} if this function did anything. + */ + applyNoHealPhase(_applyHealPhase: BooleanHolder): boolean { + return false; + } + + /** + * An apply function for PREVENT_REVIVE. Derived classes should alter this. + * @param _canBeRevived {@link BooleanHolder} Whether it should revive the fainted Pokemon. + * @returns {@link boolean} if this function did anything. + */ + applyRevivePrevention(_canBeRevived: BooleanHolder): boolean { + return true; + } + + /** + * An apply function for RANDOM_ITEM_BLACKLIST. Derived classes should alter this. + * @param _randomItem {@link ModifierTypeOption} The random item in question. + * @param _isValid {@link BooleanHolder} Whether it should load the random item. + * @returns {@link boolean} if this function did anything. + */ + applyRandomItemBlacklist(_randomItem: ModifierTypeOption | null, _isValid: BooleanHolder): boolean { + return false; + } + + /** + * An apply function for SHOP_ITEM_BLACKLIST. Derived classes should alter this. + * @param _shopItem {@link ModifierTypeOption} The shop item in question. + * @param _isValid {@link BooleanHolder} Whether the shop should have the item. + * @returns {@link boolean} if this function did anything. + */ + applyShopItemBlacklist(_shopItem: ModifierTypeOption | null, _isValid: BooleanHolder): boolean { + return false; + } + + /** + * An apply function for MOVE_BLACKLIST. Derived classes should alter this. + * @param _move {@link PokemonMove} The move in question. + * @param _isValid {@link BooleanHolder} Whether the move should be allowed. + * @returns {@link boolean} if this function did anything. + */ + applyMoveBlacklist(_move: PokemonMove, _isValid: BooleanHolder): boolean { + return false; + } + + /** + * An apply function for DELETE_POKEMON. Derived classes should alter this. + * @param _canStay {@link BooleanHolder} Whether the pokemon can stay in team after death. + * @returns {@link boolean} if this function did anything. + */ + applyDeletePokemon(_canStay: BooleanHolder): boolean { + return false; + } + + /** + * An apply function for ADD_POKEMON_TO_PARTY. Derived classes should alter this. + * @param _waveIndex {@link BooleanHolder} The current wave. + * @param _canAddToParty {@link BooleanHolder} Whether the pokemon can be caught. + * @returns {@link boolean} if this function did anything. + */ + applyAddPokemonToParty(_waveIndex: number, _canAddToParty: BooleanHolder): boolean { + return false; + } + + /** + * An apply function for SHOULD_FUSE. Derived classes should alter this. + * @param _pokemon {@link Pokemon} The first chosen pokemon for fusion. + * @param _pokemonTofuse {@link Pokemon} The second chosen pokemon for fusion. + * @param _canFuse {@link BooleanHolder} Whether the pokemons can fuse. + * @returns {@link boolean} if this function did anything. + */ + applyShouldFuse(_pokemon: Pokemon, _pokemonTofuse: Pokemon, _canFuse: BooleanHolder): boolean { + return false; + } } type ChallengeCondition = (data: GameData) => boolean; @@ -888,6 +968,123 @@ export class LowerStarterPointsChallenge extends Challenge { } } +/** + * Challenge stops pokemon from healing every 10th wave + */ +export class NoFreeHealsChallenge extends Challenge { + constructor() { + super(Challenges.NO_AUTO_HEAL, 1); + } + + applyNoHealPhase(applyHealPhase: BooleanHolder): boolean { + applyHealPhase.value = false; + return true; + } + + static loadChallenge(source: NoFreeHealsChallenge | any): NoFreeHealsChallenge { + const newChallenge = new NoFreeHealsChallenge(); + newChallenge.value = source.value; + newChallenge.severity = source.severity; + return newChallenge; + } +} + +/** + * Challenge that removes the ability to revive fallen pokemon + */ +export class HardcoreChallenge extends Challenge { + private itemBlackList = [ + "modifierType:ModifierType.REVIVE", + "modifierType:ModifierType.MAX_REVIVE", + "modifierType:ModifierType.SACRED_ASH", + "modifierType:ModifierType.REVIVER_SEED", + ]; + + constructor() { + super(Challenges.HARDCORE, 2); + } + + applyRandomItemBlacklist(randomItem: ModifierTypeOption, isValid: BooleanHolder): boolean { + if (randomItem !== null) { + isValid.value = !this.itemBlackList.includes(randomItem.type.localeKey); + } + return true; + } + + applyShopItemBlacklist(shopItem: ModifierTypeOption, isValid: BooleanHolder): boolean { + isValid.value = !this.itemBlackList.includes(shopItem.type.localeKey); + return true; + } + + applyMoveBlacklist(move: PokemonMove, moveCanBeUsed: BooleanHolder): boolean { + const moveBlacklist = [Moves.REVIVAL_BLESSING]; + moveCanBeUsed.value = !moveBlacklist.includes(move.moveId); + return true; + } + + applyRevivePrevention(canBeRevived: BooleanHolder): boolean { + canBeRevived.value = false; + return true; + } + + applyDeletePokemon(canStay: BooleanHolder): boolean { + if (this.value === 2) { + canStay.value = false; + } else { + canStay.value = true; + } + return true; + } + + override applyShouldFuse(pokemon: Pokemon, pokemonToFuse: Pokemon, canFuse: BooleanHolder): boolean { + if (pokemon!.isFainted() || pokemonToFuse.isFainted()) { + canFuse.value = false; + } + return true; + } + + static override loadChallenge(source: HardcoreChallenge | any): HardcoreChallenge { + const newChallenge = new HardcoreChallenge(); + newChallenge.value = source.value; + newChallenge.severity = source.severity; + return newChallenge; + } +} + +/** + * Challenge that limits the amount of caught pokemons by 1 per biome stage + */ +export class LimitedCatchChallenge extends Challenge { + private mysteryEncounterBlacklist = [ + MysteryEncounterType.ABSOLUTE_AVARICE, + MysteryEncounterType.DANCING_LESSONS, + MysteryEncounterType.SAFARI_ZONE, + MysteryEncounterType.THE_POKEMON_SALESMAN, + MysteryEncounterType.UNCOMMON_BREED, + ]; + constructor() { + super(Challenges.LIMITED_CATCH, 1); + } + + override applyAddPokemonToParty(waveIndex: number, canAddToParty: BooleanHolder): boolean { + const lastMystery = globalScene.lastMysteryEncounter?.encounterType; + if (lastMystery === undefined && !(waveIndex % 10 === 1)) { + canAddToParty.value = false; + } + if (!(waveIndex % 10 === 1) && !(!this.mysteryEncounterBlacklist.includes(lastMystery!) && waveIndex % 10 === 2)) { + canAddToParty.value = false; + } + return true; + } + + static override loadChallenge(source: LimitedCatchChallenge | any): LimitedCatchChallenge { + const newChallenge = new LimitedCatchChallenge(); + newChallenge.value = source.value; + newChallenge.severity = source.severity; + return newChallenge; + } +} + /** * Apply all challenges that modify starter choice. * @param challengeType {@link ChallengeType} ChallengeType.STARTER_CHOICE @@ -1039,6 +1236,90 @@ export function applyChallenges( ): boolean; export function applyChallenges(challengeType: ChallengeType.FLIP_STAT, pokemon: Pokemon, baseStats: number[]): boolean; +/** + * Apply all challenges that modify whether a pokemon can be auto healed or not in wave 10m. + * @param challengeType {@link ChallengeType} ChallengeType.NO_HEAL_PHASE + * @param applyHealPhase {@link BooleanHolder} Whether it should apply the heal phase. + * @returns True if any challenge was successfully applied. + */ +export function applyChallenges(challengeType: ChallengeType.NO_HEAL_PHASE, applyHealPhase: BooleanHolder): boolean; +/** + * Apply all challenges that modify whether a shop item should be blacklisted. + * @param challengeType {@link ChallengeType} ChallengeType.SHOP_ITEM_BLACKLIST + * @param shopItem {@link ModifierTypeOption} The shop item in question. + * @param isValid {@link BooleanHolder} Whether the shop should have the item. + * @returns True if any challenge was successfully applied. + */ +export function applyChallenges( + challengeType: ChallengeType.SHOP_ITEM_BLACKLIST, + shopItem: ModifierTypeOption | null, + isValid: BooleanHolder, +): boolean; + +/** + * Apply all challenges that modify whether a reward item should be blacklisted. + * @param challengeType {@link ChallengeType} ChallengeType.RANDOM_ITEM_BLACKLIST + * @param randomItem {@link ModifierTypeOption} The random item in question. + * @param isValid {@link BooleanHolder} Whether it should load the random item. + * @returns True if any challenge was successfully applied. + */ +export function applyChallenges( + challengeType: ChallengeType.RANDOM_ITEM_BLACKLIST, + randomItem: ModifierTypeOption | null, + isValid: BooleanHolder, +): boolean; +/** + * Apply all challenges that modify whether a pokemon move should be blacklisted. + * @param challengeType {@link ChallengeType} ChallengeType.MOVE_BLACKLIST + * @param move {@link PokemonMove} The move in question. + * @param isValid {@link BooleanHolder} Whether the move should be allowed. + * @returns True if any challenge was successfully applied. + */ +export function applyChallenges( + challengeType: ChallengeType.MOVE_BLACKLIST, + move: PokemonMove, + isValid: BooleanHolder, +): boolean; +/** + * Apply all challenges that modify whether a pokemon should be removed from the team. + * @param challengeType {@link ChallengeType} ChallengeType.DELETE_POKEMON + * @param canStay {@link BooleanHolder} Whether the pokemon can stay in team after death. + * @returns True if any challenge was successfully applied. + */ +export function applyChallenges(challengeType: ChallengeType.DELETE_POKEMON, canStay: BooleanHolder): boolean; +/** + * Apply all challenges that modify whether a pokemon should revive. + * @param challengeType {@link ChallengeType} ChallengeType.PREVENT_REVIVE + * @param canBeRevived {@link BooleanHolder} Whether it should revive the fainted Pokemon. + * @returns True if any challenge was successfully applied. + */ +export function applyChallenges(challengeType: ChallengeType.PREVENT_REVIVE, canBeRevived: BooleanHolder): boolean; +/** + * Apply all challenges that modify whether a pokemon can be caught. + * @param challengeType {@link ChallengeType} ChallengeType.ADD_POKEMON_TO_PARTY + * @param waveIndex {@link BooleanHolder} The current wave. + * @param canAddToParty {@link BooleanHolder} Whether the pokemon can be caught. + * @returns True if any challenge was successfully applied. + */ +export function applyChallenges( + challengeType: ChallengeType.ADD_POKEMON_TO_PARTY, + waveIndex: number, + canAddToParty: BooleanHolder, +): boolean; +/** + * Apply all challenges that modify whether a pokemon can fuse. + * @param challengeType {@link ChallengeType} ChallengeType.SHOULD_FUSE + * @param pokemon {@link Pokemon} The first chosen pokemon for fusion. + * @param pokemonTofuse {@link Pokemon} The second chosen pokemon for fusion. + * @param canFuse {@link BooleanHolder} Whether the pokemons can fuse. + * @returns True if any challenge was successfully applied. + */ +export function applyChallenges( + challengeType: ChallengeType.SHOULD_FUSE, + pokemon: Pokemon, + pokemonTofuse: Pokemon, + canFuse: BooleanHolder, +): boolean; export function applyChallenges(challengeType: ChallengeType, ...args: any[]): boolean { let ret = false; @@ -1087,6 +1368,30 @@ export function applyChallenges(challengeType: ChallengeType, ...args: any[]): b case ChallengeType.FLIP_STAT: ret ||= c.applyFlipStat(args[0], args[1]); break; + case ChallengeType.NO_HEAL_PHASE: + ret ||= c.applyNoHealPhase(args[0]); + break; + case ChallengeType.SHOP_ITEM_BLACKLIST: + ret ||= c.applyShopItemBlacklist(args[0], args[1]); + break; + case ChallengeType.RANDOM_ITEM_BLACKLIST: + ret ||= c.applyRandomItemBlacklist(args[0], args[1]); + break; + case ChallengeType.MOVE_BLACKLIST: + ret ||= c.applyMoveBlacklist(args[0], args[1]); + break; + case ChallengeType.DELETE_POKEMON: + ret ||= c.applyDeletePokemon(args[0]); + break; + case ChallengeType.PREVENT_REVIVE: + ret ||= c.applyRevivePrevention(args[0]); + break; + case ChallengeType.ADD_POKEMON_TO_PARTY: + ret ||= c.applyAddPokemonToParty(args[0], args[1]); + break; + case ChallengeType.SHOULD_FUSE: + ret ||= c.applyShouldFuse(args[0], args[1], args[2]); + break; } } }); @@ -1114,6 +1419,12 @@ export function copyChallenge(source: Challenge | any): Challenge { return InverseBattleChallenge.loadChallenge(source); case Challenges.FLIP_STAT: return FlipStatChallenge.loadChallenge(source); + case Challenges.NO_AUTO_HEAL: + return NoFreeHealsChallenge.loadChallenge(source); + case Challenges.HARDCORE: + return HardcoreChallenge.loadChallenge(source); + case Challenges.LIMITED_CATCH: + return LimitedCatchChallenge.loadChallenge(source); } throw new Error("Unknown challenge copied"); } @@ -1127,6 +1438,9 @@ export function initChallenges() { new FreshStartChallenge(), new InverseBattleChallenge(), new FlipStatChallenge(), + new NoFreeHealsChallenge(), + new LimitedCatchChallenge(), + new HardcoreChallenge(), ); } diff --git a/src/data/mystery-encounters/encounters/a-trainers-test-encounter.ts b/src/data/mystery-encounters/encounters/a-trainers-test-encounter.ts index 7a1c9821e89..bf6d2f5aabe 100644 --- a/src/data/mystery-encounters/encounters/a-trainers-test-encounter.ts +++ b/src/data/mystery-encounters/encounters/a-trainers-test-encounter.ts @@ -22,6 +22,7 @@ import { EggTier } from "#enums/egg-type"; import { ModifierTier } from "#enums/modifier-tier"; import { modifierTypes } from "#app/data/data-lists"; import { CLASSIC_MODE_MYSTERY_ENCOUNTER_WAVES } from "#app/constants"; +import { Challenges } from "#enums/challenges"; /** the i18n namespace for the encounter */ const namespace = "mysteryEncounters/aTrainersTest"; @@ -34,6 +35,7 @@ const namespace = "mysteryEncounters/aTrainersTest"; export const ATrainersTestEncounter: MysteryEncounter = MysteryEncounterBuilder.withEncounterType( MysteryEncounterType.A_TRAINERS_TEST, ) + .withDisallowedChallenges(Challenges.NO_AUTO_HEAL) .withEncounterTier(MysteryEncounterTier.ROGUE) .withSceneWaveRangeRequirement(100, CLASSIC_MODE_MYSTERY_ENCOUNTER_WAVES[1]) .withIntroSpriteConfigs([]) // These are set in onInit() diff --git a/src/data/mystery-encounters/encounters/slumbering-snorlax-encounter.ts b/src/data/mystery-encounters/encounters/slumbering-snorlax-encounter.ts index 483c577e851..6706e0a62f4 100644 --- a/src/data/mystery-encounters/encounters/slumbering-snorlax-encounter.ts +++ b/src/data/mystery-encounters/encounters/slumbering-snorlax-encounter.ts @@ -31,6 +31,7 @@ import { BerryType } from "#enums/berry-type"; import { Stat } from "#enums/stat"; import { CustomPokemonData } from "#app/data/custom-pokemon-data"; import { randSeedInt } from "#app/utils/common"; +import { Challenges } from "#enums/challenges"; /** i18n namespace for the encounter */ const namespace = "mysteryEncounters/slumberingSnorlax"; @@ -43,6 +44,7 @@ const namespace = "mysteryEncounters/slumberingSnorlax"; export const SlumberingSnorlaxEncounter: MysteryEncounter = MysteryEncounterBuilder.withEncounterType( MysteryEncounterType.SLUMBERING_SNORLAX, ) + .withDisallowedChallenges(Challenges.NO_AUTO_HEAL) .withEncounterTier(MysteryEncounterTier.GREAT) .withSceneWaveRangeRequirement(15, 150) .withCatchAllowed(true) diff --git a/src/data/mystery-encounters/encounters/the-winstrate-challenge-encounter.ts b/src/data/mystery-encounters/encounters/the-winstrate-challenge-encounter.ts index e2c87d8c0ae..994f1f7a221 100644 --- a/src/data/mystery-encounters/encounters/the-winstrate-challenge-encounter.ts +++ b/src/data/mystery-encounters/encounters/the-winstrate-challenge-encounter.ts @@ -31,6 +31,7 @@ import i18next from "i18next"; import { ModifierTier } from "#enums/modifier-tier"; import { CLASSIC_MODE_MYSTERY_ENCOUNTER_WAVES } from "#app/constants"; import { BattlerTagType } from "#enums/battler-tag-type"; +import { Challenges } from "#enums/challenges"; /** the i18n namespace for the encounter */ const namespace = "mysteryEncounters/theWinstrateChallenge"; @@ -43,6 +44,7 @@ const namespace = "mysteryEncounters/theWinstrateChallenge"; export const TheWinstrateChallengeEncounter: MysteryEncounter = MysteryEncounterBuilder.withEncounterType( MysteryEncounterType.THE_WINSTRATE_CHALLENGE, ) + .withDisallowedChallenges(Challenges.NO_AUTO_HEAL) .withEncounterTier(MysteryEncounterTier.ROGUE) .withSceneWaveRangeRequirement(100, CLASSIC_MODE_MYSTERY_ENCOUNTER_WAVES[1]) .withIntroSpriteConfigs([ diff --git a/src/data/mystery-encounters/utils/encounter-pokemon-utils.ts b/src/data/mystery-encounters/utils/encounter-pokemon-utils.ts index 4671869a2ba..d67a5fdc964 100644 --- a/src/data/mystery-encounters/utils/encounter-pokemon-utils.ts +++ b/src/data/mystery-encounters/utils/encounter-pokemon-utils.ts @@ -37,6 +37,8 @@ import { CustomPokemonData } from "#app/data/custom-pokemon-data"; import type { AbilityId } from "#enums/ability-id"; import type { PokeballType } from "#enums/pokeball"; import { StatusEffect } from "#enums/status-effect"; +import { BooleanHolder } from "#app/utils/common"; +import { ChallengeType, applyChallenges } from "#app/data/challenge"; /** Will give +1 level every 10 waves */ export const STANDARD_ENCOUNTER_BOOSTED_LEVEL_MODIFIER = 1; @@ -703,6 +705,17 @@ export async function catchPokemon( }); }; Promise.all([pokemon.hideInfo(), globalScene.gameData.setPokemonCaught(pokemon)]).then(() => { + const challengeCanAddToParty = new BooleanHolder(true); + applyChallenges( + ChallengeType.ADD_POKEMON_TO_PARTY, + globalScene.currentBattle.waveIndex, + challengeCanAddToParty, + ); + if (!challengeCanAddToParty.value) { + removePokemon(); + end(); + return; + } if (globalScene.getPlayerParty().length === 6) { const promptRelease = () => { globalScene.ui.showText( diff --git a/src/enums/challenges.ts b/src/enums/challenges.ts index 7b506a61a2f..5d9edcefbee 100644 --- a/src/enums/challenges.ts +++ b/src/enums/challenges.ts @@ -6,4 +6,7 @@ export enum Challenges { FRESH_START, INVERSE_BATTLE, FLIP_STAT, + NO_AUTO_HEAL, + HARDCORE, + LIMITED_CATCH, } diff --git a/src/field/pokemon.ts b/src/field/pokemon.ts index 964d66d352e..ad477b57c89 100644 --- a/src/field/pokemon.ts +++ b/src/field/pokemon.ts @@ -2504,14 +2504,36 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container { defScore *= 1 / Math.max(this.getAttackTypeEffectiveness(enemyTypes[1], opponent, false, false, undefined, true), 0.25); } + atkScore *= 1.25; //give more value for the pokemon's typing + const moveset = this.moveset; + let moveAtkScoreLenght = 0; + for (const move of moveset) { + if (move.getMove().category === MoveCategory.SPECIAL || move.getMove().category === MoveCategory.PHYSICAL) { + atkScore += opponent.getAttackTypeEffectiveness(move.getMove().type, this, false, true, undefined, true); + moveAtkScoreLenght++; + } + } + atkScore = atkScore / (moveAtkScoreLenght + 1); //calculate the median for the attack score /** * Based on this Pokemon's HP ratio compared to that of the opponent. * This ratio is multiplied by 1.5 if this Pokemon outspeeds the opponent; * however, the final ratio cannot be higher than 1. */ - let hpDiffRatio = this.getHpRatio() + (1 - opponent.getHpRatio()); - if (outspeed) { - hpDiffRatio = Math.min(hpDiffRatio * 1.5, 1); + const hpRatio = this.getHpRatio(); + const oppHpRatio = opponent.getHpRatio(); + const isDying = hpRatio <= 0.2; + let hpDiffRatio = hpRatio + (1 - oppHpRatio); + if (isDying && this.isActive(true)) { //It might be a sacrifice candidate if hp under 20% + const badMatchup = atkScore < 1.5 && defScore < 1.5; + if (!outspeed && badMatchup) { //It might not be a worthy sacrifice if it doesn't outspeed or doesn't do enough damage + hpDiffRatio *= 0.85; + } else { + hpDiffRatio = Math.min((1 - hpRatio) + (outspeed ? 0.2 : 0.1), 1); + } + } else if (outspeed) { + hpDiffRatio = Math.min(hpDiffRatio * 1.25, 1); + } else if (hpRatio > 0.2 && hpRatio <= 0.4) { //Might be considered to be switched because it's not in low enough health + hpDiffRatio = Math.min (hpDiffRatio * 0.5, 1); } return (atkScore + defScore) * hpDiffRatio; } @@ -3206,6 +3228,13 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container { public trySelectMove(moveIndex: number, ignorePp?: boolean): boolean { const move = this.getMoveset().length > moveIndex ? this.getMoveset()[moveIndex] : null; + if (move !== null) { + const isValid = new BooleanHolder(true); + applyChallenges(ChallengeType.MOVE_BLACKLIST, move!, isValid); + if (!isValid.value) { + return false; + } + } return move?.isUsable(this, ignorePp) ?? false; } diff --git a/src/modifier/modifier-type.ts b/src/modifier/modifier-type.ts index 2c848c69e18..70f0abc820b 100644 --- a/src/modifier/modifier-type.ts +++ b/src/modifier/modifier-type.ts @@ -111,6 +111,7 @@ import { NumberHolder, padInt, randSeedInt, + BooleanHolder, } from "#app/utils/common"; import { BattlerTagType } from "#enums/battler-tag-type"; import { BerryType } from "#enums/berry-type"; @@ -128,6 +129,7 @@ import { TYPE_BOOST_ITEM_BOOST_PERCENT } from "#app/constants"; import { ModifierPoolType } from "#enums/modifier-pool-type"; import { getModifierPoolForType, getModifierType } from "#app/utils/modifier-utils"; import type { ModifierTypeFunc, WeightedModifierTypeWeightFunc } from "#app/@types/modifier-types"; +import { applyChallenges } from "#app/data/challenge"; const outputModifierData = false; const useMaxWeightForOutput = false; @@ -2632,10 +2634,14 @@ function getModifierTypeOptionWithRetry( allowLuckUpgrades = allowLuckUpgrades ?? true; let candidate = getNewModifierTypeOption(party, ModifierPoolType.PLAYER, tier, undefined, 0, allowLuckUpgrades); let r = 0; + const isValidForChallenge = new BooleanHolder(true); + applyChallenges(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 + (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, @@ -2645,6 +2651,7 @@ function getModifierTypeOptionWithRetry( 0, allowLuckUpgrades, ); + applyChallenges(ChallengeType.RANDOM_ITEM_BLACKLIST, candidate, isValidForChallenge); } return candidate!; } @@ -2675,7 +2682,9 @@ export function overridePlayerModifierTypeOptions(options: ModifierTypeOption[], } export function getPlayerShopModifierTypeOptionsForWave(waveIndex: number, baseCost: number): ModifierTypeOption[] { - if (!(waveIndex % 10)) { + const isHealPhaseActive = new BooleanHolder(true); + applyChallenges(ChallengeType.NO_HEAL_PHASE, isHealPhaseActive); + if (!(waveIndex % 10) && isHealPhaseActive.value) { return []; } diff --git a/src/modifier/modifier.ts b/src/modifier/modifier.ts index e11f2c07ce8..c4519e09acd 100644 --- a/src/modifier/modifier.ts +++ b/src/modifier/modifier.ts @@ -45,6 +45,7 @@ import { FRIENDSHIP_GAIN_FROM_RARE_CANDY } from "#app/data/balance/starters"; import { applyAbAttrs, applyPostItemLostAbAttrs } from "#app/data/abilities/apply-ab-attrs"; import { globalScene } from "#app/global-scene"; import type { ModifierInstanceMap, ModifierString } from "#app/@types/modifier-types"; +import { applyChallenges } from "#app/data/challenge"; export type ModifierPredicate = (modifier: Modifier) => boolean; @@ -2446,8 +2447,13 @@ export class FusePokemonModifier extends ConsumablePokemonModifier { * @returns `true` if {@linkcode FusePokemonModifier} should be applied */ override shouldApply(playerPokemon?: PlayerPokemon, playerPokemon2?: PlayerPokemon): boolean { + const shouldFuse = new BooleanHolder(true); + applyChallenges(ChallengeType.SHOULD_FUSE, playerPokemon!, playerPokemon2!, shouldFuse); return ( - super.shouldApply(playerPokemon, playerPokemon2) && !!playerPokemon2 && this.fusePokemonId === playerPokemon2.id + super.shouldApply(playerPokemon, playerPokemon2) && + !!playerPokemon2 && + this.fusePokemonId === playerPokemon2.id && + shouldFuse.value ); } diff --git a/src/phases/attempt-capture-phase.ts b/src/phases/attempt-capture-phase.ts index f4e6725935a..84bc677f4c4 100644 --- a/src/phases/attempt-capture-phase.ts +++ b/src/phases/attempt-capture-phase.ts @@ -24,6 +24,8 @@ import { StatusEffect } from "#enums/status-effect"; import i18next from "i18next"; import { globalScene } from "#app/global-scene"; import { Gender } from "#app/data/gender"; +import { BooleanHolder } from "#app/utils/common"; +import { ChallengeType, applyChallenges } from "#app/data/challenge"; export class AttemptCapturePhase extends PokemonPhase { public readonly phaseName = "AttemptCapturePhase"; @@ -285,6 +287,17 @@ export class AttemptCapturePhase extends PokemonPhase { }); }; Promise.all([pokemon.hideInfo(), globalScene.gameData.setPokemonCaught(pokemon)]).then(() => { + const challengeCanAddToParty = new BooleanHolder(true); + applyChallenges( + ChallengeType.ADD_POKEMON_TO_PARTY, + globalScene.currentBattle.waveIndex, + challengeCanAddToParty, + ); + if (!challengeCanAddToParty.value) { + removePokemon(); + end(); + return; + } if (globalScene.getPlayerParty().length === PLAYER_PARTY_MAX_SIZE) { const promptRelease = () => { globalScene.ui.showText( diff --git a/src/phases/battle-end-phase.ts b/src/phases/battle-end-phase.ts index e1bf4c2296c..7dc2e9d4622 100644 --- a/src/phases/battle-end-phase.ts +++ b/src/phases/battle-end-phase.ts @@ -2,6 +2,8 @@ import { globalScene } from "#app/global-scene"; import { applyPostBattleAbAttrs } from "#app/data/abilities/apply-ab-attrs"; import { LapsingPersistentModifier, LapsingPokemonHeldItemModifier } from "#app/modifier/modifier"; import { BattlePhase } from "./battle-phase"; +import { BooleanHolder } from "#app/utils/common"; +import { applyChallenges, ChallengeType } from "#app/data/challenge"; export class BattleEndPhase extends BattlePhase { public readonly phaseName = "BattleEndPhase"; @@ -67,6 +69,16 @@ export class BattleEndPhase extends BattlePhase { for (const pokemon of globalScene.getPokemonAllowedInBattle()) { applyPostBattleAbAttrs("PostBattleAbAttr", pokemon, false, this.isVictory); } + const canStay = new BooleanHolder(true); + applyChallenges(ChallengeType.DELETE_POKEMON, canStay); + if (!canStay.value) { + const party = globalScene.getPlayerParty().slice(); + for (const pokemon of party) { + if (pokemon.isFainted()) { + globalScene.removePokemonFromPlayerParty(pokemon); + } + } + } if (globalScene.currentBattle.moneyScattered) { globalScene.currentBattle.pickUpScatteredMoney(); diff --git a/src/phases/command-phase.ts b/src/phases/command-phase.ts index d7264b4aff2..eea2a9d4822 100644 --- a/src/phases/command-phase.ts +++ b/src/phases/command-phase.ts @@ -218,7 +218,9 @@ export class CommandPhase extends FieldPhase { .selectionDeniedText(playerPokemon, move.moveId) : move.getName().endsWith(" (N)") ? "battle:moveNotImplemented" - : "battle:moveNoPP"; + : move.getPpRatio() + ? "battle:moveDisabled" + : "battle:moveNoPP"; const moveName = move.getName().replace(" (N)", ""); // Trims off the indicator globalScene.ui.showText( diff --git a/src/phases/party-heal-phase.ts b/src/phases/party-heal-phase.ts index 765c7dbad8e..1a017293126 100644 --- a/src/phases/party-heal-phase.ts +++ b/src/phases/party-heal-phase.ts @@ -1,6 +1,7 @@ import { globalScene } from "#app/global-scene"; -import { fixedInt } from "#app/utils/common"; +import { BooleanHolder, fixedInt } from "#app/utils/common"; import { BattlePhase } from "./battle-phase"; +import { applyChallenges, ChallengeType } from "#app/data/challenge"; export class PartyHealPhase extends BattlePhase { public readonly phaseName = "PartyHealPhase"; @@ -15,18 +16,27 @@ export class PartyHealPhase extends BattlePhase { start() { super.start(); + const isHealPhaseActive = new BooleanHolder(true); + const isReviveActive = new BooleanHolder(true); + applyChallenges(ChallengeType.NO_HEAL_PHASE, isHealPhaseActive); + if (!isHealPhaseActive.value) { + return this.end(); + } const bgmPlaying = globalScene.isBgmPlaying(); if (bgmPlaying) { globalScene.fadeOutBgm(1000, false); } globalScene.ui.fadeOut(1000).then(() => { for (const pokemon of globalScene.getPlayerParty()) { - pokemon.hp = pokemon.getMaxHp(); - pokemon.resetStatus(true, false, false, true); - for (const move of pokemon.moveset) { - move.ppUsed = 0; + applyChallenges(ChallengeType.PREVENT_REVIVE, isReviveActive); + if (isReviveActive.value || !pokemon.isFainted()) { + pokemon.hp = pokemon.getMaxHp(); + pokemon.resetStatus(true, false, false, true); + for (const move of pokemon.moveset) { + move.ppUsed = 0; + } + pokemon.updateInfo(true); } - pokemon.updateInfo(true); } const healSong = globalScene.playSoundWithoutBgm("heal"); globalScene.time.delayedCall(fixedInt(healSong.totalDuration * 1000), () => { diff --git a/src/phases/victory-phase.ts b/src/phases/victory-phase.ts index ae5b727c2a6..9ccc28d6bd7 100644 --- a/src/phases/victory-phase.ts +++ b/src/phases/victory-phase.ts @@ -8,6 +8,9 @@ import { handleMysteryEncounterVictory } from "#app/data/mystery-encounters/util import { globalScene } from "#app/global-scene"; import { timedEventManager } from "#app/global-event-manager"; +import { BooleanHolder } from "#app/utils/common"; +import { applyChallenges } from "#app/data/challenge"; + export class VictoryPhase extends PokemonPhase { public readonly phaseName = "VictoryPhase"; /** If true, indicates that the phase is intended for EXP purposes only, and not to continue a battle to next phase */ @@ -37,6 +40,8 @@ export class VictoryPhase extends PokemonPhase { return this.end(); } + const isHealPhaseActive = new BooleanHolder(true); + applyChallenges(ChallengeType.NO_HEAL_PHASE, isHealPhaseActive); if ( !globalScene .getEnemyParty() @@ -104,6 +109,10 @@ export class VictoryPhase extends PokemonPhase { ); globalScene.phaseManager.pushNew("AddEnemyBuffModifierPhase"); } + if (!isHealPhaseActive.value) { + //Push shop instead of healing phase for NoHealChallenge + globalScene.pushPhase(new SelectModifierPhase(undefined, undefined, this.getFixedBattleCustomModifiers())); + } } if (globalScene.gameMode.hasRandomBiomes || globalScene.isNewBiome()) { diff --git a/src/ui/modifier-select-ui-handler.ts b/src/ui/modifier-select-ui-handler.ts index 7f5bf997f88..68ee8f60c44 100644 --- a/src/ui/modifier-select-ui-handler.ts +++ b/src/ui/modifier-select-ui-handler.ts @@ -16,6 +16,8 @@ import i18next from "i18next"; import { ShopCursorTarget } from "#app/enums/shop-cursor-target"; import Phaser from "phaser"; import type { PokeballType } from "#enums/pokeball"; +import { applyChallenges, ChallengeType } from "#app/data/challenge"; +import { BooleanHolder } from "#app/utils/common"; export const SHOP_OPTIONS_ROW_LIMIT = 7; const SINGLE_SHOP_ROW_YOFFSET = 12; @@ -211,9 +213,16 @@ export default class ModifierSelectUiHandler extends AwaitableUiHandler { const removeHealShop = globalScene.gameMode.hasNoShop; const baseShopCost = new NumberHolder(globalScene.getWaveMoneyAmount(1)); globalScene.applyModifier(HealShopCostModifier, true, baseShopCost); - const shopTypeOptions = !removeHealShop - ? getPlayerShopModifierTypeOptionsForWave(globalScene.currentBattle.waveIndex, baseShopCost.value) - : []; + const shopTypeOptions = removeHealShop + ? [] + : getPlayerShopModifierTypeOptionsForWave(globalScene.currentBattle.waveIndex, baseShopCost.value).filter( + shopItem => { + const isValidForChallenge = new BooleanHolder(true); + applyChallenges(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; From f8a3352bb34af6d1c124cde199205b30feaa99c2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Matilde=20Sim=C3=B5es?= Date: Wed, 4 Jun 2025 21:39:21 +0100 Subject: [PATCH 2/5] =?UTF-8?q?Implement=20NuzLocke=20related=20challenges?= =?UTF-8?q?=20and=20AI=20changes=20The=20Nuzlocke=20challenges=20that=20we?= =?UTF-8?q?re=20implemented=20were=20the=20following:=20-=20"No=20Free=20H?= =?UTF-8?q?eal"=20which=20consists=20in=20disabling=20the=20auto=20heal=20?= =?UTF-8?q?that=20occurs=20every=2010th=20wave,=20replacing=20it=20with=20?= =?UTF-8?q?a=20normal=20shop=20phase.=20Changes=20made:=20=20=20=20-=20Add?= =?UTF-8?q?ed=20a=20new=20challenge=20"NO=5FAUTO=5FHEAL"=20with=20a=20chal?= =?UTF-8?q?lenge=20type=20"NO=5FHEAL=5FPHASE"=20and=20created=20a=20new=20?= =?UTF-8?q?function=20"applyNoHealPhase"=20to=20determine=20whether=20the?= =?UTF-8?q?=20Pok=C3=A9mon=20can=20heal=20if=20the=20challenge=20is=20acti?= =?UTF-8?q?ve=20("challenge.ts").=20=20=20=20-=20Added=20a=20confirmation?= =?UTF-8?q?=20on=20the=20healing=20phase=20to=20check=20if=20the=20challen?= =?UTF-8?q?ge=20is=20active=20or=20not,=20and=20if=20it=20is,=20it=20shoul?= =?UTF-8?q?d=20skip=20the=20phase=20("party-heal-phase.ts").=20=20=20=20-?= =?UTF-8?q?=20Changed=20the=20logistic=20of=20when=20the=20shop=20should?= =?UTF-8?q?=20be=20displayed,=20so=20when=20the=20challenge=20is=20active?= =?UTF-8?q?=20the=20shop=20appears=20every=2010th=20wave=20("modifier-type?= =?UTF-8?q?.ts")=20and=20actually=20push=20the=20shop=20phase=20("victory-?= =?UTF-8?q?phase.ts").=20-=20"Hardcore":=20Challenge=20divided=20into=20tw?= =?UTF-8?q?o=20modes,=20normal=20and=20hard,=20where=20fainted=20Pok=C3=A9?= =?UTF-8?q?mon=20can't=20be=20revived,=20in=20addition,=20the=20hard=20mod?= =?UTF-8?q?e=20deletes=20the=20fainted=20Pok=C3=A9mon=20so=20the=20player?= =?UTF-8?q?=20can't=20switch=20it's=20items=20after=20death.=20Changes=20m?= =?UTF-8?q?ade:=20=20=20=20-=20Added=20a=20new=20challenge=20"HARDCORE"=20?= =?UTF-8?q?with=20several=20challenge=20types=20with=20the=20correspondent?= =?UTF-8?q?=20apply=20functions=20("challenge.ts"),=20each=20one=20is=20us?= =?UTF-8?q?ed=20as=20follows:=20=20=20=20-=20RANDOM=5FITEM=5FBLACKLIST:=20?= =?UTF-8?q?filter=20the=20reward=20items=20with=20only=20the=20valid=20one?= =?UTF-8?q?'s=20("modifier-type.ts").=20=20=20=20-=20SHOP=5FITEM=5FBLACKLI?= =?UTF-8?q?ST:=20filter=20the=20shop=20items=20with=20only=20the=20valid?= =?UTF-8?q?=20one's=20("modifier-select-ui-handler.ts").=20=20=20=20-=20MO?= =?UTF-8?q?VE=5FBLACKLIST:=20checks=20if=20the=20move=20selected=20is=20al?= =?UTF-8?q?lowed=20and=20if=20not=20sends=20a=20message=20of=20no=20apply?= =?UTF-8?q?=20("pokemon.ts").=20=20=20=20-=20DELETE=5FPOKEMON:=20if=20hard?= =?UTF-8?q?=20mode=20was=20selected,=20automatically=20delete=20the=20fain?= =?UTF-8?q?ted=20Pok=C3=A9mon=20from=20the=20party=20("battle-end-pahse.ts?= =?UTF-8?q?").=20=20=20=20-=20SHOULD=5FFUSE:=20changed=20the=20logic=20of?= =?UTF-8?q?=20should=20apply=20function=20to=20prohibit=20the=20fusion=20w?= =?UTF-8?q?ith=20dead=20Pok=C3=A9mon=20("modifier.ts").=20=20=20=20-=20PRE?= =?UTF-8?q?VENT=5FREVIVE:=20prevent=20the=20gain=20of=20hp=20of=20fainted?= =?UTF-8?q?=20Pok=C3=A9mon=20("party-heal-phase.ts").=20-=20"Limited=20Cat?= =?UTF-8?q?ch":=20Only=20the=20first=20wild=20Pok=C3=A9mon=20encounter=20o?= =?UTF-8?q?f=20every=20biome=20can=20be=20added=20to=20the=20player's=20cu?= =?UTF-8?q?rrent=20party.=20Changes=20made:=20=20=20-=20Added=20a=20new=20?= =?UTF-8?q?challenge=20LIMITED=5FCATCH=20with=20a=20challenge=20type=20=20?= =?UTF-8?q?ADD=5FPOKEMON=5FTO=5FPARTY=20and=20created=20a=20new=20function?= =?UTF-8?q?=20"applyAddPokemonToParty"=20to=20determine=20whether=20the=20?= =?UTF-8?q?Pok=C3=A9mon=20can=20be=20added=20to=20the=20party,=20which=20s?= =?UTF-8?q?hould=20only=20occur=20every=2011th=20wave=20if=20it=20isn't=20?= =?UTF-8?q?a=20catchable=20mystery=20encounter=20or=20every=2012th=20wave?= =?UTF-8?q?=20if=20the=2011th=20wave=20was=20a=20catchable=20mystery=20enc?= =?UTF-8?q?ounter=20("challenge.ts").=20=20=20=20-=20Changed=20the=20logis?= =?UTF-8?q?tic=20of=20adding=20a=20Pok=C3=A9mon=20where=20it=20can=20be=20?= =?UTF-8?q?caught=20so=20that=20the=20"pokedex"=20is=20updated=20but=20the?= =?UTF-8?q?=20Pok=C3=A9mon=20isn't=20added=20to=20the=20party=20of=20the?= =?UTF-8?q?=20player=20affecting=20specifically=20mystery=20encounters=20(?= =?UTF-8?q?"encounter-pokemon-utils.ts")=20and=20added=20the=20same=20logi?= =?UTF-8?q?c=20to=20normal=20encounters.=20("attempt-capture-phase.ts")?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The changes in the game AI were as follows ("pokemon.ts"): - More accurately accounts for the Pokémon's actual moves and their effectiveness against the player instead of only the pokemon type - Introduced logic to decide when a Pokémon should be sacrificed or switched based on its HP and speed. Signed-off-by: Matilde Simões Co-authored-by: Fuad Ali --- src/phases/party-heal-phase.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/phases/party-heal-phase.ts b/src/phases/party-heal-phase.ts index 1a017293126..594f6f9072d 100644 --- a/src/phases/party-heal-phase.ts +++ b/src/phases/party-heal-phase.ts @@ -19,6 +19,7 @@ export class PartyHealPhase extends BattlePhase { const isHealPhaseActive = new BooleanHolder(true); const isReviveActive = new BooleanHolder(true); applyChallenges(ChallengeType.NO_HEAL_PHASE, isHealPhaseActive); + applyChallenges(ChallengeType.PREVENT_REVIVE, isReviveActive); if (!isHealPhaseActive.value) { return this.end(); } @@ -28,7 +29,6 @@ export class PartyHealPhase extends BattlePhase { } globalScene.ui.fadeOut(1000).then(() => { for (const pokemon of globalScene.getPlayerParty()) { - applyChallenges(ChallengeType.PREVENT_REVIVE, isReviveActive); if (isReviveActive.value || !pokemon.isFainted()) { pokemon.hp = pokemon.getMaxHp(); pokemon.resetStatus(true, false, false, true); From 14cdfaac09ca7580a75a63146fbcb7a97220852f Mon Sep 17 00:00:00 2001 From: mati-soda Date: Thu, 12 Jun 2025 22:05:17 +0100 Subject: [PATCH 3/5] =?UTF-8?q?Fixed=20conflicts=20on=20file=20imports.=20?= =?UTF-8?q?Signed-off-by:=20Matilde=20Sim=C3=B5es=20=20Co-authored-by:=20Fuad=20Ali=20?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/data/challenge.ts | 4 +- .../utils/encounter-pokemon-utils.ts | 3 +- src/enums/challenge-type.ts | 41 ++++++++++++++++++- src/modifier/modifier-type.ts | 1 + src/modifier/modifier.ts | 1 + src/phases/attempt-capture-phase.ts | 3 +- src/phases/battle-end-phase.ts | 3 +- src/phases/party-heal-phase.ts | 3 +- src/phases/victory-phase.ts | 8 +++- src/ui/modifier-select-ui-handler.ts | 3 +- 10 files changed, 61 insertions(+), 9 deletions(-) diff --git a/src/data/challenge.ts b/src/data/challenge.ts index a82ae3a1432..30e2025f3b1 100644 --- a/src/data/challenge.ts +++ b/src/data/challenge.ts @@ -19,7 +19,7 @@ import { Challenges } from "#enums/challenges"; import { SpeciesId } from "#enums/species-id"; import { TrainerType } from "#enums/trainer-type"; import { Nature } from "#enums/nature"; -import type { MoveId } from "#enums/move-id"; +import { MoveId } from "#enums/move-id"; import { TypeColor, TypeShadow } from "#enums/color"; import { ModifierTier } from "#enums/modifier-tier"; import { globalScene } from "#app/global-scene"; @@ -1017,7 +1017,7 @@ export class HardcoreChallenge extends Challenge { } applyMoveBlacklist(move: PokemonMove, moveCanBeUsed: BooleanHolder): boolean { - const moveBlacklist = [Moves.REVIVAL_BLESSING]; + const moveBlacklist = [MoveId.REVIVAL_BLESSING]; moveCanBeUsed.value = !moveBlacklist.includes(move.moveId); return true; } diff --git a/src/data/mystery-encounters/utils/encounter-pokemon-utils.ts b/src/data/mystery-encounters/utils/encounter-pokemon-utils.ts index d67a5fdc964..6912905fbf9 100644 --- a/src/data/mystery-encounters/utils/encounter-pokemon-utils.ts +++ b/src/data/mystery-encounters/utils/encounter-pokemon-utils.ts @@ -38,7 +38,8 @@ import type { AbilityId } from "#enums/ability-id"; import type { PokeballType } from "#enums/pokeball"; import { StatusEffect } from "#enums/status-effect"; import { BooleanHolder } from "#app/utils/common"; -import { ChallengeType, applyChallenges } from "#app/data/challenge"; +import { applyChallenges } from "#app/data/challenge"; +import { ChallengeType } from "#enums/challenge-type"; /** Will give +1 level every 10 waves */ export const STANDARD_ENCOUNTER_BOOSTED_LEVEL_MODIFIER = 1; diff --git a/src/enums/challenge-type.ts b/src/enums/challenge-type.ts index d9b1fce3e6e..25371c8fffa 100644 --- a/src/enums/challenge-type.ts +++ b/src/enums/challenge-type.ts @@ -65,5 +65,44 @@ export enum ChallengeType { /** * Modifies what the pokemon stats for Flip Stat Mode. */ - FLIP_STAT + FLIP_STAT, + /** + * Challenge that modifies if the player should auto heal every 10th wave + */ + NO_HEAL_PHASE, + /** + * Modifies if the shop item is blacklisted + * @see {@linkcode Challenge.applyShopItemBlacklist} + */ + SHOP_ITEM_BLACKLIST, + /** + * Modifies if the random item is blacklisted + * @see {@linkcode Challenge.applyRandomItemBlacklist} + */ + RANDOM_ITEM_BLACKLIST, + /** + * Modifies if the move is blacklisted + * @see {@linkcode Challenge.applyMoveBlacklist} + */ + MOVE_BLACKLIST, + /** + * Modifies if pokemon are allowed to be revived from fainting + * @see {@linkcode Challenge.applyRevivePrevention} + */ + PREVENT_REVIVE, + /** + * Modifies if pokemon are allowed to be revived from fainting + * @see {@linkcode Challenge.applyDeletePokemon} + */ + DELETE_POKEMON, + /** + * Challenge that modifies if the player should catch pokemon on waves other than the first + * @see {@linkcode Challenge.applyAddPokemonToParty} + */ + ADD_POKEMON_TO_PARTY, + /** + * Modifies if pokemon are allowed to fuse + * @see {@linkcode Challenge.applyShouldFuse} + */ + SHOULD_FUSE, } diff --git a/src/modifier/modifier-type.ts b/src/modifier/modifier-type.ts index 70f0abc820b..57a3297865d 100644 --- a/src/modifier/modifier-type.ts +++ b/src/modifier/modifier-type.ts @@ -130,6 +130,7 @@ import { ModifierPoolType } from "#enums/modifier-pool-type"; import { getModifierPoolForType, getModifierType } from "#app/utils/modifier-utils"; import type { ModifierTypeFunc, WeightedModifierTypeWeightFunc } from "#app/@types/modifier-types"; import { applyChallenges } from "#app/data/challenge"; +import { ChallengeType } from "#enums/challenge-type"; const outputModifierData = false; const useMaxWeightForOutput = false; diff --git a/src/modifier/modifier.ts b/src/modifier/modifier.ts index c4519e09acd..ff0f0221596 100644 --- a/src/modifier/modifier.ts +++ b/src/modifier/modifier.ts @@ -46,6 +46,7 @@ import { applyAbAttrs, applyPostItemLostAbAttrs } from "#app/data/abilities/appl import { globalScene } from "#app/global-scene"; import type { ModifierInstanceMap, ModifierString } from "#app/@types/modifier-types"; import { applyChallenges } from "#app/data/challenge"; +import { ChallengeType } from "#enums/challenge-type"; export type ModifierPredicate = (modifier: Modifier) => boolean; diff --git a/src/phases/attempt-capture-phase.ts b/src/phases/attempt-capture-phase.ts index 84bc677f4c4..13e0437ae9f 100644 --- a/src/phases/attempt-capture-phase.ts +++ b/src/phases/attempt-capture-phase.ts @@ -25,7 +25,8 @@ import i18next from "i18next"; import { globalScene } from "#app/global-scene"; import { Gender } from "#app/data/gender"; import { BooleanHolder } from "#app/utils/common"; -import { ChallengeType, applyChallenges } from "#app/data/challenge"; +import { applyChallenges } from "#app/data/challenge"; +import { ChallengeType } from "#enums/challenge-type"; export class AttemptCapturePhase extends PokemonPhase { public readonly phaseName = "AttemptCapturePhase"; diff --git a/src/phases/battle-end-phase.ts b/src/phases/battle-end-phase.ts index 7dc2e9d4622..a38d697a078 100644 --- a/src/phases/battle-end-phase.ts +++ b/src/phases/battle-end-phase.ts @@ -3,7 +3,8 @@ import { applyPostBattleAbAttrs } from "#app/data/abilities/apply-ab-attrs"; import { LapsingPersistentModifier, LapsingPokemonHeldItemModifier } from "#app/modifier/modifier"; import { BattlePhase } from "./battle-phase"; import { BooleanHolder } from "#app/utils/common"; -import { applyChallenges, ChallengeType } from "#app/data/challenge"; +import { applyChallenges } from "#app/data/challenge"; +import { ChallengeType } from "#enums/challenge-type"; export class BattleEndPhase extends BattlePhase { public readonly phaseName = "BattleEndPhase"; diff --git a/src/phases/party-heal-phase.ts b/src/phases/party-heal-phase.ts index 594f6f9072d..e93a5b642b9 100644 --- a/src/phases/party-heal-phase.ts +++ b/src/phases/party-heal-phase.ts @@ -1,7 +1,8 @@ import { globalScene } from "#app/global-scene"; import { BooleanHolder, fixedInt } from "#app/utils/common"; import { BattlePhase } from "./battle-phase"; -import { applyChallenges, ChallengeType } from "#app/data/challenge"; +import { applyChallenges } from "#app/data/challenge"; +import { ChallengeType } from "#enums/challenge-type"; export class PartyHealPhase extends BattlePhase { public readonly phaseName = "PartyHealPhase"; diff --git a/src/phases/victory-phase.ts b/src/phases/victory-phase.ts index 9ccc28d6bd7..fa54b80558e 100644 --- a/src/phases/victory-phase.ts +++ b/src/phases/victory-phase.ts @@ -8,6 +8,7 @@ import { handleMysteryEncounterVictory } from "#app/data/mystery-encounters/util import { globalScene } from "#app/global-scene"; import { timedEventManager } from "#app/global-event-manager"; +import { ChallengeType } from "#enums/challenge-type"; import { BooleanHolder } from "#app/utils/common"; import { applyChallenges } from "#app/data/challenge"; @@ -111,7 +112,12 @@ export class VictoryPhase extends PokemonPhase { } if (!isHealPhaseActive.value) { //Push shop instead of healing phase for NoHealChallenge - globalScene.pushPhase(new SelectModifierPhase(undefined, undefined, this.getFixedBattleCustomModifiers())); + globalScene.phaseManager.pushNew( + "SelectModifierPhase", + undefined, + undefined, + this.getFixedBattleCustomModifiers(), + ); } } diff --git a/src/ui/modifier-select-ui-handler.ts b/src/ui/modifier-select-ui-handler.ts index 68ee8f60c44..e9b3357ead5 100644 --- a/src/ui/modifier-select-ui-handler.ts +++ b/src/ui/modifier-select-ui-handler.ts @@ -16,7 +16,8 @@ import i18next from "i18next"; import { ShopCursorTarget } from "#app/enums/shop-cursor-target"; import Phaser from "phaser"; import type { PokeballType } from "#enums/pokeball"; -import { applyChallenges, ChallengeType } from "#app/data/challenge"; +import { applyChallenges } from "#app/data/challenge"; +import { ChallengeType } from "#enums/challenge-type"; import { BooleanHolder } from "#app/utils/common"; export const SHOP_OPTIONS_ROW_LIMIT = 7; From f57e96b7ea661f77a8a2a42ee17189c9d9a5ba82 Mon Sep 17 00:00:00 2001 From: mati-soda Date: Fri, 13 Jun 2025 00:05:18 +0100 Subject: [PATCH 4/5] Removed enemy AI implementation MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Matilde Simões Co-authored-by: Fuad Ali --- src/field/pokemon.ts | 28 +++------------------------- 1 file changed, 3 insertions(+), 25 deletions(-) diff --git a/src/field/pokemon.ts b/src/field/pokemon.ts index ad477b57c89..ef02b45f345 100644 --- a/src/field/pokemon.ts +++ b/src/field/pokemon.ts @@ -2504,36 +2504,14 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container { defScore *= 1 / Math.max(this.getAttackTypeEffectiveness(enemyTypes[1], opponent, false, false, undefined, true), 0.25); } - atkScore *= 1.25; //give more value for the pokemon's typing - const moveset = this.moveset; - let moveAtkScoreLenght = 0; - for (const move of moveset) { - if (move.getMove().category === MoveCategory.SPECIAL || move.getMove().category === MoveCategory.PHYSICAL) { - atkScore += opponent.getAttackTypeEffectiveness(move.getMove().type, this, false, true, undefined, true); - moveAtkScoreLenght++; - } - } - atkScore = atkScore / (moveAtkScoreLenght + 1); //calculate the median for the attack score /** * Based on this Pokemon's HP ratio compared to that of the opponent. * This ratio is multiplied by 1.5 if this Pokemon outspeeds the opponent; * however, the final ratio cannot be higher than 1. */ - const hpRatio = this.getHpRatio(); - const oppHpRatio = opponent.getHpRatio(); - const isDying = hpRatio <= 0.2; - let hpDiffRatio = hpRatio + (1 - oppHpRatio); - if (isDying && this.isActive(true)) { //It might be a sacrifice candidate if hp under 20% - const badMatchup = atkScore < 1.5 && defScore < 1.5; - if (!outspeed && badMatchup) { //It might not be a worthy sacrifice if it doesn't outspeed or doesn't do enough damage - hpDiffRatio *= 0.85; - } else { - hpDiffRatio = Math.min((1 - hpRatio) + (outspeed ? 0.2 : 0.1), 1); - } - } else if (outspeed) { - hpDiffRatio = Math.min(hpDiffRatio * 1.25, 1); - } else if (hpRatio > 0.2 && hpRatio <= 0.4) { //Might be considered to be switched because it's not in low enough health - hpDiffRatio = Math.min (hpDiffRatio * 0.5, 1); + let hpDiffRatio = this.getHpRatio() + (1 - opponent.getHpRatio()); + if (outspeed) { + hpDiffRatio = Math.min(hpDiffRatio * 1.5, 1); } return (atkScore + defScore) * hpDiffRatio; } From b9cc1843d92bcd89abff675da1997e304a6dfe1e Mon Sep 17 00:00:00 2001 From: mati-soda Date: Fri, 20 Jun 2025 14:28:58 +0100 Subject: [PATCH 5/5] =?UTF-8?q?Deleted=20the=20constraints=20of=20the=20au?= =?UTF-8?q?to=20heal=20challenge=20from=20the=20Mystery=20Ecounters=20Sign?= =?UTF-8?q?ed-off-by:=20Matilde=20Sim=C3=B5es=20=20Co-authored-by:=20Fuad=20Ali=20?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../mystery-encounters/encounters/a-trainers-test-encounter.ts | 2 -- .../encounters/slumbering-snorlax-encounter.ts | 2 -- .../encounters/the-winstrate-challenge-encounter.ts | 2 -- 3 files changed, 6 deletions(-) diff --git a/src/data/mystery-encounters/encounters/a-trainers-test-encounter.ts b/src/data/mystery-encounters/encounters/a-trainers-test-encounter.ts index bf6d2f5aabe..7a1c9821e89 100644 --- a/src/data/mystery-encounters/encounters/a-trainers-test-encounter.ts +++ b/src/data/mystery-encounters/encounters/a-trainers-test-encounter.ts @@ -22,7 +22,6 @@ import { EggTier } from "#enums/egg-type"; import { ModifierTier } from "#enums/modifier-tier"; import { modifierTypes } from "#app/data/data-lists"; import { CLASSIC_MODE_MYSTERY_ENCOUNTER_WAVES } from "#app/constants"; -import { Challenges } from "#enums/challenges"; /** the i18n namespace for the encounter */ const namespace = "mysteryEncounters/aTrainersTest"; @@ -35,7 +34,6 @@ const namespace = "mysteryEncounters/aTrainersTest"; export const ATrainersTestEncounter: MysteryEncounter = MysteryEncounterBuilder.withEncounterType( MysteryEncounterType.A_TRAINERS_TEST, ) - .withDisallowedChallenges(Challenges.NO_AUTO_HEAL) .withEncounterTier(MysteryEncounterTier.ROGUE) .withSceneWaveRangeRequirement(100, CLASSIC_MODE_MYSTERY_ENCOUNTER_WAVES[1]) .withIntroSpriteConfigs([]) // These are set in onInit() diff --git a/src/data/mystery-encounters/encounters/slumbering-snorlax-encounter.ts b/src/data/mystery-encounters/encounters/slumbering-snorlax-encounter.ts index 6706e0a62f4..483c577e851 100644 --- a/src/data/mystery-encounters/encounters/slumbering-snorlax-encounter.ts +++ b/src/data/mystery-encounters/encounters/slumbering-snorlax-encounter.ts @@ -31,7 +31,6 @@ import { BerryType } from "#enums/berry-type"; import { Stat } from "#enums/stat"; import { CustomPokemonData } from "#app/data/custom-pokemon-data"; import { randSeedInt } from "#app/utils/common"; -import { Challenges } from "#enums/challenges"; /** i18n namespace for the encounter */ const namespace = "mysteryEncounters/slumberingSnorlax"; @@ -44,7 +43,6 @@ const namespace = "mysteryEncounters/slumberingSnorlax"; export const SlumberingSnorlaxEncounter: MysteryEncounter = MysteryEncounterBuilder.withEncounterType( MysteryEncounterType.SLUMBERING_SNORLAX, ) - .withDisallowedChallenges(Challenges.NO_AUTO_HEAL) .withEncounterTier(MysteryEncounterTier.GREAT) .withSceneWaveRangeRequirement(15, 150) .withCatchAllowed(true) diff --git a/src/data/mystery-encounters/encounters/the-winstrate-challenge-encounter.ts b/src/data/mystery-encounters/encounters/the-winstrate-challenge-encounter.ts index 994f1f7a221..e2c87d8c0ae 100644 --- a/src/data/mystery-encounters/encounters/the-winstrate-challenge-encounter.ts +++ b/src/data/mystery-encounters/encounters/the-winstrate-challenge-encounter.ts @@ -31,7 +31,6 @@ import i18next from "i18next"; import { ModifierTier } from "#enums/modifier-tier"; import { CLASSIC_MODE_MYSTERY_ENCOUNTER_WAVES } from "#app/constants"; import { BattlerTagType } from "#enums/battler-tag-type"; -import { Challenges } from "#enums/challenges"; /** the i18n namespace for the encounter */ const namespace = "mysteryEncounters/theWinstrateChallenge"; @@ -44,7 +43,6 @@ const namespace = "mysteryEncounters/theWinstrateChallenge"; export const TheWinstrateChallengeEncounter: MysteryEncounter = MysteryEncounterBuilder.withEncounterType( MysteryEncounterType.THE_WINSTRATE_CHALLENGE, ) - .withDisallowedChallenges(Challenges.NO_AUTO_HEAL) .withEncounterTier(MysteryEncounterTier.ROGUE) .withSceneWaveRangeRequirement(100, CLASSIC_MODE_MYSTERY_ENCOUNTER_WAVES[1]) .withIntroSpriteConfigs([