[Balance] The cost of buying same-species eggs can be reduced (#6837)

* [Balance] The cost of buying same-species eggs can be reduced

After hatching a certain number of eggs for a starter,
the cost of buying same-species eggs
for that starter will be reduced (up to 50%)

* Add test to validate array lengths for egg costs
This commit is contained in:
NightKev 2025-12-09 22:00:20 -06:00 committed by GitHub
parent 46df6adab3
commit 3f5c37c881
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 77 additions and 31 deletions

View File

@ -7,10 +7,11 @@ export const CLASSIC_CANDY_FRIENDSHIP_MULTIPLIER = 3;
export const FRIENDSHIP_GAIN_FROM_BATTLE = 3; export const FRIENDSHIP_GAIN_FROM_BATTLE = 3;
export const FRIENDSHIP_GAIN_FROM_RARE_CANDY = 6; export const FRIENDSHIP_GAIN_FROM_RARE_CANDY = 6;
export const FRIENDSHIP_LOSS_FROM_FAINT = 5; export const FRIENDSHIP_LOSS_FROM_FAINT = 5;
// #endregion
/** /**
* Function to get the cumulative friendship threshold at which a candy is earned * 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 * @returns aforementioned threshold
*/ */
export function getStarterValueFriendshipCap(starterCost: number): number { export function getStarterValueFriendshipCap(starterCost: number): number {
@ -618,43 +619,72 @@ export const speciesStarterCosts = {
[SpeciesId.BLOODMOON_URSALUNA]: 5, [SpeciesId.BLOODMOON_URSALUNA]: 5,
}; };
const starterCandyCosts: { passive: number; costReduction: [number, number]; egg: number; }[] = [ interface StarterCandyCosts {
{ passive: 40, costReduction: [ 25, 60 ], egg: 30 }, // 1 Cost /** The candy cost to unlock the starter's passive ability */
{ passive: 40, costReduction: [ 25, 60 ], egg: 30 }, // 2 Cost readonly passive: number;
{ passive: 35, costReduction: [ 20, 50 ], egg: 25 }, // 3 Cost /** The candy costs to reduce the starter's point cost */
{ passive: 30, costReduction: [ 15, 40 ], egg: 20 }, // 4 Cost readonly costReduction: readonly [number, number];
{ passive: 25, costReduction: [ 12, 35 ], egg: 18 }, // 5 Cost /** The costs to buy a same-species egg */
{ passive: 20, costReduction: [ 10, 30 ], egg: 15 }, // 6 Cost readonly eggCosts: readonly [number, ...number[]];
{ passive: 15, costReduction: [ 8, 20 ], egg: 12 }, // 7 Cost /** The number of eggs required to hatch to reduce the cost for buying more eggs */
{ passive: 10, costReduction: [ 5, 15 ], egg: 10 }, // 8 Cost readonly eggCostReductionThresholds: readonly number[];
{ passive: 10, costReduction: [ 5, 15 ], egg: 10 }, // 9 Cost }
{ passive: 10, costReduction: [ 5, 15 ], egg: 10 }, // 10 Cost
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 * 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} * @param starterCost - The default point cost of the starter found in {@linkcode speciesStarterCosts}
* @returns the candy cost for passive unlock * @returns the candy cost for passive unlock
*/ */
export function getPassiveCandyCount(starterCost: number): number { 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 * 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} * @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 * @returns respective candy cost for the two cost reductions as an array 2 numbers
*/ */
export function getValueReductionCandyCounts(starterCost: number): [number, number] { export function getValueReductionCandyCounts(starterCost: number): readonly [number, number] {
return starterCandyCosts[starterCost - 1].costReduction; return allStarterCandyCosts[starterCost - 1].costReduction;
} }
/** /**
* Getter for {@linkcode starterCandyCosts} for egg purchase candy cost based on initial point cost * 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 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 * @returns the candy cost for the purchasable egg
*/ */
export function getSameSpeciesEggCandyCounts(starterCost: number): number { export function getSameSpeciesEggCandyCounts(starterCost: number, hatchCount: number): number {
return starterCandyCosts[starterCost - 1].egg; 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);
}
}

View File

@ -2020,7 +2020,8 @@ export class PokedexPageUiHandler extends MessageUiHandler {
} }
// Same species egg menu option. // 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({ options.push({
label: `×${sameSpeciesEggCost} ${i18next.t("pokedexUiHandler:sameSpeciesEgg")}`, label: `×${sameSpeciesEggCost} ${i18next.t("pokedexUiHandler:sameSpeciesEgg")}`,
handler: () => { handler: () => {
@ -2367,8 +2368,9 @@ export class PokedexPageUiHandler extends MessageUiHandler {
isSameSpeciesEggAvailable(): boolean { isSameSpeciesEggAvailable(): boolean {
// Get this species ID's starter data // Get this species ID's starter data
const starterData = globalScene.gameData.starterData[this.starterId]; 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() { setSpecies() {

View File

@ -893,11 +893,12 @@ export class PokedexUiHandler extends MessageUiHandler {
*/ */
isSameSpeciesEggAvailable(speciesId: number): boolean { isSameSpeciesEggAvailable(speciesId: number): boolean {
// Get this species ID's starter data // 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 ( return candyCount >= getSameSpeciesEggCandyCounts(speciesStarterCosts[starterId], hatchCount);
starterData.candyCount >= getSameSpeciesEggCandyCounts(speciesStarterCosts[this.getStarterSpeciesId(speciesId)])
);
} }
/** /**

View File

@ -1417,8 +1417,9 @@ export class StarterSelectUiHandler extends MessageUiHandler {
isSameSpeciesEggAvailable(speciesId: number): boolean { isSameSpeciesEggAvailable(speciesId: number): boolean {
// Get this species ID's starter data // Get this species ID's starter data
const starterData = globalScene.gameData.starterData[speciesId]; 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. // 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({ options.push({
label: `×${sameSpeciesEggCost} ${i18next.t("starterSelectUiHandler:sameSpeciesEgg")}`, label: `×${sameSpeciesEggCost} ${i18next.t("starterSelectUiHandler:sameSpeciesEgg")}`,
handler: () => { handler: () => {

View File

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