diff --git a/src/data/balance/starters.ts b/src/data/balance/starters.ts index 197a0d318bd..9e66004e95f 100644 --- a/src/data/balance/starters.ts +++ b/src/data/balance/starters.ts @@ -7,10 +7,11 @@ export const CLASSIC_CANDY_FRIENDSHIP_MULTIPLIER = 3; export const FRIENDSHIP_GAIN_FROM_BATTLE = 3; export const FRIENDSHIP_GAIN_FROM_RARE_CANDY = 6; export const FRIENDSHIP_LOSS_FROM_FAINT = 5; +// #endregion /** * Function to get the cumulative friendship threshold at which a candy is earned - * @param starterCost The cost of the starter, found in {@linkcode speciesStarterCosts} + * @param starterCost - The cost of the starter, found in {@linkcode speciesStarterCosts} * @returns aforementioned threshold */ export function getStarterValueFriendshipCap(starterCost: number): number { @@ -618,43 +619,72 @@ export const speciesStarterCosts = { [SpeciesId.BLOODMOON_URSALUNA]: 5, }; -const starterCandyCosts: { passive: number; costReduction: [number, number]; egg: number; }[] = [ - { passive: 40, costReduction: [ 25, 60 ], egg: 30 }, // 1 Cost - { passive: 40, costReduction: [ 25, 60 ], egg: 30 }, // 2 Cost - { passive: 35, costReduction: [ 20, 50 ], egg: 25 }, // 3 Cost - { passive: 30, costReduction: [ 15, 40 ], egg: 20 }, // 4 Cost - { passive: 25, costReduction: [ 12, 35 ], egg: 18 }, // 5 Cost - { passive: 20, costReduction: [ 10, 30 ], egg: 15 }, // 6 Cost - { passive: 15, costReduction: [ 8, 20 ], egg: 12 }, // 7 Cost - { passive: 10, costReduction: [ 5, 15 ], egg: 10 }, // 8 Cost - { passive: 10, costReduction: [ 5, 15 ], egg: 10 }, // 9 Cost - { passive: 10, costReduction: [ 5, 15 ], egg: 10 }, // 10 Cost +interface StarterCandyCosts { + /** The candy cost to unlock the starter's passive ability */ + readonly passive: number; + /** The candy costs to reduce the starter's point cost */ + readonly costReduction: readonly [number, number]; + /** The costs to buy a same-species egg */ + readonly eggCosts: readonly [number, ...number[]]; + /** The number of eggs required to hatch to reduce the cost for buying more eggs */ + readonly eggCostReductionThresholds: readonly number[]; +} + +const allStarterCandyCosts: readonly StarterCandyCosts[] = [ + { passive: 40, costReduction: [25, 60], eggCosts: [30, 27, 22, 15], eggCostReductionThresholds: [20, 40, 80] }, // 1 Cost + { passive: 40, costReduction: [25, 60], eggCosts: [30, 27, 22, 15], eggCostReductionThresholds: [20, 40, 80] }, // 2 Cost + { passive: 35, costReduction: [20, 50], eggCosts: [25, 22, 18, 12], eggCostReductionThresholds: [20, 40, 80] }, // 3 Cost + { passive: 30, costReduction: [15, 40], eggCosts: [20, 18, 15, 10], eggCostReductionThresholds: [15, 30, 60] }, // 4 Cost + { passive: 25, costReduction: [12, 35], eggCosts: [18, 16, 13, 9], eggCostReductionThresholds: [15, 30, 60] }, // 5 Cost + { passive: 20, costReduction: [10, 30], eggCosts: [15, 13, 11, 7], eggCostReductionThresholds: [15, 30, 60] }, // 6 Cost + { passive: 15, costReduction: [8, 20], eggCosts: [12, 10, 9, 6], eggCostReductionThresholds: [10, 20, 40] }, // 7 Cost + { passive: 10, costReduction: [5, 15], eggCosts: [10, 9, 7, 5], eggCostReductionThresholds: [10, 20, 40] }, // 8 Cost + { passive: 10, costReduction: [5, 15], eggCosts: [10, 9, 7, 5], eggCostReductionThresholds: [10, 20, 40] }, // 9 Cost + { passive: 10, costReduction: [5, 15], eggCosts: [10, 9, 7, 5], eggCostReductionThresholds: [8, 16, 32] }, // 10 Cost ]; /** - * Getter for {@linkcode starterCandyCosts} for passive unlock candy cost based on initial point cost - * @param starterCost the default point cost of the starter found in {@linkcode speciesStarterCosts} + * Getter for {@linkcode allStarterCandyCosts} for passive unlock candy cost based on initial point cost + * @param starterCost - The default point cost of the starter found in {@linkcode speciesStarterCosts} * @returns the candy cost for passive unlock */ export function getPassiveCandyCount(starterCost: number): number { - return starterCandyCosts[starterCost - 1].passive; + return allStarterCandyCosts[starterCost - 1].passive; } /** - * Getter for {@linkcode starterCandyCosts} for value reduction unlock candy cost based on initial point cost - * @param starterCost the default point cost of the starter found in {@linkcode speciesStarterCosts} + * Getter for {@linkcode allStarterCandyCosts} for value reduction unlock candy cost based on initial point cost + * @param starterCost - The default point cost of the starter found in {@linkcode speciesStarterCosts} * @returns respective candy cost for the two cost reductions as an array 2 numbers */ -export function getValueReductionCandyCounts(starterCost: number): [number, number] { - return starterCandyCosts[starterCost - 1].costReduction; +export function getValueReductionCandyCounts(starterCost: number): readonly [number, number] { + return allStarterCandyCosts[starterCost - 1].costReduction; } /** - * Getter for {@linkcode starterCandyCosts} for egg purchase candy cost based on initial point cost - * @param starterCost the default point cost of the starter found in {@linkcode speciesStarterCosts} + * Getter for {@linkcode allStarterCandyCosts} for egg purchase candy cost based on initial point cost + * @param starterCost - The default point cost of the starter found in {@linkcode speciesStarterCosts} + * @param hatchCount - The number of eggs hatched of the starter * @returns the candy cost for the purchasable egg */ -export function getSameSpeciesEggCandyCounts(starterCost: number): number { - return starterCandyCosts[starterCost - 1].egg; +export function getSameSpeciesEggCandyCounts(starterCost: number, hatchCount: number): number { + const starterCandyCosts = allStarterCandyCosts[starterCost - 1]; + let eggCostIndex = 0; + while (hatchCount >= starterCandyCosts.eggCostReductionThresholds[eggCostIndex]) { + eggCostIndex++; + } + return starterCandyCosts.eggCosts[eggCostIndex]; } +/** + * ⚠️ This is used for internal testing purposes only and will not be populated outside of the test environment. + * @internal + */ +export const __TEST_allStarterCandyCosts: readonly StarterCandyCosts[] = []; + +if (import.meta.env.NODE_ENV === "test") { + for (const starterCandyCosts of allStarterCandyCosts) { + // @ts-expect-error: done this way to keep it `readonly` + __TEST_allStarterCandyCosts.push(starterCandyCosts); + } +} \ No newline at end of file diff --git a/src/ui/handlers/pokedex-page-ui-handler.ts b/src/ui/handlers/pokedex-page-ui-handler.ts index cb9ed55bbd1..5e87872f7c2 100644 --- a/src/ui/handlers/pokedex-page-ui-handler.ts +++ b/src/ui/handlers/pokedex-page-ui-handler.ts @@ -2020,7 +2020,8 @@ export class PokedexPageUiHandler extends MessageUiHandler { } // Same species egg menu option. - const sameSpeciesEggCost = getSameSpeciesEggCandyCounts(speciesStarterCosts[this.starterId]); + const hatchCount = globalScene.gameData.dexData[this.starterId].hatchedCount; + const sameSpeciesEggCost = getSameSpeciesEggCandyCounts(speciesStarterCosts[this.starterId], hatchCount); options.push({ label: `×${sameSpeciesEggCost} ${i18next.t("pokedexUiHandler:sameSpeciesEgg")}`, handler: () => { @@ -2367,8 +2368,9 @@ export class PokedexPageUiHandler extends MessageUiHandler { isSameSpeciesEggAvailable(): boolean { // Get this species ID's starter data const starterData = globalScene.gameData.starterData[this.starterId]; + const hatchCount = globalScene.gameData.dexData[this.starterId].hatchedCount; - return starterData.candyCount >= getSameSpeciesEggCandyCounts(speciesStarterCosts[this.starterId]); + return starterData.candyCount >= getSameSpeciesEggCandyCounts(speciesStarterCosts[this.starterId], hatchCount); } setSpecies() { diff --git a/src/ui/handlers/pokedex-ui-handler.ts b/src/ui/handlers/pokedex-ui-handler.ts index b0afd88cf7a..135862fbbd4 100644 --- a/src/ui/handlers/pokedex-ui-handler.ts +++ b/src/ui/handlers/pokedex-ui-handler.ts @@ -893,11 +893,12 @@ export class PokedexUiHandler extends MessageUiHandler { */ isSameSpeciesEggAvailable(speciesId: number): boolean { // Get this species ID's starter data - const starterData = this.gameData.starterData[this.getStarterSpeciesId(speciesId)]; + const { gameData } = this; + const starterId = this.getStarterSpeciesId(speciesId); + const candyCount = gameData.starterData[starterId].candyCount; + const hatchCount = gameData.dexData[starterId].hatchedCount; - return ( - starterData.candyCount >= getSameSpeciesEggCandyCounts(speciesStarterCosts[this.getStarterSpeciesId(speciesId)]) - ); + return candyCount >= getSameSpeciesEggCandyCounts(speciesStarterCosts[starterId], hatchCount); } /** diff --git a/src/ui/handlers/starter-select-ui-handler.ts b/src/ui/handlers/starter-select-ui-handler.ts index 86d48ed992c..c487c0dcfea 100644 --- a/src/ui/handlers/starter-select-ui-handler.ts +++ b/src/ui/handlers/starter-select-ui-handler.ts @@ -1417,8 +1417,9 @@ export class StarterSelectUiHandler extends MessageUiHandler { isSameSpeciesEggAvailable(speciesId: number): boolean { // Get this species ID's starter data const starterData = globalScene.gameData.starterData[speciesId]; + const hatchedCount = globalScene.gameData.dexData[speciesId].hatchedCount; - return starterData.candyCount >= getSameSpeciesEggCandyCounts(speciesStarterCosts[speciesId]); + return starterData.candyCount >= getSameSpeciesEggCandyCounts(speciesStarterCosts[speciesId], hatchedCount); } /** @@ -2265,7 +2266,9 @@ export class StarterSelectUiHandler extends MessageUiHandler { } // Same species egg menu option. - const sameSpeciesEggCost = getSameSpeciesEggCandyCounts(speciesStarterCosts[this.lastSpecies.speciesId]); + const lastSpeciesId = this.lastSpecies.speciesId; + const hatchedCount = globalScene.gameData.dexData[lastSpeciesId].hatchedCount; + const sameSpeciesEggCost = getSameSpeciesEggCandyCounts(speciesStarterCosts[lastSpeciesId], hatchedCount); options.push({ label: `×${sameSpeciesEggCost} ${i18next.t("starterSelectUiHandler:sameSpeciesEgg")}`, handler: () => { diff --git a/test/data/balance/starter-candy-costs.test.ts b/test/data/balance/starter-candy-costs.test.ts new file mode 100644 index 00000000000..ed3ebd24c60 --- /dev/null +++ b/test/data/balance/starter-candy-costs.test.ts @@ -0,0 +1,10 @@ +import { __TEST_allStarterCandyCosts } from "#balance/starters"; +import { describe, expect, it } from "vitest"; + +describe("Starter Candy Costs", () => { + it("should have the proper length of arrays in `allStarterCandyCosts`", () => { + for (const starterCandyCosts of __TEST_allStarterCandyCosts) { + expect(starterCandyCosts.eggCosts).toHaveLength(starterCandyCosts.eggCostReductionThresholds.length + 1); + } + }); +});