diff --git a/apps/web/src/components/game/gameLayout.tsx b/apps/web/src/components/game/gameLayout.tsx index d18dfc8..6e3c95a 100644 --- a/apps/web/src/components/game/gameLayout.tsx +++ b/apps/web/src/components/game/gameLayout.tsx @@ -54,9 +54,13 @@ import { StoryToast } from "./storyToast.js"; import { TranscendencePanel } from "./transcendencePanel.js"; import { UpgradePanel } from "./upgradePanel.js"; import { VampireAchievementsPanel } from "./vampireAchievementsPanel.js"; +import { VampireAwakeningPanel } from "./vampireAwakeningPanel.js"; import { VampireBossPanel } from "./vampireBossPanel.js"; +import { VampireEquipmentPanel } from "./vampireEquipmentPanel.js"; import { VampireQuestsPanel } from "./vampireQuestsPanel.js"; +import { VampireSiringPanel } from "./vampireSiringPanel.js"; import { VampireThrallsPanel } from "./vampireThrallsPanel.js"; +import { VampireUpgradesPanel } from "./vampireUpgradesPanel.js"; import { VampireZonesPanel } from "./vampireZonesPanel.js"; type Mode = "mortal" | "goddess" | "vampire"; @@ -500,27 +504,19 @@ const GameLayout = (): JSX.Element => { } {activeMode === "vampire" && activeVampireTab === "vampire-equipment" - &&
-

{"🦇 Vampire Equipment coming soon..."}

-
+ && } {activeMode === "vampire" && activeVampireTab === "vampire-upgrades" - &&
-

{"⚔️ Vampire Upgrades coming soon..."}

-
+ && } {activeMode === "vampire" && activeVampireTab === "siring" - &&
-

{"🩸 Siring coming soon..."}

-
+ && } {activeMode === "vampire" && activeVampireTab === "vampire-awakening" - &&
-

{"💀 Vampire Awakening coming soon..."}

-
+ && } {activeMode === "vampire" && activeVampireTab === "vampire-crafting" diff --git a/apps/web/src/components/game/vampireAwakeningPanel.tsx b/apps/web/src/components/game/vampireAwakeningPanel.tsx new file mode 100644 index 0000000..66f843c --- /dev/null +++ b/apps/web/src/components/game/vampireAwakeningPanel.tsx @@ -0,0 +1,490 @@ +/** + * @file Awakening panel component for vampire meta-reset and soul shards upgrade shop. + * @copyright nhcarrigan + * @license Naomi's Public License + * @author Naomi Carrigan + */ +/* eslint-disable max-lines-per-function -- Complex component with many render paths */ +/* eslint-disable complexity -- Many conditional render paths */ +/* eslint-disable max-lines -- Large panel with awakening and shop tabs */ +/* eslint-disable max-statements -- Awakening panel manages many local state variables */ +/* eslint-disable stylistic/max-len -- Data content with long description strings */ +/* eslint-disable @typescript-eslint/naming-convention -- SCREAMING_SNAKE_CASE is conventional for module-level data constants */ +import { useState, type JSX } from "react"; +import { useGame } from "../../context/gameContext.js"; +import type { AwakeningUpgradeCategory } from "@elysium/types"; + +const finalVampireBossId = "eternal_darkness"; + +/** + * Calculates the projected soul shards yield from an awakening. + * Mirrors the server formula: MAX(1, FLOOR(SQRT(siringCount) * metaMultiplier)). + * @param siringCount - The number of sirings completed before this awakening. + * @param metaMultiplier - Multiplier from prior awakening upgrades applied to soul shards yield. + * @returns The projected soul shards earned. + */ +const calculateSoulShardsYield = ( + siringCount: number, + metaMultiplier: number, +): number => { + return Math.max(1, Math.floor(Math.sqrt(siringCount) * metaMultiplier)); +}; + +const AWAKENING_UPGRADES: Array<{ + id: string; + name: string; + description: string; + category: AwakeningUpgradeCategory; + cost: number; + multiplier: number; +}> = [ + { + category: "blood", + cost: 10, + description: "The awakened soul's hunger amplifies all blood income. All blood/s ×1.5.", + id: "awakening_blood_1", + multiplier: 1.5, + name: "Soul Hunger I", + }, + { + category: "blood", + cost: 50, + description: "A second awakening sharpens the soul's drive to consume. All blood/s ×2.", + id: "awakening_blood_2", + multiplier: 2, + name: "Soul Hunger II", + }, + { + category: "blood", + cost: 200, + description: "The awakened soul transcends ordinary hunger — all blood income triples. All blood/s ×3.", + id: "awakening_blood_3", + multiplier: 3, + name: "Soul Hunger III", + }, + { + category: "combat", + cost: 15, + description: "The awakened soul's predatory edge carries through every thrall. All thrall combat power ×1.5.", + id: "awakening_combat_1", + multiplier: 1.5, + name: "Awakened Predator I", + }, + { + category: "combat", + cost: 75, + description: "Soul shards resonate with battle instinct — combat power doubles. All thrall combat power ×2.", + id: "awakening_combat_2", + multiplier: 2, + name: "Awakened Predator II", + }, + { + category: "combat", + cost: 300, + description: "Apex awakened combat mastery triples every thrall's fighting power. All thrall combat power ×3.", + id: "awakening_combat_3", + multiplier: 3, + name: "Awakened Predator III", + }, + { + category: "siring_threshold", + cost: 30, + description: "Soul shards carry the memory of past sirings — the threshold lowers by 15%.", + id: "awakening_threshold_1", + multiplier: 0.85, + name: "Soul Memory I", + }, + { + category: "siring_threshold", + cost: 120, + description: "The awakened soul remembers every siring — the threshold drops by a further 20%.", + id: "awakening_threshold_2", + multiplier: 0.8, + name: "Soul Memory II", + }, + { + category: "siring_threshold", + cost: 480, + description: "Perfect soul memory collapses the siring threshold to a fraction of its original. Threshold ×0.7.", + id: "awakening_threshold_3", + multiplier: 0.7, + name: "Soul Memory III", + }, + { + category: "siring_ichor", + cost: 25, + description: "Soul shards amplify the ichor extracted during each siring. Ichor per siring ×1.5.", + id: "awakening_siring_ichor_1", + multiplier: 1.5, + name: "Ichor Resonance I", + }, + { + category: "siring_ichor", + cost: 100, + description: "The resonance deepens — siring yields twice the ichor. Ichor per siring ×2.", + id: "awakening_siring_ichor_2", + multiplier: 2, + name: "Ichor Resonance II", + }, + { + category: "siring_ichor", + cost: 400, + description: "Peak resonance — each siring now yields three times the ichor. Ichor per siring ×3.", + id: "awakening_siring_ichor_3", + multiplier: 3, + name: "Ichor Resonance III", + }, + { + category: "soulshards_meta", + cost: 60, + description: "The soul refines itself — future awakenings yield 50% more soul shards.", + id: "awakening_meta_1", + multiplier: 1.5, + name: "Soul Refinement I", + }, + { + category: "soulshards_meta", + cost: 250, + description: "The awakened soul's self-improvement compounds — soul shard yields double.", + id: "awakening_meta_2", + multiplier: 2, + name: "Soul Refinement II", + }, + { + category: "soulshards_meta", + cost: 1000, + description: "The apex of soul refinement — all future awakenings yield three times the soul shards.", + id: "awakening_meta_3", + multiplier: 3, + name: "Soul Refinement III", + }, +]; + +const categoryOrder: Array = [ + "blood", + "combat", + "siring_threshold", + "siring_ichor", + "soulshards_meta", +]; + +const AWAKENING_UPGRADE_CATEGORY_LABELS: Record = { + blood: "🩸 Blood Multipliers", + combat: "⚔️ Combat Multipliers", + siring_ichor: "💧 Siring Quality of Life — Ichor Yield", + siring_threshold: "🎯 Siring Quality of Life — Threshold", + soulshards_meta: "💠 Soul Shards Meta Upgrades", +}; + +type AwakeningTab = "awaken" | "shop"; + +/** + * Renders the awakening panel with vampire meta-reset and soul shards shop tabs. + * @returns The JSX element. + */ +const VampireAwakeningPanel = (): JSX.Element => { + const { + state, + reloadSilent, + formatInteger, + awaken, + buyAwakeningUpgrade, + } = useGame(); + + const [ isPending, setIsPending ] = useState(false); + const [ result, setResult ] = useState<{ + soulShardsEarned: number; + count: number; + } | null>(null); + const [ awakeningError, setAwakeningError ] = useState(null); + const [ buyingId, setBuyingId ] = useState(null); + const [ activeTab, setActiveTab ] = useState("awaken"); + + if (state === null) { + return ( +
+

{"Loading..."}

+
+ ); + } + + const { vampire } = state; + + if (vampire === undefined) { + return ( +
+

{"The Vampire expansion is not yet unlocked."}

+
+ ); + } + + const { siring, awakening, bosses } = vampire; + + const hasDefeatedFinalBoss = bosses.some((boss) => { + return boss.id === finalVampireBossId && boss.status === "defeated"; + }); + + const metaMultiplier = awakening.soulShardsMetaMultiplier; + const soulShardsPreview = calculateSoulShardsYield(siring.count, metaMultiplier); + const currentSoulShards = awakening.soulShards; + const awakeningCount = awakening.count; + + async function handleAwaken(): Promise { + setIsPending(true); + setAwakeningError(null); + try { + const data = await awaken(); + setResult({ + count: data.newAwakeningCount, + soulShardsEarned: data.soulShardsEarned, + }); + await reloadSilent(); + } catch (error_: unknown) { + setAwakeningError( + error_ instanceof Error + ? error_.message + : "Awakening failed", + ); + } finally { + setIsPending(false); + } + } + + async function handleBuyUpgrade(upgradeId: string): Promise { + setBuyingId(upgradeId); + try { + await buyAwakeningUpgrade(upgradeId); + } finally { + setBuyingId(null); + } + } + + const upgradesByCategory = categoryOrder.map((catId) => { + const label = AWAKENING_UPGRADE_CATEGORY_LABELS[catId]; + const upgrades = AWAKENING_UPGRADES.filter((upgrade) => { + return upgrade.category === catId; + }); + return { catId, label, upgrades }; + }); + + function handleAwakenClick(): void { + void handleAwaken(); + } + + function handleAwakenTabClick(): void { + setActiveTab("awaken"); + } + + function handleShopTabClick(): void { + setActiveTab("shop"); + } + + return ( +
+

{"💀 Awakening"}

+ +
+ + +
+ + {activeTab === "awaken" + && <> +

+ {"Awakening is the ultimate vampire reset. It wipes "} + {"everything"} + {" in the vampire realm — blood, sirings, thralls, and upgrades" + + " — but grants "} + {"Soul Shards"} + {", a permanent vampire currency that survives all future resets." + + " Soul Shards power upgrades that permanently amplify every vampire run."} +

+

+ + {"More sirings = more Soul Shards."} + {" Optimise your vampire run for maximum yield!"} + +

+ +
+ {awakeningCount > 0 + ?

+ {"Awakening count: "} + {awakeningCount} +

+ : null + } +

+ {"Current Soul Shards: "} + {formatInteger(currentSoulShards)} +

+

+ {"Current siring count: "} + {siring.count} +

+ {hasDefeatedFinalBoss + ?

+ {"Soul Shards on awakening: "} + + {"+"} + {formatInteger(soulShardsPreview)} + + {metaMultiplier > 1 + ? + {" (×"} + {metaMultiplier.toFixed(2)} + {" meta bonus applied)"} + + : null + } +

+ : null} +
+ + {hasDefeatedFinalBoss + ? null + :
+

+ {"🔒 "} + {"Defeat the Eternal Darkness"} + {" to unlock Awakening."} +

+

+ {"The Eternal Darkness is the final boss of the Vampire realm."} +

+
+ } + + {hasDefeatedFinalBoss + ?
+

+ {"You are ready to achieve Awakening. This action is "} + {"irreversible"} + {"."} +

+ + {awakeningError === null + ? null + :

{awakeningError}

} + {result === null + ? null + :

+ {"Awakening achieved! Earned "} + + {formatInteger(result.soulShardsEarned)} + {" Soul Shards"} + + {". This is Awakening "} + {result.count} + {". A new soul cycle begins."} +

+ } +
+ : null} + + } + + {activeTab === "shop" + &&
+

+ {"Balance: "} + + {formatInteger(currentSoulShards)} + {" Soul Shards"} + +

+

+ {"Soul Shard upgrades are "} + {"permanent"} + {" — they survive all future sirings and awakenings."} +

+ + {upgradesByCategory.map(({ catId, label, upgrades }) => { + return ( +
+

{label}

+
+ {upgrades.map((upgrade) => { + const purchased + = awakening.purchasedUpgradeIds.includes(upgrade.id); + const canAfford = currentSoulShards >= upgrade.cost; + const isLoading = buyingId === upgrade.id; + + function handleBuyClick(): void { + void handleBuyUpgrade(upgrade.id); + } + + return ( +
+
+

{upgrade.name}

+

{upgrade.description}

+

+ {purchased + ? "✅ Purchased" + : `💠 ${formatInteger(upgrade.cost)} Soul Shards`} +

+
+ {purchased + ? null + : + } +
+ ); + })} +
+
+ ); + })} +
+ } +
+ ); +}; + +export { VampireAwakeningPanel }; diff --git a/apps/web/src/components/game/vampireEquipmentPanel.tsx b/apps/web/src/components/game/vampireEquipmentPanel.tsx new file mode 100644 index 0000000..e24e47d --- /dev/null +++ b/apps/web/src/components/game/vampireEquipmentPanel.tsx @@ -0,0 +1,295 @@ +/** + * @file Vampire equipment panel for managing fangs, shrouds, and talismans. + * @copyright nhcarrigan + * @license Naomi's Public License + * @author Naomi Carrigan + */ +/* eslint-disable react/no-multi-comp -- Sub-component is tightly coupled to the panel */ +/* eslint-disable max-lines-per-function -- Complex component with many render paths */ +/* eslint-disable complexity -- VampireEquipmentCard has many conditional render paths */ + +import { type JSX, useState } from "react"; +import { useGame } from "../../context/gameContext.js"; +import type { VampireEquipment, VampireEquipmentType } from "@elysium/types"; + +const rarityColour: Record = { + common: "#9e9e9e", + epic: "#9c27b0", + legendary: "#ff9800", + rare: "#2196f3", +}; + +const rarityLabel: Record = { + common: "Common", + epic: "Epic", + legendary: "Legendary", + rare: "Rare", +}; + +/** + * Computes a human-readable bonus description for a vampire equipment item. + * @param item - The vampire equipment item. + * @returns The formatted bonus description string. + */ +const bonusDescription = (item: VampireEquipment): string => { + const parts: Array = []; + if (item.bonus.bloodMultiplier !== undefined) { + const pct = Math.round((item.bonus.bloodMultiplier - 1) * 100); + parts.push(`+${String(pct)}% Blood/s`); + } + if (item.bonus.combatMultiplier !== undefined) { + const pct = Math.round((item.bonus.combatMultiplier - 1) * 100); + parts.push(`+${String(pct)}% Thrall Combat`); + } + if (item.bonus.ichorMultiplier !== undefined) { + const pct = Math.round((item.bonus.ichorMultiplier - 1) * 100); + parts.push(`+${String(pct)}% Ichor/Siring`); + } + return parts.join(", "); +}; + +/** + * Formats a vampire equipment cost as a readable string. + * @param cost - The cost object with blood, ichor, and soulShards. + * @param cost.blood - The blood component of the cost. + * @param cost.ichor - The ichor component of the cost. + * @param cost.soulShards - The soulShards component of the cost. + * @param formatNumber - The number formatting utility function. + * @returns The formatted cost string. + */ +const costLabel = ( + cost: { blood: number; ichor: number; soulShards: number }, + formatNumber: (n: number)=> string, +): string => { + const parts: Array = []; + if (cost.blood > 0) { + parts.push(`🩸 ${formatNumber(cost.blood)}`); + } + if (cost.ichor > 0) { + parts.push(`💧 ${formatNumber(cost.ichor)}`); + } + if (cost.soulShards > 0) { + parts.push(`💠 ${formatNumber(cost.soulShards)}`); + } + return parts.join(" "); +}; + +interface VampireEquipmentCardProperties { + readonly item: VampireEquipment; + readonly blood: number; + readonly ichor: number; + readonly soulShards: number; + readonly formatNumber: (n: number)=> string; +} + +/** + * Renders a single vampire equipment card with buy/equip actions. + * @param props - The card properties. + * @param props.item - The vampire equipment data to display. + * @param props.blood - The player's current blood balance. + * @param props.ichor - The player's current ichor balance. + * @param props.soulShards - The player's current soul shards balance. + * @param props.formatNumber - The number formatting utility function. + * @returns The JSX element. + */ +const VampireEquipmentCard = ({ + item, + blood, + ichor, + soulShards, + formatNumber, +}: VampireEquipmentCardProperties): JSX.Element => { + const { buyVampireEquipment, equipVampireEquipment } = useGame(); + + const canAfford = item.cost !== undefined + && blood >= item.cost.blood + && ichor >= item.cost.ichor + && soulShards >= item.cost.soulShards; + + function handleBuy(): void { + buyVampireEquipment(item.id); + } + + function handleEquip(): void { + equipVampireEquipment(item.id); + } + + let typeEmoji = "🔮"; + if (item.type === "fang") { + typeEmoji = "🦷"; + } else if (item.type === "shroud") { + typeEmoji = "🧣"; + } + + const equippedClass = item.equipped + ? " equipped" + : ""; + const ownedClass = item.owned && !item.equipped + ? " owned" + : ""; + const lockedClass = item.owned + ? "" + : " locked"; + const cardClassName + = `goddess-equipment-card rarity-${item.rarity}${equippedClass}${ownedClass}${lockedClass}`; + + return ( +
+
+ {typeEmoji} + {item.name} + + {rarityLabel[item.rarity]} + +
+

{item.description}

+

{bonusDescription(item)}

+ {item.setId === undefined + ? null + :

{"Set: "}{item.setId}

} +
+ {item.owned && item.equipped + ? {"✅ Equipped"} + : null} + {item.owned && !item.equipped + ? + : null} + {!item.owned && item.cost !== undefined + ? + : null} + {!item.owned && item.cost === undefined + ? {"🎲 Boss Drop Only"} + : null} +
+
+ ); +}; + +type TabFilter = "all" | VampireEquipmentType; + +/** + * Renders the vampire equipment panel, displaying all fangs, shrouds, and talismans. + * @returns The JSX element. + */ +const VampireEquipmentPanel = (): JSX.Element => { + const { state, formatNumber } = useGame(); + const [ activeTab, setActiveTab ] = useState("all"); + + if (state === null) { + return ( +
+

{"Loading..."}

+
+ ); + } + + const { resources, vampire } = state; + + if (vampire === undefined) { + return ( +
+

{"The Vampire expansion is not yet unlocked."}

+
+ ); + } + + const blood = resources.blood ?? 0; + const { ichor } = vampire.siring; + const { soulShards } = vampire.awakening; + const { equipment } = vampire; + + const filteredEquipment = activeTab === "all" + ? equipment + : equipment.filter((item) => { + return item.type === activeTab; + }); + + const tabs: Array<{ id: TabFilter; label: string }> = [ + { id: "all", label: "All" }, + { id: "fang", label: "🦷 Fangs" }, + { id: "shroud", label: "🧣 Shrouds" }, + { id: "talisman", label: "🔮 Talismans" }, + ]; + + return ( +
+
+

{"🦇 Vampire Equipment"}

+
+
+ + {"🩸 Blood: "} + {formatNumber(blood)} + + + {"💧 Ichor: "} + {formatNumber(ichor)} + + + {"💠 Soul Shards: "} + {formatNumber(soulShards)} + +
+
+ {tabs.map((tab) => { + function handleTabClick(): void { + setActiveTab(tab.id); + } + return ( + + ); + })} +
+
+ {filteredEquipment.map((item) => { + return ( + + ); + })} + {filteredEquipment.length === 0 + ?

+ {"No equipment in this category yet."} +

+ : null} +
+
+ ); +}; + +export { VampireEquipmentPanel }; diff --git a/apps/web/src/components/game/vampireSiringPanel.tsx b/apps/web/src/components/game/vampireSiringPanel.tsx new file mode 100644 index 0000000..ee39fef --- /dev/null +++ b/apps/web/src/components/game/vampireSiringPanel.tsx @@ -0,0 +1,644 @@ +/** + * @file Siring panel component for vampire prestige and ichor upgrade shop. + * @copyright nhcarrigan + * @license Naomi's Public License + * @author Naomi Carrigan + */ +/* eslint-disable max-lines-per-function -- Complex component with many render paths */ +/* eslint-disable complexity -- Many conditional render paths */ +/* eslint-disable max-lines -- Large panel with siring and shop tabs */ +/* eslint-disable max-statements -- Siring panel manages many local state variables */ +/* eslint-disable stylistic/max-len -- Data content with long description strings */ +/* eslint-disable @typescript-eslint/naming-convention -- SCREAMING_SNAKE_CASE is conventional for module-level data constants */ +import { useState, type JSX } from "react"; +import { useGame } from "../../context/gameContext.js"; +import type { SiringUpgradeCategory } from "@elysium/types"; + +const baseSiringThreshold = 1_000_000; +const ichorYieldDivisor = 50_000; + +/** + * Calculates the blood threshold required for the next siring. + * Mirrors the server formula: BASE * (count + 1)^2 * thresholdMultiplier. + * @param siringCount - The number of sirings completed so far. + * @param thresholdMultiplier - An optional multiplier applied to the threshold. + * @returns The blood amount required to sire. + */ +const calculateSiringThreshold = ( + siringCount: number, + thresholdMultiplier = 1, +): number => { + return ( + baseSiringThreshold + * Math.pow(siringCount + 1, 2) + * thresholdMultiplier + ); +}; + +/** + * Calculates the projected ichor yield from a siring. + * Mirrors the server formula: MAX(1, FLOOR(SQRT(totalBloodEarned / divisor) * ichorMultiplier)). + * @param totalBloodEarned - Total blood earned in the current siring run. + * @param ichorMultiplier - Multiplier applied to the ichor yield. + * @returns The projected ichor earned. + */ +const calculateIchorYield = ( + totalBloodEarned: number, + ichorMultiplier: number, +): number => { + return Math.max( + 1, + Math.floor( + Math.sqrt(totalBloodEarned / ichorYieldDivisor) * ichorMultiplier, + ), + ); +}; + +/** + * Computes the siring production multiplier from the count. + * Each siring adds 25% to the production multiplier. + * @param count - The number of sirings completed. + * @returns The computed production multiplier. + */ +const computeSiringProductionMultiplier = (count: number): number => { + // eslint-disable-next-line stylistic/no-extra-parens -- Required by no-mixed-operators rule + return 1 + (count * 0.25); +}; + +const SIRING_UPGRADES: Array<{ + id: string; + name: string; + description: string; + category: SiringUpgradeCategory; + ichorCost: number; + multiplier: number; +}> = [ + { + category: "blood", + description: "The first drop of ichor transforms your blood instinct. All blood/s ×1.25.", + ichorCost: 5, + id: "siring_blood_1", + multiplier: 1.25, + name: "Ichor Awakening I", + }, + { + category: "blood", + description: "Sustained siring deepens the hunger that drives every thrall. All blood/s ×1.5.", + ichorCost: 15, + id: "siring_blood_2", + multiplier: 1.5, + name: "Ichor Awakening II", + }, + { + category: "blood", + description: "Each siring sharpens your command over the blood flow. All blood/s ×2.", + ichorCost: 40, + id: "siring_blood_3", + multiplier: 2, + name: "Ichor Awakening III", + }, + { + category: "blood", + description: "The bloodline resonates across every hunt and harvest. All blood/s ×5.", + ichorCost: 120, + id: "siring_blood_4", + multiplier: 5, + name: "Ichor Awakening IV", + }, + { + category: "blood", + description: "Total mastery of the siring-blood bond multiplies all income tenfold. All blood/s ×10.", + ichorCost: 350, + id: "siring_blood_5", + multiplier: 10, + name: "Ichor Awakening V", + }, + { + category: "blood", + description: "The accumulated weight of many sirings floods every vein in your domain. All blood/s ×25.", + ichorCost: 1000, + id: "siring_blood_6", + multiplier: 25, + name: "Ichor Awakening VI", + }, + { + category: "thralls", + description: "Sired blood flows through your thralls, amplifying their natural power. All thrall blood/s ×1.5.", + ichorCost: 8, + id: "siring_thralls_1", + multiplier: 1.5, + name: "Bloodline Bond I", + }, + { + category: "thralls", + description: "The bond between sire and thrall deepens, multiplying their output. All thrall blood/s ×2.", + ichorCost: 25, + id: "siring_thralls_2", + multiplier: 2, + name: "Bloodline Bond II", + }, + { + category: "thralls", + description: "Every thrall in your bloodline fights and works with supernatural coordination. All thrall blood/s ×3.", + ichorCost: 75, + id: "siring_thralls_3", + multiplier: 3, + name: "Bloodline Bond III", + }, + { + category: "thralls", + description: "The siring bond reaches its apex — every thrall becomes an extension of your will. All thrall blood/s ×5.", + ichorCost: 200, + id: "siring_thralls_4", + multiplier: 5, + name: "Bloodline Bond IV", + }, + { + category: "combat", + description: "Sired instincts sharpen your thralls' fighting edge. All thrall combat power ×1.5.", + ichorCost: 12, + id: "siring_combat_1", + multiplier: 1.5, + name: "Dark Predator I", + }, + { + category: "combat", + description: "The predator's cunning passed through siring doubles your combat effectiveness. All thrall combat power ×2.", + ichorCost: 45, + id: "siring_combat_2", + multiplier: 2, + name: "Dark Predator II", + }, + { + category: "combat", + description: "Centuries of accumulated battle memory flood into your line. All thrall combat power ×3.", + ichorCost: 150, + id: "siring_combat_3", + multiplier: 3, + name: "Dark Predator III", + }, + { + category: "combat", + description: "The ultimate expression of vampire combat mastery through the siring ritual. All thrall combat power ×5.", + ichorCost: 500, + id: "siring_combat_4", + multiplier: 5, + name: "Dark Predator IV", + }, + { + category: "ichor", + description: "The ritual of siring becomes more efficient, preserving greater ichor yield. Ichor per siring ×1.5.", + ichorCost: 20, + id: "siring_ichor_1", + multiplier: 1.5, + name: "Refined Siring I", + }, + { + category: "ichor", + description: "Deeper siring mastery extracts twice the ichor from every reset. Ichor per siring ×2.", + ichorCost: 60, + id: "siring_ichor_2", + multiplier: 2, + name: "Refined Siring II", + }, + { + category: "ichor", + description: "The siring ritual refined to its peak triples the ichor yield at reset. Ichor per siring ×3.", + ichorCost: 180, + id: "siring_ichor_3", + multiplier: 3, + name: "Refined Siring III", + }, + { + category: "utility", + description: "Siring instinct reduces the blood threshold needed for the next siring by 10%.", + ichorCost: 30, + id: "siring_threshold_1", + multiplier: 0.9, + name: "Blood Efficiency I", + }, + { + category: "utility", + description: "Further refinement lowers the siring threshold by an additional 15%.", + ichorCost: 90, + id: "siring_threshold_2", + multiplier: 0.85, + name: "Blood Efficiency II", + }, + { + category: "utility", + description: "The siring rite becomes almost effortless — threshold reduced by another 20%.", + ichorCost: 270, + id: "siring_threshold_3", + multiplier: 0.8, + name: "Blood Efficiency III", + }, + { + category: "utility", + description: "Peak efficiency — the blood threshold for siring is reduced by a further 25%.", + ichorCost: 800, + id: "siring_threshold_4", + multiplier: 0.75, + name: "Blood Efficiency IV", + }, + { + category: "utility", + description: "An ancient siring ritual accelerates the arrival of the first thrall class after each siring.", + ichorCost: 50, + id: "siring_quick_start_1", + multiplier: 1.5, + name: "Quick Fledglings I", + }, + { + category: "utility", + description: "The first fledglings after siring arrive faster and work harder for longer.", + ichorCost: 150, + id: "siring_quick_start_2", + multiplier: 2, + name: "Quick Fledglings II", + }, + { + category: "utility", + description: "Your siring bloodline passively preserves a fraction of your thrall efficiency across resets.", + ichorCost: 250, + id: "siring_persistence_1", + multiplier: 1.25, + name: "Bloodline Memory I", + }, + { + category: "utility", + description: "The bloodline memory deepens — even more efficiency is preserved through each siring.", + ichorCost: 750, + id: "siring_persistence_2", + multiplier: 1.5, + name: "Bloodline Memory II", + }, +]; + +const categoryOrder: Array = [ + "blood", + "thralls", + "combat", + "ichor", + "utility", +]; + +const SIRING_UPGRADE_CATEGORY_LABELS: Record = { + blood: "🩸 Blood Multipliers", + combat: "⚔️ Combat Multipliers", + ichor: "💧 Ichor Yield", + thralls: "🧟 Thrall Multipliers", + utility: "🎯 Quality of Life", +}; + +type SiringTab = "sire" | "shop"; + +/** + * Renders the siring panel with vampire prestige and ichor shop tabs. + * @returns The JSX element. + */ +const VampireSiringPanel = (): JSX.Element => { + const { + state, + reloadSilent, + formatInteger, + formatNumber, + sire, + buySiringUpgrade, + } = useGame(); + + const [ isPending, setIsPending ] = useState(false); + const [ result, setResult ] = useState<{ + ichorEarned: number; + count: number; + } | null>(null); + const [ siringError, setSiringError ] = useState(null); + const [ buyingId, setBuyingId ] = useState(null); + const [ activeTab, setActiveTab ] = useState("sire"); + + if (state === null) { + return ( +
+

{"Loading..."}

+
+ ); + } + + const { vampire } = state; + + if (vampire === undefined) { + return ( +
+

{"The Vampire expansion is not yet unlocked."}

+
+ ); + } + + const { siring, awakening, totalBloodEarned } = vampire; + + const thresholdSiringMultiplier = SIRING_UPGRADES.filter((upgrade) => { + return ( + upgrade.id.startsWith("siring_threshold_") + && siring.purchasedUpgradeIds.includes(upgrade.id) + ); + }).reduce((mult, upgrade) => { + return mult * upgrade.multiplier; + }, 1); + + const combinedThresholdMultiplier + = thresholdSiringMultiplier * awakening.soulShardsSiringThresholdMultiplier; + const threshold = calculateSiringThreshold(siring.count, combinedThresholdMultiplier); + const isEligible = totalBloodEarned >= threshold; + + const ichorSiringMultiplier = SIRING_UPGRADES.filter((upgrade) => { + return ( + upgrade.category === "ichor" + && siring.purchasedUpgradeIds.includes(upgrade.id) + ); + }).reduce((mult, upgrade) => { + return mult * upgrade.multiplier; + }, 1); + + const combinedIchorMultiplier + = ichorSiringMultiplier * awakening.soulShardsSiringIchorMultiplier; + const ichorPreview = calculateIchorYield(totalBloodEarned, combinedIchorMultiplier); + + const nextMultiplier = computeSiringProductionMultiplier(siring.count + 1); + const progressRatio = Math.min(totalBloodEarned / threshold, 1); + const progressPct = (progressRatio * 100).toFixed(1); + + const currentIchor = siring.ichor; + + async function handleSire(): Promise { + setIsPending(true); + setSiringError(null); + try { + const data = await sire(); + setResult({ + count: data.newSiringCount, + ichorEarned: data.ichorEarned, + }); + await reloadSilent(); + } catch (error_: unknown) { + setSiringError( + error_ instanceof Error + ? error_.message + : "Siring failed", + ); + } finally { + setIsPending(false); + } + } + + async function handleBuyUpgrade(upgradeId: string): Promise { + setBuyingId(upgradeId); + try { + await buySiringUpgrade(upgradeId); + } finally { + setBuyingId(null); + } + } + + const upgradesByCategory = categoryOrder.map((categoryId) => { + const label = SIRING_UPGRADE_CATEGORY_LABELS[categoryId]; + const upgrades = SIRING_UPGRADES.filter((upgrade) => { + return upgrade.category === categoryId; + }); + return { categoryId, label, upgrades }; + }); + + function handleSireClick(): void { + void handleSire(); + } + + function handleSireTabClick(): void { + setActiveTab("sire"); + } + + function handleShopTabClick(): void { + setActiveTab("shop"); + } + + return ( +
+

{"🩸 Siring"}

+ +
+ + +
+ + {activeTab === "sire" + && <> +

+ {"Siring is the vampire prestige layer. It resets your blood" + + " and vampire progress, but grants "} + {"Ichor"} + {" — a permanent vampire currency used to purchase powerful upgrades." + + " Each siring also permanently increases your blood/s multiplier."} +

+ +
+ {siring.count > 0 + ?

+ {"Siring count: "} + {siring.count} +

+ : null + } +

+ {"Current Ichor: "} + {formatInteger(currentIchor)} +

+

+ {"Blood this run: "} + {formatNumber(totalBloodEarned)} + {" / "} + {formatNumber(threshold)} +

+
+
+
+

+ {progressPct} + {"% of threshold"} +

+ {isEligible + ?

+ {"Ichor on siring: "} + + {"+"} + {formatInteger(ichorPreview)} + + {combinedIchorMultiplier > 1 + ? + {" (×"} + {combinedIchorMultiplier.toFixed(2)} + {" yield bonus applied)"} + + : null + } +

+ : null} +

+ {"Next production multiplier: "} + + {"×"} + {nextMultiplier.toFixed(2)} + +

+
+ + {isEligible + ? null + :
+

+ {"🔒 "} + {"Earn enough blood"} + {" to unlock siring."} +

+

+ {"You need "} + {formatNumber(threshold)} + {" total blood in the current run. You have "} + {formatNumber(totalBloodEarned)} + {"."} +

+
+ } + + {isEligible + ?
+

+ {"You are ready to sire. This action is "} + {"irreversible"} + {" within this vampire run."} +

+ + {siringError === null + ? null + :

{siringError}

} + {result === null + ? null + :

+ {"Sired! Earned "} + + {formatInteger(result.ichorEarned)} + {" Ichor"} + + {". This is Siring "} + {result.count} + {". A new bloodline cycle begins."} +

+ } +
+ : null} + + } + + {activeTab === "shop" + &&
+

+ {"Balance: "} + + {formatInteger(currentIchor)} + {" Ichor"} + +

+

+ {"Ichor upgrades are "} + {"permanent"} + {" — they survive future sirings."} +

+ + {upgradesByCategory.map(({ categoryId, label, upgrades }) => { + return ( +
+

{label}

+
+ {upgrades.map((upgrade) => { + const purchased = siring.purchasedUpgradeIds.includes(upgrade.id); + const canAfford = currentIchor >= upgrade.ichorCost; + const isLoading = buyingId === upgrade.id; + + function handleBuyClick(): void { + void handleBuyUpgrade(upgrade.id); + } + + return ( +
+
+

{upgrade.name}

+

{upgrade.description}

+

+ {purchased + ? "✅ Purchased" + : `💧 ${formatInteger(upgrade.ichorCost)} Ichor`} +

+
+ {purchased + ? null + : + } +
+ ); + })} +
+
+ ); + })} +
+ } +
+ ); +}; + +export { VampireSiringPanel }; diff --git a/apps/web/src/components/game/vampireUpgradesPanel.tsx b/apps/web/src/components/game/vampireUpgradesPanel.tsx new file mode 100644 index 0000000..d6255db --- /dev/null +++ b/apps/web/src/components/game/vampireUpgradesPanel.tsx @@ -0,0 +1,288 @@ +/** + * @file Vampire upgrades panel for purchasing vampire-realm upgrades. + * @copyright nhcarrigan + * @license Naomi's Public License + * @author Naomi Carrigan + */ +/* eslint-disable react/no-multi-comp -- Sub-component is tightly coupled to the panel */ +/* eslint-disable max-lines-per-function -- Complex component with many render paths */ + +import { useGame } from "../../context/gameContext.js"; +import type { VampireUpgrade } from "@elysium/types"; +import type { JSX } from "react"; + +/** + * Formats a vampire upgrade cost as a readable string. + * @param upgrade - The vampire upgrade. + * @param formatNumber - The number formatting utility function. + * @returns The formatted cost string. + */ +const costLabel = ( + upgrade: VampireUpgrade, + formatNumber: (n: number)=> string, +): string => { + const parts: Array = []; + if (upgrade.costBlood > 0) { + parts.push(`🩸 ${formatNumber(upgrade.costBlood)}`); + } + if (upgrade.costIchor > 0) { + parts.push(`💧 ${formatNumber(upgrade.costIchor)}`); + } + if (upgrade.costSoulShards > 0) { + parts.push(`💠 ${formatNumber(upgrade.costSoulShards)}`); + } + return parts.length > 0 + ? parts.join(" ") + : "Free"; +}; + +/** + * Returns a human-readable label for a vampire upgrade target. + * @param target - The upgrade target string. + * @returns The display label. + */ +const targetLabel = (target: VampireUpgrade["target"]): string => { + const labels: Record = { + blood: "Blood", + boss: "Boss", + global: "Global", + siring: "Siring", + thrall: "Thrall", + }; + return labels[target]; +}; + +interface VampireUpgradeCardProperties { + readonly upgrade: VampireUpgrade; + readonly blood: number; + readonly ichor: number; + readonly soulShards: number; + readonly formatNumber: (n: number)=> string; +} + +/** + * Renders a single vampire upgrade card. + * @param props - The card properties. + * @param props.upgrade - The vampire upgrade data. + * @param props.blood - The player's current blood balance. + * @param props.ichor - The player's current ichor balance. + * @param props.soulShards - The player's current soul shards balance. + * @param props.formatNumber - The number formatting utility function. + * @returns The JSX element. + */ +const VampireUpgradeCard = ({ + upgrade, + blood, + ichor, + soulShards, + formatNumber, +}: VampireUpgradeCardProperties): JSX.Element => { + const { buyVampireUpgrade } = useGame(); + + const canAfford + = blood >= upgrade.costBlood + && ichor >= upgrade.costIchor + && soulShards >= upgrade.costSoulShards; + + async function handleBuy(): Promise { + await buyVampireUpgrade(upgrade.id); + } + + const multiplierPct = Math.round((upgrade.multiplier - 1) * 100); + + if (upgrade.purchased) { + return ( +
+
+ + {"✅ "} + {upgrade.name} + + + {targetLabel(upgrade.target)} + +
+

{upgrade.description}

+

+ {`×${String(upgrade.multiplier)} (+${String(multiplierPct)}%)`} +

+
+ ); + } + + if (upgrade.unlocked) { + return ( +
+
+ {upgrade.name} + + {targetLabel(upgrade.target)} + +
+

{upgrade.description}

+

+ {`×${String(upgrade.multiplier)} (+${String(multiplierPct)}%)`} +

+ {upgrade.thrallId === undefined + ? null + :

+ {"🧟 Thrall: "} + {upgrade.thrallId} +

+ } + +
+ ); + } + + return ( +
+
+ {"🔒 ???"} + + {targetLabel(upgrade.target)} + +
+

{"Not yet unlocked."}

+
+ ); +}; + +/** + * Renders the vampire upgrades panel, displaying all available and purchased upgrades. + * @returns The JSX element. + */ +const VampireUpgradesPanel = (): JSX.Element => { + const { state, formatNumber } = useGame(); + + if (state === null) { + return ( +
+

{"Loading..."}

+
+ ); + } + + const { resources, vampire } = state; + + if (vampire === undefined) { + return ( +
+

{"The Vampire expansion is not yet unlocked."}

+
+ ); + } + + const blood = resources.blood ?? 0; + const { ichor } = vampire.siring; + const { soulShards } = vampire.awakening; + const { upgrades } = vampire; + + const purchased = upgrades.filter((upgrade) => { + return upgrade.purchased; + }); + const available = upgrades.filter((upgrade) => { + return upgrade.unlocked && !upgrade.purchased; + }); + const locked = upgrades.filter((upgrade) => { + return !upgrade.unlocked && !upgrade.purchased; + }); + + return ( +
+
+

{"⚔️ Vampire Upgrades"}

+
+
+ + {"🩸 Blood: "} + {formatNumber(blood)} + + + {"💧 Ichor: "} + {formatNumber(ichor)} + + + {"💠 Soul Shards: "} + {formatNumber(soulShards)} + +
+ {available.length > 0 + ?
+

{"Available Upgrades"}

+
+ {available.map((upgrade) => { + return ( + + ); + })} +
+
+ : null} + {locked.length > 0 + ?
+

{"Locked Upgrades"}

+
+ {locked.map((upgrade) => { + return ( + + ); + })} +
+
+ : null} + {purchased.length > 0 + ?
+

{"Purchased Upgrades"}

+
+ {purchased.map((upgrade) => { + return ( + + ); + })} +
+
+ : null} + {upgrades.length === 0 + ?

{"No vampire upgrades available yet."}

+ : null} +
+ ); +}; + +export { VampireUpgradesPanel }; diff --git a/apps/web/src/context/gameContext.tsx b/apps/web/src/context/gameContext.tsx index bdb9cef..f0a8a5f 100644 --- a/apps/web/src/context/gameContext.tsx +++ b/apps/web/src/context/gameContext.tsx @@ -22,6 +22,8 @@ import { type GameState, type GoddessBossChallengeResponse, type GoddessExploreCollectResponse, + type AwakeningResponse, + type SiringResponse, type VampireBossChallengeResponse, type LoginBonusResult, type NumberFormat, @@ -43,11 +45,15 @@ import { } from "react"; import { achieveApotheosis as achieveApotheosisApi, + awaken as awakenApi, + buyAwakeningUpgrade as buyAwakeningUpgradeApi, buyConsecrationUpgrade as buyConsecrationUpgradeApi, buyEchoUpgrade as buyEchoUpgradeApi, buyEnlightenmentUpgrade as buyEnlightenmentUpgradeApi, buyGoddessUpgrade as buyGoddessUpgradeApi, buyPrestigeUpgrade as buyPrestigeUpgradeApi, + buySiringUpgrade as buySiringUpgradeApi, + buyVampireUpgrade as buyVampireUpgradeApi, challengeBoss as challengeBossApi, challengeGoddessBoss as challengeGoddessBossApi, challengeVampireBoss as challengeVampireBossApi, @@ -64,6 +70,7 @@ import { prestige as prestigeApi, resetProgress as resetProgressApi, saveGame, + sire as sireApi, startExploration as startExplorationApi, startGoddessExploration as startGoddessExplorationApi, transcend as transcendApi, @@ -790,6 +797,43 @@ interface GameContextValue { * Buy one or more thralls (client-side blood deduction). */ buyVampireThrall: (thrallId: string, quantity: number)=> void; + + /** + * Purchase a vampire equipment item (client-side state mutation). + */ + buyVampireEquipment: (equipmentId: string)=> void; + + /** + * Equip an owned vampire equipment item (auto-unequips same slot). + */ + equipVampireEquipment: (equipmentId: string)=> void; + + /** + * Purchase a vampire upgrade using blood/ichor/soul shards. + */ + buyVampireUpgrade: (upgradeId: string)=> Promise; + + /** + * Perform a vampire siring (prestige reset) for ichor. + * @returns The siring response containing ichorEarned. + */ + sire: ()=> Promise; + + /** + * Purchase a siring upgrade from the ichor shop. + */ + buySiringUpgrade: (upgradeId: string)=> Promise; + + /** + * Perform a vampire awakening (meta-reset) for soul shards. + * @returns The awakening response containing soulShardsEarned. + */ + awaken: ()=> Promise; + + /** + * Purchase an awakening upgrade from the soul shards shop. + */ + buyAwakeningUpgrade: (upgradeId: string)=> Promise; } export interface BattleResult { @@ -2218,6 +2262,225 @@ export const GameProvider = ({ [], ); + const buyVampireEquipment = useCallback((equipmentId: string) => { + setState((previous) => { + if (previous?.vampire === undefined) { + return previous; + } + const item = previous.vampire.equipment.find((equip) => { + return equip.id === equipmentId; + }); + if (item?.owned === true) { + return previous; + } + const blood = previous.resources.blood ?? 0; + const { ichor } = previous.vampire.siring; + const { soulShards } = previous.vampire.awakening; + if ( + blood < (item?.cost?.blood ?? 0) + || ichor < (item?.cost?.ichor ?? 0) + || soulShards < (item?.cost?.soulShards ?? 0) + ) { + return previous; + } + const slotAlreadyEquipped = previous.vampire.equipment.find((equip) => { + return equip.equipped && equip.type === item?.type; + }); + return { + ...previous, + resources: { + ...previous.resources, + blood: blood - (item?.cost?.blood ?? 0), + }, + vampire: { + ...previous.vampire, + awakening: { + ...previous.vampire.awakening, + soulShards: soulShards - (item?.cost?.soulShards ?? 0), + }, + equipment: previous.vampire.equipment.map((equip) => { + if (equip.id === equipmentId) { + return { + ...equip, + equipped: slotAlreadyEquipped === undefined, + owned: true, + }; + } + if (equip.id === slotAlreadyEquipped?.id) { + return { ...equip, equipped: false }; + } + return equip; + }), + siring: { + ...previous.vampire.siring, + ichor: ichor - (item?.cost?.ichor ?? 0), + }, + }, + }; + }); + }, []); + + const equipVampireEquipment = useCallback((equipmentId: string) => { + setState((previous) => { + if (previous?.vampire === undefined) { + return previous; + } + const item = previous.vampire.equipment.find((equip) => { + return equip.id === equipmentId; + }); + if (item?.owned !== true) { + return previous; + } + const slotAlreadyEquipped = previous.vampire.equipment.find((equip) => { + return ( + equip.equipped && equip.type === item.type && equip.id !== equipmentId + ); + }); + return { + ...previous, + vampire: { + ...previous.vampire, + equipment: previous.vampire.equipment.map((equip) => { + if (equip.id === equipmentId) { + return { ...equip, equipped: true }; + } + if (equip.id === slotAlreadyEquipped?.id) { + return { ...equip, equipped: false }; + } + return equip; + }), + }, + }; + }); + }, []); + + const buyVampireUpgrade = useCallback(async(upgradeId: string) => { + try { + const result = await buyVampireUpgradeApi({ upgradeId }); + setState((previous) => { + if (previous?.vampire === undefined) { + return previous; + } + return { + ...previous, + resources: { + ...previous.resources, + blood: result.bloodRemaining, + }, + vampire: { + ...previous.vampire, + awakening: { + ...previous.vampire.awakening, + soulShards: result.soulShardsRemaining, + }, + siring: { + ...previous.vampire.siring, + ichor: result.ichorRemaining, + }, + upgrades: previous.vampire.upgrades.map((u) => { + return u.id === upgradeId + ? { ...u, purchased: true } + : u; + }), + }, + }; + }); + signatureReference.current = null; + localStorage.removeItem("elysium_save_signature"); + } catch (error_: unknown) { + logError("buy_vampire_upgrade", error_); + } + }, []); + + const sire = useCallback(async(): Promise => { + try { + const result = await sireApi({}); + await reloadSilent(); + return result; + } catch (error_: unknown) { + logError("sire", error_); + throw error_; + } + }, [ reloadSilent ]); + + const buySiringUpgrade = useCallback(async(upgradeId: string) => { + try { + const result = await buySiringUpgradeApi({ upgradeId }); + setState((previous) => { + if (previous?.vampire === undefined) { + return previous; + } + return { + ...previous, + vampire: { + ...previous.vampire, + siring: { + ...previous.vampire.siring, + ichor: result.ichorRemaining, + ichorBloodMultiplier: result.ichorBloodMultiplier, + ichorCombatMultiplier: result.ichorCombatMultiplier, + ichorThrallsMultiplier: result.ichorThrallsMultiplier, + purchasedUpgradeIds: result.purchasedUpgradeIds, + }, + }, + }; + }); + signatureReference.current = null; + localStorage.removeItem("elysium_save_signature"); + } catch (error_: unknown) { + logError("buy_siring_upgrade", error_); + } + }, []); + + const awaken = useCallback(async(): Promise => { + try { + const result = await awakenApi({}); + await reloadSilent(); + return result; + } catch (error_: unknown) { + logError("awaken", error_); + throw error_; + } + }, [ reloadSilent ]); + + const buyAwakeningUpgrade = useCallback(async(upgradeId: string) => { + try { + const result = await buyAwakeningUpgradeApi({ upgradeId }); + setState((previous) => { + if (previous?.vampire === undefined) { + return previous; + } + return { + ...previous, + vampire: { + ...previous.vampire, + awakening: { + ...previous.vampire.awakening, + purchasedUpgradeIds: + result.purchasedUpgradeIds, + soulShards: + result.soulShardsRemaining, + soulShardsBloodMultiplier: + result.soulShardsBloodMultiplier, + soulShardsCombatMultiplier: + result.soulShardsCombatMultiplier, + soulShardsMetaMultiplier: + result.soulShardsMetaMultiplier, + soulShardsSiringIchorMultiplier: + result.soulShardsSiringIchorMultiplier, + soulShardsSiringThresholdMultiplier: + result.soulShardsSiringThresholdMultiplier, + }, + }, + }; + }); + signatureReference.current = null; + localStorage.removeItem("elysium_save_signature"); + } catch (error_: unknown) { + logError("buy_awakening_upgrade", error_); + } + }, []); + const consecrate = useCallback(async() => { try { const result = await consecrateApi({}); @@ -3167,9 +3430,11 @@ export const GameProvider = ({ apotheosis, autoBossError, autoBossLastResult, + awaken, battleResult, bossError, buyAdventurer, + buyAwakeningUpgrade, buyConsecrationUpgrade, buyEchoUpgrade, buyEnlightenmentUpgrade, @@ -3178,8 +3443,11 @@ export const GameProvider = ({ buyGoddessEquipment, buyGoddessUpgrade, buyPrestigeUpgrade, + buySiringUpgrade, buyUpgrade, + buyVampireEquipment, buyVampireThrall, + buyVampireUpgrade, challengeBoss, challengeGoddessBoss, challengeVampireBoss, @@ -3212,6 +3480,7 @@ export const GameProvider = ({ enlighten, equipGoddessItem, equipItem, + equipVampireEquipment, error, failedQuestToasts, flushBossLoreToasts, @@ -3244,6 +3513,7 @@ export const GameProvider = ({ showEnlightenmentToast, showPrestigeToast, showTranscendenceToast, + sire, startExploration, startGoddessExploration, startQuest, @@ -3266,9 +3536,11 @@ export const GameProvider = ({ apotheosis, autoBossError, autoBossLastResult, + awaken, battleResult, bossError, buyAdventurer, + buyAwakeningUpgrade, buyConsecrationUpgrade, buyEchoUpgrade, buyEnlightenmentUpgrade, @@ -3277,8 +3549,11 @@ export const GameProvider = ({ buyGoddessEquipment, buyGoddessUpgrade, buyPrestigeUpgrade, + buySiringUpgrade, buyUpgrade, + buyVampireEquipment, buyVampireThrall, + buyVampireUpgrade, challengeBoss, challengeGoddessBoss, challengeVampireBoss, @@ -3311,6 +3586,7 @@ export const GameProvider = ({ enlighten, equipGoddessItem, equipItem, + equipVampireEquipment, error, failedQuestToasts, flushBossLoreToasts, @@ -3342,6 +3618,7 @@ export const GameProvider = ({ showEnlightenmentToast, showPrestigeToast, showTranscendenceToast, + sire, startExploration, startGoddessExploration, startQuest,