All checks were successful
Build and Publish Mana Loop Docker Image / build-and-publish (push) Successful in 3m9s
- Increase attunement mana conversion rates (0.2 -> 2 for Enchanter) - Hide mana types with current < 1 in ManaDisplay and LabTab - Only show owned equipment types when designing enchantments
406 lines
14 KiB
TypeScript
Executable File
406 lines
14 KiB
TypeScript
Executable File
'use client';
|
|
|
|
import { createContext, useContext, useMemo, type ReactNode } from 'react';
|
|
import { useSkillStore } from '@/lib/game/stores/skillStore';
|
|
import { useManaStore } from '@/lib/game/stores/manaStore';
|
|
import { usePrestigeStore } from '@/lib/game/stores/prestigeStore';
|
|
import { useUIStore } from '@/lib/game/stores/uiStore';
|
|
import { useCombatStore } from '@/lib/game/stores/combatStore';
|
|
import { useGameStore, useGameLoop } from '@/lib/game/stores/gameStore';
|
|
import { computeEffects, hasSpecial, SPECIAL_EFFECTS } from '@/lib/game/upgrade-effects';
|
|
import { getStudySpeedMultiplier, getStudyCostMultiplier } from '@/lib/game/constants';
|
|
import { getTierMultiplier } from '@/lib/game/skill-evolution';
|
|
import {
|
|
computeMaxMana,
|
|
computeRegen,
|
|
computeClickMana,
|
|
getMeditationBonus,
|
|
canAffordSpellCost,
|
|
calcDamage,
|
|
getFloorElement,
|
|
getBoonBonuses,
|
|
getIncursionStrength,
|
|
} from '@/lib/game/utils';
|
|
import {
|
|
ELEMENTS,
|
|
GUARDIANS,
|
|
SPELLS_DEF,
|
|
HOURS_PER_TICK,
|
|
TICK_MS,
|
|
} from '@/lib/game/constants';
|
|
import type { ElementDef, GuardianDef, SpellDef, GameAction } from '@/lib/game/types';
|
|
|
|
// Define a unified store type that combines all stores
|
|
interface UnifiedStore {
|
|
// From gameStore (coordinator)
|
|
day: number;
|
|
hour: number;
|
|
incursionStrength: number;
|
|
containmentWards: number;
|
|
initialized: boolean;
|
|
tick: () => void;
|
|
resetGame: () => void;
|
|
gatherMana: () => void;
|
|
startNewLoop: () => void;
|
|
|
|
// From manaStore
|
|
rawMana: number;
|
|
meditateTicks: number;
|
|
totalManaGathered: number;
|
|
elements: Record<string, { current: number; max: number; unlocked: boolean }>;
|
|
setRawMana: (amount: number) => void;
|
|
addRawMana: (amount: number, max: number) => void;
|
|
spendRawMana: (amount: number) => boolean;
|
|
convertMana: (element: string, amount: number) => boolean;
|
|
unlockElement: (element: string, cost: number) => boolean;
|
|
craftComposite: (target: string, recipe: string[]) => boolean;
|
|
|
|
// From skillStore
|
|
skills: Record<string, number>;
|
|
skillProgress: Record<string, number>;
|
|
skillUpgrades: Record<string, string[]>;
|
|
skillTiers: Record<string, number>;
|
|
paidStudySkills: Record<string, number>;
|
|
currentStudyTarget: { type: 'skill' | 'spell' | 'blueprint'; id: string; progress: number; required: number } | null;
|
|
parallelStudyTarget: { type: 'skill' | 'spell' | 'blueprint'; id: string; progress: number; required: number } | null;
|
|
setSkillLevel: (skillId: string, level: number) => void;
|
|
startStudyingSkill: (skillId: string, rawMana: number) => { started: boolean; cost: number };
|
|
startStudyingSpell: (spellId: string, rawMana: number, studyTime: number) => { started: boolean; cost: number };
|
|
cancelStudy: (retentionBonus: number) => void;
|
|
selectSkillUpgrade: (skillId: string, upgradeId: string) => void;
|
|
deselectSkillUpgrade: (skillId: string, upgradeId: string) => void;
|
|
commitSkillUpgrades: (skillId: string, upgradeIds: string[]) => void;
|
|
tierUpSkill: (skillId: string) => void;
|
|
getSkillUpgradeChoices: (skillId: string, milestone: 5 | 10) => { available: Array<{ id: string; name: string; desc: string; milestone: 5 | 10; effect: { type: string; stat?: string; value?: number; specialId?: string } }>; selected: string[] };
|
|
|
|
// From prestigeStore
|
|
loopCount: number;
|
|
insight: number;
|
|
totalInsight: number;
|
|
loopInsight: number;
|
|
prestigeUpgrades: Record<string, number>;
|
|
memorySlots: number;
|
|
pactSlots: number;
|
|
memories: Array<{ skillId: string; level: number; tier: number; upgrades: string[] }>;
|
|
defeatedGuardians: number[];
|
|
signedPacts: number[];
|
|
pactRitualFloor: number | null;
|
|
pactRitualProgress: number;
|
|
doPrestige: (id: string) => void;
|
|
addMemory: (memory: { skillId: string; level: number; tier: number; upgrades: string[] }) => void;
|
|
removeMemory: (skillId: string) => void;
|
|
clearMemories: () => void;
|
|
startPactRitual: (floor: number, rawMana: number) => boolean;
|
|
cancelPactRitual: () => void;
|
|
removePact: (floor: number) => void;
|
|
defeatGuardian: (floor: number) => void;
|
|
|
|
// From combatStore
|
|
currentFloor: number;
|
|
floorHP: number;
|
|
floorMaxHP: number;
|
|
maxFloorReached: number;
|
|
activeSpell: string;
|
|
currentAction: GameAction;
|
|
castProgress: number;
|
|
spells: Record<string, { learned: boolean; level: number; studyProgress?: number }>;
|
|
setAction: (action: GameAction) => void;
|
|
setSpell: (spellId: string) => void;
|
|
learnSpell: (spellId: string) => void;
|
|
advanceFloor: () => void;
|
|
|
|
// From uiStore
|
|
log: string[];
|
|
paused: boolean;
|
|
gameOver: boolean;
|
|
victory: boolean;
|
|
addLog: (message: string) => void;
|
|
togglePause: () => void;
|
|
setPaused: (paused: boolean) => void;
|
|
setGameOver: (gameOver: boolean, victory?: boolean) => void;
|
|
}
|
|
|
|
interface GameContextValue {
|
|
// Unified store for backward compatibility
|
|
store: UnifiedStore;
|
|
|
|
// Individual stores for direct access if needed
|
|
skillStore: ReturnType<typeof useSkillStore.getState>;
|
|
manaStore: ReturnType<typeof useManaStore.getState>;
|
|
prestigeStore: ReturnType<typeof usePrestigeStore.getState>;
|
|
uiStore: ReturnType<typeof useUIStore.getState>;
|
|
combatStore: ReturnType<typeof useCombatStore.getState>;
|
|
|
|
// Computed effects from upgrades
|
|
upgradeEffects: ReturnType<typeof computeEffects>;
|
|
|
|
// Derived stats
|
|
maxMana: number;
|
|
baseRegen: number;
|
|
clickMana: number;
|
|
floorElem: string;
|
|
floorElemDef: ElementDef | undefined;
|
|
isGuardianFloor: boolean;
|
|
currentGuardian: GuardianDef | undefined;
|
|
activeSpellDef: SpellDef | undefined;
|
|
meditationMultiplier: number;
|
|
incursionStrength: number;
|
|
studySpeedMult: number;
|
|
studyCostMult: number;
|
|
|
|
// Effective regen calculations
|
|
effectiveRegenWithSpecials: number;
|
|
manaCascadeBonus: number;
|
|
effectiveRegen: number;
|
|
|
|
// DPS calculation
|
|
dps: number;
|
|
|
|
// Boons
|
|
activeBoons: ReturnType<typeof getBoonBonuses>;
|
|
|
|
// Helpers
|
|
canCastSpell: (spellId: string) => boolean;
|
|
hasSpecial: (effects: ReturnType<typeof computeEffects>, specialId: string) => boolean;
|
|
SPECIAL_EFFECTS: typeof SPECIAL_EFFECTS;
|
|
}
|
|
|
|
const GameContext = createContext<GameContextValue | null>(null);
|
|
|
|
export function GameProvider({ children }: { children: ReactNode }) {
|
|
// Get all individual stores
|
|
const gameStore = useGameStore();
|
|
const skillState = useSkillStore();
|
|
const manaState = useManaStore();
|
|
const prestigeState = usePrestigeStore();
|
|
const uiState = useUIStore();
|
|
const combatState = useCombatStore();
|
|
|
|
// Create unified store object for backward compatibility
|
|
const unifiedStore = useMemo<UnifiedStore>(() => ({
|
|
// From gameStore
|
|
day: gameStore.day,
|
|
hour: gameStore.hour,
|
|
incursionStrength: gameStore.incursionStrength,
|
|
containmentWards: gameStore.containmentWards,
|
|
initialized: gameStore.initialized,
|
|
tick: gameStore.tick,
|
|
resetGame: gameStore.resetGame,
|
|
gatherMana: gameStore.gatherMana,
|
|
startNewLoop: gameStore.startNewLoop,
|
|
|
|
// From manaStore
|
|
rawMana: manaState.rawMana,
|
|
meditateTicks: manaState.meditateTicks,
|
|
totalManaGathered: manaState.totalManaGathered,
|
|
elements: manaState.elements,
|
|
setRawMana: manaState.setRawMana,
|
|
addRawMana: manaState.addRawMana,
|
|
spendRawMana: manaState.spendRawMana,
|
|
convertMana: manaState.convertMana,
|
|
unlockElement: manaState.unlockElement,
|
|
craftComposite: manaState.craftComposite,
|
|
|
|
// From skillStore
|
|
skills: skillState.skills,
|
|
skillProgress: skillState.skillProgress,
|
|
skillUpgrades: skillState.skillUpgrades,
|
|
skillTiers: skillState.skillTiers,
|
|
paidStudySkills: skillState.paidStudySkills,
|
|
currentStudyTarget: skillState.currentStudyTarget,
|
|
parallelStudyTarget: skillState.parallelStudyTarget,
|
|
setSkillLevel: skillState.setSkillLevel,
|
|
startStudyingSkill: skillState.startStudyingSkill,
|
|
startStudyingSpell: skillState.startStudyingSpell,
|
|
cancelStudy: skillState.cancelStudy,
|
|
selectSkillUpgrade: skillState.selectSkillUpgrade,
|
|
deselectSkillUpgrade: skillState.deselectSkillUpgrade,
|
|
commitSkillUpgrades: skillState.commitSkillUpgrades,
|
|
tierUpSkill: skillState.tierUpSkill,
|
|
getSkillUpgradeChoices: skillState.getSkillUpgradeChoices,
|
|
|
|
// From prestigeStore
|
|
loopCount: prestigeState.loopCount,
|
|
insight: prestigeState.insight,
|
|
totalInsight: prestigeState.totalInsight,
|
|
loopInsight: prestigeState.loopInsight,
|
|
prestigeUpgrades: prestigeState.prestigeUpgrades,
|
|
memorySlots: prestigeState.memorySlots,
|
|
pactSlots: prestigeState.pactSlots,
|
|
memories: prestigeState.memories,
|
|
defeatedGuardians: prestigeState.defeatedGuardians,
|
|
signedPacts: prestigeState.signedPacts,
|
|
pactRitualFloor: prestigeState.pactRitualFloor,
|
|
pactRitualProgress: prestigeState.pactRitualProgress,
|
|
doPrestige: prestigeState.doPrestige,
|
|
addMemory: prestigeState.addMemory,
|
|
removeMemory: prestigeState.removeMemory,
|
|
clearMemories: prestigeState.clearMemories,
|
|
startPactRitual: prestigeState.startPactRitual,
|
|
cancelPactRitual: prestigeState.cancelPactRitual,
|
|
removePact: prestigeState.removePact,
|
|
defeatGuardian: prestigeState.defeatGuardian,
|
|
|
|
// From combatStore
|
|
currentFloor: combatState.currentFloor,
|
|
floorHP: combatState.floorHP,
|
|
floorMaxHP: combatState.floorMaxHP,
|
|
maxFloorReached: combatState.maxFloorReached,
|
|
activeSpell: combatState.activeSpell,
|
|
currentAction: combatState.currentAction,
|
|
castProgress: combatState.castProgress,
|
|
spells: combatState.spells,
|
|
setAction: combatState.setAction,
|
|
setSpell: combatState.setSpell,
|
|
learnSpell: combatState.learnSpell,
|
|
advanceFloor: combatState.advanceFloor,
|
|
|
|
// From uiStore
|
|
log: uiState.logs,
|
|
paused: uiState.paused,
|
|
gameOver: uiState.gameOver,
|
|
victory: uiState.victory,
|
|
addLog: uiState.addLog,
|
|
togglePause: uiState.togglePause,
|
|
setPaused: uiState.setPaused,
|
|
setGameOver: uiState.setGameOver,
|
|
}), [gameStore, skillState, manaState, prestigeState, uiState, combatState]);
|
|
|
|
// Computed effects from upgrades
|
|
const upgradeEffects = useMemo(
|
|
() => computeEffects(skillState.skillUpgrades || {}, skillState.skillTiers || {}),
|
|
[skillState.skillUpgrades, skillState.skillTiers]
|
|
);
|
|
|
|
// Create a minimal state object for compute functions
|
|
const stateForCompute = useMemo(() => ({
|
|
skills: skillState.skills,
|
|
prestigeUpgrades: prestigeState.prestigeUpgrades,
|
|
skillUpgrades: skillState.skillUpgrades,
|
|
skillTiers: skillState.skillTiers,
|
|
signedPacts: prestigeState.signedPacts,
|
|
rawMana: manaState.rawMana,
|
|
meditateTicks: manaState.meditateTicks,
|
|
incursionStrength: gameStore.incursionStrength,
|
|
}), [skillState, prestigeState, manaState, gameStore.incursionStrength]);
|
|
|
|
// Derived stats
|
|
const maxMana = useMemo(
|
|
() => computeMaxMana(stateForCompute, upgradeEffects),
|
|
[stateForCompute, upgradeEffects]
|
|
);
|
|
|
|
const baseRegen = useMemo(
|
|
() => computeRegen(stateForCompute, upgradeEffects),
|
|
[stateForCompute, upgradeEffects]
|
|
);
|
|
|
|
const clickMana = useMemo(() => computeClickMana(stateForCompute), [stateForCompute]);
|
|
|
|
// Floor element from combat store
|
|
const floorElem = useMemo(() => getFloorElement(combatState.currentFloor), [combatState.currentFloor]);
|
|
const floorElemDef = ELEMENTS[floorElem];
|
|
const isGuardianFloor = !!GUARDIANS[combatState.currentFloor];
|
|
const currentGuardian = GUARDIANS[combatState.currentFloor];
|
|
const activeSpellDef = SPELLS_DEF[combatState.activeSpell];
|
|
|
|
const meditationMultiplier = useMemo(
|
|
() => getMeditationBonus(manaState.meditateTicks, skillState.skills, upgradeEffects.meditationEfficiency),
|
|
[manaState.meditateTicks, skillState.skills, upgradeEffects.meditationEfficiency]
|
|
);
|
|
|
|
const incursionStrength = useMemo(
|
|
() => getIncursionStrength(gameStore.day, gameStore.hour),
|
|
[gameStore.day, gameStore.hour]
|
|
);
|
|
|
|
const studySpeedMult = useMemo(
|
|
() => getStudySpeedMultiplier(skillState.skills),
|
|
[skillState.skills]
|
|
);
|
|
|
|
const studyCostMult = useMemo(
|
|
() => getStudyCostMultiplier(skillState.skills),
|
|
[skillState.skills]
|
|
);
|
|
|
|
// Effective regen calculations
|
|
const effectiveRegenWithSpecials = baseRegen * (1 - incursionStrength);
|
|
|
|
const manaCascadeBonus = hasSpecial(upgradeEffects, SPECIAL_EFFECTS.MANA_CASCADE)
|
|
? Math.floor(maxMana / 100) * 0.1
|
|
: 0;
|
|
|
|
const effectiveRegen = (effectiveRegenWithSpecials + manaCascadeBonus) * meditationMultiplier;
|
|
|
|
// Active boons
|
|
const activeBoons = useMemo(
|
|
() => getBoonBonuses(prestigeState.signedPacts),
|
|
[prestigeState.signedPacts]
|
|
);
|
|
|
|
// DPS calculation - based on active spell, attack speed, and damage
|
|
const dps = useMemo(() => {
|
|
if (!activeSpellDef) return 0;
|
|
const baseDmg = calcDamage(
|
|
{ skills: skillState.skills, signedPacts: prestigeState.signedPacts },
|
|
combatState.activeSpell,
|
|
floorElem
|
|
);
|
|
const dmgWithEffects = baseDmg * upgradeEffects.baseDamageMultiplier + upgradeEffects.baseDamageBonus;
|
|
const attackSpeed = (1 + (skillState.skills.quickCast || 0) * 0.05) * upgradeEffects.attackSpeedMultiplier;
|
|
const castSpeed = activeSpellDef.castSpeed || 1;
|
|
return dmgWithEffects * attackSpeed * castSpeed;
|
|
}, [activeSpellDef, skillState.skills, prestigeState.signedPacts, floorElem, upgradeEffects, combatState.activeSpell]);
|
|
|
|
// Helper functions
|
|
const canCastSpell = (spellId: string): boolean => {
|
|
const spell = SPELLS_DEF[spellId];
|
|
if (!spell) return false;
|
|
return canAffordSpellCost(spell.cost, manaState.rawMana, manaState.elements);
|
|
};
|
|
|
|
const value: GameContextValue = {
|
|
store: unifiedStore,
|
|
skillStore: skillState,
|
|
manaStore: manaState,
|
|
prestigeStore: prestigeState,
|
|
uiStore: uiState,
|
|
combatStore: combatState,
|
|
upgradeEffects,
|
|
maxMana,
|
|
baseRegen,
|
|
clickMana,
|
|
floorElem,
|
|
floorElemDef,
|
|
isGuardianFloor,
|
|
currentGuardian,
|
|
activeSpellDef,
|
|
meditationMultiplier,
|
|
incursionStrength,
|
|
studySpeedMult,
|
|
studyCostMult,
|
|
effectiveRegenWithSpecials,
|
|
manaCascadeBonus,
|
|
effectiveRegen,
|
|
dps,
|
|
activeBoons,
|
|
canCastSpell,
|
|
hasSpecial,
|
|
SPECIAL_EFFECTS,
|
|
};
|
|
|
|
return <GameContext.Provider value={value}>{children}</GameContext.Provider>;
|
|
}
|
|
|
|
export function useGameContext() {
|
|
const context = useContext(GameContext);
|
|
if (!context) {
|
|
throw new Error('useGameContext must be used within a GameProvider');
|
|
}
|
|
return context;
|
|
}
|
|
|
|
// Re-export useGameLoop for convenience
|
|
export { useGameLoop };
|