93ffa0768b
Build and Publish Mana Loop Docker Image / build-and-publish (push) Failing after 1m2s
- Fix Fabricator golem-2 capped perk interval from 250 to 500 (spec match) - Update golem-1 description to 'Unlock golem summoning' (spec match)
172 lines
7.9 KiB
TypeScript
172 lines
7.9 KiB
TypeScript
'use client';
|
|
|
|
import { useState, useEffect, useMemo } from 'react';
|
|
import { Button } from '@/components/ui/button';
|
|
import { Mountain } from 'lucide-react';
|
|
import { Card, CardContent } from '@/components/ui/card';
|
|
import { ManaDisplay } from '@/components/game';
|
|
import { ActionButtons } from '@/components/game';
|
|
import { ActivityLogPanel } from '@/components/game/ActivityLogPanel';
|
|
import { DebugName } from '@/components/game/debug/debug-context';
|
|
import { useGameStore, useManaStore, useCombatStore, useCraftingStore, usePrestigeStore, useAttunementStore } from '@/lib/game/stores';
|
|
import { getUnifiedEffects } from '@/lib/game/effects';
|
|
import { getMeditationBonus, getIncursionStrength } from '@/lib/game/stores';
|
|
import { computeTotalMaxMana, computeTotalRegen, computeTotalClickMana } from '@/lib/game/effects';
|
|
import { computeDisciplineEffects } from '@/lib/game/effects/discipline-effects';
|
|
import { computeConversionRates } from '@/lib/game/utils/conversion-rates';
|
|
import { getGuardianForFloor } from '@/lib/game/data/guardian-encounters';
|
|
import { ATTUNEMENTS_DEF } from '@/lib/game/data/attunements';
|
|
import type { ElementRegenBreakdown } from '@/components/game/ManaDisplay';
|
|
|
|
export function LeftPanel() {
|
|
const [isGathering, setIsGathering] = useState(false);
|
|
|
|
const rawMana = useManaStore((s) => s.rawMana);
|
|
const elements = useManaStore((s) => s.elements);
|
|
const meditateTicks = useManaStore((s) => s.meditateTicks);
|
|
const elementRegen = useManaStore((s) => s.elementRegen);
|
|
const prestigeUpgrades = usePrestigeStore((s) => s.prestigeUpgrades);
|
|
const signedPacts = usePrestigeStore((s) => s.signedPacts);
|
|
const attunements = useAttunementStore((s) => s.attunements);
|
|
const equippedInstances = useCraftingStore((s) => s.equippedInstances);
|
|
const equipmentInstances = useCraftingStore((s) => s.equipmentInstances);
|
|
const gatherMana = useGameStore((s) => s.gatherMana);
|
|
const spireMode = useCombatStore((s) => s.spireMode);
|
|
const enterSpireMode = useCombatStore((s) => s.enterSpireMode);
|
|
const currentAction = useCombatStore((s) => s.currentAction);
|
|
const designProgress = useCraftingStore((s) => s.designProgress);
|
|
const designProgress2 = useCraftingStore((s) => s.designProgress2);
|
|
const cancelDesign = useCraftingStore((s) => s.cancelDesign);
|
|
const preparationProgress = useCraftingStore((s) => s.preparationProgress);
|
|
const applicationProgress = useCraftingStore((s) => s.applicationProgress);
|
|
const equipmentCraftingProgress = useCraftingStore((s) => s.equipmentCraftingProgress);
|
|
|
|
const handleGatherStart = () => { setIsGathering(true); gatherMana(); };
|
|
const handleGatherEnd = () => { setIsGathering(false); };
|
|
|
|
useEffect(() => {
|
|
if (!isGathering) return;
|
|
let lastGatherTime = 0;
|
|
const minGatherInterval = 100;
|
|
let animationFrameId: number;
|
|
const gatherLoop = (timestamp: number) => {
|
|
if (timestamp - lastGatherTime >= minGatherInterval) {
|
|
gatherMana();
|
|
lastGatherTime = timestamp;
|
|
}
|
|
animationFrameId = requestAnimationFrame(gatherLoop);
|
|
};
|
|
animationFrameId = requestAnimationFrame(gatherLoop);
|
|
return () => cancelAnimationFrame(animationFrameId);
|
|
}, [isGathering, gatherMana]);
|
|
|
|
const upgradeEffects = getUnifiedEffects({ skillUpgrades: {}, skillTiers: {}, equippedInstances, equipmentInstances });
|
|
const disciplineEffects = computeDisciplineEffects();
|
|
const maxMana = computeTotalMaxMana({ skills: {}, prestigeUpgrades, skillUpgrades: {}, skillTiers: {}, equippedInstances, equipmentInstances }, upgradeEffects);
|
|
const baseRegen = computeTotalRegen({ skills: {}, prestigeUpgrades, skillUpgrades: {}, skillTiers: {}, equippedInstances, equipmentInstances }, upgradeEffects);
|
|
const clickMana = computeTotalClickMana({ skills: {}, skillUpgrades: {}, skillTiers: {}, equippedInstances, equipmentInstances }, upgradeEffects);
|
|
const meditationMultiplier = getMeditationBonus(meditateTicks, upgradeEffects.meditationEfficiency, disciplineEffects.meditationCapBonus);
|
|
const incursionStrength = getIncursionStrength(useGameStore((s) => s.day), useGameStore((s) => s.hour));
|
|
const effectiveRegen = baseRegen * (1 - incursionStrength) * meditationMultiplier;
|
|
|
|
// Compute per-element regen breakdown for ManaDisplay (DISC-8)
|
|
const elementRegenBreakdown = useMemo((): Record<string, ElementRegenBreakdown> | undefined => {
|
|
const pactElementMap: Record<number, string> = {};
|
|
for (const floor of signedPacts) {
|
|
const g = getGuardianForFloor(floor);
|
|
if (g?.element?.length) pactElementMap[floor] = g.element[0];
|
|
}
|
|
const grossRegen: Record<string, number> = {};
|
|
for (const [id, state] of Object.entries(attunements)) {
|
|
if (!state.active) continue;
|
|
const def = ATTUNEMENTS_DEF[id];
|
|
if (def?.primaryManaType) {
|
|
grossRegen[def.primaryManaType] = (grossRegen[def.primaryManaType] || 0)
|
|
+ (def.conversionRate || 0);
|
|
}
|
|
}
|
|
const invokerLevel = attunements.invoker?.active ? (attunements.invoker.level || 1) : 0;
|
|
const conversionResult = computeConversionRates({
|
|
disciplineEffects,
|
|
attunements,
|
|
signedPacts,
|
|
pactElementMap,
|
|
invokerLevel,
|
|
meditationMultiplier,
|
|
grossRegen,
|
|
rawGrossRegen: baseRegen,
|
|
});
|
|
const breakdown: Record<string, ElementRegenBreakdown> = {};
|
|
for (const [elem, entry] of Object.entries(conversionResult.rates)) {
|
|
if (entry.paused) continue;
|
|
const drains: Record<string, number> = {};
|
|
// This element is drained when it's a component of a higher conversion
|
|
for (const [destElem, destEntry] of Object.entries(conversionResult.rates)) {
|
|
if (destEntry.paused) continue;
|
|
if (destEntry.componentCosts[elem]) {
|
|
drains[destElem] = (drains[destElem] || 0) + destEntry.finalRate * destEntry.componentCosts[elem];
|
|
}
|
|
}
|
|
if (entry.finalRate > 0 || Object.keys(drains).length > 0) {
|
|
breakdown[elem] = { produced: entry.finalRate, drains };
|
|
}
|
|
}
|
|
return Object.keys(breakdown).length > 0 ? breakdown : undefined;
|
|
}, [disciplineEffects, attunements, signedPacts, meditationMultiplier, baseRegen]);
|
|
|
|
return (
|
|
<div className="md:w-80 space-y-3 flex-shrink-0 p-1">
|
|
{/* 1. Mana Display */}
|
|
<DebugName name="ManaDisplay">
|
|
<ManaDisplay
|
|
rawMana={rawMana}
|
|
maxMana={maxMana}
|
|
effectiveRegen={effectiveRegen}
|
|
meditationMultiplier={meditationMultiplier}
|
|
clickMana={clickMana}
|
|
isGathering={isGathering}
|
|
onGatherStart={handleGatherStart}
|
|
onGatherEnd={handleGatherEnd}
|
|
elements={elements}
|
|
elementRegen={elementRegen}
|
|
elementRegenBreakdown={elementRegenBreakdown}
|
|
/>
|
|
</DebugName>
|
|
|
|
{/* 2. Spire Entry */}
|
|
{!spireMode && (
|
|
<DebugName name="ClimbSpireButton">
|
|
<Button className="w-full bg-gradient-to-r from-amber-600 to-orange-600 hover:from-amber-700 hover:to-orange-600 text-white" size="lg" onClick={enterSpireMode}>
|
|
<Mountain className="w-5 h-5 mr-2" />
|
|
Climb the Spire
|
|
</Button>
|
|
</DebugName>
|
|
)}
|
|
|
|
{/* 3. Current Action */}
|
|
{!spireMode && (
|
|
<DebugName name="ActionButtons">
|
|
<Card className="bg-[var(--bg-surface)] border-[var(--border-subtle)]">
|
|
<CardContent className="pt-3">
|
|
<ActionButtons
|
|
currentAction={currentAction}
|
|
designProgress={designProgress}
|
|
designProgress2={designProgress2}
|
|
preparationProgress={preparationProgress}
|
|
applicationProgress={applicationProgress}
|
|
equipmentCraftingProgress={equipmentCraftingProgress}
|
|
cancelDesign={cancelDesign}
|
|
/>
|
|
</CardContent>
|
|
</Card>
|
|
</DebugName>
|
|
)}
|
|
|
|
{/* 4. Activity Log */}
|
|
<DebugName name="ActivityLogPanel">
|
|
<ActivityLogPanel />
|
|
</DebugName>
|
|
</div>
|
|
);
|
|
}
|