diff --git a/src/battle-scene.ts b/src/battle-scene.ts index 8c8906be2b0..ea15969ce61 100644 --- a/src/battle-scene.ts +++ b/src/battle-scene.ts @@ -478,8 +478,8 @@ export class BattleScene extends SceneBase { this.uiContainer = uiContainer; - const overlayWidth = this.game.canvas.width / 6; - const overlayHeight = this.game.canvas.height / 6 - 48; + const overlayWidth = this.scaledCanvas.width; + const overlayHeight = this.scaledCanvas.height - 48; this.fieldOverlay = this.add.rectangle(0, overlayHeight * -1 - 48, overlayWidth, overlayHeight, 0x424242); this.fieldOverlay.setName("rect-field-overlay"); this.fieldOverlay.setOrigin(0, 0); @@ -537,34 +537,29 @@ export class BattleScene extends SceneBase { this.candyBar.setup(); this.fieldUI.add(this.candyBar); - this.biomeWaveText = addTextObject( - this.game.canvas.width / 6 - 2, - 0, - startingWave.toString(), - TextStyle.BATTLE_INFO, - ); + this.biomeWaveText = addTextObject(this.scaledCanvas.width - 2, 0, startingWave.toString(), TextStyle.BATTLE_INFO); this.biomeWaveText.setName("text-biome-wave"); this.biomeWaveText.setOrigin(1, 0.5); this.fieldUI.add(this.biomeWaveText); - this.moneyText = addTextObject(this.game.canvas.width / 6 - 2, 0, "", TextStyle.MONEY); + this.moneyText = addTextObject(this.scaledCanvas.width - 2, 0, "", TextStyle.MONEY); this.moneyText.setName("text-money"); this.moneyText.setOrigin(1, 0.5); this.fieldUI.add(this.moneyText); - this.scoreText = addTextObject(this.game.canvas.width / 6 - 2, 0, "", TextStyle.PARTY, { fontSize: "54px" }); + this.scoreText = addTextObject(this.scaledCanvas.width - 2, 0, "", TextStyle.PARTY, { fontSize: "54px" }); this.scoreText.setName("text-score"); this.scoreText.setOrigin(1, 0.5); this.fieldUI.add(this.scoreText); - this.luckText = addTextObject(this.game.canvas.width / 6 - 2, 0, "", TextStyle.PARTY, { fontSize: "54px" }); + this.luckText = addTextObject(this.scaledCanvas.width - 2, 0, "", TextStyle.PARTY, { fontSize: "54px" }); this.luckText.setName("text-luck"); this.luckText.setOrigin(1, 0.5); this.luckText.setVisible(false); this.fieldUI.add(this.luckText); this.luckLabelText = addTextObject( - this.game.canvas.width / 6 - 2, + this.scaledCanvas.width - 2, 0, i18next.t("common:luckIndicator"), TextStyle.PARTY, @@ -586,10 +581,7 @@ export class BattleScene extends SceneBase { this.spriteSparkleHandler = new PokemonSpriteSparkleHandler(); this.spriteSparkleHandler.setup(); - this.pokemonInfoContainer = new PokemonInfoContainer( - this.game.canvas.width / 6 + 52, - -(this.game.canvas.height / 6) + 66, - ); + this.pokemonInfoContainer = new PokemonInfoContainer(this.scaledCanvas.width + 52, -this.scaledCanvas.height + 66); this.pokemonInfoContainer.setup(); this.fieldUI.add(this.pokemonInfoContainer); @@ -2054,7 +2046,7 @@ export class BattleScene extends SceneBase { } else { this.luckText.setTint(0xffef5c, 0x47ff69, 0x6b6bff, 0xff6969); } - this.luckLabelText.setX(this.game.canvas.width / 6 - 2 - (this.luckText.displayWidth + 2)); + this.luckLabelText.setX(this.scaledCanvas.width - 2 - (this.luckText.displayWidth + 2)); this.tweens.add({ targets: labels, duration: duration, @@ -2088,7 +2080,7 @@ export class BattleScene extends SceneBase { const enemyModifierCount = this.enemyModifiers.filter(m => m.isIconVisible()).length; const biomeWaveTextHeight = this.biomeWaveText.getBottomLeft().y - this.biomeWaveText.getTopLeft().y; this.biomeWaveText.setY( - -(this.game.canvas.height / 6) + + -this.scaledCanvas.height + (enemyModifierCount ? (enemyModifierCount <= 12 ? 15 : 24) : 0) + biomeWaveTextHeight / 2, ); @@ -2100,7 +2092,7 @@ export class BattleScene extends SceneBase { const offsetY = (this.scoreText.visible ? this.scoreText : this.moneyText).y + 15; this.partyExpBar.setY(offsetY); this.candyBar.setY(offsetY + 15); - this.ui?.achvBar.setY(this.game.canvas.height / 6 + offsetY); + this.ui?.achvBar.setY(this.scaledCanvas.height + offsetY); } /** diff --git a/src/data/challenge.ts b/src/data/challenge.ts index 1a1a3774f8f..724d1f302da 100644 --- a/src/data/challenge.ts +++ b/src/data/challenge.ts @@ -1,29 +1,26 @@ import type { FixedBattleConfig } from "#app/battle"; import { getRandomTrainerFunc } from "#app/battle"; import { defaultStarterSpecies } from "#app/constants"; -import { globalScene } from "#app/global-scene"; -import { pokemonEvolutions } from "#balance/pokemon-evolutions"; import { speciesStarterCosts } from "#balance/starters"; -import { pokemonFormChanges } from "#data/pokemon-forms"; import type { PokemonSpecies } from "#data/pokemon-species"; import { BattleType } from "#enums/battle-type"; -import { ChallengeType } from "#enums/challenge-type"; import { Challenges } from "#enums/challenges"; import { TypeColor, TypeShadow } from "#enums/color"; import { ClassicFixedBossWaves } from "#enums/fixed-boss-waves"; import { ModifierTier } from "#enums/modifier-tier"; -import type { MoveId } from "#enums/move-id"; +import { MoveId } from "#enums/move-id"; import type { MoveSourceType } from "#enums/move-source-type"; import { Nature } from "#enums/nature"; import { PokemonType } from "#enums/pokemon-type"; import { SpeciesId } from "#enums/species-id"; import { TrainerType } from "#enums/trainer-type"; import { TrainerVariant } from "#enums/trainer-variant"; -import type { Pokemon } from "#field/pokemon"; +import type { EnemyPokemon, PlayerPokemon, Pokemon } from "#field/pokemon"; import { Trainer } from "#field/trainer"; +import type { ModifierTypeOption } from "#modifiers/modifier-type"; import { PokemonMove } from "#moves/pokemon-move"; import type { DexAttrProps, GameData } from "#system/game-data"; -import { BooleanHolder, isBetween, type NumberHolder, randSeedItem } from "#utils/common"; +import { type BooleanHolder, isBetween, type NumberHolder, randSeedItem } from "#utils/common"; import { deepCopy } from "#utils/data"; import { getPokemonSpecies, getPokemonSpeciesForm } from "#utils/pokemon-utils"; import { toCamelCase, toSnakeCase } from "#utils/strings"; @@ -341,6 +338,83 @@ export abstract class Challenge { applyFlipStat(_pokemon: Pokemon, _baseStats: number[]) { return false; } + + /** + * An apply function for PARTY_HEAL. Derived classes should alter this. + * @param _status - Whether party healing is enabled or not + * @returns Whether this function did anything + */ + applyPartyHeal(_status: BooleanHolder): boolean { + return false; + } + + /** + * An apply function for SHOP. Derived classes should alter this. + * @param _status - Whether the shop is or is not available after a wave + * @returns Whether this function did anything + */ + applyShop(_status: BooleanHolder) { + return false; + } + + /** + * An apply function for POKEMON_ADD_TO_PARTY. Derived classes should alter this. + * @param _pokemon - The pokemon being caught + * @param _status - Whether the pokemon can be added to the party or not + * @return Whether this function did anything + */ + applyPokemonAddToParty(_pokemon: EnemyPokemon, _status: BooleanHolder): boolean { + return false; + } + + /** + * An apply function for POKEMON_FUSION. Derived classes should alter this. + * @param _pokemon - The pokemon being checked + * @param _status - Whether the selected pokemon is allowed to fuse or not + * @returns Whether this function did anything + */ + applyPokemonFusion(_pokemon: PlayerPokemon, _status: BooleanHolder): boolean { + return false; + } + + /** + * An apply function for POKEMON_MOVE. Derived classes should alter this. + * @param _moveId - The {@linkcode MoveId} being checked + * @param _status - A {@linkcode BooleanHolder} containing the move's usability status + * @returns Whether this function did anything + */ + applyPokemonMove(_moveId: MoveId, _status: BooleanHolder): boolean { + return false; + } + + /** + * An apply function for SHOP_ITEM. Derived classes should alter this. + * @param _shopItem - The item being checked + * @param _status - Whether the item should be added to the shop or not + * @returns Whether this function did anything + */ + applyShopItem(_shopItem: ModifierTypeOption | null, _status: BooleanHolder): boolean { + return false; + } + + /** + * An apply function for WAVE_REWARD. Derived classes should alter this. + * @param _reward - The reward being checked + * @param _status - Whether the reward should be added to the reward options or not + * @returns Whether this function did anything + */ + applyWaveReward(_reward: ModifierTypeOption | null, _status: BooleanHolder): boolean { + return false; + } + + /** + * An apply function for PREVENT_REVIVE. Derived classes should alter this. + * @param _status - Whether fainting is a permanent status or not + * @returns Whether this function did anything + */ + applyPreventRevive(_status: BooleanHolder): boolean { + return false; + } } type ChallengeCondition = (data: GameData) => boolean; @@ -864,208 +938,108 @@ export class LowerStarterPointsChallenge extends Challenge { } /** - * Apply all challenges that modify starter choice. - * @param challengeType {@link ChallengeType} ChallengeType.STARTER_CHOICE - * @param pokemon {@link PokemonSpecies} The pokemon to check the validity of. - * @param valid {@link BooleanHolder} A BooleanHolder, the value gets set to false if the pokemon isn't allowed. - * @param dexAttr {@link DexAttrProps} The dex attributes of the pokemon. - * @returns True if any challenge was successfully applied. + * Implements a No Support challenge */ -export function applyChallenges( - challengeType: ChallengeType.STARTER_CHOICE, - pokemon: PokemonSpecies, - valid: BooleanHolder, - dexAttr: DexAttrProps, -): boolean; -/** - * Apply all challenges that modify available total starter points. - * @param challengeType {@link ChallengeType} ChallengeType.STARTER_POINTS - * @param points {@link NumberHolder} The amount of points you have available. - * @returns True if any challenge was successfully applied. - */ -export function applyChallenges(challengeType: ChallengeType.STARTER_POINTS, points: NumberHolder): boolean; -/** - * Apply all challenges that modify the cost of a starter. - * @param challengeType {@link ChallengeType} ChallengeType.STARTER_COST - * @param species {@link SpeciesId} The pokemon to change the cost of. - * @param points {@link NumberHolder} The cost of the pokemon. - * @returns True if any challenge was successfully applied. - */ -export function applyChallenges( - challengeType: ChallengeType.STARTER_COST, - species: SpeciesId, - cost: NumberHolder, -): boolean; -/** - * Apply all challenges that modify a starter after selection. - * @param challengeType {@link ChallengeType} ChallengeType.STARTER_MODIFY - * @param pokemon {@link Pokemon} The starter pokemon to modify. - * @returns True if any challenge was successfully applied. - */ -export function applyChallenges(challengeType: ChallengeType.STARTER_MODIFY, pokemon: Pokemon): boolean; -/** - * Apply all challenges that what pokemon you can have in battle. - * @param challengeType {@link ChallengeType} ChallengeType.POKEMON_IN_BATTLE - * @param pokemon {@link Pokemon} The pokemon to check the validity of. - * @param valid {@link BooleanHolder} A BooleanHolder, the value gets set to false if the pokemon isn't allowed. - * @returns True if any challenge was successfully applied. - */ -export function applyChallenges( - challengeType: ChallengeType.POKEMON_IN_BATTLE, - pokemon: Pokemon, - valid: BooleanHolder, -): boolean; -/** - * Apply all challenges that modify what fixed battles there are. - * @param challengeType {@link ChallengeType} ChallengeType.FIXED_BATTLES - * @param waveIndex {@link Number} The current wave index. - * @param battleConfig {@link FixedBattleConfig} The battle config to modify. - * @returns True if any challenge was successfully applied. - */ -export function applyChallenges( - challengeType: ChallengeType.FIXED_BATTLES, - waveIndex: number, - battleConfig: FixedBattleConfig, -): boolean; -/** - * Apply all challenges that modify type effectiveness. - * @param challengeType {@linkcode ChallengeType} ChallengeType.TYPE_EFFECTIVENESS - * @param effectiveness {@linkcode NumberHolder} The current effectiveness of the move. - * @returns True if any challenge was successfully applied. - */ -export function applyChallenges(challengeType: ChallengeType.TYPE_EFFECTIVENESS, effectiveness: NumberHolder): boolean; -/** - * Apply all challenges that modify what level AI are. - * @param challengeType {@link ChallengeType} ChallengeType.AI_LEVEL - * @param level {@link NumberHolder} The generated level of the pokemon. - * @param levelCap {@link Number} The maximum level cap for the current wave. - * @param isTrainer {@link Boolean} Whether this is a trainer pokemon. - * @param isBoss {@link Boolean} Whether this is a non-trainer boss pokemon. - * @returns True if any challenge was successfully applied. - */ -export function applyChallenges( - challengeType: ChallengeType.AI_LEVEL, - level: NumberHolder, - levelCap: number, - isTrainer: boolean, - isBoss: boolean, -): boolean; -/** - * Apply all challenges that modify how many move slots the AI has. - * @param challengeType {@link ChallengeType} ChallengeType.AI_MOVE_SLOTS - * @param pokemon {@link Pokemon} The pokemon being considered. - * @param moveSlots {@link NumberHolder} The amount of move slots. - * @returns True if any challenge was successfully applied. - */ -export function applyChallenges( - challengeType: ChallengeType.AI_MOVE_SLOTS, - pokemon: Pokemon, - moveSlots: NumberHolder, -): boolean; -/** - * Apply all challenges that modify whether a pokemon has its passive. - * @param challengeType {@link ChallengeType} ChallengeType.PASSIVE_ACCESS - * @param pokemon {@link Pokemon} The pokemon to modify. - * @param hasPassive {@link BooleanHolder} Whether it has its passive. - * @returns True if any challenge was successfully applied. - */ -export function applyChallenges( - challengeType: ChallengeType.PASSIVE_ACCESS, - pokemon: Pokemon, - hasPassive: BooleanHolder, -): boolean; -/** - * Apply all challenges that modify the game modes settings. - * @param challengeType {@link ChallengeType} ChallengeType.GAME_MODE_MODIFY - * @returns True if any challenge was successfully applied. - */ -export function applyChallenges(challengeType: ChallengeType.GAME_MODE_MODIFY): boolean; -/** - * Apply all challenges that modify what level a pokemon can access a move. - * @param challengeType {@link ChallengeType} ChallengeType.MOVE_ACCESS - * @param pokemon {@link Pokemon} What pokemon would learn the move. - * @param moveSource {@link MoveSourceType} What source the pokemon would get the move from. - * @param move {@link MoveId} The move in question. - * @param level {@link NumberHolder} The level threshold for access. - * @returns True if any challenge was successfully applied. - */ -export function applyChallenges( - challengeType: ChallengeType.MOVE_ACCESS, - pokemon: Pokemon, - moveSource: MoveSourceType, - move: MoveId, - level: NumberHolder, -): boolean; -/** - * Apply all challenges that modify what weight a pokemon gives to move generation - * @param challengeType {@link ChallengeType} ChallengeType.MOVE_WEIGHT - * @param pokemon {@link Pokemon} What pokemon would learn the move. - * @param moveSource {@link MoveSourceType} What source the pokemon would get the move from. - * @param move {@link MoveId} The move in question. - * @param weight {@link NumberHolder} The weight of the move. - * @returns True if any challenge was successfully applied. - */ -export function applyChallenges( - challengeType: ChallengeType.MOVE_WEIGHT, - pokemon: Pokemon, - moveSource: MoveSourceType, - move: MoveId, - weight: NumberHolder, -): boolean; +export class LimitedSupportChallenge extends Challenge { + constructor() { + super(Challenges.LIMITED_SUPPORT, 3); + } -export function applyChallenges(challengeType: ChallengeType.FLIP_STAT, pokemon: Pokemon, baseStats: number[]): boolean; - -export function applyChallenges(challengeType: ChallengeType, ...args: any[]): boolean { - let ret = false; - globalScene.gameMode.challenges.forEach(c => { - if (c.value !== 0) { - switch (challengeType) { - case ChallengeType.STARTER_CHOICE: - ret ||= c.applyStarterChoice(args[0], args[1], args[2]); - break; - case ChallengeType.STARTER_POINTS: - ret ||= c.applyStarterPoints(args[0]); - break; - case ChallengeType.STARTER_COST: - ret ||= c.applyStarterCost(args[0], args[1]); - break; - case ChallengeType.STARTER_MODIFY: - ret ||= c.applyStarterModify(args[0]); - break; - case ChallengeType.POKEMON_IN_BATTLE: - ret ||= c.applyPokemonInBattle(args[0], args[1]); - break; - case ChallengeType.FIXED_BATTLES: - ret ||= c.applyFixedBattle(args[0], args[1]); - break; - case ChallengeType.TYPE_EFFECTIVENESS: - ret ||= c.applyTypeEffectiveness(args[0]); - break; - case ChallengeType.AI_LEVEL: - ret ||= c.applyLevelChange(args[0], args[1], args[2], args[3]); - break; - case ChallengeType.AI_MOVE_SLOTS: - ret ||= c.applyMoveSlot(args[0], args[1]); - break; - case ChallengeType.PASSIVE_ACCESS: - ret ||= c.applyPassiveAccess(args[0], args[1]); - break; - case ChallengeType.GAME_MODE_MODIFY: - ret ||= c.applyGameModeModify(); - break; - case ChallengeType.MOVE_ACCESS: - ret ||= c.applyMoveAccessLevel(args[0], args[1], args[2], args[3]); - break; - case ChallengeType.MOVE_WEIGHT: - ret ||= c.applyMoveWeight(args[0], args[1], args[2], args[3]); - break; - case ChallengeType.FLIP_STAT: - ret ||= c.applyFlipStat(args[0], args[1]); - break; - } + override applyPartyHeal(status: BooleanHolder): boolean { + if (status.value) { + status.value = this.value === 2; + return true; } - }); - return ret; + return false; + } + + override applyShop(status: BooleanHolder): boolean { + if (status.value) { + status.value = this.value === 1; + return true; + } + return false; + } + + static override loadChallenge(source: LimitedSupportChallenge | any): LimitedSupportChallenge { + const newChallenge = new LimitedSupportChallenge(); + newChallenge.value = source.value; + newChallenge.severity = source.severity; + return newChallenge; + } +} + +/** + * Implements a Limited Catch challenge + */ +export class LimitedCatchChallenge extends Challenge { + constructor() { + super(Challenges.LIMITED_CATCH, 1); + } + + override applyPokemonAddToParty(pokemon: EnemyPokemon, status: BooleanHolder): boolean { + if (status.value) { + status.value = pokemon.metWave % 10 === 1; + return true; + } + return false; + } + + static override loadChallenge(source: LimitedCatchChallenge | any): LimitedCatchChallenge { + const newChallenge = new LimitedCatchChallenge(); + newChallenge.value = source.value; + newChallenge.severity = source.severity; + return newChallenge; + } +} + +/** + * Implements a Permanent Faint challenge + */ +export class HardcoreChallenge extends Challenge { + constructor() { + super(Challenges.HARDCORE, 1); + } + + override applyPokemonFusion(pokemon: PlayerPokemon, status: BooleanHolder): boolean { + if (!status.value) { + status.value = pokemon.isFainted(); + return true; + } + return false; + } + + override applyShopItem(shopItem: ModifierTypeOption | null, status: BooleanHolder): boolean { + status.value = shopItem?.type.group !== "revive"; + return true; + } + + override applyWaveReward(reward: ModifierTypeOption | null, status: BooleanHolder): boolean { + return this.applyShopItem(reward, status); + } + + override applyPokemonMove(moveId: MoveId, status: BooleanHolder) { + if (status.value) { + status.value = moveId !== MoveId.REVIVAL_BLESSING; + return true; + } + return false; + } + + override applyPreventRevive(status: BooleanHolder): boolean { + if (!status.value) { + status.value = true; + return true; + } + return false; + } + + static override loadChallenge(source: HardcoreChallenge | any): HardcoreChallenge { + const newChallenge = new HardcoreChallenge(); + newChallenge.value = source.value; + newChallenge.severity = source.severity; + return newChallenge; + } } /** @@ -1089,6 +1063,12 @@ export function copyChallenge(source: Challenge | any): Challenge { return InverseBattleChallenge.loadChallenge(source); case Challenges.FLIP_STAT: return FlipStatChallenge.loadChallenge(source); + case Challenges.LIMITED_CATCH: + return LimitedCatchChallenge.loadChallenge(source); + case Challenges.LIMITED_SUPPORT: + return LimitedSupportChallenge.loadChallenge(source); + case Challenges.HARDCORE: + return HardcoreChallenge.loadChallenge(source); } throw new Error("Unknown challenge copied"); } @@ -1097,87 +1077,13 @@ export const allChallenges: Challenge[] = []; export function initChallenges() { allChallenges.push( + new FreshStartChallenge(), + new HardcoreChallenge(), + new LimitedCatchChallenge(), + new LimitedSupportChallenge(), new SingleGenerationChallenge(), new SingleTypeChallenge(), - new FreshStartChallenge(), new InverseBattleChallenge(), new FlipStatChallenge(), ); } - -/** - * Apply all challenges to the given starter (and form) to check its validity. - * Differs from {@linkcode checkSpeciesValidForChallenge} which only checks form changes. - * @param species - The {@linkcode PokemonSpecies} to check the validity of. - * @param dexAttr - The {@linkcode DexAttrProps | dex attributes} of the species, including its form index. - * @param soft - If `true`, allow it if it could become valid through evolution or form change. - * @returns `true` if the species is considered valid. - */ -export function checkStarterValidForChallenge(species: PokemonSpecies, props: DexAttrProps, soft: boolean) { - if (!soft) { - const isValidForChallenge = new BooleanHolder(true); - applyChallenges(ChallengeType.STARTER_CHOICE, species, isValidForChallenge, props); - return isValidForChallenge.value; - } - // We check the validity of every evolution and form change, and require that at least one is valid - const speciesToCheck = [species.speciesId]; - while (speciesToCheck.length) { - const checking = speciesToCheck.pop(); - // Linter complains if we don't handle this - if (!checking) { - return false; - } - const checkingSpecies = getPokemonSpecies(checking); - if (checkSpeciesValidForChallenge(checkingSpecies, props, true)) { - return true; - } - if (checking && pokemonEvolutions.hasOwnProperty(checking)) { - pokemonEvolutions[checking].forEach(e => { - // Form check to deal with cases such as Basculin -> Basculegion - // TODO: does this miss anything if checking forms of a stage 2 Pokémon? - if (!e?.preFormKey || e.preFormKey === species.forms[props.formIndex].formKey) { - speciesToCheck.push(e.speciesId); - } - }); - } - } - return false; -} - -/** - * Apply all challenges to the given species (and form) to check its validity. - * Differs from {@linkcode checkStarterValidForChallenge} which also checks evolutions. - * @param species - The {@linkcode PokemonSpecies} to check the validity of. - * @param dexAttr - The {@linkcode DexAttrProps | dex attributes} of the species, including its form index. - * @param soft - If `true`, allow it if it could become valid through a form change. - * @returns `true` if the species is considered valid. - */ -function checkSpeciesValidForChallenge(species: PokemonSpecies, props: DexAttrProps, soft: boolean) { - const isValidForChallenge = new BooleanHolder(true); - applyChallenges(ChallengeType.STARTER_CHOICE, species, isValidForChallenge, props); - if (!soft || !pokemonFormChanges.hasOwnProperty(species.speciesId)) { - return isValidForChallenge.value; - } - // If the form in props is valid, return true before checking other form changes - if (soft && isValidForChallenge.value) { - return true; - } - - const result = pokemonFormChanges[species.speciesId].some(f1 => { - // Exclude form changes that require the mon to be on the field to begin with - if (!("item" in f1.trigger)) { - return false; - } - - return species.forms.some((f2, formIndex) => { - if (f1.formKey === f2.formKey) { - const formProps = { ...props, formIndex }; - const isFormValidForChallenge = new BooleanHolder(true); - applyChallenges(ChallengeType.STARTER_CHOICE, species, isFormValidForChallenge, formProps); - return isFormValidForChallenge.value; - } - return false; - }); - }); - return result; -} diff --git a/src/data/moves/move.ts b/src/data/moves/move.ts index 757f57e0b1f..067bd05c2ae 100644 --- a/src/data/moves/move.ts +++ b/src/data/moves/move.ts @@ -22,7 +22,6 @@ import { TypeBoostTag, } from "#data/battler-tags"; import { getBerryEffectFunc } from "#data/berry"; -import { applyChallenges } from "#data/challenge"; import { allAbilities, allMoves } from "#data/data-lists"; import { SpeciesFormChangeRevertWeatherFormTrigger } from "#data/form-change-triggers"; import { DelayedAttackTag } from "#data/positional-tags/positional-tag"; @@ -93,6 +92,7 @@ import { BooleanHolder, type Constructor, isNullOrUndefined, NumberHolder, randS import { getEnumValues } from "#utils/enums"; import { toTitleCase } from "#utils/strings"; import i18next from "i18next"; +import { applyChallenges } from "#utils/challenge-utils"; /** * A function used to conditionally determine execution of a given {@linkcode MoveAttr}. @@ -124,7 +124,7 @@ export abstract class Move implements Localizable { /** * Check if the move is of the given subclass without requiring `instanceof`. * - * ⚠️ Does _not_ work for {@linkcode ChargingAttackMove} and {@linkcode ChargingSelfStatusMove} subclasses. For those, + * ! Does _not_ work for {@linkcode ChargingAttackMove} and {@linkcode ChargingSelfStatusMove} subclasses. For those, * use {@linkcode isChargingMove} instead. * * @param moveKind - The string name of the move to check against @@ -6906,13 +6906,19 @@ export class RandomMoveAttr extends CallMoveAttr { * @param move Move being used * @param args Unused */ - override apply(user: Pokemon, target: Pokemon, move: Move, args: any[]): boolean { + override apply(user: Pokemon, target: Pokemon, _move: Move, args: any[]): boolean { + // TODO: Move this into the constructor to avoid constructing this every call const moveIds = getEnumValues(MoveId).map(m => !this.invalidMoves.has(m) && !allMoves[m].name.endsWith(" (N)") ? m : MoveId.NONE); let moveId: MoveId = MoveId.NONE; + const moveStatus = new BooleanHolder(true); do { moveId = this.getMoveOverride() ?? moveIds[user.randBattleSeedInt(moveIds.length)]; + moveStatus.value = moveId !== MoveId.NONE; + if (user.isPlayer()) { + applyChallenges(ChallengeType.POKEMON_MOVE, moveId, moveStatus); + } } - while (moveId === MoveId.NONE); + while (!moveStatus.value); return super.apply(user, target, allMoves[moveId], args); } } diff --git a/src/data/moves/pokemon-move.ts b/src/data/moves/pokemon-move.ts index d3f68fe9db4..3c96cbea598 100644 --- a/src/data/moves/pokemon-move.ts +++ b/src/data/moves/pokemon-move.ts @@ -1,8 +1,10 @@ import { allMoves } from "#data/data-lists"; -import type { MoveId } from "#enums/move-id"; +import { ChallengeType } from "#enums/challenge-type"; +import { MoveId } from "#enums/move-id"; import type { Pokemon } from "#field/pokemon"; import type { Move } from "#moves/move"; -import { toDmgValue } from "#utils/common"; +import { applyChallenges } from "#utils/challenge-utils"; +import { BooleanHolder, toDmgValue } from "#utils/common"; /** * Wrapper class for the {@linkcode Move} class for Pokemon to interact with. @@ -45,16 +47,18 @@ export class PokemonMove { * @returns Whether this {@linkcode PokemonMove} can be selected by this Pokemon. */ isUsable(pokemon: Pokemon, ignorePp = false, ignoreRestrictionTags = false): boolean { + const move = this.getMove(); // TODO: Add Sky Drop's 1 turn stall - if (this.moveId && !ignoreRestrictionTags && pokemon.isMoveRestricted(this.moveId, pokemon)) { - return false; + const usability = new BooleanHolder( + !move.name.endsWith(" (N)") && + (ignorePp || this.ppUsed < this.getMovePp() || move.pp === -1) && + // TODO: Review if the `MoveId.NONE` check is even necessary anymore + !(this.moveId !== MoveId.NONE && !ignoreRestrictionTags && pokemon.isMoveRestricted(this.moveId, pokemon)), + ); + if (pokemon.isPlayer()) { + applyChallenges(ChallengeType.POKEMON_MOVE, move.id, usability); } - - if (this.getMove().name.endsWith(" (N)")) { - return false; - } - - return ignorePp || this.ppUsed < this.getMovePp() || this.getMove().pp === -1; + return usability.value; } getMove(): Move { diff --git a/src/data/mystery-encounters/encounters/bug-type-superfan-encounter.ts b/src/data/mystery-encounters/encounters/bug-type-superfan-encounter.ts index d60ebe690ac..d86f1511207 100644 --- a/src/data/mystery-encounters/encounters/bug-type-superfan-encounter.ts +++ b/src/data/mystery-encounters/encounters/bug-type-superfan-encounter.ts @@ -715,15 +715,13 @@ function doBugTypeMoveTutor(): Promise { const moveOptions = globalScene.currentBattle.mysteryEncounter!.misc.moveTutorOptions; await showEncounterDialogue(`${namespace}:battle_won`, `${namespace}:speaker`); - const overlayScale = 1; const moveInfoOverlay = new MoveInfoOverlay({ delayVisibility: false, - scale: overlayScale, onSide: true, right: true, x: 1, - y: -MoveInfoOverlay.getHeight(overlayScale, true) - 1, - width: globalScene.game.canvas.width / 6 - 2, + y: -MoveInfoOverlay.getHeight(true) - 1, + width: globalScene.scaledCanvas.width - 2, }); globalScene.ui.add(moveInfoOverlay); diff --git a/src/data/mystery-encounters/encounters/global-trade-system-encounter.ts b/src/data/mystery-encounters/encounters/global-trade-system-encounter.ts index 347092fe0b4..083a6d06801 100644 --- a/src/data/mystery-encounters/encounters/global-trade-system-encounter.ts +++ b/src/data/mystery-encounters/encounters/global-trade-system-encounter.ts @@ -564,14 +564,14 @@ function generateTradeOption(alreadyUsedSpecies: PokemonSpecies[], originalBst?: function showTradeBackground() { return new Promise(resolve => { - const tradeContainer = globalScene.add.container(0, -globalScene.game.canvas.height / 6); + const tradeContainer = globalScene.add.container(0, -globalScene.scaledCanvas.height); tradeContainer.setName("Trade Background"); const flyByStaticBg = globalScene.add.rectangle( 0, 0, - globalScene.game.canvas.width / 6, - globalScene.game.canvas.height / 6, + globalScene.scaledCanvas.width, + globalScene.scaledCanvas.height, 0, ); flyByStaticBg.setName("Black Background"); diff --git a/src/data/mystery-encounters/encounters/weird-dream-encounter.ts b/src/data/mystery-encounters/encounters/weird-dream-encounter.ts index b4a2ba4abd5..57b066e2ba2 100644 --- a/src/data/mystery-encounters/encounters/weird-dream-encounter.ts +++ b/src/data/mystery-encounters/encounters/weird-dream-encounter.ts @@ -649,15 +649,15 @@ function getTransformedSpecies( } function doShowDreamBackground() { - const transformationContainer = globalScene.add.container(0, -globalScene.game.canvas.height / 6); + const transformationContainer = globalScene.add.container(0, -globalScene.scaledCanvas.height); transformationContainer.name = "Dream Background"; // In case it takes a bit for video to load const transformationStaticBg = globalScene.add.rectangle( 0, 0, - globalScene.game.canvas.width / 6, - globalScene.game.canvas.height / 6, + globalScene.scaledCanvas.width, + globalScene.scaledCanvas.height, 0, ); transformationStaticBg.setName("Black Background"); diff --git a/src/data/mystery-encounters/utils/encounter-pokemon-utils.ts b/src/data/mystery-encounters/utils/encounter-pokemon-utils.ts index 19f06707257..7617fb5a89e 100644 --- a/src/data/mystery-encounters/utils/encounter-pokemon-utils.ts +++ b/src/data/mystery-encounters/utils/encounter-pokemon-utils.ts @@ -13,6 +13,7 @@ import { CustomPokemonData } from "#data/pokemon-data"; import type { PokemonSpecies } from "#data/pokemon-species"; import { getStatusEffectCatchRateMultiplier } from "#data/status-effect"; import type { AbilityId } from "#enums/ability-id"; +import { ChallengeType } from "#enums/challenge-type"; import { PlayerGender } from "#enums/player-gender"; import type { PokeballType } from "#enums/pokeball"; import type { PokemonType } from "#enums/pokemon-type"; @@ -33,7 +34,8 @@ import { achvs } from "#system/achv"; import type { PartyOption } from "#ui/party-ui-handler"; import { PartyUiMode } from "#ui/party-ui-handler"; import { SummaryUiMode } from "#ui/summary-ui-handler"; -import { isNullOrUndefined, randSeedInt } from "#utils/common"; +import { applyChallenges } from "#utils/challenge-utils"; +import { BooleanHolder, isNullOrUndefined, randSeedInt } from "#utils/common"; import { getPokemonSpecies } from "#utils/pokemon-utils"; import i18next from "i18next"; @@ -706,6 +708,13 @@ export async function catchPokemon( }); }; Promise.all([pokemon.hideInfo(), globalScene.gameData.setPokemonCaught(pokemon)]).then(() => { + const addStatus = new BooleanHolder(true); + applyChallenges(ChallengeType.POKEMON_ADD_TO_PARTY, pokemon, addStatus); + if (!addStatus.value) { + removePokemon(); + end(); + return; + } if (globalScene.getPlayerParty().length === 6) { const promptRelease = () => { globalScene.ui.showText( diff --git a/src/enums/challenge-type.ts b/src/enums/challenge-type.ts index d9b1fce3e6e..053bcf92011 100644 --- a/src/enums/challenge-type.ts +++ b/src/enums/challenge-type.ts @@ -65,5 +65,45 @@ export enum ChallengeType { /** * Modifies what the pokemon stats for Flip Stat Mode. */ - FLIP_STAT + FLIP_STAT, + /** + * Challenges which conditionally enable or disable automatic party healing during biome transitions + * @see {@linkcode Challenge.applyPartyHealAvailability} + */ + PARTY_HEAL, + /** + * Challenges which conditionally enable or disable the shop + * @see {@linkcode Challenge.applyShopAvailability} + */ + SHOP, + /** + * Challenges which validate whether a pokemon can be added to the player's party or not + * @see {@linkcode Challenge.applyCatchAvailability} + */ + POKEMON_ADD_TO_PARTY, + /** + * Challenges which validate whether a pokemon is allowed to fuse or not + * @see {@linkcode Challenge.applyFusionAvailability} + */ + POKEMON_FUSION, + /** + * Challenges which validate whether particular moves can or cannot be used + * @see {@linkcode Challenge.applyMoveAvailability} + */ + POKEMON_MOVE, + /** + * Challenges which validate whether particular items are or are not sold in the shop + * @see {@linkcode Challenge.applyShopItems} + */ + SHOP_ITEM, + /** + * Challenges which validate whether particular items will be given as a reward after a wave + * @see {@linkcode Challenge.applyWaveRewards} + */ + WAVE_REWARD, + /** + * Challenges which prevent recovery from fainting + * @see {@linkcode Challenge.applyPermanentFaint} + */ + PREVENT_REVIVE, } diff --git a/src/enums/challenges.ts b/src/enums/challenges.ts index 7b506a61a2f..8d4f4c7a22a 100644 --- a/src/enums/challenges.ts +++ b/src/enums/challenges.ts @@ -6,4 +6,7 @@ export enum Challenges { FRESH_START, INVERSE_BATTLE, FLIP_STAT, + LIMITED_CATCH, + LIMITED_SUPPORT, + HARDCORE, } diff --git a/src/field/damage-number-handler.ts b/src/field/damage-number-handler.ts index 1bbacc19566..112b08631b0 100644 --- a/src/field/damage-number-handler.ts +++ b/src/field/damage-number-handler.ts @@ -30,7 +30,7 @@ export class DamageNumberHandler { const baseScale = target.getSpriteScale() / 6; const damageNumber = addTextObject( target.x, - -(globalScene.game.canvas.height / 6) + target.y - target.getSprite().height / 2, + -globalScene.scaledCanvas.height + target.y - target.getSprite().height / 2, formatStat(amount, true), TextStyle.SUMMARY, ); diff --git a/src/field/pokemon.ts b/src/field/pokemon.ts index d4f332d887c..bc069aa1fc2 100644 --- a/src/field/pokemon.ts +++ b/src/field/pokemon.ts @@ -39,7 +39,6 @@ import { TrappedTag, TypeImmuneTag, } from "#data/battler-tags"; -import { applyChallenges } from "#data/challenge"; import { allAbilities, allMoves } from "#data/data-lists"; import { getLevelTotalExp } from "#data/exp"; import { @@ -148,6 +147,7 @@ import { EnemyBattleInfo } from "#ui/enemy-battle-info"; import type { PartyOption } from "#ui/party-ui-handler"; import { PartyUiHandler, PartyUiMode } from "#ui/party-ui-handler"; import { PlayerBattleInfo } from "#ui/player-battle-info"; +import { applyChallenges } from "#utils/challenge-utils"; import { BooleanHolder, type Constructor, @@ -4558,8 +4558,17 @@ export abstract class Pokemon extends Phaser.GameObjects.Container { } const key = this.species.getCryKey(this.formIndex); - let rate = 0.85; - const cry = globalScene.playSound(key, { rate: rate }) as AnySound; + const crySoundConfig = { rate: 0.85, detune: 0 }; + if (this.isPlayer()) { + // If fainting is permanent, emphasize impact + const preventRevive = new BooleanHolder(false); + applyChallenges(ChallengeType.PREVENT_REVIVE, preventRevive); + if (preventRevive.value) { + crySoundConfig.detune = -100; + crySoundConfig.rate = 0.7; + } + } + const cry = globalScene.playSound(key, crySoundConfig) as AnySound; if (!cry || globalScene.fieldVolume === 0) { callback(); return; @@ -4578,7 +4587,7 @@ export abstract class Pokemon extends Phaser.GameObjects.Container { delay: fixedInt(delay), repeat: -1, callback: () => { - frameThreshold = sprite.anims.msPerFrame / rate; + frameThreshold = sprite.anims.msPerFrame / crySoundConfig.rate; frameProgress += delay; while (frameProgress > frameThreshold) { if (sprite.anims.duration) { @@ -4588,8 +4597,7 @@ export abstract class Pokemon extends Phaser.GameObjects.Container { frameProgress -= frameThreshold; } if (cry && !cry.pendingRemove) { - rate *= 0.99; - cry.setRate(rate); + cry.setRate(crySoundConfig.rate * 0.99); } else { faintCryTimer?.destroy(); faintCryTimer = null; diff --git a/src/game-mode.ts b/src/game-mode.ts index c5ab120e218..82f7b4fa77f 100644 --- a/src/game-mode.ts +++ b/src/game-mode.ts @@ -2,8 +2,7 @@ import { FixedBattleConfig } from "#app/battle"; import { CHALLENGE_MODE_MYSTERY_ENCOUNTER_WAVES, CLASSIC_MODE_MYSTERY_ENCOUNTER_WAVES } from "#app/constants"; import { globalScene } from "#app/global-scene"; import Overrides from "#app/overrides"; -import type { Challenge } from "#data/challenge"; -import { allChallenges, applyChallenges, copyChallenge } from "#data/challenge"; +import { allChallenges, type Challenge, copyChallenge } from "#data/challenge"; import { getDailyStartingBiome } from "#data/daily-run"; import { allSpecies } from "#data/data-lists"; import type { PokemonSpecies } from "#data/pokemon-species"; @@ -14,7 +13,8 @@ import { GameModes } from "#enums/game-modes"; import { SpeciesId } from "#enums/species-id"; import type { Arena } from "#field/arena"; import { classicFixedBattles, type FixedBattleConfigs } from "#trainers/fixed-battle-configs"; -import { isNullOrUndefined, randSeedInt, randSeedItem } from "#utils/common"; +import { applyChallenges } from "#utils/challenge-utils"; +import { BooleanHolder, isNullOrUndefined, randSeedInt, randSeedItem } from "#utils/common"; import i18next from "i18next"; interface GameModeConfig { @@ -311,6 +311,16 @@ export class GameMode implements GameModeConfig { return this.battleConfig[waveIndex]; } + /** + * Check if the current game mode has the shop enabled or not + * @returns Whether the shop is available in the current mode + */ + public getShopStatus(): boolean { + const status = new BooleanHolder(!this.hasNoShop); + applyChallenges(ChallengeType.SHOP, status); + return status.value; + } + getClearScoreBonus(): number { switch (this.modeId) { case GameModes.CLASSIC: diff --git a/src/modifier/modifier-type.ts b/src/modifier/modifier-type.ts index b359ec756e6..9ec9ee0ad48 100644 --- a/src/modifier/modifier-type.ts +++ b/src/modifier/modifier-type.ts @@ -14,6 +14,7 @@ import { pokemonFormChanges, SpeciesFormChangeCondition } from "#data/pokemon-fo import { getStatusEffectDescriptor } from "#data/status-effect"; import { BattlerTagType } from "#enums/battler-tag-type"; import { BerryType } from "#enums/berry-type"; +import { ChallengeType } from "#enums/challenge-type"; import { FormChangeItem } from "#enums/form-change-item"; import { ModifierPoolType } from "#enums/modifier-pool-type"; import { ModifierTier } from "#enums/modifier-tier"; @@ -116,7 +117,16 @@ import type { ModifierTypeFunc, WeightedModifierTypeWeightFunc } from "#types/mo import type { PokemonMoveSelectFilter, PokemonSelectFilter } from "#ui/party-ui-handler"; import { PartyUiHandler } from "#ui/party-ui-handler"; import { getModifierTierTextTint } from "#ui/text"; -import { formatMoney, isNullOrUndefined, NumberHolder, padInt, randSeedInt, randSeedItem } from "#utils/common"; +import { applyChallenges } from "#utils/challenge-utils"; +import { + BooleanHolder, + formatMoney, + isNullOrUndefined, + NumberHolder, + padInt, + randSeedInt, + randSeedItem, +} from "#utils/common"; import { getEnumKeys, getEnumValues } from "#utils/enums"; import { getModifierPoolForType, getModifierType } from "#utils/modifier-utils"; import i18next from "i18next"; @@ -533,7 +543,9 @@ export class PokemonReviveModifierType extends PokemonHpRestoreModifierType { ); this.selectFilter = (pokemon: PlayerPokemon) => { - if (pokemon.hp) { + const selectStatus = new BooleanHolder(pokemon.hp !== 0); + applyChallenges(ChallengeType.PREVENT_REVIVE, selectStatus); + if (selectStatus.value) { return PartyUiHandler.NoEffectMessage; } return null; @@ -1011,6 +1023,7 @@ class AllPokemonFullReviveModifierType extends AllPokemonFullHpRestoreModifierTy "modifierType:ModifierType.AllPokemonFullReviveModifierType", (_type, _args) => new PokemonHpRestoreModifier(this, -1, 0, 100, false, true), ); + this.group = "revive"; } } @@ -1262,7 +1275,9 @@ export class FusePokemonModifierType extends PokemonModifierType { iconImage, (_type, args) => new FusePokemonModifier(this, (args[0] as PlayerPokemon).id, (args[1] as PlayerPokemon).id), (pokemon: PlayerPokemon) => { - if (pokemon.isFusion()) { + const selectStatus = new BooleanHolder(pokemon.isFusion()); + applyChallenges(ChallengeType.POKEMON_FUSION, pokemon, selectStatus); + if (selectStatus.value) { return PartyUiHandler.NoEffectMessage; } return null; @@ -2574,11 +2589,15 @@ function getModifierTypeOptionWithRetry( ): ModifierTypeOption { allowLuckUpgrades = allowLuckUpgrades ?? true; let candidate = getNewModifierTypeOption(party, ModifierPoolType.PLAYER, tier, undefined, 0, allowLuckUpgrades); + const candidateValidity = new BooleanHolder(true); + applyChallenges(ChallengeType.WAVE_REWARD, candidate, candidateValidity); let r = 0; 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) || + !candidateValidity.value ) { candidate = getNewModifierTypeOption( party, @@ -2588,6 +2607,7 @@ function getModifierTypeOptionWithRetry( 0, allowLuckUpgrades, ); + applyChallenges(ChallengeType.WAVE_REWARD, candidate, candidateValidity); } return candidate!; } @@ -2648,7 +2668,15 @@ export function getPlayerShopModifierTypeOptionsForWave(waveIndex: number, baseC [new ModifierTypeOption(modifierTypeInitObj.FULL_RESTORE(), 0, baseCost * 2.25)], [new ModifierTypeOption(modifierTypeInitObj.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(shopItem => { + const status = new BooleanHolder(true); + applyChallenges(ChallengeType.SHOP_ITEM, shopItem, status); + return status.value; + }); } export function getEnemyBuffModifierForWave( diff --git a/src/phases/attempt-capture-phase.ts b/src/phases/attempt-capture-phase.ts index fcddd23dd20..aea39cff294 100644 --- a/src/phases/attempt-capture-phase.ts +++ b/src/phases/attempt-capture-phase.ts @@ -12,6 +12,7 @@ import { } from "#data/pokeball"; import { getStatusEffectCatchRateMultiplier } from "#data/status-effect"; import { BattlerIndex } from "#enums/battler-index"; +import { ChallengeType } from "#enums/challenge-type"; import type { PokeballType } from "#enums/pokeball"; import { StatusEffect } from "#enums/status-effect"; import { UiMode } from "#enums/ui-mode"; @@ -23,6 +24,8 @@ import { achvs } from "#system/achv"; import type { PartyOption } from "#ui/party-ui-handler"; import { PartyUiMode } from "#ui/party-ui-handler"; import { SummaryUiMode } from "#ui/summary-ui-handler"; +import { applyChallenges } from "#utils/challenge-utils"; +import { BooleanHolder } from "#utils/common"; import i18next from "i18next"; // TODO: Refactor and split up to allow for overriding capture chance @@ -287,6 +290,13 @@ export class AttemptCapturePhase extends PokemonPhase { }); }; Promise.all([pokemon.hideInfo(), globalScene.gameData.setPokemonCaught(pokemon)]).then(() => { + const addStatus = new BooleanHolder(true); + applyChallenges(ChallengeType.POKEMON_ADD_TO_PARTY, pokemon, addStatus); + if (!addStatus.value) { + removePokemon(); + end(); + return; + } if (globalScene.getPlayerParty().length === PLAYER_PARTY_MAX_SIZE) { const promptRelease = () => { globalScene.ui.showText( diff --git a/src/phases/command-phase.ts b/src/phases/command-phase.ts index 016d4ff5d3b..c7eb466157d 100644 --- a/src/phases/command-phase.ts +++ b/src/phases/command-phase.ts @@ -9,6 +9,7 @@ import { ArenaTagType } from "#enums/arena-tag-type"; import { BattleType } from "#enums/battle-type"; import { BattlerTagType } from "#enums/battler-tag-type"; import { BiomeId } from "#enums/biome-id"; +import { ChallengeType } from "#enums/challenge-type"; import { Command } from "#enums/command"; import { FieldPosition } from "#enums/field-position"; import { MoveId } from "#enums/move-id"; @@ -21,6 +22,8 @@ import type { MoveTargetSet } from "#moves/move"; import { getMoveTargets } from "#moves/move-utils"; import { FieldPhase } from "#phases/field-phase"; import type { TurnMove } from "#types/turn-move"; +import { applyChallenges } from "#utils/challenge-utils"; +import { BooleanHolder } from "#utils/common"; import i18next from "i18next"; export class CommandPhase extends FieldPhase { @@ -210,16 +213,27 @@ export class CommandPhase extends FieldPhase { const move = user.getMoveset()[cursor]; globalScene.ui.setMode(UiMode.MESSAGE); - // Decides between a Disabled, Not Implemented, or No PP translation message - const errorMessage = user.isMoveRestricted(move.moveId, user) - ? user.getRestrictingTag(move.moveId, user)!.selectionDeniedText(user, move.moveId) - : move.getName().endsWith(" (N)") - ? "battle:moveNotImplemented" - : "battle:moveNoPP"; + // Set the translation key for why the move cannot be selected + let cannotSelectKey: string; + const moveStatus = new BooleanHolder(true); + applyChallenges(ChallengeType.POKEMON_MOVE, move.moveId, moveStatus); + if (!moveStatus.value) { + cannotSelectKey = "battle:moveCannotUseChallenge"; + } else if (move.getPpRatio() === 0) { + cannotSelectKey = "battle:moveNoPP"; + } else if (move.getName().endsWith(" (N)")) { + cannotSelectKey = "battle:moveNotImplemented"; + } else if (user.isMoveRestricted(move.moveId, user)) { + cannotSelectKey = user.getRestrictingTag(move.moveId, user)!.selectionDeniedText(user, move.moveId); + } else { + // TODO: Consider a message that signals a being unusable for an unknown reason + cannotSelectKey = ""; + } + const moveName = move.getName().replace(" (N)", ""); // Trims off the indicator globalScene.ui.showText( - i18next.t(errorMessage, { moveName: moveName }), + i18next.t(cannotSelectKey, { moveName: moveName }), null, () => { globalScene.ui.clearText(); diff --git a/src/phases/egg-hatch-phase.ts b/src/phases/egg-hatch-phase.ts index 94923ae8c1f..e9d28e0fe2a 100644 --- a/src/phases/egg-hatch-phase.ts +++ b/src/phases/egg-hatch-phase.ts @@ -148,9 +148,9 @@ export class EggHatchPhase extends Phase { this.eggHatchOverlay = globalScene.add.rectangle( 0, - -globalScene.game.canvas.height / 6, - globalScene.game.canvas.width / 6, - globalScene.game.canvas.height / 6, + -globalScene.scaledCanvas.height, + globalScene.scaledCanvas.width, + globalScene.scaledCanvas.height, 0xffffff, ); this.eggHatchOverlay.setOrigin(0, 0); diff --git a/src/phases/end-card-phase.ts b/src/phases/end-card-phase.ts index b9b383db13d..5ce0ca47b99 100644 --- a/src/phases/end-card-phase.ts +++ b/src/phases/end-card-phase.ts @@ -25,8 +25,8 @@ export class EndCardPhase extends Phase { globalScene.field.add(this.endCard); this.text = addTextObject( - globalScene.game.canvas.width / 12, - globalScene.game.canvas.height / 6 - 16, + globalScene.scaledCanvas.width / 2, + globalScene.scaledCanvas.height - 16, i18next.t("battle:congratulations"), TextStyle.SUMMARY, { fontSize: "128px" }, diff --git a/src/phases/evolution-phase.ts b/src/phases/evolution-phase.ts index cad79455af3..ad3db97d520 100644 --- a/src/phases/evolution-phase.ts +++ b/src/phases/evolution-phase.ts @@ -90,16 +90,16 @@ export class EvolutionPhase extends Phase { .setVisible(false); this.evolutionBgOverlay = globalScene.add - .rectangle(0, 0, globalScene.game.canvas.width / 6, globalScene.game.canvas.height / 6, 0x262626) + .rectangle(0, 0, globalScene.scaledCanvas.width, globalScene.scaledCanvas.height, 0x262626) .setOrigin(0) .setAlpha(0); this.evolutionContainer.add([this.evolutionBaseBg, this.evolutionBgOverlay, this.evolutionBg]); this.evolutionOverlay = globalScene.add.rectangle( 0, - -globalScene.game.canvas.height / 6, - globalScene.game.canvas.width / 6, - globalScene.game.canvas.height / 6 - 48, + -globalScene.scaledCanvas.height, + globalScene.scaledCanvas.width, + globalScene.scaledCanvas.height - 48, 0xffffff, ); this.evolutionOverlay.setOrigin(0).setAlpha(0); diff --git a/src/phases/party-heal-phase.ts b/src/phases/party-heal-phase.ts index 80d8b315102..1030d5eb9d9 100644 --- a/src/phases/party-heal-phase.ts +++ b/src/phases/party-heal-phase.ts @@ -1,6 +1,8 @@ import { globalScene } from "#app/global-scene"; +import { ChallengeType } from "#enums/challenge-type"; import { BattlePhase } from "#phases/battle-phase"; -import { fixedInt } from "#utils/common"; +import { applyChallenges } from "#utils/challenge-utils"; +import { BooleanHolder, fixedInt } from "#utils/common"; export class PartyHealPhase extends BattlePhase { public readonly phaseName = "PartyHealPhase"; @@ -20,7 +22,14 @@ export class PartyHealPhase extends BattlePhase { globalScene.fadeOutBgm(1000, false); } globalScene.ui.fadeOut(1000).then(() => { + const preventRevive = new BooleanHolder(false); + applyChallenges(ChallengeType.PREVENT_REVIVE, preventRevive); for (const pokemon of globalScene.getPlayerParty()) { + // Prevent reviving fainted pokemon during certain challenges + if (pokemon.isFainted() && preventRevive.value) { + continue; + } + pokemon.hp = pokemon.getMaxHp(); pokemon.resetStatus(true, false, false, true); for (const move of pokemon.moveset) { diff --git a/src/phases/select-biome-phase.ts b/src/phases/select-biome-phase.ts index ab96bf5c45e..f4fd11636fc 100644 --- a/src/phases/select-biome-phase.ts +++ b/src/phases/select-biome-phase.ts @@ -1,11 +1,13 @@ import { globalScene } from "#app/global-scene"; import { biomeLinks, getBiomeName } from "#balance/biomes"; import { BiomeId } from "#enums/biome-id"; +import { ChallengeType } from "#enums/challenge-type"; import { UiMode } from "#enums/ui-mode"; import { MapModifier, MoneyInterestModifier } from "#modifiers/modifier"; import { BattlePhase } from "#phases/battle-phase"; import type { OptionSelectItem } from "#ui/abstact-option-select-ui-handler"; -import { randSeedInt } from "#utils/common"; +import { applyChallenges } from "#utils/challenge-utils"; +import { BooleanHolder, randSeedInt } from "#utils/common"; export class SelectBiomePhase extends BattlePhase { public readonly phaseName = "SelectBiomePhase"; @@ -20,7 +22,11 @@ export class SelectBiomePhase extends BattlePhase { const setNextBiome = (nextBiome: BiomeId) => { if (nextWaveIndex % 10 === 1) { globalScene.applyModifiers(MoneyInterestModifier, true); - globalScene.phaseManager.unshiftNew("PartyHealPhase", false); + const healStatus = new BooleanHolder(true); + applyChallenges(ChallengeType.PARTY_HEAL, healStatus); + if (healStatus.value) { + globalScene.phaseManager.unshiftNew("PartyHealPhase", false); + } } globalScene.phaseManager.unshiftNew("SwitchBiomePhase", nextBiome); this.end(); diff --git a/src/phases/select-starter-phase.ts b/src/phases/select-starter-phase.ts index d6bd252c77d..ef3fa74bd44 100644 --- a/src/phases/select-starter-phase.ts +++ b/src/phases/select-starter-phase.ts @@ -1,7 +1,6 @@ import { globalScene } from "#app/global-scene"; import Overrides from "#app/overrides"; import { Phase } from "#app/phase"; -import { applyChallenges } from "#data/challenge"; import { SpeciesFormChangeMoveLearnedTrigger } from "#data/form-change-triggers"; import { Gender } from "#data/gender"; import { ChallengeType } from "#enums/challenge-type"; @@ -10,6 +9,7 @@ import { UiMode } from "#enums/ui-mode"; import { overrideHeldItems, overrideModifiers } from "#modifiers/modifier"; import { SaveSlotUiMode } from "#ui/save-slot-select-ui-handler"; import type { Starter } from "#ui/starter-select-ui-handler"; +import { applyChallenges } from "#utils/challenge-utils"; import { isNullOrUndefined } from "#utils/common"; import { getPokemonSpecies } from "#utils/pokemon-utils"; import SoundFade from "phaser3-rex-plugins/plugins/soundfade"; diff --git a/src/phases/victory-phase.ts b/src/phases/victory-phase.ts index 4b1a79d7443..c0f4a32d7e1 100644 --- a/src/phases/victory-phase.ts +++ b/src/phases/victory-phase.ts @@ -3,10 +3,13 @@ import { globalScene } from "#app/global-scene"; import { modifierTypes } from "#data/data-lists"; import { BattleType } from "#enums/battle-type"; import type { BattlerIndex } from "#enums/battler-index"; +import { ChallengeType } from "#enums/challenge-type"; import { ClassicFixedBossWaves } from "#enums/fixed-boss-waves"; import type { CustomModifierSettings } from "#modifiers/modifier-type"; import { handleMysteryEncounterVictory } from "#mystery-encounters/encounter-phase-utils"; import { PokemonPhase } from "#phases/pokemon-phase"; +import { applyChallenges } from "#utils/challenge-utils"; +import { BooleanHolder } from "#utils/common"; export class VictoryPhase extends PokemonPhase { public readonly phaseName = "VictoryPhase"; @@ -63,7 +66,9 @@ export class VictoryPhase extends PokemonPhase { break; } } - if (globalScene.currentBattle.waveIndex % 10) { + const healStatus = new BooleanHolder(globalScene.currentBattle.waveIndex % 10 === 0); + applyChallenges(ChallengeType.PARTY_HEAL, healStatus); + if (!healStatus.value) { globalScene.phaseManager.pushNew( "SelectModifierPhase", undefined, diff --git a/src/system/achv.ts b/src/system/achv.ts index 69eade02e35..383d07252e6 100644 --- a/src/system/achv.ts +++ b/src/system/achv.ts @@ -5,6 +5,7 @@ import { FlipStatChallenge, FreshStartChallenge, InverseBattleChallenge, + LimitedCatchChallenge, SingleGenerationChallenge, SingleTypeChallenge, } from "#data/challenge"; @@ -922,6 +923,19 @@ export const achvs = { c.value > 0 && globalScene.gameMode.challenges.some(c => c.id === Challenges.INVERSE_BATTLE && c.value > 0), ).setSecret(), + // TODO: Decide on icon + NUZLOCKE: new ChallengeAchv( + "NUZLOCKE", + "", + "NUZLOCKE.description", + "leaf_stone", + 100, + c => + c instanceof LimitedCatchChallenge && + c.value > 0 && + globalScene.gameMode.challenges.some(c => c.id === Challenges.HARDCORE && c.value > 0) && + globalScene.gameMode.challenges.some(c => c.id === Challenges.FRESH_START && c.value > 0), + ), BREEDERS_IN_SPACE: new Achv("BREEDERS_IN_SPACE", "", "BREEDERS_IN_SPACE.description", "moon_stone", 50).setSecret(), }; diff --git a/src/system/game-data.ts b/src/system/game-data.ts index d899afa19ef..ae559072e35 100644 --- a/src/system/game-data.ts +++ b/src/system/game-data.ts @@ -11,7 +11,6 @@ import { speciesEggMoves } from "#balance/egg-moves"; import { pokemonPrevolutions } from "#balance/pokemon-evolutions"; import { speciesStarterCosts } from "#balance/starters"; import { ArenaTrapTag } from "#data/arena-tag"; -import { applyChallenges } from "#data/challenge"; import { allMoves, allSpecies } from "#data/data-lists"; import type { Egg } from "#data/egg"; import { pokemonFormChanges } from "#data/pokemon-forms"; @@ -63,6 +62,7 @@ import { VoucherType, vouchers } from "#system/voucher"; import { trainerConfigs } from "#trainers/trainer-config"; import type { DexData, DexEntry } from "#types/dex-data"; import { RUN_HISTORY_LIMIT } from "#ui/run-history-ui-handler"; +import { applyChallenges } from "#utils/challenge-utils"; import { executeIf, fixedInt, isLocal, NumberHolder, randInt, randSeedItem } from "#utils/common"; import { decrypt, encrypt } from "#utils/data"; import { getEnumKeys } from "#utils/enums"; diff --git a/src/timed-event-manager.ts b/src/timed-event-manager.ts index 136467ad826..ed92a1c9ca5 100644 --- a/src/timed-event-manager.ts +++ b/src/timed-event-manager.ts @@ -663,7 +663,7 @@ export class TimedEventDisplay extends Phaser.GameObjects.Container { console.log(this.event.bannerKey); const padding = 5; const showTimer = this.event.eventType !== EventType.NO_TIMER_DISPLAY; - const yPosition = globalScene.game.canvas.height / 6 - padding - (showTimer ? 10 : 0) - (this.event.yOffset ?? 0); + const yPosition = globalScene.scaledCanvas.height - padding - (showTimer ? 10 : 0) - (this.event.yOffset ?? 0); this.banner = new Phaser.GameObjects.Image(globalScene, this.availableWidth / 2, yPosition - padding, key); this.banner.setName("img-event-banner"); this.banner.setOrigin(0.5, 1); diff --git a/src/ui/abstact-option-select-ui-handler.ts b/src/ui/abstact-option-select-ui-handler.ts index 2fb0159b6ef..b7279bc2d30 100644 --- a/src/ui/abstact-option-select-ui-handler.ts +++ b/src/ui/abstact-option-select-ui-handler.ts @@ -66,7 +66,7 @@ export abstract class AbstractOptionSelectUiHandler extends UiHandler { setup() { const ui = this.getUi(); - this.optionSelectContainer = globalScene.add.container(globalScene.game.canvas.width / 6 - 1, -48); + this.optionSelectContainer = globalScene.add.container(globalScene.scaledCanvas.width - 1, -48); this.optionSelectContainer.setName(`option-select-${this.mode ? UiMode[this.mode] : "UNKNOWN"}`); this.optionSelectContainer.setVisible(false); ui.add(this.optionSelectContainer); @@ -135,7 +135,7 @@ export abstract class AbstractOptionSelectUiHandler extends UiHandler { this.optionSelectText.setName("text-option-select"); this.optionSelectTextContainer.add(this.optionSelectText); this.optionSelectContainer.setPosition( - globalScene.game.canvas.width / 6 - 1 - (this.config?.xOffset || 0), + globalScene.scaledCanvas.width - 1 - (this.config?.xOffset || 0), -48 + (this.config?.yOffset || 0), ); this.optionSelectBg.width = Math.max(this.optionSelectText.displayWidth + 24, this.getWindowWidth()); diff --git a/src/ui/achv-bar.ts b/src/ui/achv-bar.ts index bb1ef95c9de..0f71bcbfde0 100644 --- a/src/ui/achv-bar.ts +++ b/src/ui/achv-bar.ts @@ -21,7 +21,7 @@ export class AchvBar extends Phaser.GameObjects.Container { public shown: boolean; constructor() { - super(globalScene, globalScene.game.canvas.width / 6, 0); + super(globalScene, globalScene.scaledCanvas.width, 0); this.playerGender = globalScene.gameData.gender; } @@ -118,7 +118,7 @@ export class AchvBar extends Phaser.GameObjects.Container { globalScene.tweens.add({ targets: this, - x: globalScene.game.canvas.width / 6 - this.bg.width / 2, + x: globalScene.scaledCanvas.width - this.bg.width / 2, duration: 500, ease: "Sine.easeOut", }); @@ -136,7 +136,7 @@ export class AchvBar extends Phaser.GameObjects.Container { globalScene.tweens.add({ targets: this, - x: globalScene.game.canvas.width / 6, + x: globalScene.scaledCanvas.width, duration: 500, ease: "Sine.easeIn", onComplete: () => { diff --git a/src/ui/achvs-ui-handler.ts b/src/ui/achvs-ui-handler.ts index 01fd1d45a61..2c04e24e0f2 100644 --- a/src/ui/achvs-ui-handler.ts +++ b/src/ui/achvs-ui-handler.ts @@ -72,9 +72,9 @@ export class AchvsUiHandler extends MessageUiHandler { const ui = this.getUi(); /** Width of the global canvas / 6 */ - const WIDTH = globalScene.game.canvas.width / 6; + const WIDTH = globalScene.scaledCanvas.width; /** Height of the global canvas / 6 */ - const HEIGHT = globalScene.game.canvas.height / 6; + const HEIGHT = globalScene.scaledCanvas.height; this.mainContainer = globalScene.add.container(1, -HEIGHT + 1); diff --git a/src/ui/ball-ui-handler.ts b/src/ui/ball-ui-handler.ts index 67beb0eba84..99dabd893df 100644 --- a/src/ui/ball-ui-handler.ts +++ b/src/ui/ball-ui-handler.ts @@ -37,7 +37,7 @@ export class BallUiHandler extends UiHandler { const optionsText = addTextObject(0, 0, optionsTextContent, TextStyle.WINDOW, { align: "right", maxLines: 6 }); const optionsTextWidth = optionsText.displayWidth; this.pokeballSelectContainer = globalScene.add.container( - globalScene.game.canvas.width / 6 - 51 - Math.max(64, optionsTextWidth), + globalScene.scaledCanvas.width - 51 - Math.max(64, optionsTextWidth), -49, ); this.pokeballSelectContainer.setVisible(false); diff --git a/src/ui/base-stats-overlay.ts b/src/ui/base-stats-overlay.ts index e3ba472475a..3b432e13096 100644 --- a/src/ui/base-stats-overlay.ts +++ b/src/ui/base-stats-overlay.ts @@ -16,7 +16,6 @@ interface BaseStatsOverlaySettings { const HEIGHT = 120; const BORDER = 8; -const GLOBAL_SCALE = 6; const shortStats = ["HP", "ATK", "DEF", "SPATK", "SPDEF", "SPD"]; export class BaseStatsOverlay extends Phaser.GameObjects.Container implements InfoToggle { @@ -109,7 +108,7 @@ export class BaseStatsOverlay extends Phaser.GameObjects.Container implements In // width of this element static getWidth(_scale: number): number { - return globalScene.game.canvas.width / GLOBAL_SCALE / 2; + return globalScene.scaledCanvas.width / 2; } // height of this element diff --git a/src/ui/battle-info/player-battle-info.ts b/src/ui/battle-info/player-battle-info.ts index 62a2eddecb9..8784495e6d2 100644 --- a/src/ui/battle-info/player-battle-info.ts +++ b/src/ui/battle-info/player-battle-info.ts @@ -39,7 +39,7 @@ export class PlayerBattleInfo extends BattleInfo { statOverflow: 1, }, }; - super(Math.floor(globalScene.game.canvas.width / 6) - 10, -72, true, posParams); + super(Math.floor(globalScene.scaledCanvas.width) - 10, -72, true, posParams); this.hpNumbersContainer = globalScene.add.container(-15, 10).setName("container_hp"); diff --git a/src/ui/battle-message-ui-handler.ts b/src/ui/battle-message-ui-handler.ts index b58897b9022..2ecca06172b 100644 --- a/src/ui/battle-message-ui-handler.ts +++ b/src/ui/battle-message-ui-handler.ts @@ -94,7 +94,7 @@ export class BattleMessageUiHandler extends MessageUiHandler { this.levelUpStatsContainer = levelUpStatsContainer; - const levelUpStatsLabelsContent = addTextObject(globalScene.game.canvas.width / 6 - 73, -94, "", TextStyle.WINDOW, { + const levelUpStatsLabelsContent = addTextObject(globalScene.scaledCanvas.width - 73, -94, "", TextStyle.WINDOW, { maxLines: 6, }); let levelUpStatsLabelText = ""; @@ -106,7 +106,7 @@ export class BattleMessageUiHandler extends MessageUiHandler { levelUpStatsLabelsContent.x -= levelUpStatsLabelsContent.displayWidth; const levelUpStatsBg = addWindow( - globalScene.game.canvas.width / 6, + globalScene.scaledCanvas.width, -100, 80 + levelUpStatsLabelsContent.displayWidth, 100, @@ -117,7 +117,7 @@ export class BattleMessageUiHandler extends MessageUiHandler { levelUpStatsContainer.add(levelUpStatsLabelsContent); const levelUpStatsIncrContent = addTextObject( - globalScene.game.canvas.width / 6 - 50, + globalScene.scaledCanvas.width - 50, -94, "+\n+\n+\n+\n+\n+", TextStyle.WINDOW, @@ -128,7 +128,7 @@ export class BattleMessageUiHandler extends MessageUiHandler { this.levelUpStatsIncrContent = levelUpStatsIncrContent; const levelUpStatsValuesContent = addBBCodeTextObject( - globalScene.game.canvas.width / 6 - 7, + globalScene.scaledCanvas.width - 7, -94, "", TextStyle.WINDOW, diff --git a/src/ui/candy-bar.ts b/src/ui/candy-bar.ts index 239b963227b..b29ac3ec520 100644 --- a/src/ui/candy-bar.ts +++ b/src/ui/candy-bar.ts @@ -19,7 +19,7 @@ export class CandyBar extends Phaser.GameObjects.Container { public shown: boolean; constructor() { - super(globalScene, globalScene.game.canvas.width / 6, -(globalScene.game.canvas.height / 6) + 15); + super(globalScene, globalScene.scaledCanvas.width, -globalScene.scaledCanvas.height + 15); } setup(): void { @@ -80,7 +80,7 @@ export class CandyBar extends Phaser.GameObjects.Container { this.tween = globalScene.tweens.add({ targets: this, - x: globalScene.game.canvas.width / 6 - (this.bg.width - 5), + x: globalScene.scaledCanvas.width - (this.bg.width - 5), duration: 500, ease: "Sine.easeOut", onComplete: () => { @@ -111,7 +111,7 @@ export class CandyBar extends Phaser.GameObjects.Container { this.tween = globalScene.tweens.add({ targets: this, - x: globalScene.game.canvas.width / 6, + x: globalScene.scaledCanvas.width, duration: 500, ease: "Sine.easeIn", onComplete: () => { diff --git a/src/ui/challenges-select-ui-handler.ts b/src/ui/challenges-select-ui-handler.ts index 4a7ab7641a3..b37ddcc6a5f 100644 --- a/src/ui/challenges-select-ui-handler.ts +++ b/src/ui/challenges-select-ui-handler.ts @@ -58,11 +58,11 @@ export class GameChallengesUiHandler extends UiHandler { this.widestTextBox = 0; - this.challengesContainer = globalScene.add.container(1, -(globalScene.game.canvas.height / 6) + 1); + this.challengesContainer = globalScene.add.container(1, -globalScene.scaledCanvas.height + 1); this.challengesContainer.setName("challenges"); this.challengesContainer.setInteractive( - new Phaser.Geom.Rectangle(0, 0, globalScene.game.canvas.width / 6, globalScene.game.canvas.height / 6), + new Phaser.Geom.Rectangle(0, 0, globalScene.scaledCanvas.width, globalScene.scaledCanvas.height), Phaser.Geom.Rectangle.Contains, ); @@ -79,7 +79,7 @@ export class GameChallengesUiHandler extends UiHandler { this.challengesContainer.add(bgOverlay); // TODO: Change this back to /9 when adding in difficulty - const headerBg = addWindow(0, 0, globalScene.game.canvas.width / 6, 24); + const headerBg = addWindow(0, 0, globalScene.scaledCanvas.width, 24); headerBg.setName("window-header-bg"); headerBg.setOrigin(0, 0); diff --git a/src/ui/char-sprite.ts b/src/ui/char-sprite.ts index 381421086ff..1ab00291ff6 100644 --- a/src/ui/char-sprite.ts +++ b/src/ui/char-sprite.ts @@ -10,7 +10,7 @@ export class CharSprite extends Phaser.GameObjects.Container { public shown: boolean; constructor() { - super(globalScene, globalScene.game.canvas.width / 6 + 32, -42); + super(globalScene, globalScene.scaledCanvas.width + 32, -42); } setup(): void { @@ -49,7 +49,7 @@ export class CharSprite extends Phaser.GameObjects.Container { globalScene.tweens.add({ targets: this, - x: globalScene.game.canvas.width / 6 - 102, + x: globalScene.scaledCanvas.width - 102, duration: 750, ease: "Cubic.easeOut", onComplete: () => { @@ -95,7 +95,7 @@ export class CharSprite extends Phaser.GameObjects.Container { globalScene.tweens.add({ targets: this, - x: globalScene.game.canvas.width / 6 + 32, + x: globalScene.scaledCanvas.width + 32, duration: 750, ease: "Cubic.easeIn", onComplete: () => { diff --git a/src/ui/confirm-ui-handler.ts b/src/ui/confirm-ui-handler.ts index b2f35931278..aeef7fc1c91 100644 --- a/src/ui/confirm-ui-handler.ts +++ b/src/ui/confirm-ui-handler.ts @@ -69,7 +69,7 @@ export class ConfirmUiHandler extends AbstractOptionSelectUiHandler { const xOffset = args.length >= 7 && args[6] !== null ? (args[6] as number) : 0; const yOffset = args.length >= 8 && args[7] !== null ? (args[7] as number) : 0; - this.optionSelectContainer.setPosition(globalScene.game.canvas.width / 6 - 1 + xOffset, -48 + yOffset); + this.optionSelectContainer.setPosition(globalScene.scaledCanvas.width - 1 + xOffset, -48 + yOffset); this.setCursor(this.switchCheck ? this.switchCheckCursor : 0); return true; @@ -103,7 +103,7 @@ export class ConfirmUiHandler extends AbstractOptionSelectUiHandler { const xOffset = args.length >= 4 && args[3] !== null ? (args[3] as number) : 0; const yOffset = args.length >= 5 && args[4] !== null ? (args[4] as number) : 0; - this.optionSelectContainer.setPosition(globalScene.game.canvas.width / 6 - 1 + xOffset, -48 + yOffset); + this.optionSelectContainer.setPosition(globalScene.scaledCanvas.width - 1 + xOffset, -48 + yOffset); this.setCursor(this.switchCheck ? this.switchCheckCursor : 0); diff --git a/src/ui/egg-gacha-ui-handler.ts b/src/ui/egg-gacha-ui-handler.ts index 5dcf05e2606..0287e0ee7a5 100644 --- a/src/ui/egg-gacha-ui-handler.ts +++ b/src/ui/egg-gacha-ui-handler.ts @@ -174,7 +174,7 @@ export class EggGachaUiHandler extends MessageUiHandler { const ui = this.getUi(); - this.eggGachaContainer = globalScene.add.container(0, -globalScene.game.canvas.height / 6).setVisible(false); + this.eggGachaContainer = globalScene.add.container(0, -globalScene.scaledCanvas.height).setVisible(false); ui.add(this.eggGachaContainer); const bg = globalScene.add.nineslice(0, 0, "default_bg", undefined, 320, 180, 0, 0, 16, 0).setOrigin(0); @@ -212,7 +212,7 @@ export class EggGachaUiHandler extends MessageUiHandler { this.eggGachaOptionSelectBg = addWindow(0, 0, eggGachaOptionSelectWidth, 16 + 576 * this.scale).setOrigin(1); this.eggGachaOptionsContainer = globalScene.add - .container(globalScene.game.canvas.width / 6, 148) + .container(globalScene.scaledCanvas.width, 148) .add(this.eggGachaOptionSelectBg); this.eggGachaContainer.add(this.eggGachaOptionsContainer); @@ -278,7 +278,7 @@ export class EggGachaUiHandler extends MessageUiHandler { this.eggGachaContainer.add(this.eggGachaOptionsContainer); for (const voucher of getEnumValues(VoucherType)) { - const container = globalScene.add.container(globalScene.game.canvas.width / 6 - 56 * voucher, 0); + const container = globalScene.add.container(globalScene.scaledCanvas.width - 56 * voucher, 0); const bg = addWindow(0, 0, 56, 22).setOrigin(1, 0); container.add(bg); diff --git a/src/ui/egg-hatch-scene-handler.ts b/src/ui/egg-hatch-scene-handler.ts index 5b2c9d40cfa..6536ef2af80 100644 --- a/src/ui/egg-hatch-scene-handler.ts +++ b/src/ui/egg-hatch-scene-handler.ts @@ -19,7 +19,7 @@ export class EggHatchSceneHandler extends UiHandler { } setup() { - this.eggHatchContainer = globalScene.add.container(0, -globalScene.game.canvas.height / 6); + this.eggHatchContainer = globalScene.add.container(0, -globalScene.scaledCanvas.height); globalScene.fieldUI.add(this.eggHatchContainer); const eggLightraysAnimFrames = globalScene.anims.generateFrameNames("egg_lightrays", { start: 0, end: 3 }); diff --git a/src/ui/egg-list-ui-handler.ts b/src/ui/egg-list-ui-handler.ts index 42f969b9d38..2516903f631 100644 --- a/src/ui/egg-list-ui-handler.ts +++ b/src/ui/egg-list-ui-handler.ts @@ -36,11 +36,11 @@ export class EggListUiHandler extends MessageUiHandler { setup() { const ui = this.getUi(); - this.eggListContainer = globalScene.add.container(0, -globalScene.game.canvas.height / 6).setVisible(false); + this.eggListContainer = globalScene.add.container(0, -globalScene.scaledCanvas.height).setVisible(false); ui.add(this.eggListContainer); const bgColor = globalScene.add - .rectangle(0, 0, globalScene.game.canvas.width / 6, globalScene.game.canvas.height / 6, 0x006860) + .rectangle(0, 0, globalScene.scaledCanvas.width, globalScene.scaledCanvas.height, 0x006860) .setOrigin(0); const eggListBg = globalScene.add.image(0, 0, "egg_list_bg").setOrigin(0); @@ -69,9 +69,7 @@ export class EggListUiHandler extends MessageUiHandler { .withUpdateGridCallBack(() => this.updateEggIcons()) .withUpdateSingleElementCallback((i: number) => this.setEggDetails(i)); - this.eggListMessageBoxContainer = globalScene.add - .container(0, globalScene.game.canvas.height / 6) - .setVisible(false); + this.eggListMessageBoxContainer = globalScene.add.container(0, globalScene.scaledCanvas.height).setVisible(false); const eggListMessageBox = addWindow(1, -1, 318, 28).setOrigin(0, 1); this.eggListMessageBoxContainer.add(eggListMessageBox); diff --git a/src/ui/egg-summary-ui-handler.ts b/src/ui/egg-summary-ui-handler.ts index aa4e8974318..c66075dd910 100644 --- a/src/ui/egg-summary-ui-handler.ts +++ b/src/ui/egg-summary-ui-handler.ts @@ -59,11 +59,11 @@ export class EggSummaryUiHandler extends MessageUiHandler { setup() { const ui = this.getUi(); - this.summaryContainer = globalScene.add.container(0, -globalScene.game.canvas.height / 6); + this.summaryContainer = globalScene.add.container(0, -globalScene.scaledCanvas.height); this.summaryContainer.setVisible(false); ui.add(this.summaryContainer); - this.eggHatchContainer = globalScene.add.container(0, -globalScene.game.canvas.height / 6); + this.eggHatchContainer = globalScene.add.container(0, -globalScene.scaledCanvas.height); this.eggHatchContainer.setVisible(false); ui.add(this.eggHatchContainer); @@ -92,7 +92,7 @@ export class EggSummaryUiHandler extends MessageUiHandler { iconContainerX + numCols * iconSize, iconContainerY + 3, 4, - globalScene.game.canvas.height / 6 - 20, + globalScene.scaledCanvas.height - 20, numRows, ); this.summaryContainer.add(scrollBar); diff --git a/src/ui/evolution-scene-handler.ts b/src/ui/evolution-scene-handler.ts index c22cf31faaa..0333594dc36 100644 --- a/src/ui/evolution-scene-handler.ts +++ b/src/ui/evolution-scene-handler.ts @@ -22,7 +22,7 @@ export class EvolutionSceneHandler extends MessageUiHandler { const ui = this.getUi(); - this.evolutionContainer = globalScene.add.container(0, -globalScene.game.canvas.height / 6); + this.evolutionContainer = globalScene.add.container(0, -globalScene.scaledCanvas.height); const messageBg = globalScene.add.sprite(0, 0, "bg", globalScene.windowType).setOrigin(0, 1).setVisible(false); diff --git a/src/ui/fight-ui-handler.ts b/src/ui/fight-ui-handler.ts index 42f8cba5df4..1d856079939 100644 --- a/src/ui/fight-ui-handler.ts +++ b/src/ui/fight-ui-handler.ts @@ -105,15 +105,13 @@ export class FightUiHandler extends UiHandler implements InfoToggle { ]); // prepare move overlay - const overlayScale = 1; this.moveInfoOverlay = new MoveInfoOverlay({ delayVisibility: true, - scale: overlayScale, onSide: true, right: true, x: 0, - y: -MoveInfoOverlay.getHeight(overlayScale, true), - width: globalScene.game.canvas.width / 6 + 4, + y: -MoveInfoOverlay.getHeight(true), + width: globalScene.scaledCanvas.width + 4, hideEffectBox: true, hideBg: true, }); diff --git a/src/ui/filter-text.ts b/src/ui/filter-text.ts index ff7119dd778..d5809292c25 100644 --- a/src/ui/filter-text.ts +++ b/src/ui/filter-text.ts @@ -62,7 +62,7 @@ export class FilterText extends Phaser.GameObjects.Container { this.dialogueMessageBox = addWindow( -this.textPadding, 0, - globalScene.game.canvas.width / 6 + this.textPadding * 2, + globalScene.scaledCanvas.width + this.textPadding * 2, 49, false, false, diff --git a/src/ui/login-form-ui-handler.ts b/src/ui/login-form-ui-handler.ts index 524eaeece86..67b2c81b94c 100644 --- a/src/ui/login-form-ui-handler.ts +++ b/src/ui/login-form-ui-handler.ts @@ -49,7 +49,7 @@ export class LoginFormUiHandler extends FormModalUiHandler { private buildExternalPartyContainer() { this.externalPartyContainer = globalScene.add.container(0, 0); this.externalPartyContainer.setInteractive( - new Phaser.Geom.Rectangle(0, 0, globalScene.game.canvas.width / 12, globalScene.game.canvas.height / 12), + new Phaser.Geom.Rectangle(0, 0, globalScene.scaledCanvas.width / 2, globalScene.scaledCanvas.height / 2), Phaser.Geom.Rectangle.Contains, ); this.externalPartyTitle = addTextObject(0, 4, "", TextStyle.SETTINGS_LABEL); diff --git a/src/ui/menu-ui-handler.ts b/src/ui/menu-ui-handler.ts index 30bf9eeff75..e419997456b 100644 --- a/src/ui/menu-ui-handler.ts +++ b/src/ui/menu-ui-handler.ts @@ -96,10 +96,10 @@ export class MenuUiHandler extends MessageUiHandler { ui.bgmBar = this.bgmBar; - this.menuContainer = globalScene.add.container(1, -(globalScene.game.canvas.height / 6) + 1); + this.menuContainer = globalScene.add.container(1, -globalScene.scaledCanvas.height + 1); this.menuContainer.setName("menu"); this.menuContainer.setInteractive( - new Phaser.Geom.Rectangle(0, 0, globalScene.game.canvas.width / 6, globalScene.game.canvas.height / 6), + new Phaser.Geom.Rectangle(0, 0, globalScene.scaledCanvas.width, globalScene.scaledCanvas.height), Phaser.Geom.Rectangle.Contains, ); @@ -146,10 +146,10 @@ export class MenuUiHandler extends MessageUiHandler { this.scale = getTextStyleOptions(TextStyle.WINDOW, globalScene.uiTheme).scale; this.menuBg = addWindow( - globalScene.game.canvas.width / 6 - (this.optionSelectText.displayWidth + 25), + globalScene.scaledCanvas.width - (this.optionSelectText.displayWidth + 25), 0, this.optionSelectText.displayWidth + 19 + 24 * this.scale, - globalScene.game.canvas.height / 6 - 2, + globalScene.scaledCanvas.height - 2, ); this.menuBg.setOrigin(0, 0); @@ -174,7 +174,7 @@ export class MenuUiHandler extends MessageUiHandler { this.dialogueMessageBox = addWindow( -this.textPadding, 0, - globalScene.game.canvas.width / 6 + this.textPadding * 2, + globalScene.scaledCanvas.width + this.textPadding * 2, 49, false, false, diff --git a/src/ui/modal-ui-handler.ts b/src/ui/modal-ui-handler.ts index 4d1456eec51..51a6a21a29c 100644 --- a/src/ui/modal-ui-handler.ts +++ b/src/ui/modal-ui-handler.ts @@ -46,7 +46,7 @@ export abstract class ModalUiHandler extends UiHandler { this.modalContainer = globalScene.add.container(0, 0); this.modalContainer.setInteractive( - new Phaser.Geom.Rectangle(0, 0, globalScene.game.canvas.width / 6, globalScene.game.canvas.height / 6), + new Phaser.Geom.Rectangle(0, 0, globalScene.scaledCanvas.width, globalScene.scaledCanvas.height), Phaser.Geom.Rectangle.Contains, ); @@ -105,8 +105,8 @@ export abstract class ModalUiHandler extends UiHandler { const overlay = globalScene.add.rectangle( (this.getWidth() + marginLeft + marginRight) / 2, (this.getHeight() + marginTop + marginBottom) / 2, - globalScene.game.canvas.width / 6, - globalScene.game.canvas.height / 6, + globalScene.scaledCanvas.width, + globalScene.scaledCanvas.height, 0, ); overlay.setOrigin(0.5, 0.5); @@ -159,8 +159,8 @@ export abstract class ModalUiHandler extends UiHandler { const width = this.getWidth(config); const height = this.getHeight(config); this.modalContainer.setPosition( - (globalScene.game.canvas.width / 6 - (width + (marginRight - marginLeft))) / 2, - (-globalScene.game.canvas.height / 6 - (height + (marginBottom - marginTop))) / 2, + (globalScene.scaledCanvas.width - (width + (marginRight - marginLeft))) / 2, + (-globalScene.scaledCanvas.height - (height + (marginBottom - marginTop))) / 2, ); this.modalBg.setSize(width, height); diff --git a/src/ui/modifier-select-ui-handler.ts b/src/ui/modifier-select-ui-handler.ts index 16eecf6993d..6edd7d5d70d 100644 --- a/src/ui/modifier-select-ui-handler.ts +++ b/src/ui/modifier-select-ui-handler.ts @@ -86,10 +86,7 @@ export class ModifierSelectUiHandler extends AwaitableUiHandler { transferButtonText.setOrigin(1, 0); this.transferButtonContainer.add(transferButtonText); - this.checkButtonContainer = globalScene.add.container( - globalScene.game.canvas.width / 6 - 1, - OPTION_BUTTON_YPOSITION, - ); + this.checkButtonContainer = globalScene.add.container(globalScene.scaledCanvas.width - 1, OPTION_BUTTON_YPOSITION); this.checkButtonContainer.setName("use-btn"); this.checkButtonContainer.setVisible(false); ui.add(this.checkButtonContainer); @@ -129,8 +126,8 @@ export class ModifierSelectUiHandler extends AwaitableUiHandler { this.lockRarityButtonContainer.add(this.lockRarityButtonText); this.continueButtonContainer = globalScene.add.container( - globalScene.game.canvas.width / 12, - -(globalScene.game.canvas.height / 12), + globalScene.scaledCanvas.width / 2, + -(globalScene.scaledCanvas.height / 2), ); this.continueButtonContainer.setVisible(false); ui.add(this.continueButtonContainer); @@ -146,15 +143,13 @@ export class ModifierSelectUiHandler extends AwaitableUiHandler { this.continueButtonContainer.add(continueButtonText); // prepare move overlay - const overlayScale = 1; this.moveInfoOverlay = new MoveInfoOverlay({ delayVisibility: true, - scale: overlayScale, onSide: true, right: true, x: 1, - y: -MoveInfoOverlay.getHeight(overlayScale, true) - 1, - width: globalScene.game.canvas.width / 6 - 2, + y: -MoveInfoOverlay.getHeight(true) - 1, + width: globalScene.scaledCanvas.width - 2, }); ui.add(this.moveInfoOverlay); // register the overlay to receive toggle events @@ -209,20 +204,20 @@ export class ModifierSelectUiHandler extends AwaitableUiHandler { this.updateRerollCostText(); const typeOptions = args[1] as ModifierTypeOption[]; - const removeHealShop = globalScene.gameMode.hasNoShop; + const hasShop = globalScene.gameMode.getShopStatus(); const baseShopCost = new NumberHolder(globalScene.getWaveMoneyAmount(1)); globalScene.applyModifier(HealShopCostModifier, true, baseShopCost); - const shopTypeOptions = !removeHealShop + const shopTypeOptions = hasShop ? getPlayerShopModifierTypeOptionsForWave(globalScene.currentBattle.waveIndex, baseShopCost.value) : []; const optionsYOffset = shopTypeOptions.length > SHOP_OPTIONS_ROW_LIMIT ? -SINGLE_SHOP_ROW_YOFFSET : -DOUBLE_SHOP_ROW_YOFFSET; for (let m = 0; m < typeOptions.length; m++) { - const sliceWidth = globalScene.game.canvas.width / 6 / (typeOptions.length + 2); + const sliceWidth = globalScene.scaledCanvas.width / (typeOptions.length + 2); const option = new ModifierOption( sliceWidth * (m + 1) + sliceWidth * 0.5, - -globalScene.game.canvas.height / 12 + optionsYOffset, + -globalScene.scaledCanvas.height / 2 + optionsYOffset, typeOptions[m], ); option.setScale(0.5); @@ -243,10 +238,10 @@ export class ModifierSelectUiHandler extends AwaitableUiHandler { row ? SHOP_OPTIONS_ROW_LIMIT : 0, row ? undefined : SHOP_OPTIONS_ROW_LIMIT, ); - const sliceWidth = globalScene.game.canvas.width / 6 / (rowOptions.length + 2); + const sliceWidth = globalScene.scaledCanvas.width / (rowOptions.length + 2); const option = new ModifierOption( sliceWidth * (col + 1) + sliceWidth * 0.5, - -globalScene.game.canvas.height / 12 - globalScene.game.canvas.height / 32 - (42 - (28 * row - 1)), + -globalScene.scaledCanvas.height / 2 - globalScene.game.canvas.height / 32 - (42 - (28 * row - 1)), shopTypeOptions[m], ); option.setScale(0.375); @@ -370,7 +365,7 @@ export class ModifierSelectUiHandler extends AwaitableUiHandler { if (globalScene.shopCursorTarget === ShopCursorTarget.CHECK_TEAM) { this.setRowCursor(0); this.setCursor(2); - } else if (globalScene.shopCursorTarget === ShopCursorTarget.SHOP && globalScene.gameMode.hasNoShop) { + } else if (globalScene.shopCursorTarget === ShopCursorTarget.SHOP && !hasShop) { this.setRowCursor(ShopCursorTarget.REWARDS); this.setCursor(0); } else { @@ -558,27 +553,27 @@ export class ModifierSelectUiHandler extends AwaitableUiHandler { // Continue button when no shop items this.cursorObj.setScale(1.25); this.cursorObj.setPosition( - globalScene.game.canvas.width / 18 + 23, - -globalScene.game.canvas.height / 12 - + globalScene.scaledCanvas.width / 3 + 23, + -globalScene.scaledCanvas.height / 2 - (this.shopOptionsRows.length > 1 ? SINGLE_SHOP_ROW_YOFFSET - 2 : DOUBLE_SHOP_ROW_YOFFSET - 2), ); ui.showText(i18next.t("modifierSelectUiHandler:continueNextWaveDescription")); return ret; } - const sliceWidth = globalScene.game.canvas.width / 6 / (options.length + 2); + const sliceWidth = globalScene.scaledCanvas.width / (options.length + 2); if (this.rowCursor < 2) { // Cursor on free items this.cursorObj.setPosition( sliceWidth * (cursor + 1) + sliceWidth * 0.5 - 20, - -globalScene.game.canvas.height / 12 - + -globalScene.scaledCanvas.height / 2 - (this.shopOptionsRows.length > 1 ? SINGLE_SHOP_ROW_YOFFSET - 2 : DOUBLE_SHOP_ROW_YOFFSET - 2), ); } else { // Cursor on paying items this.cursorObj.setPosition( sliceWidth * (cursor + 1) + sliceWidth * 0.5 - 16, - -globalScene.game.canvas.height / 12 - + -globalScene.scaledCanvas.height / 2 - globalScene.game.canvas.height / 32 - (-14 + 28 * (this.rowCursor - (this.shopOptionsRows.length - 1))), ); diff --git a/src/ui/move-info-overlay.ts b/src/ui/move-info-overlay.ts index f8632eb244e..f98630260db 100644 --- a/src/ui/move-info-overlay.ts +++ b/src/ui/move-info-overlay.ts @@ -10,17 +10,24 @@ import { fixedInt, getLocalizedSpriteKey } from "#utils/common"; import i18next from "i18next"; export interface MoveInfoOverlaySettings { - delayVisibility?: boolean; // if true, showing the overlay will only set it to active and populate the fields and the handler using this field has to manually call setVisible later. - scale?: number; // scale the box? A scale of 0.5 is recommended - top?: boolean; // should the effect box be on top? - right?: boolean; // should the effect box be on the right? - onSide?: boolean; // should the effect be on the side? ignores top argument if true - //location and width of the component; unaffected by scaling + /** + * If true, showing the overlay will only set it to active and populate the fields + * and the handler using this field has to manually call `setVisible` later. + */ + delayVisibility?: boolean; + /** Whether the effect box should be on top */ + top?: boolean; + /** Whether the effect box should be on the right */ + right?: boolean; + /** Whether the effect box should be on the side. Overrides the `top` param if `true`. */ + onSide?: boolean; + /** `x` position of the component, unaffected by scaling */ x?: number; + /** `y` position of the component, unaffected by scaling */ y?: number; - /** Default is always half the screen, regardless of scale */ + /** Width of the component, unaffected by scaling. Defaults to half the screen width. */ width?: number; - /** Determines whether to display the small secondary box */ + /** Whether to display the small secondary box */ hideEffectBox?: boolean; hideBg?: boolean; } @@ -54,12 +61,11 @@ export class MoveInfoOverlay extends Phaser.GameObjects.Container implements Inf options.top = false; } super(globalScene, options?.x, options?.y); - const scale = options?.scale || 1; // set up the scale - this.setScale(scale); + this.setScale(1); this.options = options || {}; // prepare the description box - const width = (options?.width || MoveInfoOverlay.getWidth(scale)) / scale; // divide by scale as we always want this to be half a window wide + const width = options?.width || MoveInfoOverlay.getWidth(); // we always want this to be half a window wide this.descBg = addWindow( options?.onSide && !options?.right ? EFF_WIDTH : 0, options?.top ? EFF_HEIGHT : 0, @@ -88,19 +94,19 @@ export class MoveInfoOverlay extends Phaser.GameObjects.Container implements Inf y: options?.y || 0, }; if (maskPointOrigin.x < 0) { - maskPointOrigin.x += globalScene.game.canvas.width / GLOBAL_SCALE; + maskPointOrigin.x += globalScene.scaledCanvas.width; } if (maskPointOrigin.y < 0) { - maskPointOrigin.y += globalScene.game.canvas.height / GLOBAL_SCALE; + maskPointOrigin.y += globalScene.scaledCanvas.height; } const moveDescriptionTextMaskRect = globalScene.make.graphics(); moveDescriptionTextMaskRect.fillStyle(0xff0000); moveDescriptionTextMaskRect.fillRect( - maskPointOrigin.x + ((options?.onSide && !options?.right ? EFF_WIDTH : 0) + BORDER) * scale, - maskPointOrigin.y + ((options?.top ? EFF_HEIGHT : 0) + BORDER - 2) * scale, - width - ((options?.onSide ? EFF_WIDTH : 0) - BORDER * 2) * scale, - (DESC_HEIGHT - (BORDER - 2) * 2) * scale, + maskPointOrigin.x + ((options?.onSide && !options?.right ? EFF_WIDTH : 0) + BORDER), + maskPointOrigin.y + ((options?.top ? EFF_HEIGHT : 0) + BORDER - 2), + width - ((options?.onSide ? EFF_WIDTH : 0) - BORDER * 2), + DESC_HEIGHT - (BORDER - 2) * 2, ); moveDescriptionTextMaskRect.setScale(6); const moveDescriptionTextMask = this.createGeometryMask(moveDescriptionTextMaskRect); @@ -233,12 +239,12 @@ export class MoveInfoOverlay extends Phaser.GameObjects.Container implements Inf } // width of this element - static getWidth(_scale: number): number { - return globalScene.game.canvas.width / GLOBAL_SCALE / 2; + static getWidth(): number { + return globalScene.scaledCanvas.width / 2; } // height of this element - static getHeight(scale: number, onSide?: boolean): number { - return (onSide ? Math.max(EFF_HEIGHT, DESC_HEIGHT) : EFF_HEIGHT + DESC_HEIGHT) * scale; + static getHeight(onSide?: boolean): number { + return onSide ? Math.max(EFF_HEIGHT, DESC_HEIGHT) : EFF_HEIGHT + DESC_HEIGHT; } } diff --git a/src/ui/mystery-encounter-ui-handler.ts b/src/ui/mystery-encounter-ui-handler.ts index b6bc464855c..881c375fa8a 100644 --- a/src/ui/mystery-encounter-ui-handler.ts +++ b/src/ui/mystery-encounter-ui-handler.ts @@ -471,7 +471,7 @@ export class MysteryEncounterUiHandler extends UiHandler { // View Party Button const viewPartyText = addBBCodeTextObject( - globalScene.game.canvas.width / 6, + globalScene.scaledCanvas.width, -24, getBBCodeFrag(i18next.t("mysteryEncounterMessages:view_party_button"), TextStyle.PARTY), TextStyle.PARTY, diff --git a/src/ui/party-exp-bar.ts b/src/ui/party-exp-bar.ts index 952a1f8227a..c9567ceb042 100644 --- a/src/ui/party-exp-bar.ts +++ b/src/ui/party-exp-bar.ts @@ -14,7 +14,7 @@ export class PartyExpBar extends Phaser.GameObjects.Container { public shown: boolean; constructor() { - super(globalScene, globalScene.game.canvas.width / 6, -(globalScene.game.canvas.height / 6) + 15); + super(globalScene, globalScene.scaledCanvas.width, -globalScene.scaledCanvas.height + 15); } setup(): void { @@ -66,7 +66,7 @@ export class PartyExpBar extends Phaser.GameObjects.Container { this.tween = globalScene.tweens.add({ targets: this, - x: globalScene.game.canvas.width / 6 - (this.bg.width - 5), + x: globalScene.scaledCanvas.width - (this.bg.width - 5), duration: 500 / Math.pow(2, globalScene.expGainsSpeed), ease: "Sine.easeOut", onComplete: () => { @@ -92,7 +92,7 @@ export class PartyExpBar extends Phaser.GameObjects.Container { this.tween = globalScene.tweens.add({ targets: this, - x: globalScene.game.canvas.width / 6, + x: globalScene.scaledCanvas.width, duration: 500, ease: "Sine.easeIn", onComplete: () => { diff --git a/src/ui/party-ui-handler.ts b/src/ui/party-ui-handler.ts index 0337e487200..ff5e7246a6f 100644 --- a/src/ui/party-ui-handler.ts +++ b/src/ui/party-ui-handler.ts @@ -1,7 +1,6 @@ import { globalScene } from "#app/global-scene"; import { getPokemonNameWithAffix } from "#app/messages"; import { pokemonEvolutions } from "#balance/pokemon-evolutions"; -import { applyChallenges } from "#data/challenge"; import { allMoves } from "#data/data-lists"; import { SpeciesFormChangeItemTrigger } from "#data/form-change-triggers"; import { Gender, getGenderColor, getGenderSymbol } from "#data/gender"; @@ -26,6 +25,7 @@ import { MoveInfoOverlay } from "#ui/move-info-overlay"; import { PokemonIconAnimHandler, PokemonIconAnimMode } from "#ui/pokemon-icon-anim-handler"; import { addBBCodeTextObject, addTextObject, getTextColor } from "#ui/text"; import { addWindow } from "#ui/ui-theme"; +import { applyChallenges } from "#utils/challenge-utils"; import { BooleanHolder, getLocalizedSpriteKey, randInt } from "#utils/common"; import { toTitleCase } from "#utils/strings"; import i18next from "i18next"; @@ -311,7 +311,7 @@ export class PartyUiHandler extends MessageUiHandler { this.partyCancelButton = partyCancelButton; - this.optionsContainer = globalScene.add.container(globalScene.game.canvas.width / 6 - 1, -1); + this.optionsContainer = globalScene.add.container(globalScene.scaledCanvas.width - 1, -1); partyContainer.add(this.optionsContainer); this.iconAnimHandler = new PokemonIconAnimHandler(); @@ -323,14 +323,12 @@ export class PartyUiHandler extends MessageUiHandler { this.partyDiscardModeButton = partyDiscardModeButton; - // prepare move overlay. in case it appears to be too big, set the overlayScale to .5 - const overlayScale = 1; + // prepare move overlay this.moveInfoOverlay = new MoveInfoOverlay({ - scale: overlayScale, top: true, x: 1, - y: -MoveInfoOverlay.getHeight(overlayScale) - 1, - width: globalScene.game.canvas.width / 12 - 30, + y: -MoveInfoOverlay.getHeight() - 1, + width: globalScene.scaledCanvas.width / 2 - 30, }); ui.add(this.moveInfoOverlay); diff --git a/src/ui/pokeball-tray.ts b/src/ui/pokeball-tray.ts index 9720aa42090..b1522af0e27 100644 --- a/src/ui/pokeball-tray.ts +++ b/src/ui/pokeball-tray.ts @@ -10,7 +10,7 @@ export class PokeballTray extends Phaser.GameObjects.Container { public shown: boolean; constructor(player: boolean) { - super(globalScene, player ? globalScene.game.canvas.width / 6 : 0, player ? -72 : -144); + super(globalScene, player ? globalScene.scaledCanvas.width : 0, player ? -72 : -144); this.player = player; } @@ -36,7 +36,7 @@ export class PokeballTray extends Phaser.GameObjects.Container { .map((_, i) => globalScene.add.sprite( (this.player ? -83 : 76) + - (globalScene.game.canvas.width / 6) * (this.player ? -1 : 1) + + globalScene.scaledCanvas.width * (this.player ? -1 : 1) + 10 * i * (this.player ? 1 : -1), -8, "pb_tray_ball", @@ -67,7 +67,7 @@ export class PokeballTray extends Phaser.GameObjects.Container { this.bg.alpha = 1; this.balls.forEach((ball, b) => { - ball.x += (globalScene.game.canvas.width / 6 + 104) * (this.player ? 1 : -1); + ball.x += (globalScene.scaledCanvas.width + 104) * (this.player ? 1 : -1); let ballFrame = "ball"; if (b >= party.length) { ballFrame = "empty"; @@ -115,7 +115,7 @@ export class PokeballTray extends Phaser.GameObjects.Container { this.balls.forEach((ball, b) => { globalScene.tweens.add({ targets: ball, - x: `${this.player ? "-" : "+"}=${globalScene.game.canvas.width / 6}`, + x: `${this.player ? "-" : "+"}=${globalScene.scaledCanvas.width}`, duration: 250, delay: b * 100, ease: "Sine.easeIn", diff --git a/src/ui/pokedex-info-overlay.ts b/src/ui/pokedex-info-overlay.ts index 0f2f5fa3dde..9c5876318ec 100644 --- a/src/ui/pokedex-info-overlay.ts +++ b/src/ui/pokedex-info-overlay.ts @@ -7,7 +7,6 @@ import { fixedInt } from "#utils/common"; export interface PokedexInfoOverlaySettings { delayVisibility?: boolean; // if true, showing the overlay will only set it to active and populate the fields and the handler using this field has to manually call setVisible later. - scale?: number; // scale the box? A scale of 0.5 is recommended //location and width of the component; unaffected by scaling x?: number; y?: number; @@ -36,17 +35,15 @@ export class PokedexInfoOverlay extends Phaser.GameObjects.Container implements private maskPointOriginX: number; private maskPointOriginY: number; - public scale: number; public width: number; constructor(options?: PokedexInfoOverlaySettings) { super(globalScene, options?.x, options?.y); - this.scale = options?.scale || 1; // set up the scale - this.setScale(this.scale); + this.setScale(1); this.options = options || {}; // prepare the description box - this.width = (options?.width || PokedexInfoOverlay.getWidth(this.scale)) / this.scale; // divide by scale as we always want this to be half a window wide + this.width = options?.width || PokedexInfoOverlay.getWidth(); // we always want this to be half a window wide this.descBg = addWindow(0, 0, this.width, DESC_HEIGHT); this.descBg.setOrigin(0, 0); this.add(this.descBg); @@ -61,19 +58,19 @@ export class PokedexInfoOverlay extends Phaser.GameObjects.Container implements this.maskPointOriginY = options?.y || 0; if (this.maskPointOriginX < 0) { - this.maskPointOriginX += globalScene.game.canvas.width / GLOBAL_SCALE; + this.maskPointOriginX += globalScene.scaledCanvas.width; } if (this.maskPointOriginY < 0) { - this.maskPointOriginY += globalScene.game.canvas.height / GLOBAL_SCALE; + this.maskPointOriginY += globalScene.scaledCanvas.height; } this.textMaskRect = globalScene.make.graphics(); this.textMaskRect.fillStyle(0xff0000); this.textMaskRect.fillRect( - this.maskPointOriginX + BORDER * this.scale, - this.maskPointOriginY + (BORDER - 2) * this.scale, - this.width - BORDER * 2 * this.scale, - (DESC_HEIGHT - (BORDER - 2) * 2) * this.scale, + this.maskPointOriginX + BORDER, + this.maskPointOriginY + (BORDER - 2), + this.width - BORDER * 2, + DESC_HEIGHT - (BORDER - 2) * 2, ); this.textMaskRect.setScale(6); const textMask = this.createGeometryMask(this.textMaskRect); @@ -111,10 +108,10 @@ export class PokedexInfoOverlay extends Phaser.GameObjects.Container implements this.textMaskRect.clear(); this.textMaskRect.fillStyle(0xff0000); this.textMaskRect.fillRect( - this.maskPointOriginX + BORDER * this.scale, - this.maskPointOriginY + (BORDER - 2) * this.scale + (48 - newHeight), - this.width - BORDER * 2 * this.scale, - (newHeight - (BORDER - 2) * 2) * this.scale, + this.maskPointOriginX + BORDER, + this.maskPointOriginY + (BORDER - 2) + (48 - newHeight), + this.width - BORDER * 2, + newHeight - (BORDER - 2) * 2, ); const updatedMask = this.createGeometryMask(this.textMaskRect); this.desc.setMask(updatedMask); @@ -167,12 +164,12 @@ export class PokedexInfoOverlay extends Phaser.GameObjects.Container implements } // width of this element - static getWidth(_scale: number): number { - return globalScene.game.canvas.width / GLOBAL_SCALE / 2; + static getWidth(): number { + return globalScene.scaledCanvas.width / 2; } // height of this element - static getHeight(scale: number, _onSide?: boolean): number { - return DESC_HEIGHT * scale; + static getHeight(): number { + return DESC_HEIGHT; } } diff --git a/src/ui/pokedex-page-ui-handler.ts b/src/ui/pokedex-page-ui-handler.ts index 227b86c4d4d..fbfb6b615a4 100644 --- a/src/ui/pokedex-page-ui-handler.ts +++ b/src/ui/pokedex-page-ui-handler.ts @@ -299,15 +299,15 @@ export class PokedexPageUiHandler extends MessageUiHandler { const langSettingKey = Object.keys(languageSettings).find(lang => currentLanguage.includes(lang)) ?? "en"; const textSettings = languageSettings[langSettingKey]; - this.starterSelectContainer = globalScene.add.container(0, -globalScene.game.canvas.height / 6); + this.starterSelectContainer = globalScene.add.container(0, -globalScene.scaledCanvas.height); this.starterSelectContainer.setVisible(false); ui.add(this.starterSelectContainer); const bgColor = globalScene.add.rectangle( 0, 0, - globalScene.game.canvas.width / 6, - globalScene.game.canvas.height / 6, + globalScene.scaledCanvas.width, + globalScene.scaledCanvas.height, 0x006860, ); bgColor.setOrigin(0, 0); @@ -602,7 +602,7 @@ export class PokedexPageUiHandler extends MessageUiHandler { this.filterInstructionsContainer.setVisible(true); this.starterSelectContainer.add(this.filterInstructionsContainer); - this.starterSelectMessageBoxContainer = globalScene.add.container(0, globalScene.game.canvas.height / 6); + this.starterSelectMessageBoxContainer = globalScene.add.container(0, globalScene.scaledCanvas.height); this.starterSelectMessageBoxContainer.setVisible(false); this.starterSelectContainer.add(this.starterSelectMessageBoxContainer); @@ -629,7 +629,7 @@ export class PokedexPageUiHandler extends MessageUiHandler { this.menuContainer = globalScene.add.container(-130, 0); this.menuContainer.setName("menu"); this.menuContainer.setInteractive( - new Phaser.Geom.Rectangle(0, 0, globalScene.game.canvas.width / 6, globalScene.game.canvas.height / 6), + new Phaser.Geom.Rectangle(0, 0, globalScene.scaledCanvas.width, globalScene.scaledCanvas.height), Phaser.Geom.Rectangle.Contains, ); @@ -659,10 +659,10 @@ export class PokedexPageUiHandler extends MessageUiHandler { this.scale = getTextStyleOptions(TextStyle.WINDOW, globalScene.uiTheme).scale; this.menuBg = addWindow( - globalScene.game.canvas.width / 6 - 83, + globalScene.scaledCanvas.width - 83, 0, this.optionSelectText.displayWidth + 19 + 24 * this.scale, - globalScene.game.canvas.height / 6 - 2, + globalScene.scaledCanvas.height - 2, ); this.menuBg.setOrigin(0, 0); @@ -682,19 +682,16 @@ export class PokedexPageUiHandler extends MessageUiHandler { this.menuContainer.bringToTop(this.baseStatsOverlay); // add the info overlay last to be the top most ui element and prevent the IVs from overlaying this - const overlayScale = 1; this.moveInfoOverlay = new MoveInfoOverlay({ - scale: overlayScale, top: true, x: 1, - y: globalScene.game.canvas.height / 6 - MoveInfoOverlay.getHeight(overlayScale) - 29, + y: globalScene.scaledCanvas.height - MoveInfoOverlay.getHeight() - 29, }); this.starterSelectContainer.add(this.moveInfoOverlay); this.infoOverlay = new PokedexInfoOverlay({ - scale: overlayScale, x: 1, - y: globalScene.game.canvas.height / 6 - PokedexInfoOverlay.getHeight(overlayScale) - 29, + y: globalScene.scaledCanvas.height - PokedexInfoOverlay.getHeight() - 29, }); this.starterSelectContainer.add(this.infoOverlay); @@ -1103,7 +1100,7 @@ export class PokedexPageUiHandler extends MessageUiHandler { this.starterSelectMessageBoxContainer.setY(0); this.message.setY(4); } else { - this.starterSelectMessageBoxContainer.setY(globalScene.game.canvas.height / 6); + this.starterSelectMessageBoxContainer.setY(globalScene.scaledCanvas.height); this.starterSelectMessageBox.setOrigin(0, 1); this.message.setY(singleLine ? -22 : -37); } diff --git a/src/ui/pokedex-ui-handler.ts b/src/ui/pokedex-ui-handler.ts index cd1dc312f4d..75a3e5162e3 100644 --- a/src/ui/pokedex-ui-handler.ts +++ b/src/ui/pokedex-ui-handler.ts @@ -245,15 +245,15 @@ export class PokedexUiHandler extends MessageUiHandler { const langSettingKey = Object.keys(languageSettings).find(lang => currentLanguage.includes(lang)) ?? "en"; const textSettings = languageSettings[langSettingKey]; - this.starterSelectContainer = globalScene.add.container(0, -globalScene.game.canvas.height / 6); + this.starterSelectContainer = globalScene.add.container(0, -globalScene.scaledCanvas.height); this.starterSelectContainer.setVisible(false); ui.add(this.starterSelectContainer); const bgColor = globalScene.add.rectangle( 0, 0, - globalScene.game.canvas.width / 6, - globalScene.game.canvas.height / 6, + globalScene.scaledCanvas.width, + globalScene.scaledCanvas.height, 0x006860, ); bgColor.setOrigin(0, 0); @@ -544,7 +544,7 @@ export class PokedexUiHandler extends MessageUiHandler { this.type2Icon.setOrigin(0, 0); this.starterSelectContainer.add(this.type2Icon); - this.starterSelectMessageBoxContainer = globalScene.add.container(0, globalScene.game.canvas.height / 6); + this.starterSelectMessageBoxContainer = globalScene.add.container(0, globalScene.scaledCanvas.height); this.starterSelectMessageBoxContainer.setVisible(false); this.starterSelectContainer.add(this.starterSelectMessageBoxContainer); @@ -784,7 +784,7 @@ export class PokedexUiHandler extends MessageUiHandler { this.starterSelectMessageBoxContainer.setY(0); this.message.setY(4); } else { - this.starterSelectMessageBoxContainer.setY(globalScene.game.canvas.height / 6); + this.starterSelectMessageBoxContainer.setY(globalScene.scaledCanvas.height); this.starterSelectMessageBox.setOrigin(0, 1); this.message.setY(singleLine ? -22 : -37); } diff --git a/src/ui/run-history-ui-handler.ts b/src/ui/run-history-ui-handler.ts index 00aa47ae65d..457c48654a3 100644 --- a/src/ui/run-history-ui-handler.ts +++ b/src/ui/run-history-ui-handler.ts @@ -54,14 +54,14 @@ export class RunHistoryUiHandler extends MessageUiHandler { const loadSessionBg = globalScene.add.rectangle( 0, 0, - globalScene.game.canvas.width / 6, - -globalScene.game.canvas.height / 6, + globalScene.scaledCanvas.width, + -globalScene.scaledCanvas.height, 0x006860, ); loadSessionBg.setOrigin(0, 0); this.runSelectContainer.add(loadSessionBg); - this.runContainerInitialY = -globalScene.game.canvas.height / 6 + 8; + this.runContainerInitialY = -globalScene.scaledCanvas.height + 8; this.runsContainer = globalScene.add.container(8, this.runContainerInitialY); this.runSelectContainer.add(this.runsContainer); diff --git a/src/ui/run-info-ui-handler.ts b/src/ui/run-info-ui-handler.ts index 465e48a45ad..667a22a001a 100644 --- a/src/ui/run-info-ui-handler.ts +++ b/src/ui/run-info-ui-handler.ts @@ -74,7 +74,7 @@ export class RunInfoUiHandler extends UiHandler { } override async setup() { - this.runContainer = globalScene.add.container(1, -(globalScene.game.canvas.height / 6) + 1); + this.runContainer = globalScene.add.container(1, -globalScene.scaledCanvas.height + 1); // The import of the modifiersModule is loaded here to sidestep async/await issues. this.modifiersModule = Modifier; this.runContainer.setVisible(false); @@ -120,7 +120,7 @@ export class RunInfoUiHandler extends UiHandler { // Creates Header and adds to this.runContainer this.addHeader(); - this.statsBgWidth = (globalScene.game.canvas.width / 6 - 2) / 3; + this.statsBgWidth = (globalScene.scaledCanvas.width - 2) / 3; // Creates Run Result Container this.runResultContainer = globalScene.add.container(0, 24); @@ -147,7 +147,7 @@ export class RunInfoUiHandler extends UiHandler { this.showParty(true); this.runContainer.setInteractive( - new Phaser.Geom.Rectangle(0, 0, globalScene.game.canvas.width / 6, globalScene.game.canvas.height / 6), + new Phaser.Geom.Rectangle(0, 0, globalScene.scaledCanvas.width, globalScene.scaledCanvas.height), Phaser.Geom.Rectangle.Contains, ); this.getUi().bringToTop(this.runContainer); @@ -174,7 +174,7 @@ export class RunInfoUiHandler extends UiHandler { * It does not check if the run has any PokemonHeldItemModifiers though. */ private addHeader() { - const headerBg = addWindow(0, 0, globalScene.game.canvas.width / 6 - 2, 24); + const headerBg = addWindow(0, 0, globalScene.scaledCanvas.width - 2, 24); headerBg.setOrigin(0, 0); this.runContainer.add(headerBg); if (this.runInfo.modifiers.length !== 0) { @@ -723,7 +723,7 @@ export class RunInfoUiHandler extends UiHandler { private parsePartyInfo(): void { const party = this.runInfo.party; const currentLanguage = i18next.resolvedLanguage ?? "en"; - const windowHeight = (globalScene.game.canvas.height / 6 - 23) / 6; + const windowHeight = (globalScene.scaledCanvas.height - 23) / 6; party.forEach((p: PokemonData, i: number) => { const pokemonInfoWindow = new RoundRectangle(globalScene, 0, 14, this.statsBgWidth * 2 + 10, windowHeight - 2, 3); @@ -971,8 +971,8 @@ export class RunInfoUiHandler extends UiHandler { endCard.setOrigin(0); endCard.setScale(0.5); const text = addTextObject( - globalScene.game.canvas.width / 12, - globalScene.game.canvas.height / 6 - 16, + globalScene.scaledCanvas.width / 2, + globalScene.scaledCanvas.height - 16, i18next.t("battle:congratulations"), TextStyle.SUMMARY, { fontSize: "128px" }, diff --git a/src/ui/save-slot-select-ui-handler.ts b/src/ui/save-slot-select-ui-handler.ts index 9da34e672f1..5b1566b4913 100644 --- a/src/ui/save-slot-select-ui-handler.ts +++ b/src/ui/save-slot-select-ui-handler.ts @@ -54,14 +54,14 @@ export class SaveSlotSelectUiHandler extends MessageUiHandler { const loadSessionBg = globalScene.add.rectangle( 0, 0, - globalScene.game.canvas.width / 6, - -globalScene.game.canvas.height / 6, + globalScene.scaledCanvas.width, + -globalScene.scaledCanvas.height, 0x006860, ); loadSessionBg.setOrigin(0, 0); this.saveSlotSelectContainer.add(loadSessionBg); - this.sessionSlotsContainerInitialY = -globalScene.game.canvas.height / 6 + 8; + this.sessionSlotsContainerInitialY = -globalScene.scaledCanvas.height + 8; this.sessionSlotsContainer = globalScene.add.container(8, this.sessionSlotsContainerInitialY); this.saveSlotSelectContainer.add(this.sessionSlotsContainer); diff --git a/src/ui/saving-icon-handler.ts b/src/ui/saving-icon-handler.ts index 6923017218f..00c8b8b526c 100644 --- a/src/ui/saving-icon-handler.ts +++ b/src/ui/saving-icon-handler.ts @@ -8,7 +8,7 @@ export class SavingIconHandler extends Phaser.GameObjects.Container { private shown: boolean; constructor() { - super(globalScene, globalScene.game.canvas.width / 6 - 4, globalScene.game.canvas.height / 6 - 4); + super(globalScene, globalScene.scaledCanvas.width - 4, globalScene.scaledCanvas.height - 4); } setup(): void { diff --git a/src/ui/settings/abstract-binding-ui-handler.ts b/src/ui/settings/abstract-binding-ui-handler.ts index eb68456a69d..b2bda2455bb 100644 --- a/src/ui/settings/abstract-binding-ui-handler.ts +++ b/src/ui/settings/abstract-binding-ui-handler.ts @@ -73,8 +73,8 @@ export abstract class AbstractBindingUiHandler extends UiHandler { // Setup backgrounds and text objects for UI. this.titleBg = addWindow( - globalScene.game.canvas.width / 6 - this.getWindowWidth(), - -(globalScene.game.canvas.height / 6) + 28 + 21, + globalScene.scaledCanvas.width - this.getWindowWidth(), + -globalScene.scaledCanvas.height + 28 + 21, this.getWindowWidth(), 24, ); @@ -82,8 +82,8 @@ export abstract class AbstractBindingUiHandler extends UiHandler { this.optionSelectContainer.add(this.titleBg); this.actionBg = addWindow( - globalScene.game.canvas.width / 6 - this.getWindowWidth(), - -(globalScene.game.canvas.height / 6) + this.getWindowHeight() + 28 + 21 + 21, + globalScene.scaledCanvas.width - this.getWindowWidth(), + -globalScene.scaledCanvas.height + this.getWindowHeight() + 28 + 21 + 21, this.getWindowWidth(), 24, ); @@ -102,8 +102,8 @@ export abstract class AbstractBindingUiHandler extends UiHandler { this.optionSelectContainer.add(this.timerText); this.optionSelectBg = addWindow( - globalScene.game.canvas.width / 6 - this.getWindowWidth(), - -(globalScene.game.canvas.height / 6) + this.getWindowHeight() + 28, + globalScene.scaledCanvas.width - this.getWindowWidth(), + -globalScene.scaledCanvas.height + this.getWindowHeight() + 28, this.getWindowWidth(), this.getWindowHeight(), ); diff --git a/src/ui/settings/abstract-control-settings-ui-handler.ts b/src/ui/settings/abstract-control-settings-ui-handler.ts index ee9e990ee2a..b40676fc97c 100644 --- a/src/ui/settings/abstract-control-settings-ui-handler.ts +++ b/src/ui/settings/abstract-control-settings-ui-handler.ts @@ -96,11 +96,11 @@ export abstract class AbstractControlSettingsUiHandler extends UiHandler { const ui = this.getUi(); this.navigationIcons = {}; - this.settingsContainer = globalScene.add.container(1, -(globalScene.game.canvas.height / 6) + 1); + this.settingsContainer = globalScene.add.container(1, -globalScene.scaledCanvas.height + 1); this.settingsContainer.setName(`settings-${this.titleSelected}`); this.settingsContainer.setInteractive( - new Phaser.Geom.Rectangle(0, 0, globalScene.game.canvas.width / 6, globalScene.game.canvas.height / 6), + new Phaser.Geom.Rectangle(0, 0, globalScene.scaledCanvas.width, globalScene.scaledCanvas.height), Phaser.Geom.Rectangle.Contains, ); @@ -109,15 +109,15 @@ export abstract class AbstractControlSettingsUiHandler extends UiHandler { this.optionsBg = addWindow( 0, this.navigationContainer.height, - globalScene.game.canvas.width / 6 - 2, - globalScene.game.canvas.height / 6 - 16 - this.navigationContainer.height - 2, + globalScene.scaledCanvas.width - 2, + globalScene.scaledCanvas.height - 16 - this.navigationContainer.height - 2, ); this.optionsBg.setOrigin(0, 0); this.actionsBg = addWindow( 0, - globalScene.game.canvas.height / 6 - this.navigationContainer.height, - globalScene.game.canvas.width / 6 - 2, + globalScene.scaledCanvas.height - this.navigationContainer.height, + globalScene.scaledCanvas.width - 2, 22, ); this.actionsBg.setOrigin(0, 0); @@ -597,7 +597,7 @@ export abstract class AbstractControlSettingsUiHandler extends UiHandler { // Check if the cursor object exists, if not, create it. if (!this.cursorObj) { - const cursorWidth = globalScene.game.canvas.width / 6 - (this.scrollBar.visible ? 16 : 10); + const cursorWidth = globalScene.scaledCanvas.width - (this.scrollBar.visible ? 16 : 10); this.cursorObj = globalScene.add.nineslice(0, 0, "summary_moves_cursor", undefined, cursorWidth, 16, 1, 1, 1, 1); this.cursorObj.setOrigin(0, 0); // Set the origin to the top-left corner. this.optionsContainer.add(this.cursorObj); // Add the cursor to the options container. diff --git a/src/ui/settings/abstract-settings-ui-handler.ts b/src/ui/settings/abstract-settings-ui-handler.ts index 81d733220fc..91d5aec984a 100644 --- a/src/ui/settings/abstract-settings-ui-handler.ts +++ b/src/ui/settings/abstract-settings-ui-handler.ts @@ -56,10 +56,10 @@ export class AbstractSettingsUiHandler extends MessageUiHandler { setup() { const ui = this.getUi(); - this.settingsContainer = globalScene.add.container(1, -(globalScene.game.canvas.height / 6) + 1); + this.settingsContainer = globalScene.add.container(1, -globalScene.scaledCanvas.height + 1); this.settingsContainer.setName(`settings-${this.title}`); this.settingsContainer.setInteractive( - new Phaser.Geom.Rectangle(0, 0, globalScene.game.canvas.width / 6, globalScene.game.canvas.height / 6 - 20), + new Phaser.Geom.Rectangle(0, 0, globalScene.scaledCanvas.width, globalScene.scaledCanvas.height - 20), Phaser.Geom.Rectangle.Contains, ); @@ -70,16 +70,16 @@ export class AbstractSettingsUiHandler extends MessageUiHandler { this.optionsBg = addWindow( 0, this.navigationContainer.height, - globalScene.game.canvas.width / 6 - 2, - globalScene.game.canvas.height / 6 - 16 - this.navigationContainer.height - 2, + globalScene.scaledCanvas.width - 2, + globalScene.scaledCanvas.height - 16 - this.navigationContainer.height - 2, ); this.optionsBg.setName("window-options-bg"); this.optionsBg.setOrigin(0, 0); const actionsBg = addWindow( 0, - globalScene.game.canvas.height / 6 - this.navigationContainer.height, - globalScene.game.canvas.width / 6 - 2, + globalScene.scaledCanvas.height - this.navigationContainer.height, + globalScene.scaledCanvas.width - 2, 22, ); actionsBg.setOrigin(0, 0); @@ -375,7 +375,7 @@ export class AbstractSettingsUiHandler extends MessageUiHandler { const ret = super.setCursor(cursor); if (!this.cursorObj) { - const cursorWidth = globalScene.game.canvas.width / 6 - (this.scrollBar.visible ? 16 : 10); + const cursorWidth = globalScene.scaledCanvas.width - (this.scrollBar.visible ? 16 : 10); this.cursorObj = globalScene.add.nineslice(0, 0, "summary_moves_cursor", undefined, cursorWidth, 16, 1, 1, 1, 1); this.cursorObj.setOrigin(0, 0); this.optionsContainer.add(this.cursorObj); diff --git a/src/ui/settings/navigation-menu.ts b/src/ui/settings/navigation-menu.ts index 2f3aa50f7f3..b889ce57b61 100644 --- a/src/ui/settings/navigation-menu.ts +++ b/src/ui/settings/navigation-menu.ts @@ -124,7 +124,7 @@ export class NavigationMenu extends Phaser.GameObjects.Container { */ setup() { const navigationManager = NavigationManager.getInstance(); - const headerBg = addWindow(0, 0, globalScene.game.canvas.width / 6 - 2, 24); + const headerBg = addWindow(0, 0, globalScene.scaledCanvas.width - 2, 24); headerBg.setOrigin(0, 0); this.add(headerBg); this.width = headerBg.width; diff --git a/src/ui/starter-select-ui-handler.ts b/src/ui/starter-select-ui-handler.ts index 5a8e6b803a8..d4690005c5b 100644 --- a/src/ui/starter-select-ui-handler.ts +++ b/src/ui/starter-select-ui-handler.ts @@ -16,7 +16,6 @@ import { POKERUS_STARTER_COUNT, speciesStarterCosts, } from "#balance/starters"; -import { applyChallenges, checkStarterValidForChallenge } from "#data/challenge"; import { allAbilities, allMoves, allSpecies } from "#data/data-lists"; import { Egg, getEggTierForSpecies } from "#data/egg"; import { GrowthRate, getGrowthRateColor } from "#data/exp"; @@ -59,6 +58,7 @@ import { StarterContainer } from "#ui/starter-container"; import { StatsContainer } from "#ui/stats-container"; import { addBBCodeTextObject, addTextObject } from "#ui/text"; import { addWindow } from "#ui/ui-theme"; +import { applyChallenges, checkStarterValidForChallenge } from "#utils/challenge-utils"; import { BooleanHolder, fixedInt, @@ -1050,12 +1050,10 @@ export class StarterSelectUiHandler extends MessageUiHandler { globalScene.add.existing(this.statsContainer); // add the info overlay last to be the top most ui element and prevent the IVs from overlaying this - const overlayScale = 1; this.moveInfoOverlay = new MoveInfoOverlay({ - scale: overlayScale, top: true, x: 1, - y: globalScene.game.canvas.height / 6 - MoveInfoOverlay.getHeight(overlayScale) - 29, + y: globalScene.scaledCanvas.height / 6 - MoveInfoOverlay.getHeight() - 29, }); this.starterSelectContainer.add([ @@ -1311,7 +1309,7 @@ export class StarterSelectUiHandler extends MessageUiHandler { this.starterSelectMessageBoxContainer.setY(0); this.message.setY(4); } else { - this.starterSelectMessageBoxContainer.setY(globalScene.game.canvas.height / 6); + this.starterSelectMessageBoxContainer.setY(globalScene.scaledCanvas.height); this.starterSelectMessageBox.setOrigin(0, 1); this.message.setY(singleLine ? -22 : -37); } diff --git a/src/ui/title-ui-handler.ts b/src/ui/title-ui-handler.ts index bd49c6dd430..36e37500a64 100644 --- a/src/ui/title-ui-handler.ts +++ b/src/ui/title-ui-handler.ts @@ -37,7 +37,7 @@ export class TitleUiHandler extends OptionSelectUiHandler { const ui = this.getUi(); - this.titleContainer = globalScene.add.container(0, -(globalScene.game.canvas.height / 6)); + this.titleContainer = globalScene.add.container(0, -globalScene.scaledCanvas.height); this.titleContainer.setName("title"); this.titleContainer.setAlpha(0); ui.add(this.titleContainer); @@ -54,7 +54,7 @@ export class TitleUiHandler extends OptionSelectUiHandler { this.playerCountLabel = addTextObject( // Actual y position will be determined after the title menu has been populated with options - globalScene.game.canvas.width / 6 - 2, + globalScene.scaledCanvas.width - 2, 0, `? ${i18next.t("menu:playersOnline")}`, TextStyle.MESSAGE, @@ -132,7 +132,7 @@ export class TitleUiHandler extends OptionSelectUiHandler { if (ret) { // Moving player count to top of the menu - this.playerCountLabel.setY(globalScene.game.canvas.height / 6 - 13 - this.getWindowHeight()); + this.playerCountLabel.setY(globalScene.scaledCanvas.height - 13 - this.getWindowHeight()); this.splashMessage = randItem(getSplashMessages()); this.splashMessageText.setText( diff --git a/src/ui/ui.ts b/src/ui/ui.ts index 847294dcfc6..d5baea07ed5 100644 --- a/src/ui/ui.ts +++ b/src/ui/ui.ts @@ -123,7 +123,7 @@ export class UI extends Phaser.GameObjects.Container { private overlayActive: boolean; constructor() { - super(globalScene, 0, globalScene.game.canvas.height / 6); + super(globalScene, 0, globalScene.scaledCanvas.height); this.mode = UiMode.MESSAGE; this.modeChain = []; @@ -183,13 +183,7 @@ export class UI extends Phaser.GameObjects.Container { for (const handler of this.handlers) { handler.setup(); } - this.overlay = globalScene.add.rectangle( - 0, - 0, - globalScene.game.canvas.width / 6, - globalScene.game.canvas.height / 6, - 0, - ); + this.overlay = globalScene.add.rectangle(0, 0, globalScene.scaledCanvas.width, globalScene.scaledCanvas.height, 0); this.overlay.setName("rect-ui-overlay"); this.overlay.setOrigin(0, 0); globalScene.uiContainer.add(this.overlay); @@ -440,15 +434,15 @@ export class UI extends Phaser.GameObjects.Container { if (isTouch) { // If we are in the top left quadrant on mobile, move the tooltip to the top right corner if (pointerX <= globalScene.game.canvas.width / 2 && pointerY <= globalScene.game.canvas.height / 2) { - x = globalScene.game.canvas.width / 6 - tooltipWidth - padding; + x = globalScene.scaledCanvas.width - tooltipWidth - padding; } } else { // If the tooltip would go offscreen on the right, or is close to it, move to the left of the cursor - if (x + tooltipWidth + padding > globalScene.game.canvas.width / 6) { + if (x + tooltipWidth + padding > globalScene.scaledCanvas.width) { x = Math.max(padding, pointerX / 6 - tooltipWidth - padding); } // If the tooltip would go offscreen at the bottom, or is close to it, move above the cursor - if (y + tooltipHeight + padding > globalScene.game.canvas.height / 6) { + if (y + tooltipHeight + padding > globalScene.scaledCanvas.height) { y = Math.max(padding, pointerY / 6 - tooltipHeight - padding); } } diff --git a/src/utils/challenge-utils.ts b/src/utils/challenge-utils.ts new file mode 100644 index 00000000000..43297027e04 --- /dev/null +++ b/src/utils/challenge-utils.ts @@ -0,0 +1,409 @@ +import type { FixedBattleConfig } from "#app/battle"; +import { globalScene } from "#app/global-scene"; +import { pokemonEvolutions } from "#balance/pokemon-evolutions"; +import { pokemonFormChanges } from "#data/pokemon-forms"; +import type { PokemonSpecies } from "#data/pokemon-species"; +import { ChallengeType } from "#enums/challenge-type"; +import type { MoveId } from "#enums/move-id"; +import type { MoveSourceType } from "#enums/move-source-type"; +import type { SpeciesId } from "#enums/species-id"; +import type { EnemyPokemon, PlayerPokemon, Pokemon } from "#field/pokemon"; +import type { ModifierTypeOption } from "#modifiers/modifier-type"; +import type { DexAttrProps } from "#system/game-data"; +import { BooleanHolder, type NumberHolder } from "./common"; +import { getPokemonSpecies } from "./pokemon-utils"; + +/** + * @param challengeType - {@linkcode ChallengeType.STARTER_CHOICE} + * @param pokemon - The {@linkcode PokemonSpecies} to check the validity of + * @param valid - A {@linkcode BooleanHolder} holding the checked species' validity; + * will be set to `false` if the species is disallowed + * @param dexAttr - The {@linkcode DexAttrProps | Dex attributes} of the species + * @returns `true` if any challenge was successfully applied + */ +export function applyChallenges( + challengeType: ChallengeType.STARTER_CHOICE, + pokemon: PokemonSpecies, + valid: BooleanHolder, + dexAttr: DexAttrProps, +): boolean; +/** + * Apply all challenges that modify available total starter points. + * @param challengeType {@link ChallengeType} ChallengeType.STARTER_POINTS + * @param points {@link NumberHolder} The amount of points you have available. + * @returns True if any challenge was successfully applied. + */ +export function applyChallenges(challengeType: ChallengeType.STARTER_POINTS, points: NumberHolder): boolean; +/** + * Apply all challenges that modify the cost of a starter. + * @param challengeType {@link ChallengeType} ChallengeType.STARTER_COST + * @param species {@link SpeciesId} The pokemon to change the cost of. + * @param points {@link NumberHolder} The cost of the pokemon. + * @returns True if any challenge was successfully applied. + */ +export function applyChallenges( + challengeType: ChallengeType.STARTER_COST, + species: SpeciesId, + cost: NumberHolder, +): boolean; +/** + * Apply all challenges that modify a starter after selection. + * @param challengeType {@link ChallengeType} ChallengeType.STARTER_MODIFY + * @param pokemon {@link Pokemon} The starter pokemon to modify. + * @returns True if any challenge was successfully applied. + */ +export function applyChallenges(challengeType: ChallengeType.STARTER_MODIFY, pokemon: Pokemon): boolean; +/** + * Apply all challenges that what pokemon you can have in battle. + * @param challengeType {@link ChallengeType} ChallengeType.POKEMON_IN_BATTLE + * @param pokemon {@link Pokemon} The pokemon to check the validity of. + * @param valid {@link BooleanHolder} A BooleanHolder, the value gets set to false if the pokemon isn't allowed. + * @returns True if any challenge was successfully applied. + */ +export function applyChallenges( + challengeType: ChallengeType.POKEMON_IN_BATTLE, + pokemon: Pokemon, + valid: BooleanHolder, +): boolean; +/** + * Apply all challenges that modify what fixed battles there are. + * @param challengeType {@link ChallengeType} ChallengeType.FIXED_BATTLES + * @param waveIndex {@link Number} The current wave index. + * @param battleConfig {@link FixedBattleConfig} The battle config to modify. + * @returns True if any challenge was successfully applied. + */ +export function applyChallenges( + challengeType: ChallengeType.FIXED_BATTLES, + waveIndex: number, + battleConfig: FixedBattleConfig, +): boolean; +/** + * Apply all challenges that modify type effectiveness. + * @param challengeType {@linkcode ChallengeType} ChallengeType.TYPE_EFFECTIVENESS + * @param effectiveness {@linkcode NumberHolder} The current effectiveness of the move. + * @returns True if any challenge was successfully applied. + */ +export function applyChallenges(challengeType: ChallengeType.TYPE_EFFECTIVENESS, effectiveness: NumberHolder): boolean; +/** + * Apply all challenges that modify what level AI are. + * @param challengeType {@link ChallengeType} ChallengeType.AI_LEVEL + * @param level {@link NumberHolder} The generated level of the pokemon. + * @param levelCap {@link Number} The maximum level cap for the current wave. + * @param isTrainer {@link Boolean} Whether this is a trainer pokemon. + * @param isBoss {@link Boolean} Whether this is a non-trainer boss pokemon. + * @returns True if any challenge was successfully applied. + */ +export function applyChallenges( + challengeType: ChallengeType.AI_LEVEL, + level: NumberHolder, + levelCap: number, + isTrainer: boolean, + isBoss: boolean, +): boolean; +/** + * Apply all challenges that modify how many move slots the AI has. + * @param challengeType {@link ChallengeType} ChallengeType.AI_MOVE_SLOTS + * @param pokemon {@link Pokemon} The pokemon being considered. + * @param moveSlots {@link NumberHolder} The amount of move slots. + * @returns True if any challenge was successfully applied. + */ +export function applyChallenges( + challengeType: ChallengeType.AI_MOVE_SLOTS, + pokemon: Pokemon, + moveSlots: NumberHolder, +): boolean; +/** + * Apply all challenges that modify whether a pokemon has its passive. + * @param challengeType {@link ChallengeType} ChallengeType.PASSIVE_ACCESS + * @param pokemon {@link Pokemon} The pokemon to modify. + * @param hasPassive {@link BooleanHolder} Whether it has its passive. + * @returns True if any challenge was successfully applied. + */ +export function applyChallenges( + challengeType: ChallengeType.PASSIVE_ACCESS, + pokemon: Pokemon, + hasPassive: BooleanHolder, +): boolean; +/** + * Apply all challenges that modify the game modes settings. + * @param challengeType {@link ChallengeType} ChallengeType.GAME_MODE_MODIFY + * @returns True if any challenge was successfully applied. + */ +export function applyChallenges(challengeType: ChallengeType.GAME_MODE_MODIFY): boolean; +/** + * Apply all challenges that modify what level a pokemon can access a move. + * @param challengeType {@link ChallengeType} ChallengeType.MOVE_ACCESS + * @param pokemon {@link Pokemon} What pokemon would learn the move. + * @param moveSource {@link MoveSourceType} What source the pokemon would get the move from. + * @param move {@link MoveId} The move in question. + * @param level {@link NumberHolder} The level threshold for access. + * @returns True if any challenge was successfully applied. + */ +export function applyChallenges( + challengeType: ChallengeType.MOVE_ACCESS, + pokemon: Pokemon, + moveSource: MoveSourceType, + move: MoveId, + level: NumberHolder, +): boolean; +/** + * Apply all challenges that modify what weight a pokemon gives to move generation + * @param challengeType {@link ChallengeType} ChallengeType.MOVE_WEIGHT + * @param pokemon {@link Pokemon} What pokemon would learn the move. + * @param moveSource {@link MoveSourceType} What source the pokemon would get the move from. + * @param move {@link MoveId} The move in question. + * @param weight {@link NumberHolder} The weight of the move. + * @returns True if any challenge was successfully applied. + */ +export function applyChallenges( + challengeType: ChallengeType.MOVE_WEIGHT, + pokemon: Pokemon, + moveSource: MoveSourceType, + move: MoveId, + weight: NumberHolder, +): boolean; + +export function applyChallenges(challengeType: ChallengeType.FLIP_STAT, pokemon: Pokemon, baseStats: number[]): boolean; + +/** + * Apply all challenges that conditionally enable or disable automatic party healing during biome transitions + * @param challengeType - {@linkcode ChallengeType.PARTY_HEAL} + * @param status - Whether party healing is enabled or not + * @returns `true` if any challenge was successfully applied, `false` otherwise + */ +export function applyChallenges(challengeType: ChallengeType.PARTY_HEAL, status: BooleanHolder): boolean; + +/** + * Apply all challenges that conditionally enable or disable the shop + * @param challengeType - {@linkcode ChallengeType.SHOP} + * @param status - Whether party healing is enabled or not + * @returns `true` if any challenge was successfully applied, `false` otherwise + */ +export function applyChallenges(challengeType: ChallengeType.SHOP, status: BooleanHolder): boolean; + +/** + * Apply all challenges that validate whether a pokemon can be added to the player's party or not + * @param challengeType - {@linkcode ChallengeType.POKEMON_ADD_TO_PARTY} + * @param pokemon - The pokemon being caught + * @param status - Whether the pokemon can be added to the party or not + * @return `true` if any challenge was sucessfully applied, `false` otherwise + */ +export function applyChallenges( + challengeType: ChallengeType.POKEMON_ADD_TO_PARTY, + pokemon: EnemyPokemon, + status: BooleanHolder, +): boolean; + +/** + * Apply all challenges that validate whether a pokemon is allowed to fuse or not + * @param challengeType - {@linkcode ChallengeType.POKEMON_FUSION} + * @param pokemon - The pokemon being checked + * @param status - Whether the selected pokemon is allowed to fuse or not + * @return `true` if any challenge was sucessfully applied, `false` otherwise + */ +export function applyChallenges( + challengeType: ChallengeType.POKEMON_FUSION, + pokemon: PlayerPokemon, + status: BooleanHolder, +): boolean; + +/** + * Apply all challenges that validate whether particular moves can or cannot be used + * @param challengeType - {@linkcode ChallengeType.POKEMON_MOVE} + * @param moveId - The move being checked + * @param status - Whether the move can be used or not + * @return `true` if any challenge was sucessfully applied, `false` otherwise + */ +export function applyChallenges( + challengeType: ChallengeType.POKEMON_MOVE, + moveId: MoveId, + status: BooleanHolder, +): boolean; + +/** + * Apply all challenges that validate whether particular items are or are not sold in the shop + * @param challengeType - {@linkcode ChallengeType.SHOP_ITEM} + * @param shopItem - The item being checked + * @param status - Whether the item should be added to the shop or not + * @return `true` if any challenge was sucessfully applied, `false` otherwise + */ +export function applyChallenges( + challengeType: ChallengeType.SHOP_ITEM, + shopItem: ModifierTypeOption | null, + status: BooleanHolder, +): boolean; + +/** + * Apply all challenges that validate whether particular items will be given as a reward after a wave + * @param challengeType - {@linkcode ChallengeType.WAVE_REWARD} + * @param reward - The reward being checked + * @param status - Whether the reward should be added to the reward options or not + * @return `true` if any challenge was sucessfully applied, `false` otherwise + */ +export function applyChallenges( + challengeType: ChallengeType.WAVE_REWARD, + reward: ModifierTypeOption | null, + status: BooleanHolder, +): boolean; + +/** + * Apply all challenges that prevent recovery from fainting + * @param challengeType - {@linkcode ChallengeType.PREVENT_REVIVE} + * @param status - Whether fainting is a permanent status or not + * @return `true` if any challenge was sucessfully applied, `false` otherwise + */ +export function applyChallenges(challengeType: ChallengeType.PREVENT_REVIVE, status: BooleanHolder): boolean; + +export function applyChallenges(challengeType: ChallengeType, ...args: any[]): boolean { + let ret = false; + globalScene.gameMode.challenges.forEach(c => { + if (c.value !== 0) { + switch (challengeType) { + case ChallengeType.STARTER_CHOICE: + ret ||= c.applyStarterChoice(args[0], args[1], args[2]); + break; + case ChallengeType.STARTER_POINTS: + ret ||= c.applyStarterPoints(args[0]); + break; + case ChallengeType.STARTER_COST: + ret ||= c.applyStarterCost(args[0], args[1]); + break; + case ChallengeType.STARTER_MODIFY: + ret ||= c.applyStarterModify(args[0]); + break; + case ChallengeType.POKEMON_IN_BATTLE: + ret ||= c.applyPokemonInBattle(args[0], args[1]); + break; + case ChallengeType.FIXED_BATTLES: + ret ||= c.applyFixedBattle(args[0], args[1]); + break; + case ChallengeType.TYPE_EFFECTIVENESS: + ret ||= c.applyTypeEffectiveness(args[0]); + break; + case ChallengeType.AI_LEVEL: + ret ||= c.applyLevelChange(args[0], args[1], args[2], args[3]); + break; + case ChallengeType.AI_MOVE_SLOTS: + ret ||= c.applyMoveSlot(args[0], args[1]); + break; + case ChallengeType.PASSIVE_ACCESS: + ret ||= c.applyPassiveAccess(args[0], args[1]); + break; + case ChallengeType.GAME_MODE_MODIFY: + ret ||= c.applyGameModeModify(); + break; + case ChallengeType.MOVE_ACCESS: + ret ||= c.applyMoveAccessLevel(args[0], args[1], args[2], args[3]); + break; + case ChallengeType.MOVE_WEIGHT: + ret ||= c.applyMoveWeight(args[0], args[1], args[2], args[3]); + break; + case ChallengeType.FLIP_STAT: + ret ||= c.applyFlipStat(args[0], args[1]); + break; + case ChallengeType.PARTY_HEAL: + ret ||= c.applyPartyHeal(args[0]); + break; + case ChallengeType.SHOP: + ret ||= c.applyShop(args[0]); + break; + case ChallengeType.POKEMON_ADD_TO_PARTY: + ret ||= c.applyPokemonAddToParty(args[0], args[1]); + break; + case ChallengeType.POKEMON_FUSION: + ret ||= c.applyPokemonFusion(args[0], args[1]); + break; + case ChallengeType.POKEMON_MOVE: + ret ||= c.applyPokemonMove(args[0], args[1]); + break; + case ChallengeType.SHOP_ITEM: + ret ||= c.applyShopItem(args[0], args[1]); + break; + case ChallengeType.WAVE_REWARD: + ret ||= c.applyWaveReward(args[0], args[1]); + break; + case ChallengeType.PREVENT_REVIVE: + ret ||= c.applyPreventRevive(args[0]); + break; + } + } + }); + return ret; +} + +/** + * Apply all challenges to the given starter (and form) to check its validity. + * Differs from {@linkcode checkSpeciesValidForChallenge} which only checks form changes. + * @param species - The {@linkcode PokemonSpecies} to check the validity of. + * @param dexAttr - The {@linkcode DexAttrProps | dex attributes} of the species, including its form index. + * @param soft - If `true`, allow it if it could become valid through evolution or form change. + * @returns `true` if the species is considered valid. + */ +export function checkStarterValidForChallenge(species: PokemonSpecies, props: DexAttrProps, soft: boolean) { + if (!soft) { + const isValidForChallenge = new BooleanHolder(true); + applyChallenges(ChallengeType.STARTER_CHOICE, species, isValidForChallenge, props); + return isValidForChallenge.value; + } + // We check the validity of every evolution and form change, and require that at least one is valid + const speciesToCheck = [species.speciesId]; + while (speciesToCheck.length) { + const checking = speciesToCheck.pop(); + // Linter complains if we don't handle this + if (!checking) { + return false; + } + const checkingSpecies = getPokemonSpecies(checking); + if (checkSpeciesValidForChallenge(checkingSpecies, props, true)) { + return true; + } + if (checking && pokemonEvolutions.hasOwnProperty(checking)) { + pokemonEvolutions[checking].forEach(e => { + // Form check to deal with cases such as Basculin -> Basculegion + // TODO: does this miss anything if checking forms of a stage 2 Pokémon? + if (!e?.preFormKey || e.preFormKey === species.forms[props.formIndex].formKey) { + speciesToCheck.push(e.speciesId); + } + }); + } + } + return false; +} + +/** + * Apply all challenges to the given species (and form) to check its validity. + * Differs from {@linkcode checkStarterValidForChallenge} which also checks evolutions. + * @param species - The {@linkcode PokemonSpecies} to check the validity of. + * @param dexAttr - The {@linkcode DexAttrProps | dex attributes} of the species, including its form index. + * @param soft - If `true`, allow it if it could become valid through a form change. + * @returns `true` if the species is considered valid. + */ +function checkSpeciesValidForChallenge(species: PokemonSpecies, props: DexAttrProps, soft: boolean) { + const isValidForChallenge = new BooleanHolder(true); + applyChallenges(ChallengeType.STARTER_CHOICE, species, isValidForChallenge, props); + if (!soft || !pokemonFormChanges.hasOwnProperty(species.speciesId)) { + return isValidForChallenge.value; + } + // If the form in props is valid, return true before checking other form changes + if (soft && isValidForChallenge.value) { + return true; + } + + const result = pokemonFormChanges[species.speciesId].some(f1 => { + // Exclude form changes that require the mon to be on the field to begin with + if (!("item" in f1.trigger)) { + return false; + } + + return species.forms.some((f2, formIndex) => { + if (f1.formKey === f2.formKey) { + const formProps = { ...props, formIndex }; + const isFormValidForChallenge = new BooleanHolder(true); + applyChallenges(ChallengeType.STARTER_CHOICE, species, isFormValidForChallenge, formProps); + return isFormValidForChallenge.value; + } + return false; + }); + }); + return result; +} diff --git a/test/challenges/hardcore.test.ts b/test/challenges/hardcore.test.ts new file mode 100644 index 00000000000..0f4ab1b9f02 --- /dev/null +++ b/test/challenges/hardcore.test.ts @@ -0,0 +1,169 @@ +import { Status } from "#data/status-effect"; +import { AbilityId } from "#enums/ability-id"; +import { Button } from "#enums/buttons"; +import { Challenges } from "#enums/challenges"; +import { MoveId } from "#enums/move-id"; +import { ShopCursorTarget } from "#enums/shop-cursor-target"; +import { SpeciesId } from "#enums/species-id"; +import { StatusEffect } from "#enums/status-effect"; +import { UiMode } from "#enums/ui-mode"; +import { GameManager } from "#test/test-utils/game-manager"; +import { ModifierSelectUiHandler } from "#ui/modifier-select-ui-handler"; +import Phaser from "phaser"; +import { afterEach, beforeAll, beforeEach, describe, expect, it } from "vitest"; + +describe("Challenges - 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, 1, 1); + game.override + .battleStyle("single") + .enemySpecies(SpeciesId.VOLTORB) + .enemyAbility(AbilityId.BALL_FETCH) + .enemyMoveset(MoveId.SPLASH) + .moveset(MoveId.SPLASH); + }); + + it("should render Revival Blessing unusable by players only", async () => { + game.override.enemyMoveset(MoveId.REVIVAL_BLESSING).moveset(MoveId.REVIVAL_BLESSING); + await game.challengeMode.startBattle([SpeciesId.NUZLEAF]); + + const player = game.field.getPlayerPokemon(); + const revBlessing = player.getMoveset()[0]; + expect(revBlessing.isUsable(player)).toBe(false); + + game.move.select(MoveId.REVIVAL_BLESSING); + await game.toEndOfTurn(); + + // Player struggled due to only move being the unusable Revival Blessing + expect(player).toHaveUsedMove(MoveId.STRUGGLE); + expect(game.field.getEnemyPokemon()).toHaveUsedMove(MoveId.REVIVAL_BLESSING); + }); + + it("prevents REVIVE items in shop and in wave rewards", async () => { + game.override.startingWave(181).startingLevel(200); + await game.challengeMode.startBattle(); + + game.move.select(MoveId.SPLASH); + await game.doKillOpponents(); + + await game.phaseInterceptor.to("SelectModifierPhase"); + expect(game.scene.ui.getMode()).to.equal(UiMode.MODIFIER_SELECT); + const modifierSelectHandler = game.scene.ui.handlers.find( + h => h instanceof ModifierSelectUiHandler, + ) as ModifierSelectUiHandler; + expect( + modifierSelectHandler.options.find(reward => reward.modifierTypeOption.type.group === "revive"), + ).toBeUndefined(); + expect( + modifierSelectHandler.shopOptionsRows.find(row => + row.find(item => item.modifierTypeOption.type.group === "revive"), + ), + ).toBeUndefined(); + }); + + it("prevents the automatic party heal from reviving fainted Pokémon", async () => { + game.override.startingWave(10).startingLevel(200); + await game.challengeMode.startBattle([SpeciesId.NUZLEAF, SpeciesId.WHISMUR]); + + const faintedPokemon = game.scene.getPlayerParty()[1]; + faintedPokemon.hp = 0; + faintedPokemon.status = new Status(StatusEffect.FAINT); + expect(faintedPokemon.isFainted()).toBe(true); + + game.move.select(MoveId.SPLASH); + await game.doKillOpponents(); + + await game.toNextWave(); + + expect(faintedPokemon.isFainted()).toBe(true); + }); + + // TODO: Couldn't figure out how to select party Pokémon + it.skip("prevents fusion with a fainted Pokémon", async () => { + game.override.itemRewards([{ name: "DNA_SPLICERS" }]); + await game.challengeMode.startBattle([SpeciesId.NUZLEAF, SpeciesId.WHISMUR]); + + const faintedPokemon = game.scene.getPlayerParty()[1]; + faintedPokemon.hp = 0; + faintedPokemon.status = new Status(StatusEffect.FAINT); + expect(faintedPokemon.isFainted()).toBe(true); + + game.move.select(MoveId.RAZOR_LEAF); + await game.doKillOpponents(); + + await game.phaseInterceptor.to("SelectModifierPhase"); + game.onNextPrompt( + "SelectModifierPhase", + UiMode.MODIFIER_SELECT, + () => { + const handler = game.scene.ui.getHandler() as ModifierSelectUiHandler; + // Traverse to and select first modifier + handler.setCursor(0); + handler.setRowCursor(ShopCursorTarget.REWARDS); + handler.processInput(Button.ACTION); + + // Go to fainted Pokémon and try to select it + handler.processInput(Button.RIGHT); + handler.processInput(Button.ACTION); + handler.processInput(Button.ACTION); + handler.processInput(Button.ACTION); + + expect(game.scene.getPlayerParty().length).toBe(2); + }, + () => game.isCurrentPhase("CommandPhase") || game.isCurrentPhase("NewBattlePhase"), + true, + ); + }); + + // TODO: Couldn't figure out how to select party Pokémon + it.skip("prevents fainted Pokémon from being revived", async () => { + game.override.itemRewards([{ name: "MAX_REVIVE" }]); + await game.challengeMode.startBattle([SpeciesId.NUZLEAF, SpeciesId.WHISMUR]); + + const faintedPokemon = game.scene.getPlayerParty()[1]; + faintedPokemon.hp = 0; + faintedPokemon.status = new Status(StatusEffect.FAINT); + expect(faintedPokemon.isFainted()).toBe(true); + + game.move.select(MoveId.RAZOR_LEAF); + await game.doKillOpponents(); + + await game.phaseInterceptor.to("SelectModifierPhase"); + game.onNextPrompt( + "SelectModifierPhase", + UiMode.MODIFIER_SELECT, + () => { + const handler = game.scene.ui.getHandler() as ModifierSelectUiHandler; + // Traverse to and select first modifier + handler.setCursor(0); + handler.setRowCursor(ShopCursorTarget.REWARDS); + handler.processInput(Button.ACTION); + + // Go to fainted Pokémon and try to select it + handler.processInput(Button.RIGHT); + handler.processInput(Button.ACTION); + handler.processInput(Button.ACTION); + handler.processInput(Button.ACTION); + + expect(faintedPokemon.isFainted()).toBe(true); + }, + () => game.isCurrentPhase("CommandPhase") || game.isCurrentPhase("NewBattlePhase"), + true, + ); + }); +}); diff --git a/test/challenges/limited-catch.test.ts b/test/challenges/limited-catch.test.ts new file mode 100644 index 00000000000..80be52df2fb --- /dev/null +++ b/test/challenges/limited-catch.test.ts @@ -0,0 +1,55 @@ +import { AbilityId } from "#enums/ability-id"; +import { Challenges } from "#enums/challenges"; +import { MoveId } from "#enums/move-id"; +import { PokeballType } from "#enums/pokeball"; +import { SpeciesId } from "#enums/species-id"; +import { GameManager } from "#test/test-utils/game-manager"; +import Phaser from "phaser"; +import { afterEach, beforeAll, beforeEach, describe, expect, it } from "vitest"; + +describe("Challenges - 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, 1, 1); + game.override + .battleStyle("single") + .enemySpecies(SpeciesId.VOLTORB) + .enemyAbility(AbilityId.BALL_FETCH) + .enemyMoveset(MoveId.SPLASH) + .startingModifier([{ name: "MASTER_BALL", count: 1 }]); + }); + + it("should allow wild Pokémon to be caught on X1 waves", async () => { + game.override.startingWave(31); + await game.challengeMode.startBattle([SpeciesId.NUZLEAF]); + + game.doThrowPokeball(PokeballType.MASTER_BALL); + await game.toEndOfTurn(); + + expect(game.scene.getPlayerParty()).toHaveLength(2); + }); + + it("should prevent Pokémon from being caught on non-X1 waves", async () => { + game.override.startingWave(53); + await game.challengeMode.startBattle([SpeciesId.NUZLEAF]); + + game.doThrowPokeball(PokeballType.MASTER_BALL); + await game.toEndOfTurn(); + + expect(game.scene.getPlayerParty()).toHaveLength(1); + }); +}); diff --git a/test/challenges/limited-support.test.ts b/test/challenges/limited-support.test.ts new file mode 100644 index 00000000000..5c0eb2bd420 --- /dev/null +++ b/test/challenges/limited-support.test.ts @@ -0,0 +1,90 @@ +import { AbilityId } from "#enums/ability-id"; +import { Challenges } from "#enums/challenges"; +import { MoveId } from "#enums/move-id"; +import { SpeciesId } from "#enums/species-id"; +import { UiMode } from "#enums/ui-mode"; +import { GameManager } from "#test/test-utils/game-manager"; +import { ModifierSelectUiHandler } from "#ui/modifier-select-ui-handler"; +import Phaser from "phaser"; +import { afterEach, beforeAll, beforeEach, describe, expect, it } from "vitest"; + +describe("Challenges - Limited Support", () => { + let phaserGame: Phaser.Game; + let game: GameManager; + + beforeAll(() => { + phaserGame = new Phaser.Game({ + type: Phaser.HEADLESS, + }); + }); + + afterEach(() => { + game.phaseInterceptor.restoreOg(); + }); + + beforeEach(() => { + game = new GameManager(phaserGame); + + game.override + .battleStyle("single") + .enemySpecies(SpeciesId.VOLTORB) + .enemyAbility(AbilityId.BALL_FETCH) + .enemyMoveset(MoveId.SPLASH); + }); + + it('should disable the shop in "No Shop"', async () => { + game.override.startingWave(181); + game.challengeMode.addChallenge(Challenges.LIMITED_SUPPORT, 2, 1); + await game.challengeMode.startBattle([SpeciesId.NUZLEAF]); + + game.move.use(MoveId.SPLASH); + await game.doKillOpponents(); + await game.phaseInterceptor.to("SelectModifierPhase"); + + expect(game.scene.ui.getMode()).toBe(UiMode.MODIFIER_SELECT); + const modifierSelectHandler = game.scene.ui.handlers.find( + h => h instanceof ModifierSelectUiHandler, + ) as ModifierSelectUiHandler; + expect(modifierSelectHandler.shopOptionsRows).toHaveLength(0); + }); + + it('should disable the automatic party heal in "No Heal"', async () => { + game.override.startingWave(10); + game.challengeMode.addChallenge(Challenges.LIMITED_SUPPORT, 1, 1); + await game.challengeMode.startBattle([SpeciesId.NUZLEAF]); + + const playerPokemon = game.field.getPlayerPokemon(); + playerPokemon.hp /= 2; + + game.move.use(MoveId.SPLASH); + await game.doKillOpponents(); + await game.toNextWave(); + + expect(playerPokemon).not.toHaveFullHp(); + }); + + it('should disable both automatic party healing and shop in "Both"', async () => { + game.override.startingWave(10); + game.challengeMode.addChallenge(Challenges.LIMITED_SUPPORT, 3, 1); + await game.challengeMode.startBattle([SpeciesId.NUZLEAF]); + + const playerPokemon = game.field.getPlayerPokemon(); + playerPokemon.hp /= 2; + + game.move.use(MoveId.SPLASH); + await game.doKillOpponents(); + await game.toNextWave(); + + expect(playerPokemon).not.toHaveFullHp(); + + game.move.use(MoveId.SPLASH); + await game.doKillOpponents(); + await game.phaseInterceptor.to("SelectModifierPhase"); + + expect(game.scene.ui.getMode()).toBe(UiMode.MODIFIER_SELECT); + const modifierSelectHandler = game.scene.ui.handlers.find( + h => h instanceof ModifierSelectUiHandler, + ) as ModifierSelectUiHandler; + expect(modifierSelectHandler.shopOptionsRows).toHaveLength(0); + }); +});