/* eslint-disable max-lines-per-function -- Test suites naturally have many cases */ /* eslint-disable max-lines -- Test suites naturally have many cases */ /* eslint-disable @typescript-eslint/consistent-type-assertions -- Tests build minimal state objects */ import { describe, expect, it } from "vitest"; import { buildPostAwakeningState, calculateSoulShardsYield, computeAwakeningMultipliers, isEligibleForAwakening, } from "../../src/services/awakening.js"; import type { GameState } from "@elysium/types"; const makeAwakening = (overrides: Record = {}) => ({ count: 0, purchasedUpgradeIds: [] as Array, soulShards: 0, soulShardsBloodMultiplier: 1, soulShardsCombatMultiplier: 1, soulShardsMetaMultiplier: 1, soulShardsSiringIchorMultiplier: 1, soulShardsSiringThresholdMultiplier: 1, ...overrides, }); const makeSiring = (overrides: Record = {}) => ({ count: 0, ichor: 0, productionMultiplier: 1, purchasedUpgradeIds: [] as Array, ...overrides, }); const makeVampireState = (overrides: Record = {}) => ({ achievements: [] as Array<{ id: string; unlockedAt: number | null }>, awakening: makeAwakening(), baseClickPower: 1, bosses: [] as Array<{ id: string; status: string; bountyIchorClaimed?: boolean }>, equipment: [] as Array<{ id: string; owned: boolean; equipped: boolean }>, eternalSovereignty: { count: 0 }, exploration: { areas: [] as Array<{ id: string; status: string }>, craftedBloodMultiplier: 1, craftedCombatMultiplier: 1, craftedIchorMultiplier: 1, craftedRecipeIds: [] as Array, materials: [] as Array<{ materialId: string; quantity: number }>, }, lastTickAt: 0, lifetimeBloodEarned: 0, lifetimeBossesDefeated: 0, lifetimeQuestsCompleted: 0, quests: [] as Array<{ id: string; status: string }>, siring: makeSiring(), thralls: [] as Array<{ id: string; count: number }>, totalBloodEarned: 0, upgrades: [] as Array<{ id: string; purchased: boolean }>, zones: [] as Array<{ id: string; status: string }>, ...overrides, }); const makeState = (overrides: Partial = {}): GameState => ({ player: { discordId: "test_id", username: "u", discriminator: "0", avatar: null, totalGoldEarned: 0, totalClicks: 0, characterName: "T" }, resources: { gold: 0, essence: 0, crystals: 0, runestones: 0 }, adventurers: [], upgrades: [], quests: [], bosses: [], equipment: [], achievements: [], zones: [], exploration: { areas: [], materials: [], craftedRecipeIds: [], craftedGoldMultiplier: 1, craftedEssenceMultiplier: 1, craftedClickMultiplier: 1, craftedCombatMultiplier: 1 }, companions: { unlockedCompanionIds: [], activeCompanionId: null }, prestige: { count: 0, runestones: 0, productionMultiplier: 1, purchasedUpgradeIds: [] }, baseClickPower: 1, lastTickAt: 0, schemaVersion: 1, ...overrides, } as GameState); describe("isEligibleForAwakening", () => { it("returns false when vampire state is undefined", () => { const state = makeState(); expect(isEligibleForAwakening(state)).toBe(false); }); it("returns false when bosses array is empty", () => { const state = makeState({ vampire: makeVampireState() as GameState["vampire"] }); expect(isEligibleForAwakening(state)).toBe(false); }); it("returns false when eternal_darkness boss is present but not defeated", () => { const state = makeState({ vampire: makeVampireState({ bosses: [ { id: "eternal_darkness", status: "available" } ], }) as GameState["vampire"], }); expect(isEligibleForAwakening(state)).toBe(false); }); it("returns false when eternal_darkness boss is in_progress", () => { const state = makeState({ vampire: makeVampireState({ bosses: [ { id: "eternal_darkness", status: "in_progress" } ], }) as GameState["vampire"], }); expect(isEligibleForAwakening(state)).toBe(false); }); it("returns true when eternal_darkness boss is defeated", () => { const state = makeState({ vampire: makeVampireState({ bosses: [ { id: "eternal_darkness", status: "defeated" } ], }) as GameState["vampire"], }); expect(isEligibleForAwakening(state)).toBe(true); }); it("returns true even with other bosses in the array", () => { const state = makeState({ vampire: makeVampireState({ bosses: [ { id: "some_other_boss", status: "defeated" }, { id: "eternal_darkness", status: "defeated" }, ], }) as GameState["vampire"], }); expect(isEligibleForAwakening(state)).toBe(true); }); it("returns false when only other bosses are defeated (not eternal_darkness)", () => { const state = makeState({ vampire: makeVampireState({ bosses: [ { id: "some_other_boss", status: "defeated" } ], }) as GameState["vampire"], }); expect(isEligibleForAwakening(state)).toBe(false); }); }); describe("calculateSoulShardsYield", () => { it("returns 1 as minimum yield when siring count is 0", () => { expect(calculateSoulShardsYield(0, 1)).toBe(1); }); it("returns 1 as minimum yield when result would be below 1", () => { // sqrt(0) * 100 = 0 → max(1, 0) = 1 expect(calculateSoulShardsYield(0, 100)).toBe(1); }); it("computes floor(sqrt(4) * 1) = 2", () => { expect(calculateSoulShardsYield(4, 1)).toBe(2); }); it("computes floor(sqrt(9) * 1) = 3", () => { expect(calculateSoulShardsYield(9, 1)).toBe(3); }); it("applies meta multiplier correctly", () => { // floor(sqrt(4) * 2) = floor(2 * 2) = 4 expect(calculateSoulShardsYield(4, 2)).toBe(4); }); it("floors fractional results", () => { // floor(sqrt(2) * 1) = floor(1.414...) = 1 expect(calculateSoulShardsYield(2, 1)).toBe(1); }); it("floors fractional results with multiplier", () => { // floor(sqrt(9) * 1.5) = floor(3 * 1.5) = floor(4.5) = 4 expect(calculateSoulShardsYield(9, 1.5)).toBe(4); }); it("returns at least 1 even with very small siring count and no multiplier", () => { expect(calculateSoulShardsYield(1, 1)).toBe(1); }); }); describe("computeAwakeningMultipliers", () => { it("returns all 1s with empty purchasedUpgradeIds", () => { const result = computeAwakeningMultipliers([]); expect(result.soulShardsBloodMultiplier).toBe(1); expect(result.soulShardsCombatMultiplier).toBe(1); expect(result.soulShardsMetaMultiplier).toBe(1); expect(result.soulShardsSiringIchorMultiplier).toBe(1); expect(result.soulShardsSiringThresholdMultiplier).toBe(1); }); it("applies blood upgrade when purchased", () => { // awakening_blood_1 has multiplier 1.5 in blood category const result = computeAwakeningMultipliers([ "awakening_blood_1" ]); expect(result.soulShardsBloodMultiplier).toBe(1.5); expect(result.soulShardsCombatMultiplier).toBe(1); expect(result.soulShardsMetaMultiplier).toBe(1); expect(result.soulShardsSiringIchorMultiplier).toBe(1); expect(result.soulShardsSiringThresholdMultiplier).toBe(1); }); it("stacks multiple blood upgrades multiplicatively", () => { // awakening_blood_1 (×1.5) × awakening_blood_2 (×2) = 3.0 const result = computeAwakeningMultipliers([ "awakening_blood_1", "awakening_blood_2" ]); expect(result.soulShardsBloodMultiplier).toBe(3); }); it("applies combat upgrade when purchased", () => { // awakening_combat_1 has multiplier 1.5 in combat category const result = computeAwakeningMultipliers([ "awakening_combat_1" ]); expect(result.soulShardsCombatMultiplier).toBe(1.5); expect(result.soulShardsBloodMultiplier).toBe(1); }); it("stacks multiple combat upgrades multiplicatively", () => { // awakening_combat_1 (×1.5) × awakening_combat_2 (×2) = 3.0 const result = computeAwakeningMultipliers([ "awakening_combat_1", "awakening_combat_2" ]); expect(result.soulShardsCombatMultiplier).toBe(3); }); it("applies siring threshold upgrade when purchased", () => { // awakening_threshold_1 has multiplier 0.85 in siring_threshold category const result = computeAwakeningMultipliers([ "awakening_threshold_1" ]); expect(result.soulShardsSiringThresholdMultiplier).toBe(0.85); }); it("stacks multiple threshold upgrades multiplicatively", () => { // awakening_threshold_1 (×0.85) × awakening_threshold_2 (×0.8) = 0.68 const result = computeAwakeningMultipliers([ "awakening_threshold_1", "awakening_threshold_2" ]); expect(result.soulShardsSiringThresholdMultiplier).toBeCloseTo(0.68); }); it("applies siring ichor upgrade when purchased", () => { // awakening_siring_ichor_1 has multiplier 1.5 in siring_ichor category const result = computeAwakeningMultipliers([ "awakening_siring_ichor_1" ]); expect(result.soulShardsSiringIchorMultiplier).toBe(1.5); }); it("stacks multiple siring ichor upgrades multiplicatively", () => { // awakening_siring_ichor_1 (×1.5) × awakening_siring_ichor_2 (×2) = 3.0 const result = computeAwakeningMultipliers([ "awakening_siring_ichor_1", "awakening_siring_ichor_2" ]); expect(result.soulShardsSiringIchorMultiplier).toBe(3); }); it("applies meta upgrade when purchased", () => { // awakening_meta_1 has multiplier 1.5 in soulshards_meta category const result = computeAwakeningMultipliers([ "awakening_meta_1" ]); expect(result.soulShardsMetaMultiplier).toBe(1.5); }); it("stacks multiple meta upgrades multiplicatively", () => { // awakening_meta_1 (×1.5) × awakening_meta_2 (×2) = 3.0 const result = computeAwakeningMultipliers([ "awakening_meta_1", "awakening_meta_2" ]); expect(result.soulShardsMetaMultiplier).toBe(3); }); it("applies upgrades from multiple categories independently", () => { const result = computeAwakeningMultipliers([ "awakening_blood_1", "awakening_combat_1", "awakening_meta_1", ]); expect(result.soulShardsBloodMultiplier).toBe(1.5); expect(result.soulShardsCombatMultiplier).toBe(1.5); expect(result.soulShardsMetaMultiplier).toBe(1.5); expect(result.soulShardsSiringIchorMultiplier).toBe(1); expect(result.soulShardsSiringThresholdMultiplier).toBe(1); }); it("ignores unknown upgrade ids gracefully", () => { const result = computeAwakeningMultipliers([ "totally_fake_upgrade_id" ]); expect(result.soulShardsBloodMultiplier).toBe(1); expect(result.soulShardsCombatMultiplier).toBe(1); expect(result.soulShardsMetaMultiplier).toBe(1); expect(result.soulShardsSiringIchorMultiplier).toBe(1); expect(result.soulShardsSiringThresholdMultiplier).toBe(1); }); }); describe("buildPostAwakeningState", () => { it("increments awakening count by 1", () => { const vampire = makeVampireState({ awakening: makeAwakening({ count: 2, soulShards: 5 }), siring: makeSiring({ count: 4 }), }); const state = makeState({ vampire: vampire as GameState["vampire"] }); const { updatedVampire } = buildPostAwakeningState(state); expect(updatedVampire.awakening.count).toBe(3); }); it("adds soulShardsEarned to existing soul shards", () => { const vampire = makeVampireState({ awakening: makeAwakening({ count: 0, soulShards: 10 }), siring: makeSiring({ count: 4 }), }); const state = makeState({ vampire: vampire as GameState["vampire"] }); const { soulShardsEarned, updatedVampire } = buildPostAwakeningState(state); expect(soulShardsEarned).toBeGreaterThanOrEqual(1); expect(updatedVampire.awakening.soulShards).toBe(10 + soulShardsEarned); }); it("uses metaMultiplier from awakening when computing soul shards yield", () => { // metaMultiplier = 1.5, siring count = 4 → floor(sqrt(4) * 1.5) = floor(3) = 3 const vampire = makeVampireState({ awakening: makeAwakening({ soulShardsMetaMultiplier: 1.5 }), siring: makeSiring({ count: 4 }), }); const state = makeState({ vampire: vampire as GameState["vampire"] }); const { soulShardsEarned } = buildPostAwakeningState(state); expect(soulShardsEarned).toBe(3); }); it("preserves purchased upgrade ids across awakening", () => { const vampire = makeVampireState({ awakening: makeAwakening({ purchasedUpgradeIds: [ "awakening_blood_1" ] }), siring: makeSiring({ count: 4 }), }); const state = makeState({ vampire: vampire as GameState["vampire"] }); const { updatedVampire } = buildPostAwakeningState(state); expect(updatedVampire.awakening.purchasedUpgradeIds).toContain("awakening_blood_1"); }); it("recomputes multipliers based on existing purchased upgrade ids", () => { const vampire = makeVampireState({ awakening: makeAwakening({ purchasedUpgradeIds: [ "awakening_blood_1" ] }), siring: makeSiring({ count: 4 }), }); const state = makeState({ vampire: vampire as GameState["vampire"] }); const { updatedVampire } = buildPostAwakeningState(state); // awakening_blood_1 has multiplier 1.5 expect(updatedVampire.awakening.soulShardsBloodMultiplier).toBe(1.5); }); it("preserves achievements across awakening", () => { const achievements = [ { id: "ach_1", unlockedAt: 1000 } ]; const vampire = makeVampireState({ achievements, siring: makeSiring({ count: 1 }) }); const state = makeState({ vampire: vampire as GameState["vampire"] }); const { updatedVampire } = buildPostAwakeningState(state); expect(updatedVampire.achievements).toEqual(achievements); }); it("preserves equipment across awakening", () => { const equipment = [ { id: "eq_1", owned: true, equipped: true } ]; const vampire = makeVampireState({ equipment, siring: makeSiring({ count: 1 }) }); const state = makeState({ vampire: vampire as GameState["vampire"] }); const { updatedVampire } = buildPostAwakeningState(state); expect(updatedVampire.equipment).toEqual(equipment); }); it("preserves eternalSovereignty count across awakening", () => { const vampire = makeVampireState({ eternalSovereignty: { count: 5 }, siring: makeSiring({ count: 1 }), }); const state = makeState({ vampire: vampire as GameState["vampire"] }); const { updatedVampire } = buildPostAwakeningState(state); expect(updatedVampire.eternalSovereignty.count).toBe(5); }); it("preserves lifetime blood earned across awakening", () => { const vampire = makeVampireState({ lifetimeBloodEarned: 9_999_999, siring: makeSiring({ count: 1 }), }); const state = makeState({ vampire: vampire as GameState["vampire"] }); const { updatedVampire } = buildPostAwakeningState(state); expect(updatedVampire.lifetimeBloodEarned).toBe(9_999_999); }); it("preserves lifetime bosses defeated across awakening", () => { const vampire = makeVampireState({ lifetimeBossesDefeated: 42, siring: makeSiring({ count: 1 }), }); const state = makeState({ vampire: vampire as GameState["vampire"] }); const { updatedVampire } = buildPostAwakeningState(state); expect(updatedVampire.lifetimeBossesDefeated).toBe(42); }); it("preserves lifetime quests completed across awakening", () => { const vampire = makeVampireState({ lifetimeQuestsCompleted: 17, siring: makeSiring({ count: 1 }), }); const state = makeState({ vampire: vampire as GameState["vampire"] }); const { updatedVampire } = buildPostAwakeningState(state); expect(updatedVampire.lifetimeQuestsCompleted).toBe(17); }); it("resets totalBloodEarned to 0", () => { const vampire = makeVampireState({ siring: makeSiring({ count: 1 }), totalBloodEarned: 500_000, }); const state = makeState({ vampire: vampire as GameState["vampire"] }); const { updatedVampire } = buildPostAwakeningState(state); expect(updatedVampire.totalBloodEarned).toBe(0); }); it("resets siring count to 0 on fresh vampire state", () => { const vampire = makeVampireState({ siring: makeSiring({ count: 25 }) }); const state = makeState({ vampire: vampire as GameState["vampire"] }); const { updatedVampire } = buildPostAwakeningState(state); expect(updatedVampire.siring.count).toBe(0); }); it("preserves bountyIchorClaimed flag on bosses that match fresh boss list", () => { // Provide an existing boss with bountyIchorClaimed = true for a boss that exists in defaultVampireBosses // We pass it through the bosses array and check that the flag survives the merge const vampire = makeVampireState({ bosses: [ { id: "eternal_darkness", status: "defeated", bountyIchorClaimed: true } ], siring: makeSiring({ count: 4 }), }); const state = makeState({ vampire: vampire as GameState["vampire"] }); const { updatedVampire } = buildPostAwakeningState(state); // eternal_darkness should exist in the fresh boss list; its bountyIchorClaimed should be true const eternDark = updatedVampire.bosses.find((b) => { return b.id === "eternal_darkness"; }); // The boss may or may not exist in default data; if it does, the flag is preserved if (eternDark !== undefined) { expect(eternDark.bountyIchorClaimed).toBe(true); } }); it("returns minimum 1 soul shard even when siring count is 0", () => { const vampire = makeVampireState({ siring: makeSiring({ count: 0 }) }); const state = makeState({ vampire: vampire as GameState["vampire"] }); const { soulShardsEarned } = buildPostAwakeningState(state); expect(soulShardsEarned).toBe(1); }); });