diff --git a/src/data/challenge.ts b/src/data/challenge.ts index ea780f8aeb2..68b59789e64 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"; @@ -19,8 +21,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"; @@ -237,6 +240,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. @@ -797,10 +809,60 @@ export class FreshStartChallenge extends Challenge { return true; } + applyStarterSelectModify(speciesId: SpeciesId, dexEntry: DexEntry, starterDataEntry: StarterDataEntry): boolean { + // Remove all egg moves + starterDataEntry.eggMoves = 0; + console.log("I AM APPLYING, ", starterDataEntry.eggMoves); + + // 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; + + // Set all ivs to 15 + dexEntry.ivs = [15, 15, 15, 15, 15, 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) { + const formMask = (DexAttr.DEFAULT_FORM << 2n) - 1n; // Sets 10%-PC to 10%-AB and 50%-PC to 50%-AB + dexEntry.caughtAttr &= formMask; + } + 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 - pokemon.nature = Nature.HARDY; // Neutral nature let validMoves = pokemon.species .getLevelMoves() .filter(m => isBetween(m[0], 1, 5)) @@ -827,7 +889,6 @@ 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 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 90cbf6e18cc..2cabbb1ad62 100644 --- a/src/system/game-data.ts +++ b/src/system/game-data.ts @@ -64,7 +64,7 @@ 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 { executeIf, fixedInt, isLocal, isNullOrUndefined, NumberHolder, randInt, randSeedItem } from "#utils/common"; import { decrypt, encrypt } from "#utils/data"; import { getEnumKeys } from "#utils/enums"; import { getPokemonSpecies } from "#utils/pokemon-utils"; @@ -2103,13 +2103,17 @@ export class GameData { }; } - getStarterSpeciesDefaultAbilityIndex(species: PokemonSpecies): number { - const abilityAttr = this.starterData[species.speciesId].abilityAttr; + getStarterSpeciesDefaultAbilityIndex(species: PokemonSpecies, abilityAttr?: number): number { + if (isNullOrUndefined(abilityAttr)) { + 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 { + if (isNullOrUndefined(dexEntry)) { + 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 83a38ed7fe0..40831d799bc 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"; @@ -400,8 +401,10 @@ export class StarterSelectUiHandler extends MessageUiHandler { private starterSelectCallback: StarterSelectCallback | null; private starterPreferences: StarterPreferences; + private originalStarterPreferences: StarterPreferences; protected blockInput = false; + private allowTera: boolean; constructor() { super(UiMode.STARTER_SELECT); @@ -1126,25 +1129,32 @@ 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(); + console.log("Loaded", this.originalStarterPreferences[1]); + 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(); @@ -1154,6 +1164,7 @@ export class StarterSelectUiHandler extends MessageUiHandler { this.setUpgradeAnimation(icon, species); }); + console.log("Initiated", this.originalStarterPreferences[1]); this.resetFilters(); this.updateStarters(); @@ -1179,10 +1190,14 @@ 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 } = this.getSpeciesData(species.speciesId, !ignoreChallenge); + const starterData = starterDataEntry; // no preferences or Pokemon wasn't caught, return empty attribute if (!starterAttributes || !dexEntry.caughtAttr) { @@ -1262,6 +1277,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; } @@ -1714,7 +1740,8 @@ export class StarterSelectUiHandler extends MessageUiHandler { globalScene.gameData.getSpeciesDexAttrProps(species, this.getCurrentDexProps(species.speciesId)), this.isPartyValid(), ); - const isCaught = globalScene.gameData.dexData[species.speciesId].caughtAttr; + const { dexEntry } = this.getSpeciesData(species.speciesId); + const isCaught = dexEntry.caughtAttr; return ( !isDupe && isValidForChallenge && currentPartyValue + starterCost <= this.getValueLimit() && isCaught ); @@ -1781,9 +1808,11 @@ export class StarterSelectUiHandler extends MessageUiHandler { } } else { let starterContainer: StarterContainer; - const starterData = globalScene.gameData.starterData[this.lastSpecies.speciesId]; + const { starterDataEntry } = this.getSpeciesData(this.lastSpecies.speciesId); + const starterData = 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) { @@ -1999,6 +2028,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 @@ -2067,6 +2097,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); @@ -2080,6 +2111,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); @@ -2102,6 +2134,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); @@ -2328,6 +2361,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; @@ -2351,6 +2385,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, { @@ -2360,6 +2395,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, { @@ -2388,7 +2424,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, @@ -2399,6 +2437,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, }); @@ -2408,7 +2447,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 = starterDataEntry.abilityAttr; const hasAbility1 = abilityAttr & AbilityAttr.ABILITY_1; let newAbilityIndex = this.abilityCursor; do { @@ -2430,6 +2469,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(); @@ -2451,6 +2491,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, }); @@ -2461,12 +2502,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, }); @@ -2713,16 +2756,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.setSpeciesDetails(this.lastSpecies, { forSeen: false }); @@ -2994,8 +3037,9 @@ 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 } = this.getSpeciesData(container.species.speciesId); + const caughtAttr = dexEntry?.caughtAttr || BigInt(0); + const starterData = starterDataEntry; const isStarterProgressable = speciesEggMoves.hasOwnProperty(container.species.speciesId); // Gen filter @@ -3227,12 +3271,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) { @@ -3268,10 +3312,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; @@ -3285,15 +3327,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 @@ -3393,11 +3431,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(); @@ -3562,11 +3609,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) { @@ -3583,6 +3631,7 @@ export class StarterSelectUiHandler extends MessageUiHandler { variant: props.variant, abilityIndex: defaultAbilityIndex, natureIndex: defaultNature, + teraType: starterAttributes?.tera, }); } @@ -3663,14 +3712,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; @@ -3717,7 +3786,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); @@ -3740,15 +3809,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; @@ -3858,8 +3926,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) { @@ -3886,7 +3955,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) { @@ -3967,13 +4036,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 @@ -3981,9 +4050,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 => @@ -4010,9 +4077,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(""); @@ -4045,10 +4110,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(), ); @@ -4064,6 +4134,8 @@ export class StarterSelectUiHandler extends MessageUiHandler { this.tryUpdateValue(); this.updateInstructions(); + + saveStarterPreferences(this.originalStarterPreferences); } setTypeIcons(type1: PokemonType | null, type2: PokemonType | null): void { @@ -4157,9 +4229,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 { @@ -4318,14 +4388,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], @@ -4382,7 +4450,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 @@ -4450,7 +4519,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), @@ -4458,8 +4527,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(); } } @@ -4501,7 +4571,7 @@ export class StarterSelectUiHandler extends MessageUiHandler { clear(): void { super.clear(); - saveStarterPreferences(this.starterPreferences); + this.clearStarterPreferences(); this.cursor = -1; this.hideInstructions(); this.activeTooltip = undefined; @@ -4544,5 +4614,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;