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 { 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 };
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user