feat: vampire equipment, upgrades, siring, and awakening panels

This commit is contained in:
2026-04-16 12:36:05 -07:00
committed by Naomi Carrigan
parent bd88eecda5
commit a7598dca12
6 changed files with 2002 additions and 12 deletions
+8 -12
View File
@@ -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"
&& <div className="vampire-placeholder">
<p>{"🦇 Vampire Equipment coming soon..."}</p>
</div>
&& <VampireEquipmentPanel />
}
{activeMode === "vampire"
&& activeVampireTab === "vampire-upgrades"
&& <div className="vampire-placeholder">
<p>{"⚔️ Vampire Upgrades coming soon..."}</p>
</div>
&& <VampireUpgradesPanel />
}
{activeMode === "vampire"
&& activeVampireTab === "siring"
&& <div className="vampire-placeholder">
<p>{"🩸 Siring coming soon..."}</p>
</div>
&& <VampireSiringPanel />
}
{activeMode === "vampire"
&& activeVampireTab === "vampire-awakening"
&& <div className="vampire-placeholder">
<p>{"💀 Vampire Awakening coming soon..."}</p>
</div>
&& <VampireAwakeningPanel />
}
{activeMode === "vampire"
&& activeVampireTab === "vampire-crafting"
@@ -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<AwakeningUpgradeCategory> = [
"blood",
"combat",
"siring_threshold",
"siring_ichor",
"soulshards_meta",
];
const AWAKENING_UPGRADE_CATEGORY_LABELS: Record<AwakeningUpgradeCategory, string> = {
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<string | null>(null);
const [ buyingId, setBuyingId ] = useState<string | null>(null);
const [ activeTab, setActiveTab ] = useState<AwakeningTab>("awaken");
if (state === null) {
return (
<section className="panel">
<p>{"Loading..."}</p>
</section>
);
}
const { vampire } = state;
if (vampire === undefined) {
return (
<section className="panel">
<p>{"The Vampire expansion is not yet unlocked."}</p>
</section>
);
}
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<void> {
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<void> {
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 (
<section className="panel enlightenment-panel">
<h2>{"💀 Awakening"}</h2>
<div className="prestige-tabs">
<button
className={`prestige-tab ${activeTab === "awaken"
? "active"
: ""}`}
onClick={handleAwakenTabClick}
type="button"
>
{"Awaken"}
</button>
<button
className={`prestige-tab ${activeTab === "shop"
? "active"
: ""}`}
onClick={handleShopTabClick}
type="button"
>
{"💠 Soul Shards Shop ("}
{formatInteger(currentSoulShards)}
{" soul shards)"}
</button>
</div>
{activeTab === "awaken"
&& <>
<p className="transcendence-intro">
{"Awakening is the ultimate vampire reset. It wipes "}
<strong>{"everything"}</strong>
{" in the vampire realm — blood, sirings, thralls, and upgrades"
+ " — but grants "}
<strong>{"Soul Shards"}</strong>
{", a permanent vampire currency that survives all future resets."
+ " Soul Shards power upgrades that permanently amplify every vampire run."}
</p>
<p className="transcendence-intro">
<em>
{"More sirings = more Soul Shards."}
{" Optimise your vampire run for maximum yield!"}
</em>
</p>
<div className="transcendence-status">
{awakeningCount > 0
? <p>
{"Awakening count: "}
<strong>{awakeningCount}</strong>
</p>
: null
}
<p>
{"Current Soul Shards: "}
<strong>{formatInteger(currentSoulShards)}</strong>
</p>
<p>
{"Current siring count: "}
<strong>{siring.count}</strong>
</p>
{hasDefeatedFinalBoss
? <p className="echo-preview">
{"Soul Shards on awakening: "}
<strong>
{"+"}
{formatInteger(soulShardsPreview)}
</strong>
{metaMultiplier > 1
? <span className="echo-meta-bonus">
{" (×"}
{metaMultiplier.toFixed(2)}
{" meta bonus applied)"}
</span>
: null
}
</p>
: null}
</div>
{hasDefeatedFinalBoss
? null
: <div className="transcendence-locked">
<p>
{"🔒 "}
<strong>{"Defeat the Eternal Darkness"}</strong>
{" to unlock Awakening."}
</p>
<p className="transcendence-hint">
{"The Eternal Darkness is the final boss of the Vampire realm."}
</p>
</div>
}
{hasDefeatedFinalBoss
? <div className="prestige-form">
<p>
{"You are ready to achieve Awakening. This action is "}
<strong>{"irreversible"}</strong>
{"."}
</p>
<button
className="transcendence-button"
disabled={isPending}
onClick={handleAwakenClick}
type="button"
>
{isPending
? "Awakening..."
: `💀 Awaken (+${formatInteger(soulShardsPreview)} Soul Shards)`}
</button>
{awakeningError === null
? null
: <p className="error">{awakeningError}</p>}
{result === null
? null
: <p className="success">
{"Awakening achieved! Earned "}
<strong>
{formatInteger(result.soulShardsEarned)}
{" Soul Shards"}
</strong>
{". This is Awakening "}
{result.count}
{". A new soul cycle begins."}
</p>
}
</div>
: null}
</>
}
{activeTab === "shop"
&& <div className="echo-shop">
<p className="shop-balance">
{"Balance: "}
<strong>
{formatInteger(currentSoulShards)}
{" Soul Shards"}
</strong>
</p>
<p className="echo-shop-description">
{"Soul Shard upgrades are "}
<strong>{"permanent"}</strong>
{" — they survive all future sirings and awakenings."}
</p>
{upgradesByCategory.map(({ catId, label, upgrades }) => {
return (
<div className="shop-category" key={catId}>
<h3>{label}</h3>
<div className="shop-upgrades">
{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 (
<div
className={`shop-upgrade-card echo-upgrade-card ${
purchased
? "purchased"
: ""
} ${!canAfford && !purchased
? "unaffordable"
: ""}`}
key={upgrade.id}
>
<div className="shop-upgrade-info">
<h4>{upgrade.name}</h4>
<p>{upgrade.description}</p>
<p className="upgrade-cost">
{purchased
? "✅ Purchased"
: `💠 ${formatInteger(upgrade.cost)} Soul Shards`}
</p>
</div>
{purchased
? null
: <button
className="upgrade-buy-button"
disabled={!canAfford || isLoading}
onClick={handleBuyClick}
type="button"
>
{isLoading
? "Buying..."
: "Buy"}
</button>
}
</div>
);
})}
</div>
</div>
);
})}
</div>
}
</section>
);
};
export { VampireAwakeningPanel };
@@ -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<string, string> = {
common: "#9e9e9e",
epic: "#9c27b0",
legendary: "#ff9800",
rare: "#2196f3",
};
const rarityLabel: Record<string, string> = {
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<string> = [];
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<string> = [];
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 (
<div className={cardClassName}>
<div className="equipment-card-header">
<span className="equipment-type-icon">{typeEmoji}</span>
<span className="equipment-name">{item.name}</span>
<span
className="equipment-rarity-badge"
style={{ color: rarityColour[item.rarity] }}
>
{rarityLabel[item.rarity]}
</span>
</div>
<p className="equipment-description">{item.description}</p>
<p className="equipment-bonus">{bonusDescription(item)}</p>
{item.setId === undefined
? null
: <p className="equipment-set">{"Set: "}{item.setId}</p>}
<div className="equipment-card-actions">
{item.owned && item.equipped
? <span className="equipment-equipped-badge">{"✅ Equipped"}</span>
: null}
{item.owned && !item.equipped
? <button
className="btn-equip"
onClick={handleEquip}
type="button"
>
{"Equip"}
</button>
: null}
{!item.owned && item.cost !== undefined
? <button
className="btn-buy"
disabled={!canAfford}
onClick={handleBuy}
title={canAfford
? ""
: "Not enough resources"}
type="button"
>
{"Buy — "}
{costLabel(item.cost, formatNumber)}
</button>
: null}
{!item.owned && item.cost === undefined
? <span className="equipment-drop-hint">{"🎲 Boss Drop Only"}</span>
: null}
</div>
</div>
);
};
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<TabFilter>("all");
if (state === null) {
return (
<section className="panel">
<p>{"Loading..."}</p>
</section>
);
}
const { resources, vampire } = state;
if (vampire === undefined) {
return (
<section className="panel">
<p>{"The Vampire expansion is not yet unlocked."}</p>
</section>
);
}
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 (
<section className="panel goddess-equipment-panel">
<div className="panel-header">
<h2>{"🦇 Vampire Equipment"}</h2>
</div>
<div className="panel-resource-bar">
<span className="resource-item">
{"🩸 Blood: "}
{formatNumber(blood)}
</span>
<span className="resource-item">
{"💧 Ichor: "}
{formatNumber(ichor)}
</span>
<span className="resource-item">
{"💠 Soul Shards: "}
{formatNumber(soulShards)}
</span>
</div>
<div className="equipment-tabs">
{tabs.map((tab) => {
function handleTabClick(): void {
setActiveTab(tab.id);
}
return (
<button
className={`tab-btn${activeTab === tab.id
? " active"
: ""}`}
key={tab.id}
onClick={handleTabClick}
type="button"
>
{tab.label}
</button>
);
})}
</div>
<div className="equipment-grid">
{filteredEquipment.map((item) => {
return (
<VampireEquipmentCard
blood={blood}
formatNumber={formatNumber}
ichor={ichor}
item={item}
key={item.id}
soulShards={soulShards}
/>
);
})}
{filteredEquipment.length === 0
? <p className="empty-state">
{"No equipment in this category yet."}
</p>
: null}
</div>
</section>
);
};
export { VampireEquipmentPanel };
@@ -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<SiringUpgradeCategory> = [
"blood",
"thralls",
"combat",
"ichor",
"utility",
];
const SIRING_UPGRADE_CATEGORY_LABELS: Record<SiringUpgradeCategory, string> = {
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<string | null>(null);
const [ buyingId, setBuyingId ] = useState<string | null>(null);
const [ activeTab, setActiveTab ] = useState<SiringTab>("sire");
if (state === null) {
return (
<section className="panel">
<p>{"Loading..."}</p>
</section>
);
}
const { vampire } = state;
if (vampire === undefined) {
return (
<section className="panel">
<p>{"The Vampire expansion is not yet unlocked."}</p>
</section>
);
}
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<void> {
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<void> {
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 (
<section className="panel consecration-panel">
<h2>{"🩸 Siring"}</h2>
<div className="prestige-tabs">
<button
className={`prestige-tab ${activeTab === "sire"
? "active"
: ""}`}
onClick={handleSireTabClick}
type="button"
>
{"Sire"}
</button>
<button
className={`prestige-tab ${activeTab === "shop"
? "active"
: ""}`}
onClick={handleShopTabClick}
type="button"
>
{"💧 Ichor Shop ("}
{formatInteger(currentIchor)}
{" ichor)"}
</button>
</div>
{activeTab === "sire"
&& <>
<p className="transcendence-intro">
{"Siring is the vampire prestige layer. It resets your blood"
+ " and vampire progress, but grants "}
<strong>{"Ichor"}</strong>
{" — a permanent vampire currency used to purchase powerful upgrades."
+ " Each siring also permanently increases your blood/s multiplier."}
</p>
<div className="transcendence-status">
{siring.count > 0
? <p>
{"Siring count: "}
<strong>{siring.count}</strong>
</p>
: null
}
<p>
{"Current Ichor: "}
<strong>{formatInteger(currentIchor)}</strong>
</p>
<p>
{"Blood this run: "}
<strong>{formatNumber(totalBloodEarned)}</strong>
{" / "}
<strong>{formatNumber(threshold)}</strong>
</p>
<div className="prestige-progress-bar">
<div
className="prestige-progress-fill"
style={{ width: `${progressPct}%` }}
/>
</div>
<p className="prestige-progress-label">
{progressPct}
{"% of threshold"}
</p>
{isEligible
? <p className="echo-preview">
{"Ichor on siring: "}
<strong>
{"+"}
{formatInteger(ichorPreview)}
</strong>
{combinedIchorMultiplier > 1
? <span className="echo-meta-bonus">
{" (×"}
{combinedIchorMultiplier.toFixed(2)}
{" yield bonus applied)"}
</span>
: null
}
</p>
: null}
<p>
{"Next production multiplier: "}
<strong>
{"×"}
{nextMultiplier.toFixed(2)}
</strong>
</p>
</div>
{isEligible
? null
: <div className="transcendence-locked">
<p>
{"🔒 "}
<strong>{"Earn enough blood"}</strong>
{" to unlock siring."}
</p>
<p className="transcendence-hint">
{"You need "}
{formatNumber(threshold)}
{" total blood in the current run. You have "}
{formatNumber(totalBloodEarned)}
{"."}
</p>
</div>
}
{isEligible
? <div className="prestige-form">
<p>
{"You are ready to sire. This action is "}
<strong>{"irreversible"}</strong>
{" within this vampire run."}
</p>
<button
className="transcendence-button"
disabled={isPending}
onClick={handleSireClick}
type="button"
>
{isPending
? "Siring..."
: `🩸 Sire (+${formatInteger(ichorPreview)} Ichor)`}
</button>
{siringError === null
? null
: <p className="error">{siringError}</p>}
{result === null
? null
: <p className="success">
{"Sired! Earned "}
<strong>
{formatInteger(result.ichorEarned)}
{" Ichor"}
</strong>
{". This is Siring "}
{result.count}
{". A new bloodline cycle begins."}
</p>
}
</div>
: null}
</>
}
{activeTab === "shop"
&& <div className="echo-shop">
<p className="shop-balance">
{"Balance: "}
<strong>
{formatInteger(currentIchor)}
{" Ichor"}
</strong>
</p>
<p className="echo-shop-description">
{"Ichor upgrades are "}
<strong>{"permanent"}</strong>
{" — they survive future sirings."}
</p>
{upgradesByCategory.map(({ categoryId, label, upgrades }) => {
return (
<div className="shop-category" key={categoryId}>
<h3>{label}</h3>
<div className="shop-upgrades">
{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 (
<div
className={`shop-upgrade-card echo-upgrade-card ${
purchased
? "purchased"
: ""
} ${!canAfford && !purchased
? "unaffordable"
: ""}`}
key={upgrade.id}
>
<div className="shop-upgrade-info">
<h4>{upgrade.name}</h4>
<p>{upgrade.description}</p>
<p className="upgrade-cost">
{purchased
? "✅ Purchased"
: `💧 ${formatInteger(upgrade.ichorCost)} Ichor`}
</p>
</div>
{purchased
? null
: <button
className="upgrade-buy-button"
disabled={!canAfford || isLoading}
onClick={handleBuyClick}
type="button"
>
{isLoading
? "Buying..."
: "Buy"}
</button>
}
</div>
);
})}
</div>
</div>
);
})}
</div>
}
</section>
);
};
export { VampireSiringPanel };
@@ -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<string> = [];
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<VampireUpgrade["target"], string> = {
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<void> {
await buyVampireUpgrade(upgrade.id);
}
const multiplierPct = Math.round((upgrade.multiplier - 1) * 100);
if (upgrade.purchased) {
return (
<div className="goddess-upgrade-card purchased">
<div className="upgrade-card-header">
<span className="upgrade-name">
{"✅ "}
{upgrade.name}
</span>
<span className="upgrade-target-badge">
{targetLabel(upgrade.target)}
</span>
</div>
<p className="upgrade-description">{upgrade.description}</p>
<p className="upgrade-effect">
{`×${String(upgrade.multiplier)} (+${String(multiplierPct)}%)`}
</p>
</div>
);
}
if (upgrade.unlocked) {
return (
<div className={`goddess-upgrade-card available${canAfford
? ""
: " cannot-afford"}`}>
<div className="upgrade-card-header">
<span className="upgrade-name">{upgrade.name}</span>
<span className="upgrade-target-badge">
{targetLabel(upgrade.target)}
</span>
</div>
<p className="upgrade-description">{upgrade.description}</p>
<p className="upgrade-effect">
{`×${String(upgrade.multiplier)} (+${String(multiplierPct)}%)`}
</p>
{upgrade.thrallId === undefined
? null
: <p className="upgrade-disciple">
{"🧟 Thrall: "}
{upgrade.thrallId}
</p>
}
<button
className="btn-buy"
disabled={!canAfford}
// eslint-disable-next-line @typescript-eslint/no-misused-promises -- intentional async handler
onClick={handleBuy}
title={canAfford
? ""
: "Not enough resources"}
type="button"
>
{"Buy — "}
{costLabel(upgrade, formatNumber)}
</button>
</div>
);
}
return (
<div className="goddess-upgrade-card locked">
<div className="upgrade-card-header">
<span className="upgrade-name">{"🔒 ???"}</span>
<span className="upgrade-target-badge">
{targetLabel(upgrade.target)}
</span>
</div>
<p className="upgrade-description">{"Not yet unlocked."}</p>
</div>
);
};
/**
* 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 (
<section className="panel">
<p>{"Loading..."}</p>
</section>
);
}
const { resources, vampire } = state;
if (vampire === undefined) {
return (
<section className="panel">
<p>{"The Vampire expansion is not yet unlocked."}</p>
</section>
);
}
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 (
<section className="panel goddess-upgrades-panel">
<div className="panel-header">
<h2>{"⚔️ Vampire Upgrades"}</h2>
</div>
<div className="panel-resource-bar">
<span className="resource-item">
{"🩸 Blood: "}
{formatNumber(blood)}
</span>
<span className="resource-item">
{"💧 Ichor: "}
{formatNumber(ichor)}
</span>
<span className="resource-item">
{"💠 Soul Shards: "}
{formatNumber(soulShards)}
</span>
</div>
{available.length > 0
? <section className="upgrades-section">
<h3 className="section-heading">{"Available Upgrades"}</h3>
<div className="upgrades-grid">
{available.map((upgrade) => {
return (
<VampireUpgradeCard
blood={blood}
formatNumber={formatNumber}
ichor={ichor}
key={upgrade.id}
soulShards={soulShards}
upgrade={upgrade}
/>
);
})}
</div>
</section>
: null}
{locked.length > 0
? <section className="upgrades-section">
<h3 className="section-heading">{"Locked Upgrades"}</h3>
<div className="upgrades-grid">
{locked.map((upgrade) => {
return (
<VampireUpgradeCard
blood={blood}
formatNumber={formatNumber}
ichor={ichor}
key={upgrade.id}
soulShards={soulShards}
upgrade={upgrade}
/>
);
})}
</div>
</section>
: null}
{purchased.length > 0
? <section className="upgrades-section">
<h3 className="section-heading">{"Purchased Upgrades"}</h3>
<div className="upgrades-grid">
{purchased.map((upgrade) => {
return (
<VampireUpgradeCard
blood={blood}
formatNumber={formatNumber}
ichor={ichor}
key={upgrade.id}
soulShards={soulShards}
upgrade={upgrade}
/>
);
})}
</div>
</section>
: null}
{upgrades.length === 0
? <p className="empty-state">{"No vampire upgrades available yet."}</p>
: null}
</section>
);
};
export { VampireUpgradesPanel };
+277
View File
@@ -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<void>;
/**
* Perform a vampire siring (prestige reset) for ichor.
* @returns The siring response containing ichorEarned.
*/
sire: ()=> Promise<SiringResponse>;
/**
* Purchase a siring upgrade from the ichor shop.
*/
buySiringUpgrade: (upgradeId: string)=> Promise<void>;
/**
* Perform a vampire awakening (meta-reset) for soul shards.
* @returns The awakening response containing soulShardsEarned.
*/
awaken: ()=> Promise<AwakeningResponse>;
/**
* Purchase an awakening upgrade from the soul shards shop.
*/
buyAwakeningUpgrade: (upgradeId: string)=> Promise<void>;
}
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<SiringResponse> => {
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<AwakeningResponse> => {
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,