/** * @file Adventurer panel component for hiring and managing adventurers. * @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 -- Complex component with many render paths */ import { type JSX, useState } from "react"; import { useGame } from "../../context/gameContext.js"; import { computeEffectiveAdventurerStats } from "../../engine/tick.js"; import { cdnImage } from "../../utils/cdn.js"; import { LockToggle } from "../ui/lockToggle.js"; import type { Adventurer } from "@elysium/types"; type BatchSize = 1 | 5 | 10 | 25 | 100 | "max"; const batchOptions: Array = [ 1, 5, 10, 25, 100, "max" ]; /** * Parses a localStorage string back into a valid BatchSize, defaulting to 1. * @param stored - The raw string from localStorage (or null if absent). * @returns A valid BatchSize value. */ const parseBatchSize = (stored: string | null): BatchSize => { if (stored === "max") { return "max"; } const numeric = Number(stored); if (numeric === 5) { return 5; } if (numeric === 10) { return 10; } if (numeric === 25) { return 25; } if (numeric === 100) { return 100; } return 1; }; /** * Computes the total cost to buy a batch of adventurers. * @param adventurer - The adventurer to buy. * @param quantity - The number to buy. * @returns The total gold cost. */ const computeBatchCost = (adventurer: Adventurer, quantity: number): number => { let total = 0; for (let index = 0; index < quantity; index = index + 1) { const cost = adventurer.baseCost * Math.pow(1.15, adventurer.count + index); total = total + cost; } return total; }; /** * Computes the maximum number of adventurers affordable with given gold. * @param adventurer - The adventurer type. * @param gold - The available gold. * @returns The maximum affordable quantity. */ const computeMaxAffordable = (adventurer: Adventurer, gold: number): number => { let total = 0; let quantity = 0; for (let index = 0; index < 100_000; index = index + 1) { const cost = adventurer.baseCost * Math.pow(1.15, adventurer.count + index); if (total + cost > gold) { break; } total = total + cost; quantity = quantity + 1; } return quantity; }; interface EffectiveAdventurerStats { readonly combatPower: number; readonly essencePerSecond: number; readonly goldPerSecond: number; } interface AdventurerCardProperties { readonly adventurer: Adventurer; readonly currentGold: number; readonly batchSize: BatchSize; readonly unlockHint: string | undefined; readonly formatNumber: (n: number)=> string; readonly effectiveStats: EffectiveAdventurerStats; } /** * Renders a single adventurer card with buy controls. * @param props - The adventurer card properties. * @param props.adventurer - The adventurer data. * @param props.currentGold - The current gold available. * @param props.batchSize - The selected batch size. * @param props.unlockHint - Optional quest name that unlocks this adventurer. * @param props.formatNumber - The number formatting utility function. * @param props.effectiveStats - The post-multiplier per-unit stats. * @returns The JSX element. */ const AdventurerCard = ({ adventurer, currentGold, batchSize, unlockHint, formatNumber, effectiveStats, }: AdventurerCardProperties): JSX.Element => { const { buyAdventurer } = useGame(); const resolvedQuantity = batchSize === "max" ? computeMaxAffordable(adventurer, currentGold) : batchSize; const cost = computeBatchCost(adventurer, resolvedQuantity); const canAfford = resolvedQuantity > 0 && currentGold >= cost; function handleBuy(): void { buyAdventurer(adventurer.id, resolvedQuantity); } const maxSuffix = batchSize === "max" && resolvedQuantity > 0 ? ` (×${String(resolvedQuantity)})` : ""; const buttonLabel = adventurer.unlocked ? `🪙 ${formatNumber(Math.ceil(cost))}${maxSuffix}` : "🔒 Locked"; return (
{adventurer.name}

{adventurer.name}

{formatNumber(effectiveStats.goldPerSecond)} {" gold/s each"}

{adventurer.essencePerSecond > 0 &&

{formatNumber(effectiveStats.essencePerSecond)} {" essence/s each"}

}

{formatNumber(effectiveStats.combatPower)} {" combat power each"}

{"×"} {adventurer.count}
{!adventurer.unlocked && unlockHint !== undefined ?

{"📜 Complete: "} {unlockHint}

: null}
); }; /** * Renders the adventurer panel with all available adventurers. * @returns The JSX element. */ const AdventurerPanel = (): JSX.Element => { const { state, formatNumber, toggleAutoAdventurer } = useGame(); const [ showLocked, setShowLocked ] = useState(true); const [ batchSize, setBatchSize ] = useState(() => { return parseBatchSize(localStorage.getItem("elysium_batch_size")); }); if (state === null) { return (

{"Loading..."}

); } const locked = state.adventurers.filter((adventurer) => { return !adventurer.unlocked; }); const visible = showLocked ? state.adventurers : state.adventurers.filter((adventurer) => { return adventurer.unlocked; }); const adventurerUnlockHints = new Map(); for (const quest of state.quests) { for (const reward of quest.rewards) { if (reward.type === "adventurer" && reward.targetId !== undefined) { adventurerUnlockHints.set(reward.targetId, quest.name); } } } const autoAdventurerUnlocked = state.prestige.purchasedUpgradeIds.includes( "auto_adventurer", ); const autoAdventurerOn = state.autoAdventurer === true; function handleToggle(): void { setShowLocked((current) => { return !current; }); } return (

{"Adventurers"}

{autoAdventurerUnlocked ? : null }
{batchOptions.map((option) => { function handleBatchSelect(): void { setBatchSize(option); localStorage.setItem("elysium_batch_size", String(option)); } return ( ); })}
{visible.map((adventurer) => { return ( ); })}
); }; export { AdventurerPanel };