diff --git a/apps/web/src/components/game/AdventurerPanel.tsx b/apps/web/src/components/game/AdventurerPanel.tsx index 2ae128b..24183e3 100644 --- a/apps/web/src/components/game/AdventurerPanel.tsx +++ b/apps/web/src/components/game/AdventurerPanel.tsx @@ -12,20 +12,48 @@ const CLASS_ICONS: Record = { 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 (
@@ -41,10 +69,12 @@ const AdventurerCard = ({ adventurer, currentGold, unlockHint, formatNumber }: A {!adventurer.unlocked && unlockHint && (

📜 Complete: {unlockHint}

@@ -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(1); if (!state) return

Loading...

; @@ -81,11 +112,24 @@ export const AdventurerPanel = (): React.JSX.Element => { onToggle={() => { setShowLocked((v) => !v); }} />
+
+ {BATCH_OPTIONS.map((option) => ( + + ))} +
{visible.map((adventurer) => ( void; - /** Buy an adventurer */ - buyAdventurer: (adventurerId: string) => void; + /** Buy one or more of an adventurer tier; quantity > 1 buys in batch */ + buyAdventurer: (adventurerId: string, quantity: number) => void; /** Buy an upgrade */ buyUpgrade: (upgradeId: string) => void; /** Purchase a buyable equipment item */ @@ -439,20 +439,31 @@ export const GameProvider = ({ children }: { children: React.ReactNode }): React }); }, []); - const buyAdventurer = useCallback((adventurerId: string) => { + const buyAdventurer = useCallback((adventurerId: string, quantity: number) => { setState((prev) => { if (!prev) return prev; const adventurer = prev.adventurers.find((a) => a.id === adventurerId); if (!adventurer || !adventurer.unlocked) return prev; - const cost = 10 * Math.pow(1.15, adventurer.count); - if (prev.resources.gold < cost) return prev; + let gold = prev.resources.gold; + let count = adventurer.count; + let purchased = 0; + + for (let i = 0; i < quantity; i++) { + const cost = 10 * Math.pow(1.15, count); + if (gold < cost) break; + gold -= cost; + count++; + purchased++; + } + + if (purchased === 0) return prev; return { ...prev, - resources: { ...prev.resources, gold: prev.resources.gold - cost }, + resources: { ...prev.resources, gold }, adventurers: prev.adventurers.map((a) => - a.id === adventurerId ? { ...a, count: a.count + 1 } : a, + a.id === adventurerId ? { ...a, count } : a, ), }; }); diff --git a/apps/web/src/styles.css b/apps/web/src/styles.css index df5813e..5f43341 100644 --- a/apps/web/src/styles.css +++ b/apps/web/src/styles.css @@ -248,6 +248,36 @@ body { } /* ===================== ADVENTURERS ===================== */ +.batch-selector { + display: flex; + flex-wrap: wrap; + gap: 0.4rem; + margin-bottom: 0.75rem; +} + +.batch-button { + background: var(--colour-surface); + border: 1px solid var(--colour-border); + border-radius: var(--radius); + color: var(--colour-text); + cursor: pointer; + font-size: 0.8rem; + font-weight: 600; + padding: 0.3rem 0.6rem; + transition: background 0.15s, border-color 0.15s, color 0.15s; +} + +.batch-button:hover { + border-color: var(--colour-accent); + color: var(--colour-accent); +} + +.batch-button.active { + background: var(--colour-accent); + border-color: var(--colour-accent); + color: #fff; +} + .adventurer-list { display: flex; flex-direction: column;