generated from nhcarrigan/template
feat: vampire equipment, upgrades, siring, and awakening panels
This commit is contained in:
@@ -54,9 +54,13 @@ import { StoryToast } from "./storyToast.js";
|
|||||||
import { TranscendencePanel } from "./transcendencePanel.js";
|
import { TranscendencePanel } from "./transcendencePanel.js";
|
||||||
import { UpgradePanel } from "./upgradePanel.js";
|
import { UpgradePanel } from "./upgradePanel.js";
|
||||||
import { VampireAchievementsPanel } from "./vampireAchievementsPanel.js";
|
import { VampireAchievementsPanel } from "./vampireAchievementsPanel.js";
|
||||||
|
import { VampireAwakeningPanel } from "./vampireAwakeningPanel.js";
|
||||||
import { VampireBossPanel } from "./vampireBossPanel.js";
|
import { VampireBossPanel } from "./vampireBossPanel.js";
|
||||||
|
import { VampireEquipmentPanel } from "./vampireEquipmentPanel.js";
|
||||||
import { VampireQuestsPanel } from "./vampireQuestsPanel.js";
|
import { VampireQuestsPanel } from "./vampireQuestsPanel.js";
|
||||||
|
import { VampireSiringPanel } from "./vampireSiringPanel.js";
|
||||||
import { VampireThrallsPanel } from "./vampireThrallsPanel.js";
|
import { VampireThrallsPanel } from "./vampireThrallsPanel.js";
|
||||||
|
import { VampireUpgradesPanel } from "./vampireUpgradesPanel.js";
|
||||||
import { VampireZonesPanel } from "./vampireZonesPanel.js";
|
import { VampireZonesPanel } from "./vampireZonesPanel.js";
|
||||||
|
|
||||||
type Mode = "mortal" | "goddess" | "vampire";
|
type Mode = "mortal" | "goddess" | "vampire";
|
||||||
@@ -500,27 +504,19 @@ const GameLayout = (): JSX.Element => {
|
|||||||
}
|
}
|
||||||
{activeMode === "vampire"
|
{activeMode === "vampire"
|
||||||
&& activeVampireTab === "vampire-equipment"
|
&& activeVampireTab === "vampire-equipment"
|
||||||
&& <div className="vampire-placeholder">
|
&& <VampireEquipmentPanel />
|
||||||
<p>{"🦇 Vampire Equipment coming soon..."}</p>
|
|
||||||
</div>
|
|
||||||
}
|
}
|
||||||
{activeMode === "vampire"
|
{activeMode === "vampire"
|
||||||
&& activeVampireTab === "vampire-upgrades"
|
&& activeVampireTab === "vampire-upgrades"
|
||||||
&& <div className="vampire-placeholder">
|
&& <VampireUpgradesPanel />
|
||||||
<p>{"⚔️ Vampire Upgrades coming soon..."}</p>
|
|
||||||
</div>
|
|
||||||
}
|
}
|
||||||
{activeMode === "vampire"
|
{activeMode === "vampire"
|
||||||
&& activeVampireTab === "siring"
|
&& activeVampireTab === "siring"
|
||||||
&& <div className="vampire-placeholder">
|
&& <VampireSiringPanel />
|
||||||
<p>{"🩸 Siring coming soon..."}</p>
|
|
||||||
</div>
|
|
||||||
}
|
}
|
||||||
{activeMode === "vampire"
|
{activeMode === "vampire"
|
||||||
&& activeVampireTab === "vampire-awakening"
|
&& activeVampireTab === "vampire-awakening"
|
||||||
&& <div className="vampire-placeholder">
|
&& <VampireAwakeningPanel />
|
||||||
<p>{"💀 Vampire Awakening coming soon..."}</p>
|
|
||||||
</div>
|
|
||||||
}
|
}
|
||||||
{activeMode === "vampire"
|
{activeMode === "vampire"
|
||||||
&& activeVampireTab === "vampire-crafting"
|
&& 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 };
|
||||||
@@ -22,6 +22,8 @@ import {
|
|||||||
type GameState,
|
type GameState,
|
||||||
type GoddessBossChallengeResponse,
|
type GoddessBossChallengeResponse,
|
||||||
type GoddessExploreCollectResponse,
|
type GoddessExploreCollectResponse,
|
||||||
|
type AwakeningResponse,
|
||||||
|
type SiringResponse,
|
||||||
type VampireBossChallengeResponse,
|
type VampireBossChallengeResponse,
|
||||||
type LoginBonusResult,
|
type LoginBonusResult,
|
||||||
type NumberFormat,
|
type NumberFormat,
|
||||||
@@ -43,11 +45,15 @@ import {
|
|||||||
} from "react";
|
} from "react";
|
||||||
import {
|
import {
|
||||||
achieveApotheosis as achieveApotheosisApi,
|
achieveApotheosis as achieveApotheosisApi,
|
||||||
|
awaken as awakenApi,
|
||||||
|
buyAwakeningUpgrade as buyAwakeningUpgradeApi,
|
||||||
buyConsecrationUpgrade as buyConsecrationUpgradeApi,
|
buyConsecrationUpgrade as buyConsecrationUpgradeApi,
|
||||||
buyEchoUpgrade as buyEchoUpgradeApi,
|
buyEchoUpgrade as buyEchoUpgradeApi,
|
||||||
buyEnlightenmentUpgrade as buyEnlightenmentUpgradeApi,
|
buyEnlightenmentUpgrade as buyEnlightenmentUpgradeApi,
|
||||||
buyGoddessUpgrade as buyGoddessUpgradeApi,
|
buyGoddessUpgrade as buyGoddessUpgradeApi,
|
||||||
buyPrestigeUpgrade as buyPrestigeUpgradeApi,
|
buyPrestigeUpgrade as buyPrestigeUpgradeApi,
|
||||||
|
buySiringUpgrade as buySiringUpgradeApi,
|
||||||
|
buyVampireUpgrade as buyVampireUpgradeApi,
|
||||||
challengeBoss as challengeBossApi,
|
challengeBoss as challengeBossApi,
|
||||||
challengeGoddessBoss as challengeGoddessBossApi,
|
challengeGoddessBoss as challengeGoddessBossApi,
|
||||||
challengeVampireBoss as challengeVampireBossApi,
|
challengeVampireBoss as challengeVampireBossApi,
|
||||||
@@ -64,6 +70,7 @@ import {
|
|||||||
prestige as prestigeApi,
|
prestige as prestigeApi,
|
||||||
resetProgress as resetProgressApi,
|
resetProgress as resetProgressApi,
|
||||||
saveGame,
|
saveGame,
|
||||||
|
sire as sireApi,
|
||||||
startExploration as startExplorationApi,
|
startExploration as startExplorationApi,
|
||||||
startGoddessExploration as startGoddessExplorationApi,
|
startGoddessExploration as startGoddessExplorationApi,
|
||||||
transcend as transcendApi,
|
transcend as transcendApi,
|
||||||
@@ -790,6 +797,43 @@ interface GameContextValue {
|
|||||||
* Buy one or more thralls (client-side blood deduction).
|
* Buy one or more thralls (client-side blood deduction).
|
||||||
*/
|
*/
|
||||||
buyVampireThrall: (thrallId: string, quantity: number)=> void;
|
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 {
|
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() => {
|
const consecrate = useCallback(async() => {
|
||||||
try {
|
try {
|
||||||
const result = await consecrateApi({});
|
const result = await consecrateApi({});
|
||||||
@@ -3167,9 +3430,11 @@ export const GameProvider = ({
|
|||||||
apotheosis,
|
apotheosis,
|
||||||
autoBossError,
|
autoBossError,
|
||||||
autoBossLastResult,
|
autoBossLastResult,
|
||||||
|
awaken,
|
||||||
battleResult,
|
battleResult,
|
||||||
bossError,
|
bossError,
|
||||||
buyAdventurer,
|
buyAdventurer,
|
||||||
|
buyAwakeningUpgrade,
|
||||||
buyConsecrationUpgrade,
|
buyConsecrationUpgrade,
|
||||||
buyEchoUpgrade,
|
buyEchoUpgrade,
|
||||||
buyEnlightenmentUpgrade,
|
buyEnlightenmentUpgrade,
|
||||||
@@ -3178,8 +3443,11 @@ export const GameProvider = ({
|
|||||||
buyGoddessEquipment,
|
buyGoddessEquipment,
|
||||||
buyGoddessUpgrade,
|
buyGoddessUpgrade,
|
||||||
buyPrestigeUpgrade,
|
buyPrestigeUpgrade,
|
||||||
|
buySiringUpgrade,
|
||||||
buyUpgrade,
|
buyUpgrade,
|
||||||
|
buyVampireEquipment,
|
||||||
buyVampireThrall,
|
buyVampireThrall,
|
||||||
|
buyVampireUpgrade,
|
||||||
challengeBoss,
|
challengeBoss,
|
||||||
challengeGoddessBoss,
|
challengeGoddessBoss,
|
||||||
challengeVampireBoss,
|
challengeVampireBoss,
|
||||||
@@ -3212,6 +3480,7 @@ export const GameProvider = ({
|
|||||||
enlighten,
|
enlighten,
|
||||||
equipGoddessItem,
|
equipGoddessItem,
|
||||||
equipItem,
|
equipItem,
|
||||||
|
equipVampireEquipment,
|
||||||
error,
|
error,
|
||||||
failedQuestToasts,
|
failedQuestToasts,
|
||||||
flushBossLoreToasts,
|
flushBossLoreToasts,
|
||||||
@@ -3244,6 +3513,7 @@ export const GameProvider = ({
|
|||||||
showEnlightenmentToast,
|
showEnlightenmentToast,
|
||||||
showPrestigeToast,
|
showPrestigeToast,
|
||||||
showTranscendenceToast,
|
showTranscendenceToast,
|
||||||
|
sire,
|
||||||
startExploration,
|
startExploration,
|
||||||
startGoddessExploration,
|
startGoddessExploration,
|
||||||
startQuest,
|
startQuest,
|
||||||
@@ -3266,9 +3536,11 @@ export const GameProvider = ({
|
|||||||
apotheosis,
|
apotheosis,
|
||||||
autoBossError,
|
autoBossError,
|
||||||
autoBossLastResult,
|
autoBossLastResult,
|
||||||
|
awaken,
|
||||||
battleResult,
|
battleResult,
|
||||||
bossError,
|
bossError,
|
||||||
buyAdventurer,
|
buyAdventurer,
|
||||||
|
buyAwakeningUpgrade,
|
||||||
buyConsecrationUpgrade,
|
buyConsecrationUpgrade,
|
||||||
buyEchoUpgrade,
|
buyEchoUpgrade,
|
||||||
buyEnlightenmentUpgrade,
|
buyEnlightenmentUpgrade,
|
||||||
@@ -3277,8 +3549,11 @@ export const GameProvider = ({
|
|||||||
buyGoddessEquipment,
|
buyGoddessEquipment,
|
||||||
buyGoddessUpgrade,
|
buyGoddessUpgrade,
|
||||||
buyPrestigeUpgrade,
|
buyPrestigeUpgrade,
|
||||||
|
buySiringUpgrade,
|
||||||
buyUpgrade,
|
buyUpgrade,
|
||||||
|
buyVampireEquipment,
|
||||||
buyVampireThrall,
|
buyVampireThrall,
|
||||||
|
buyVampireUpgrade,
|
||||||
challengeBoss,
|
challengeBoss,
|
||||||
challengeGoddessBoss,
|
challengeGoddessBoss,
|
||||||
challengeVampireBoss,
|
challengeVampireBoss,
|
||||||
@@ -3311,6 +3586,7 @@ export const GameProvider = ({
|
|||||||
enlighten,
|
enlighten,
|
||||||
equipGoddessItem,
|
equipGoddessItem,
|
||||||
equipItem,
|
equipItem,
|
||||||
|
equipVampireEquipment,
|
||||||
error,
|
error,
|
||||||
failedQuestToasts,
|
failedQuestToasts,
|
||||||
flushBossLoreToasts,
|
flushBossLoreToasts,
|
||||||
@@ -3342,6 +3618,7 @@ export const GameProvider = ({
|
|||||||
showEnlightenmentToast,
|
showEnlightenmentToast,
|
||||||
showPrestigeToast,
|
showPrestigeToast,
|
||||||
showTranscendenceToast,
|
showTranscendenceToast,
|
||||||
|
sire,
|
||||||
startExploration,
|
startExploration,
|
||||||
startGoddessExploration,
|
startGoddessExploration,
|
||||||
startQuest,
|
startQuest,
|
||||||
|
|||||||
Reference in New Issue
Block a user