[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>
This commit is contained in:
Wlowscha 2025-08-21 01:26:17 +02:00 committed by GitHub
parent 14a0a23abc
commit e85b479032
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 264 additions and 93 deletions

View File

@ -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;
}

View File

@ -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}

View File

@ -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;

View File

@ -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 = {};
}
}

View File

@ -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;