feat: add batch size selector for hiring adventurers

Closes #12
This commit is contained in:
2026-03-07 13:35:18 -08:00
committed by Naomi Carrigan
parent cddf4fa692
commit 3856dda734
3 changed files with 99 additions and 14 deletions
@@ -12,20 +12,48 @@ const CLASS_ICONS: Record<string, string> = {
paladin: "🛡️",
};
const adventurerCost = (adventurer: Adventurer): number =>
Math.ceil(10 * Math.pow(1.15, adventurer.count));
type BatchSize = 1 | 5 | 10 | 25 | 100 | "max";
const BATCH_OPTIONS: BatchSize[] = [1, 5, 10, 25, 100, "max"];
const computeBatchCost = (adventurer: Adventurer, quantity: number): number => {
let total = 0;
for (let i = 0; i < quantity; i++) {
total += 10 * Math.pow(1.15, adventurer.count + i);
}
return total;
};
const computeMaxAffordable = (adventurer: Adventurer, gold: number): number => {
let total = 0;
let quantity = 0;
for (let i = 0; i < 100_000; i++) {
const cost = 10 * Math.pow(1.15, adventurer.count + i);
if (total + cost > gold) break;
total += cost;
quantity++;
}
return quantity;
};
interface AdventurerCardProps {
adventurer: Adventurer;
currentGold: number;
batchSize: BatchSize;
unlockHint?: string | undefined;
formatNumber: (n: number) => string;
}
const AdventurerCard = ({ adventurer, currentGold, unlockHint, formatNumber }: AdventurerCardProps): React.JSX.Element => {
const AdventurerCard = ({ adventurer, currentGold, batchSize, unlockHint, formatNumber }: AdventurerCardProps): React.JSX.Element => {
const { buyAdventurer } = useGame();
const cost = adventurerCost(adventurer);
const canAfford = currentGold >= cost;
const resolvedQuantity =
batchSize === "max" ? computeMaxAffordable(adventurer, currentGold) : batchSize;
const cost = computeBatchCost(adventurer, resolvedQuantity);
const canAfford = resolvedQuantity > 0 && currentGold >= cost;
const handleBuy = (): void => {
buyAdventurer(adventurer.id, resolvedQuantity);
};
return (
<div className={`adventurer-card ${!adventurer.unlocked ? "locked" : ""}`}>
@@ -41,10 +69,12 @@ const AdventurerCard = ({ adventurer, currentGold, unlockHint, formatNumber }: A
<button
className="buy-button"
disabled={!canAfford || !adventurer.unlocked}
onClick={() => { buyAdventurer(adventurer.id); }}
onClick={handleBuy}
type="button"
>
{adventurer.unlocked ? `🪙 ${formatNumber(cost)}` : "🔒 Locked"}
{adventurer.unlocked
? `🪙 ${formatNumber(Math.ceil(cost))}${batchSize === "max" && resolvedQuantity > 0 ? ` (×${resolvedQuantity})` : ""}`
: "🔒 Locked"}
</button>
{!adventurer.unlocked && unlockHint && (
<p className="unlock-hint">📜 Complete: {unlockHint}</p>
@@ -56,6 +86,7 @@ const AdventurerCard = ({ adventurer, currentGold, unlockHint, formatNumber }: A
export const AdventurerPanel = (): React.JSX.Element => {
const { state, formatNumber } = useGame();
const [showLocked, setShowLocked] = useState(true);
const [batchSize, setBatchSize] = useState<BatchSize>(1);
if (!state) return <section className="panel"><p>Loading...</p></section>;
@@ -81,11 +112,24 @@ export const AdventurerPanel = (): React.JSX.Element => {
onToggle={() => { setShowLocked((v) => !v); }}
/>
</div>
<div className="batch-selector">
{BATCH_OPTIONS.map((option) => (
<button
key={option}
className={`batch-button ${batchSize === option ? "active" : ""}`}
onClick={() => { setBatchSize(option); }}
type="button"
>
{option === "max" ? "xMax" : `x${option}`}
</button>
))}
</div>
<div className="adventurer-list">
{visible.map((adventurer) => (
<AdventurerCard
key={adventurer.id}
adventurer={adventurer}
batchSize={batchSize}
currentGold={state.resources.gold}
unlockHint={adventurerUnlockHints.get(adventurer.id)}
formatNumber={formatNumber}