From e85b47903237c63a52388db7645f6c5a692e1c41 Mon Sep 17 00:00:00 2001 From: Wlowscha <54003515+Wlowscha@users.noreply.github.com> Date: Thu, 21 Aug 2025 01:26:17 +0200 Subject: [PATCH] [UI/UX][Challenge] Starter select screen shows reduced information in fresh start (#6277) * Introduced `getSpeciesData` function * Saving and loading starter preferences as intended without conflicts * Hiding shiny star and hidden ability icon, showing correct nature * Forcing first tera type always * Fixed bug which prevented tera from displaying the saved preference; tera resets in fresh start * Ensuring that label text updates correctly * Removed large commented code, plus lock on hardy nature. * Excluded specific forms from fresh start, fixed bug with default nature * Removed Rockruff from restricted forms * Moves update correctly when switched around * Making tera type persist in preferences * Saving starter preferences after every change * Tera type not resetting randomly * Removed some log messages * Incorporated suggestions from review * Only allowing unlocked neutral natures, or default to hardy if no natures are unlocked * Capping ivs at 15 instead of setting them at 15 * Apply suggestions from code review Co-authored-by: Sirz Benjie <142067137+SirzBenjie@users.noreply.github.com> * Hiding various things * Ran biome --------- Co-authored-by: Sirz Benjie <142067137+SirzBenjie@users.noreply.github.com> --- src/data/challenge.ts | 72 +++++++- src/enums/challenge-type.ts | 5 + src/system/game-data.ts | 8 +- src/ui/starter-select-ui-handler.ts | 252 ++++++++++++++++++---------- src/utils/challenge-utils.ts | 20 ++- 5 files changed, 264 insertions(+), 93 deletions(-) diff --git a/src/data/challenge.ts b/src/data/challenge.ts index 186a62f95fc..95385620df4 100644 --- a/src/data/challenge.ts +++ b/src/data/challenge.ts @@ -3,9 +3,11 @@ import { getRandomTrainerFunc } from "#app/battle"; import { defaultStarterSpecies } from "#app/constants"; import { speciesStarterCosts } from "#balance/starters"; import type { PokemonSpecies } from "#data/pokemon-species"; +import { AbilityAttr } from "#enums/ability-attr"; import { BattleType } from "#enums/battle-type"; import { Challenges } from "#enums/challenges"; import { TypeColor, TypeShadow } from "#enums/color"; +import { DexAttr } from "#enums/dex-attr"; import { ClassicFixedBossWaves } from "#enums/fixed-boss-waves"; import { ModifierTier } from "#enums/modifier-tier"; import { MoveId } from "#enums/move-id"; @@ -18,8 +20,9 @@ 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 type { DexAttrProps, GameData, StarterDataEntry } from "#system/game-data"; import { RibbonData, type RibbonFlag } from "#system/ribbons/ribbon-data"; +import type { DexEntry } from "#types/dex-data"; import { type BooleanHolder, isBetween, type NumberHolder, randSeedItem } from "#utils/common"; import { deepCopy } from "#utils/data"; import { getPokemonSpecies, getPokemonSpeciesForm } from "#utils/pokemon-utils"; @@ -236,6 +239,15 @@ export abstract class Challenge { return false; } + /** + * An apply function for STARTER_SELECT_MODIFY challenges. Derived classes should alter this. + * @param _pokemon {@link Pokemon} The starter pokemon to modify. + * @returns {@link boolean} Whether this function did anything. + */ + applyStarterSelectModify(_speciesId: SpeciesId, _dexEntry: DexEntry, _starterDataEntry: StarterDataEntry): boolean { + return false; + } + /** * An apply function for STARTER_MODIFY challenges. Derived classes should alter this. * @param _pokemon {@link Pokemon} The starter pokemon to modify. @@ -796,6 +808,58 @@ export class FreshStartChallenge extends Challenge { return true; } + applyStarterSelectModify(speciesId: SpeciesId, dexEntry: DexEntry, starterDataEntry: StarterDataEntry): boolean { + // Remove all egg moves + starterDataEntry.eggMoves = 0; + + // Remove hidden and passive ability + const defaultAbilities = AbilityAttr.ABILITY_1 | AbilityAttr.ABILITY_2; + starterDataEntry.abilityAttr &= defaultAbilities; + starterDataEntry.passiveAttr = 0; + + // Remove cost reduction + starterDataEntry.valueReduction = 0; + + // Remove natures except for the default ones + const neutralNaturesAttr = + (1 << (Nature.HARDY + 1)) | + (1 << (Nature.DOCILE + 1)) | + (1 << (Nature.SERIOUS + 1)) | + (1 << (Nature.BASHFUL + 1)) | + (1 << (Nature.QUIRKY + 1)); + dexEntry.natureAttr &= neutralNaturesAttr; + + // Cap all ivs at 15 + for (let i = 0; i < 6; i++) { + dexEntry.ivs[i] = Math.min(dexEntry.ivs[i], 15); + } + + // Removes shiny and variants + dexEntry.caughtAttr &= ~DexAttr.SHINY; + dexEntry.caughtAttr &= ~(DexAttr.VARIANT_2 | DexAttr.VARIANT_3); + + // Remove unlocked forms for specific species + if (speciesId === SpeciesId.ZYGARDE) { + // Sets ability from power construct to aura break + const formMask = (DexAttr.DEFAULT_FORM << 2n) - 1n; + dexEntry.caughtAttr &= formMask; + } else if ( + [ + SpeciesId.PIKACHU, + SpeciesId.EEVEE, + SpeciesId.PICHU, + SpeciesId.ROTOM, + SpeciesId.MELOETTA, + SpeciesId.FROAKIE, + ].includes(speciesId) + ) { + const formMask = (DexAttr.DEFAULT_FORM << 1n) - 1n; // These mons are set to form 0 because they're meant to be unlocks or mid-run form changes + dexEntry.caughtAttr &= formMask; + } + + return true; + } + applyStarterModify(pokemon: Pokemon): boolean { pokemon.abilityIndex = pokemon.abilityIndex % 2; // Always base ability, if you set it to hidden it wraps to first ability pokemon.passive = false; // Passive isn't unlocked @@ -825,12 +889,14 @@ export class FreshStartChallenge extends Challenge { SpeciesId.ROTOM, SpeciesId.MELOETTA, SpeciesId.FROAKIE, - SpeciesId.ROCKRUFF, ].includes(pokemon.species.speciesId) ) { pokemon.formIndex = 0; // These mons are set to form 0 because they're meant to be unlocks or mid-run form changes } - pokemon.ivs = [15, 15, 15, 15, 15, 15]; // Default IVs of 15 for all stats (Updated to 15 from 10 in 1.2.0) + // Cap all ivs at 15 + for (let i = 0; i < 6; i++) { + pokemon.ivs[i] = Math.min(pokemon.ivs[i], 15); + } pokemon.teraType = pokemon.species.type1; // Always primary tera type return true; } diff --git a/src/enums/challenge-type.ts b/src/enums/challenge-type.ts index 053bcf92011..f3a4b7c68f9 100644 --- a/src/enums/challenge-type.ts +++ b/src/enums/challenge-type.ts @@ -18,6 +18,11 @@ export enum ChallengeType { * @see {@link Challenge.applyStarterPointCost} */ STARTER_COST, + /** + * Challenges which modify the starter data in starter select + * @see {@link Challenge.applyStarterSelectModify} + */ + STARTER_SELECT_MODIFY, /** * Challenges which modify your starters in some way * @see {@link Challenge.applyStarterModify} diff --git a/src/system/game-data.ts b/src/system/game-data.ts index 14224751262..3a4dafb2de2 100644 --- a/src/system/game-data.ts +++ b/src/system/game-data.ts @@ -2103,13 +2103,13 @@ export class GameData { }; } - getStarterSpeciesDefaultAbilityIndex(species: PokemonSpecies): number { - const abilityAttr = this.starterData[species.speciesId].abilityAttr; + getStarterSpeciesDefaultAbilityIndex(species: PokemonSpecies, abilityAttr?: number): number { + abilityAttr ??= this.starterData[species.speciesId].abilityAttr; return abilityAttr & AbilityAttr.ABILITY_1 ? 0 : !species.ability2 || abilityAttr & AbilityAttr.ABILITY_2 ? 1 : 2; } - getSpeciesDefaultNature(species: PokemonSpecies): Nature { - const dexEntry = this.dexData[species.speciesId]; + getSpeciesDefaultNature(species: PokemonSpecies, dexEntry?: DexEntry): Nature { + dexEntry ??= this.dexData[species.speciesId]; for (let n = 0; n < 25; n++) { if (dexEntry.natureAttr & (1 << (n + 1))) { return n as Nature; diff --git a/src/ui/starter-select-ui-handler.ts b/src/ui/starter-select-ui-handler.ts index 42ebea5f416..c1ab7b3cc1c 100644 --- a/src/ui/starter-select-ui-handler.ts +++ b/src/ui/starter-select-ui-handler.ts @@ -27,6 +27,7 @@ import { AbilityAttr } from "#enums/ability-attr"; import { AbilityId } from "#enums/ability-id"; import { Button } from "#enums/buttons"; import { ChallengeType } from "#enums/challenge-type"; +import { Challenges } from "#enums/challenges"; import { Device } from "#enums/devices"; import { DexAttr } from "#enums/dex-attr"; import { DropDownColumn } from "#enums/drop-down-column"; @@ -44,7 +45,7 @@ import { BattleSceneEventType } from "#events/battle-scene"; import type { Variant } from "#sprites/variant"; import { getVariantIcon, getVariantTint } from "#sprites/variant"; import { achvs } from "#system/achv"; -import type { DexAttrProps, StarterAttributes, StarterMoveset } from "#system/game-data"; +import type { DexAttrProps, StarterAttributes, StarterDataEntry, StarterMoveset } from "#system/game-data"; import { RibbonData } from "#system/ribbons/ribbon-data"; import { SettingKeyboard } from "#system/settings-keyboard"; import type { DexEntry } from "#types/dex-data"; @@ -300,6 +301,7 @@ export class StarterSelectUiHandler extends MessageUiHandler { private pokemonMoveBgs: Phaser.GameObjects.NineSlice[]; private pokemonMoveLabels: Phaser.GameObjects.Text[]; private pokemonAdditionalMoveCountLabel: Phaser.GameObjects.Text; + private eggMovesLabel: Phaser.GameObjects.Text; private pokemonEggMovesContainer: Phaser.GameObjects.Container; private pokemonEggMoveContainers: Phaser.GameObjects.Container[]; private pokemonEggMoveBgs: Phaser.GameObjects.NineSlice[]; @@ -400,6 +402,7 @@ export class StarterSelectUiHandler extends MessageUiHandler { private starterSelectCallback: StarterSelectCallback | null; private starterPreferences: StarterPreferences; + private originalStarterPreferences: StarterPreferences; /** * Used to check whether any moves were swapped using the reorder menu, to decide @@ -408,6 +411,7 @@ export class StarterSelectUiHandler extends MessageUiHandler { private hasSwappedMoves = false; protected blockInput = false; + private allowTera: boolean; constructor() { super(UiMode.STARTER_SELECT); @@ -880,14 +884,14 @@ export class StarterSelectUiHandler extends MessageUiHandler { this.pokemonEggMovesContainer = globalScene.add.container(102, 85).setScale(0.375); - const eggMovesLabel = addTextObject( + this.eggMovesLabel = addTextObject( -46, 0, i18next.t("starterSelectUiHandler:eggMoves"), TextStyle.WINDOW_ALT, ).setOrigin(0.5, 0); - this.pokemonEggMovesContainer.add(eggMovesLabel); + this.pokemonEggMovesContainer.add(this.eggMovesLabel); for (let m = 0; m < 4; m++) { const eggMoveContainer = globalScene.add.container(0, 16 + 14 * m); @@ -1132,25 +1136,31 @@ export class StarterSelectUiHandler extends MessageUiHandler { } show(args: any[]): boolean { - if (!this.starterPreferences) { - // starterPreferences haven't been loaded yet - this.starterPreferences = loadStarterPreferences(); - } this.moveInfoOverlay.clear(); // clear this when removing a menu; the cancel button doesn't seem to trigger this automatically on controllers this.pokerusSpecies = getPokerusStarters(); + this.allowTera = globalScene.gameData.achvUnlocks.hasOwnProperty(achvs.TERASTALLIZE.id); + if (args.length >= 1 && args[0] instanceof Function) { super.show(args); this.starterSelectCallback = args[0] as StarterSelectCallback; this.starterSelectContainer.setVisible(true); + this.starterPreferences = loadStarterPreferences(); + this.originalStarterPreferences = loadStarterPreferences(); + this.allSpecies.forEach((species, s) => { const icon = this.starterContainers[s].icon; - const dexEntry = globalScene.gameData.dexData[species.speciesId]; + const { dexEntry } = this.getSpeciesData(species.speciesId); // Initialize the StarterAttributes for this species - this.starterPreferences[species.speciesId] = this.initStarterPrefs(species); + this.starterPreferences[species.speciesId] = this.initStarterPrefs(species, this.starterPreferences); + this.originalStarterPreferences[species.speciesId] = this.initStarterPrefs( + species, + this.originalStarterPreferences, + true, + ); if (dexEntry.caughtAttr) { icon.clearTint(); @@ -1161,6 +1171,18 @@ export class StarterSelectUiHandler extends MessageUiHandler { this.setUpgradeAnimation(icon, species); }); + if (globalScene.gameMode.hasChallenge(Challenges.FRESH_START)) { + for (const container of this.pokemonEggMoveContainers) { + container.setVisible(false); + } + this.eggMovesLabel.setVisible(false); + // This is not enough, we need individual checks in setStarterSpecies too! :) + this.pokemonPassiveDisabledIcon.setVisible(false); + this.pokemonPassiveLabelText.setVisible(false); + this.pokemonPassiveLockedIcon.setVisible(false); + this.pokemonPassiveText.setVisible(false); + } + this.resetFilters(); this.updateStarters(); @@ -1185,10 +1207,13 @@ export class StarterSelectUiHandler extends MessageUiHandler { * @param species The species to get Starter Preferences for * @returns StarterAttributes for the species */ - initStarterPrefs(species: PokemonSpecies): StarterAttributes { - const starterAttributes = this.starterPreferences[species.speciesId]; - const dexEntry = globalScene.gameData.dexData[species.speciesId]; - const starterData = globalScene.gameData.starterData[species.speciesId]; + initStarterPrefs( + species: PokemonSpecies, + preferences: StarterPreferences, + ignoreChallenge = false, + ): StarterAttributes { + const starterAttributes = preferences[species.speciesId]; + const { dexEntry, starterDataEntry: starterData } = this.getSpeciesData(species.speciesId, !ignoreChallenge); // no preferences or Pokemon wasn't caught, return empty attribute if (!starterAttributes || !dexEntry.caughtAttr) { @@ -1268,6 +1293,17 @@ export class StarterSelectUiHandler extends MessageUiHandler { } } + if (starterAttributes.tera !== undefined) { + // If somehow we have an illegal tera type, it is reset here + if (!(starterAttributes.tera === species.type1 || starterAttributes.tera === species?.type2)) { + starterAttributes.tera = species.type1; + } + // In fresh start challenge, the tera type is always reset to the first one + if (globalScene.gameMode.hasChallenge(Challenges.FRESH_START) && !ignoreChallenge) { + starterAttributes.tera = species.type1; + } + } + return starterAttributes; } @@ -1720,7 +1756,7 @@ export class StarterSelectUiHandler extends MessageUiHandler { globalScene.gameData.getSpeciesDexAttrProps(species, this.getCurrentDexProps(species.speciesId)), this.isPartyValid(), ); - const isCaught = globalScene.gameData.dexData[species.speciesId].caughtAttr; + const isCaught = this.getSpeciesData(species.speciesId).dexEntry.caughtAttr; return ( !isDupe && isValidForChallenge && currentPartyValue + starterCost <= this.getValueLimit() && isCaught ); @@ -1787,9 +1823,10 @@ export class StarterSelectUiHandler extends MessageUiHandler { } } else { let starterContainer: StarterContainer; - const starterData = globalScene.gameData.starterData[this.lastSpecies.speciesId]; + const starterData = this.getSpeciesData(this.lastSpecies.speciesId).starterDataEntry; // prepare persistent starter data to store changes let starterAttributes = this.starterPreferences[this.lastSpecies.speciesId]; + const originalStarterAttributes = this.originalStarterPreferences[this.lastSpecies.speciesId]; // this gets the correct pokemon cursor depending on whether you're in the starter screen or the party icons if (!this.starterIconsCursorObj.visible) { @@ -2014,6 +2051,7 @@ export class StarterSelectUiHandler extends MessageUiHandler { starterAttributes = this.starterPreferences[this.lastSpecies.speciesId] = {}; } starterAttributes.nature = n; + originalStarterAttributes.nature = starterAttributes.nature; this.clearText(); ui.setMode(UiMode.STARTER_SELECT); // set nature for starter @@ -2082,6 +2120,7 @@ export class StarterSelectUiHandler extends MessageUiHandler { label: i18next.t("starterSelectUiHandler:addToFavorites"), handler: () => { starterAttributes.favorite = true; + originalStarterAttributes.favorite = true; // if the starter container not exists, it means the species is not in the filtered starters if (starterContainer) { starterContainer.favoriteIcon.setVisible(starterAttributes.favorite); @@ -2095,6 +2134,7 @@ export class StarterSelectUiHandler extends MessageUiHandler { label: i18next.t("starterSelectUiHandler:removeFromFavorites"), handler: () => { starterAttributes.favorite = false; + originalStarterAttributes.favorite = false; // if the starter container not exists, it means the species is not in the filtered starters if (starterContainer) { starterContainer.favoriteIcon.setVisible(starterAttributes.favorite); @@ -2117,6 +2157,7 @@ export class StarterSelectUiHandler extends MessageUiHandler { (sanitizedName: string) => { ui.playSelect(); starterAttributes.nickname = sanitizedName; + originalStarterAttributes.nickname = sanitizedName; const name = decodeURIComponent(escape(atob(starterAttributes.nickname))); if (name.length > 0) { this.pokemonNameText.setText(name); @@ -2142,7 +2183,7 @@ export class StarterSelectUiHandler extends MessageUiHandler { const options: any[] = []; // TODO: add proper type // Unlock passive option - if (!(passiveAttr & PassiveAttr.UNLOCKED)) { + if (!(passiveAttr & PassiveAttr.UNLOCKED) && !globalScene.gameMode.hasChallenge(Challenges.FRESH_START)) { const passiveCost = getPassiveCandyCount(speciesStarterCosts[this.lastSpecies.speciesId]); options.push({ label: `×${passiveCost} ${i18next.t("starterSelectUiHandler:unlockPassive")}`, @@ -2180,7 +2221,7 @@ export class StarterSelectUiHandler extends MessageUiHandler { // Reduce cost option const valueReduction = starterData.valueReduction; - if (valueReduction < valueReductionMax) { + if (valueReduction < valueReductionMax && !globalScene.gameMode.hasChallenge(Challenges.FRESH_START)) { const reductionCost = getValueReductionCandyCounts(speciesStarterCosts[this.lastSpecies.speciesId])[ valueReduction ]; @@ -2343,6 +2384,7 @@ export class StarterSelectUiHandler extends MessageUiHandler { this.pokemonShinyIcon.setFrame(getVariantIcon(newVariant)).setTint(tint).setVisible(true); starterAttributes.shiny = true; + originalStarterAttributes.shiny = true; } else { // If shiny, we update the variant let newVariant = props.variant; @@ -2366,6 +2408,7 @@ export class StarterSelectUiHandler extends MessageUiHandler { } } while (newVariant !== props.variant); starterAttributes.variant = newVariant; // store the selected variant + originalStarterAttributes.variant = newVariant; if (this.speciesStarterDexEntry!.caughtAttr & DexAttr.NON_SHINY && newVariant <= props.variant) { // If we have run out of variants, go back to non shiny this.setSpeciesDetails(this.lastSpecies, { @@ -2375,6 +2418,7 @@ export class StarterSelectUiHandler extends MessageUiHandler { this.pokemonShinyIcon.setVisible(false); success = true; starterAttributes.shiny = false; + originalStarterAttributes.shiny = false; } else { // If going to a higher variant, or only shiny forms are caught, go to next variant this.setSpeciesDetails(this.lastSpecies, { @@ -2403,7 +2447,9 @@ export class StarterSelectUiHandler extends MessageUiHandler { } } while (newFormIndex !== props.formIndex); starterAttributes.form = newFormIndex; // store the selected form + originalStarterAttributes.form = newFormIndex; starterAttributes.tera = this.lastSpecies.forms[newFormIndex].type1; + originalStarterAttributes.tera = starterAttributes.tera; this.setSpeciesDetails(this.lastSpecies, { formIndex: newFormIndex, teraType: starterAttributes.tera, @@ -2414,6 +2460,7 @@ export class StarterSelectUiHandler extends MessageUiHandler { case Button.CYCLE_GENDER: if (this.canCycleGender) { starterAttributes.female = !props.female; + originalStarterAttributes.female = starterAttributes.female; this.setSpeciesDetails(this.lastSpecies, { female: !props.female, }); @@ -2423,7 +2470,7 @@ export class StarterSelectUiHandler extends MessageUiHandler { case Button.CYCLE_ABILITY: if (this.canCycleAbility) { const abilityCount = this.lastSpecies.getAbilityCount(); - const abilityAttr = globalScene.gameData.starterData[this.lastSpecies.speciesId].abilityAttr; + const abilityAttr = starterData.abilityAttr; const hasAbility1 = abilityAttr & AbilityAttr.ABILITY_1; let newAbilityIndex = this.abilityCursor; do { @@ -2445,6 +2492,7 @@ export class StarterSelectUiHandler extends MessageUiHandler { } } while (newAbilityIndex !== this.abilityCursor); starterAttributes.ability = newAbilityIndex; // store the selected ability + originalStarterAttributes.ability = newAbilityIndex; const { visible: tooltipVisible } = globalScene.ui.getTooltip(); @@ -2466,6 +2514,7 @@ export class StarterSelectUiHandler extends MessageUiHandler { const newNature = natures[natureIndex < natures.length - 1 ? natureIndex + 1 : 0]; // store cycled nature as default starterAttributes.nature = newNature as unknown as number; + originalStarterAttributes.nature = starterAttributes.nature; this.setSpeciesDetails(this.lastSpecies, { natureIndex: newNature, }); @@ -2476,12 +2525,14 @@ export class StarterSelectUiHandler extends MessageUiHandler { if (this.canCycleTera) { const speciesForm = getPokemonSpeciesForm(this.lastSpecies.speciesId, starterAttributes.form ?? 0); if (speciesForm.type1 === this.teraCursor && !isNullOrUndefined(speciesForm.type2)) { - starterAttributes.tera = speciesForm.type2!; + starterAttributes.tera = speciesForm.type2; + originalStarterAttributes.tera = starterAttributes.tera; this.setSpeciesDetails(this.lastSpecies, { - teraType: speciesForm.type2!, + teraType: speciesForm.type2, }); } else { starterAttributes.tera = speciesForm.type1; + originalStarterAttributes.tera = starterAttributes.tera; this.setSpeciesDetails(this.lastSpecies, { teraType: speciesForm.type1, }); @@ -2728,16 +2779,16 @@ export class StarterSelectUiHandler extends MessageUiHandler { } const updatedMoveset = starterMoveset.slice() as StarterMoveset; const formIndex = globalScene.gameData.getSpeciesDexAttrProps(this.lastSpecies, this.dexAttrCursor).formIndex; - const starterData = globalScene.gameData.starterData[speciesId]; + const starterDataEntry = globalScene.gameData.starterData[speciesId]; // species has different forms if (pokemonFormLevelMoves.hasOwnProperty(speciesId)) { // Species has forms with different movesets - if (!starterData.moveset || Array.isArray(starterData.moveset)) { - starterData.moveset = {}; + if (!starterDataEntry.moveset || Array.isArray(starterDataEntry.moveset)) { + starterDataEntry.moveset = {}; } - starterData.moveset[formIndex] = updatedMoveset; + starterDataEntry.moveset[formIndex] = updatedMoveset; } else { - starterData.moveset = updatedMoveset; + starterDataEntry.moveset = updatedMoveset; } this.hasSwappedMoves = true; this.setSpeciesDetails(this.lastSpecies, { forSeen: false }); @@ -3009,8 +3060,8 @@ export class StarterSelectUiHandler extends MessageUiHandler { container.cost = globalScene.gameData.getSpeciesStarterValue(container.species.speciesId); // First, ensure you have the caught attributes for the species else default to bigint 0 - const caughtAttr = globalScene.gameData.dexData[container.species.speciesId]?.caughtAttr || BigInt(0); - const starterData = globalScene.gameData.starterData[container.species.speciesId]; + const { dexEntry, starterDataEntry: starterData } = this.getSpeciesData(container.species.speciesId); + const caughtAttr = dexEntry?.caughtAttr ?? BigInt(0); const isStarterProgressable = speciesEggMoves.hasOwnProperty(container.species.speciesId); // Gen filter @@ -3242,12 +3293,12 @@ export class StarterSelectUiHandler extends MessageUiHandler { onScreenFirstIndex + maxRows * maxColumns - 1, ); - const gameData = globalScene.gameData; - this.starterSelectScrollBar.setScrollCursor(this.scrollCursor); let pokerusCursorIndex = 0; this.filteredStarterContainers.forEach((container, i) => { + const { dexEntry, starterDataEntry } = this.getSpeciesData(container.species.speciesId); + const pos = calcStarterPosition(i, this.scrollCursor); container.setPosition(pos.x, pos.y); if (i < onScreenFirstIndex || i > onScreenLastIndex) { @@ -3283,10 +3334,8 @@ export class StarterSelectUiHandler extends MessageUiHandler { container.label.setVisible(true); const speciesVariants = - speciesId && gameData.dexData[speciesId].caughtAttr & DexAttr.SHINY - ? [DexAttr.DEFAULT_VARIANT, DexAttr.VARIANT_2, DexAttr.VARIANT_3].filter( - v => !!(gameData.dexData[speciesId].caughtAttr & v), - ) + speciesId && dexEntry.caughtAttr & DexAttr.SHINY + ? [DexAttr.DEFAULT_VARIANT, DexAttr.VARIANT_2, DexAttr.VARIANT_3].filter(v => !!(dexEntry.caughtAttr & v)) : []; for (let v = 0; v < 3; v++) { const hasVariant = speciesVariants.length > v; @@ -3300,15 +3349,11 @@ export class StarterSelectUiHandler extends MessageUiHandler { } } - container.starterPassiveBgs.setVisible(!!gameData.starterData[speciesId].passiveAttr); - container.hiddenAbilityIcon.setVisible( - !!gameData.dexData[speciesId].caughtAttr && !!(gameData.starterData[speciesId].abilityAttr & 4), - ); + container.starterPassiveBgs.setVisible(!!starterDataEntry.passiveAttr); + container.hiddenAbilityIcon.setVisible(!!dexEntry.caughtAttr && !!(starterDataEntry.abilityAttr & 4)); container.classicWinIcon - .setVisible(gameData.starterData[speciesId].classicWinCount > 0) - .setTexture( - gameData.dexData[speciesId].ribbons.has(RibbonData.NUZLOCKE) ? "champion_ribbon_emerald" : "champion_ribbon", - ); + .setVisible(starterDataEntry.classicWinCount > 0) + .setTexture(dexEntry.ribbons.has(RibbonData.NUZLOCKE) ? "champion_ribbon_emerald" : "champion_ribbon"); container.favoriteIcon.setVisible(this.starterPreferences[speciesId]?.favorite ?? false); // 'Candy Icon' mode @@ -3408,11 +3453,20 @@ export class StarterSelectUiHandler extends MessageUiHandler { } setSpecies(species: PokemonSpecies | null) { - this.speciesStarterDexEntry = species ? globalScene.gameData.dexData[species.speciesId] : null; - this.dexAttrCursor = species ? this.getCurrentDexProps(species.speciesId) : 0n; - this.abilityCursor = species ? globalScene.gameData.getStarterSpeciesDefaultAbilityIndex(species) : 0; - this.natureCursor = species ? globalScene.gameData.getSpeciesDefaultNature(species) : 0; - this.teraCursor = species ? species.type1 : PokemonType.UNKNOWN; + this.speciesStarterDexEntry = null; + this.dexAttrCursor = 0n; + this.abilityCursor = 0; + this.natureCursor = 0; + this.teraCursor = PokemonType.UNKNOWN; + + if (species) { + const { dexEntry } = this.getSpeciesData(species.speciesId); + this.speciesStarterDexEntry = dexEntry; + this.dexAttrCursor = this.getCurrentDexProps(species.speciesId); + this.abilityCursor = globalScene.gameData.getStarterSpeciesDefaultAbilityIndex(species); + this.natureCursor = globalScene.gameData.getSpeciesDefaultNature(species, dexEntry); + this.teraCursor = species.type1; + } if (!species && globalScene.ui.getTooltip().visible) { globalScene.ui.hideTooltip(); @@ -3577,11 +3631,12 @@ export class StarterSelectUiHandler extends MessageUiHandler { teraType: this.starterTeras[starterIndex], }); } else { - const defaultDexAttr = this.getCurrentDexProps(species.speciesId); const defaultAbilityIndex = starterAttributes?.ability ?? globalScene.gameData.getStarterSpeciesDefaultAbilityIndex(species); // load default nature from stater save data, if set - const defaultNature = starterAttributes?.nature || globalScene.gameData.getSpeciesDefaultNature(species); + const { dexEntry } = this.getSpeciesData(species.speciesId); + const defaultNature = + starterAttributes?.nature || globalScene.gameData.getSpeciesDefaultNature(species, dexEntry); props = globalScene.gameData.getSpeciesDexAttrProps(species, defaultDexAttr); if (starterAttributes?.variant && !Number.isNaN(starterAttributes.variant)) { if (props.shiny) { @@ -3598,6 +3653,7 @@ export class StarterSelectUiHandler extends MessageUiHandler { variant: props.variant, abilityIndex: defaultAbilityIndex, natureIndex: defaultNature, + teraType: starterAttributes?.tera, }); } @@ -3678,14 +3734,34 @@ export class StarterSelectUiHandler extends MessageUiHandler { } } + getSpeciesData( + speciesId: SpeciesId, + applyChallenge = true, + ): { dexEntry: DexEntry; starterDataEntry: StarterDataEntry } { + const dexEntry = globalScene.gameData.dexData[speciesId]; + const starterDataEntry = globalScene.gameData.starterData[speciesId]; + + const copiedDexEntry = { ...dexEntry }; + const copiedStarterDataEntry = { ...starterDataEntry }; + if (applyChallenge) { + applyChallenges(ChallengeType.STARTER_SELECT_MODIFY, speciesId, copiedDexEntry, copiedStarterDataEntry); + } + return { dexEntry: { ...copiedDexEntry }, starterDataEntry: { ...copiedStarterDataEntry } }; + } + setSpeciesDetails(species: PokemonSpecies, options: SpeciesDetails = {}): void { let { shiny, formIndex, female, variant, abilityIndex, natureIndex, teraType } = options; const forSeen: boolean = options.forSeen ?? false; const oldProps = species ? globalScene.gameData.getSpeciesDexAttrProps(species, this.dexAttrCursor) : null; const oldAbilityIndex = this.abilityCursor > -1 ? this.abilityCursor : globalScene.gameData.getStarterSpeciesDefaultAbilityIndex(species); - const oldNatureIndex = - this.natureCursor > -1 ? this.natureCursor : globalScene.gameData.getSpeciesDefaultNature(species); + let oldNatureIndex = -1; + if (species) { + const { dexEntry } = this.getSpeciesData(species.speciesId); + oldNatureIndex = + this.natureCursor > -1 ? this.natureCursor : globalScene.gameData.getSpeciesDefaultNature(species, dexEntry); + } + const oldTeraType = this.teraCursor > -1 ? this.teraCursor : species ? species.type1 : PokemonType.UNKNOWN; this.dexAttrCursor = 0n; this.abilityCursor = -1; this.natureCursor = -1; @@ -3698,6 +3774,8 @@ export class StarterSelectUiHandler extends MessageUiHandler { !isNullOrUndefined(shiny) || !isNullOrUndefined(variant); + const isFreshStartChallenge = globalScene.gameMode.hasChallenge(Challenges.FRESH_START); + if (this.activeTooltip === "CANDY") { if (this.lastSpecies && this.pokemonCandyContainer.visible) { const { currentFriendship, friendshipCap } = this.getFriendship(this.lastSpecies.speciesId); @@ -3732,7 +3810,7 @@ export class StarterSelectUiHandler extends MessageUiHandler { ); // TODO: is this bang correct? this.abilityCursor = abilityIndex !== undefined ? abilityIndex : (abilityIndex = oldAbilityIndex); this.natureCursor = natureIndex !== undefined ? natureIndex : (natureIndex = oldNatureIndex); - this.teraCursor = !isNullOrUndefined(teraType) ? teraType : (teraType = species.type1); + this.teraCursor = !isNullOrUndefined(teraType) ? teraType : (teraType = oldTeraType); const [isInParty, partyIndex]: [boolean, number] = this.isInParty(species); // we use this to firstly check if the pokemon is in the party, and if so, to get the party index in order to update the icon image if (isInParty) { this.updatePartyIcon(species, partyIndex); @@ -3755,15 +3833,14 @@ export class StarterSelectUiHandler extends MessageUiHandler { this.speciesStarterMoves = []; if (species) { - const dexEntry = globalScene.gameData.dexData[species.speciesId]; - const abilityAttr = globalScene.gameData.starterData[species.speciesId].abilityAttr; + const { dexEntry, starterDataEntry } = this.getSpeciesData(species.speciesId); + const caughtAttr = dexEntry.caughtAttr || BigInt(0); + const abilityAttr = starterDataEntry.abilityAttr; - const caughtAttr = globalScene.gameData.dexData[species.speciesId]?.caughtAttr || BigInt(0); - - if (!dexEntry.caughtAttr) { + if (!caughtAttr) { const props = globalScene.gameData.getSpeciesDexAttrProps(species, this.getCurrentDexProps(species.speciesId)); const defaultAbilityIndex = globalScene.gameData.getStarterSpeciesDefaultAbilityIndex(species); - const defaultNature = globalScene.gameData.getSpeciesDefaultNature(species); + const defaultNature = globalScene.gameData.getSpeciesDefaultNature(species, dexEntry); if (shiny === undefined || shiny !== props.shiny) { shiny = props.shiny; @@ -3873,8 +3950,9 @@ export class StarterSelectUiHandler extends MessageUiHandler { this.canCycleNature = globalScene.gameData.getNaturesForAttr(dexEntry.natureAttr).length > 1; this.canCycleTera = !this.statsMode && - globalScene.gameData.achvUnlocks.hasOwnProperty(achvs.TERASTALLIZE.id) && - !isNullOrUndefined(getPokemonSpeciesForm(species.speciesId, formIndex ?? 0).type2); + this.allowTera && + !isNullOrUndefined(getPokemonSpeciesForm(species.speciesId, formIndex ?? 0).type2) && + !globalScene.gameMode.hasChallenge(Challenges.FRESH_START); } if (dexEntry.caughtAttr && species.malePercent !== null) { @@ -3901,7 +3979,7 @@ export class StarterSelectUiHandler extends MessageUiHandler { .setColor(this.getTextColor(!isHidden ? TextStyle.SUMMARY_ALT : TextStyle.SUMMARY_GOLD)) .setShadowColor(this.getTextColor(!isHidden ? TextStyle.SUMMARY_ALT : TextStyle.SUMMARY_GOLD, true)); - const passiveAttr = globalScene.gameData.starterData[species.speciesId].passiveAttr; + const passiveAttr = starterDataEntry.passiveAttr; const passiveAbility = allAbilities[this.lastSpecies.getPassiveAbility(formIndex)]; if (this.pokemonAbilityText.visible) { @@ -3927,11 +4005,11 @@ export class StarterSelectUiHandler extends MessageUiHandler { const textAlpha = isUnlocked && isEnabled ? 1 : 0.5; this.pokemonPassiveLabelText - .setVisible(true) + .setVisible(!isFreshStartChallenge) .setColor(this.getTextColor(TextStyle.SUMMARY_ALT)) .setShadowColor(this.getTextColor(TextStyle.SUMMARY_ALT, true)); this.pokemonPassiveText - .setVisible(true) + .setVisible(!isFreshStartChallenge) .setText(passiveAbility.name) .setColor(this.getTextColor(textStyle)) .setAlpha(textAlpha) @@ -3957,9 +4035,11 @@ export class StarterSelectUiHandler extends MessageUiHandler { y: this.pokemonPassiveText.y + this.pokemonPassiveText.displayHeight / 2, }; this.pokemonPassiveDisabledIcon - .setVisible(isUnlocked && !isEnabled) + .setVisible(isUnlocked && !isEnabled && !isFreshStartChallenge) + .setPosition(iconPosition.x, iconPosition.y); + this.pokemonPassiveLockedIcon + .setVisible(!isUnlocked && !isFreshStartChallenge) .setPosition(iconPosition.x, iconPosition.y); - this.pokemonPassiveLockedIcon.setVisible(!isUnlocked).setPosition(iconPosition.x, iconPosition.y); } else if (this.activeTooltip === "PASSIVE") { // No passive and passive tooltip is active > hide it globalScene.ui.hideTooltip(); @@ -3982,13 +4062,13 @@ export class StarterSelectUiHandler extends MessageUiHandler { this.speciesStarterMoves.push(...levelMoves.filter(lm => lm[0] > 0 && lm[0] <= 5).map(lm => lm[1])); if (speciesEggMoves.hasOwnProperty(species.speciesId)) { for (let em = 0; em < 4; em++) { - if (globalScene.gameData.starterData[species.speciesId].eggMoves & (1 << em)) { + if (starterDataEntry.eggMoves & (1 << em)) { this.speciesStarterMoves.push(speciesEggMoves[species.speciesId][em]); } } } - const speciesMoveData = globalScene.gameData.starterData[species.speciesId].moveset; + const speciesMoveData = starterDataEntry.moveset; const moveData: StarterMoveset | null = speciesMoveData ? Array.isArray(speciesMoveData) ? speciesMoveData @@ -3996,9 +4076,7 @@ export class StarterSelectUiHandler extends MessageUiHandler { : null; const availableStarterMoves = this.speciesStarterMoves.concat( speciesEggMoves.hasOwnProperty(species.speciesId) - ? speciesEggMoves[species.speciesId].filter( - (_: any, em: number) => globalScene.gameData.starterData[species.speciesId].eggMoves & (1 << em), - ) + ? speciesEggMoves[species.speciesId].filter((_: any, em: number) => starterDataEntry.eggMoves & (1 << em)) : [], ); this.starterMoveset = (moveData || (this.speciesStarterMoves.slice(0, 4) as StarterMoveset)).filter(m => @@ -4025,9 +4103,7 @@ export class StarterSelectUiHandler extends MessageUiHandler { this.setTypeIcons(speciesForm.type1, speciesForm.type2); this.teraIcon.setFrame(PokemonType[this.teraCursor].toLowerCase()); - this.teraIcon.setVisible( - !this.statsMode && globalScene.gameData.achvUnlocks.hasOwnProperty(achvs.TERASTALLIZE.id), - ); + this.teraIcon.setVisible(!this.statsMode && this.allowTera); } else { this.pokemonAbilityText.setText(""); this.pokemonPassiveText.setText(""); @@ -4060,10 +4136,15 @@ export class StarterSelectUiHandler extends MessageUiHandler { } const hasEggMoves = species && speciesEggMoves.hasOwnProperty(species.speciesId); + let eggMoves = 0; + if (species) { + const { starterDataEntry } = this.getSpeciesData(this.lastSpecies.speciesId); + eggMoves = starterDataEntry.eggMoves; + } for (let em = 0; em < 4; em++) { const eggMove = hasEggMoves ? allMoves[speciesEggMoves[species.speciesId][em]] : null; - const eggMoveUnlocked = eggMove && globalScene.gameData.starterData[species.speciesId].eggMoves & (1 << em); + const eggMoveUnlocked = eggMove && eggMoves & (1 << em); this.pokemonEggMoveBgs[em].setFrame( PokemonType[eggMove ? eggMove.type : PokemonType.UNKNOWN].toString().toLowerCase(), ); @@ -4079,6 +4160,8 @@ export class StarterSelectUiHandler extends MessageUiHandler { this.tryUpdateValue(); this.updateInstructions(); + + saveStarterPreferences(this.originalStarterPreferences); } setTypeIcons(type1: PokemonType | null, type2: PokemonType | null): void { @@ -4172,9 +4255,7 @@ export class StarterSelectUiHandler extends MessageUiHandler { textStyle = TextStyle.SUMMARY_GOLD; break; } - if (baseStarterValue - starterValue > 0) { - starter.label.setColor(this.getTextColor(textStyle)).setShadowColor(this.getTextColor(textStyle, true)); - } + starter.label.setColor(this.getTextColor(textStyle)).setShadowColor(this.getTextColor(textStyle, true)); } tryUpdateValue(add?: number, addingToParty?: boolean): boolean { @@ -4333,14 +4414,12 @@ export class StarterSelectUiHandler extends MessageUiHandler { originalStarterSelectCallback?.( new Array(this.starterSpecies.length).fill(0).map((_, i) => { const starterSpecies = thisObj.starterSpecies[i]; + const { starterDataEntry } = this.getSpeciesData(starterSpecies.speciesId); return { species: starterSpecies, dexAttr: thisObj.starterAttr[i], abilityIndex: thisObj.starterAbilityIndexes[i], - passive: !( - globalScene.gameData.starterData[starterSpecies.speciesId].passiveAttr ^ - (PassiveAttr.ENABLED | PassiveAttr.UNLOCKED) - ), + passive: !(starterDataEntry.passiveAttr ^ (PassiveAttr.ENABLED | PassiveAttr.UNLOCKED)), nature: thisObj.starterNatures[i] as Nature, teraType: thisObj.starterTeras[i] as PokemonType, moveset: thisObj.starterMovesets[i], @@ -4397,7 +4476,8 @@ export class StarterSelectUiHandler extends MessageUiHandler { */ getCurrentDexProps(speciesId: number): bigint { let props = 0n; - const caughtAttr = globalScene.gameData.dexData[speciesId].caughtAttr; + const { dexEntry } = this.getSpeciesData(speciesId); + const caughtAttr = dexEntry.caughtAttr; /* this checks the gender of the pokemon; this works by checking a) that the starter preferences for the species exist, and if so, is it female. If so, it'll add DexAttr.FEMALE to our temp props * It then checks b) if the caughtAttr for the pokemon is female and NOT male - this means that the ONLY gender we've gotten is female, and we need to add DexAttr.FEMALE to our temp props @@ -4465,7 +4545,7 @@ export class StarterSelectUiHandler extends MessageUiHandler { this.pokemonSprite.setVisible(!!this.speciesStarterDexEntry?.caughtAttr); //@ts-expect-error this.statsContainer.updateIvs(null); // TODO: resolve ts-ignore. !?!? - this.teraIcon.setVisible(globalScene.gameData.achvUnlocks.hasOwnProperty(achvs.TERASTALLIZE.id)); + this.teraIcon.setVisible(this.allowTera); const props = globalScene.gameData.getSpeciesDexAttrProps( this.lastSpecies, this.getCurrentDexProps(this.lastSpecies.speciesId), @@ -4473,8 +4553,9 @@ export class StarterSelectUiHandler extends MessageUiHandler { const formIndex = props.formIndex; this.canCycleTera = !this.statsMode && - globalScene.gameData.achvUnlocks.hasOwnProperty(achvs.TERASTALLIZE.id) && - !isNullOrUndefined(getPokemonSpeciesForm(this.lastSpecies.speciesId, formIndex ?? 0).type2); + this.allowTera && + !isNullOrUndefined(getPokemonSpeciesForm(this.lastSpecies.speciesId, formIndex ?? 0).type2) && + !globalScene.gameMode.hasChallenge(Challenges.FRESH_START); this.updateInstructions(); } } @@ -4516,7 +4597,7 @@ export class StarterSelectUiHandler extends MessageUiHandler { clear(): void { super.clear(); - saveStarterPreferences(this.starterPreferences); + this.clearStarterPreferences(); this.cursor = -1; this.hideInstructions(); this.activeTooltip = undefined; @@ -4559,5 +4640,6 @@ export class StarterSelectUiHandler extends MessageUiHandler { */ clearStarterPreferences() { this.starterPreferences = {}; + this.originalStarterPreferences = {}; } } diff --git a/src/utils/challenge-utils.ts b/src/utils/challenge-utils.ts index c4fac3a0323..b0c162a74ed 100644 --- a/src/utils/challenge-utils.ts +++ b/src/utils/challenge-utils.ts @@ -10,7 +10,8 @@ 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 type { DexAttrProps, StarterDataEntry } from "#system/game-data"; +import type { DexEntry } from "#types/dex-data"; import { BooleanHolder, type NumberHolder } from "./common"; import { getPokemonSpecies } from "./pokemon-utils"; @@ -47,6 +48,20 @@ export function applyChallenges( species: SpeciesId, cost: NumberHolder, ): boolean; +/** + * Apply all challenges that modify selectable starter data. + * @param challengeType {@link ChallengeType} ChallengeType.STARTER_SELECT_MODIFY + * @param speciesId {@link SpeciesId} The speciesId of the pokemon + * @param dexEntry {@link DexEntry} The pokedex data associated to the pokemon. + * @param starterDataEntry {@link StarterDataEntry} The starter data associated to the pokemon. + * @returns True if any challenge was successfully applied. + */ +export function applyChallenges( + challengeType: ChallengeType.STARTER_SELECT_MODIFY, + speciesId: SpeciesId, + dexEntry: DexEntry, + starterDataEntry: StarterDataEntry, +): boolean; /** * Apply all challenges that modify a starter after selection. * @param challengeType {@link ChallengeType} ChallengeType.STARTER_MODIFY @@ -269,6 +284,9 @@ export function applyChallenges(challengeType: ChallengeType, ...args: any[]): b case ChallengeType.STARTER_COST: ret ||= c.applyStarterCost(args[0], args[1]); break; + case ChallengeType.STARTER_SELECT_MODIFY: + ret ||= c.applyStarterSelectModify(args[0], args[1], args[2]); + break; case ChallengeType.STARTER_MODIFY: ret ||= c.applyStarterModify(args[0]); break;