198 lines
5.7 KiB
TypeScript
Executable File
198 lines
5.7 KiB
TypeScript
Executable File
// ─── Mana Slice ───────────────────────────────────────────────────────────────
|
|
// Manages raw mana, elements, and meditation
|
|
|
|
import type { StateCreator } from 'zustand';
|
|
import type { GameState, ElementState, SpellCost } from '../types';
|
|
import { ELEMENTS, MANA_PER_ELEMENT, BASE_UNLOCKED_ELEMENTS } from '../constants';
|
|
import { computeMaxMana, computeElementMax, computeClickMana, canAffordSpellCost, getMeditationBonus } from './computed';
|
|
import { computeEffects } from '../upgrade-effects';
|
|
|
|
export interface ManaSlice {
|
|
// State
|
|
rawMana: number;
|
|
totalManaGathered: number;
|
|
meditateTicks: number;
|
|
elements: Record<string, ElementState>;
|
|
|
|
// Actions
|
|
gatherMana: () => void;
|
|
convertMana: (element: string, amount: number) => void;
|
|
unlockElement: (element: string) => void;
|
|
craftComposite: (target: string) => void;
|
|
|
|
// Computed getters
|
|
getMaxMana: () => number;
|
|
getRegen: () => number;
|
|
getClickMana: () => number;
|
|
getMeditationMultiplier: () => number;
|
|
}
|
|
|
|
export const createManaSlice = (
|
|
set: StateCreator<GameState>['set'],
|
|
get: () => GameState
|
|
): ManaSlice => ({
|
|
rawMana: 10,
|
|
totalManaGathered: 0,
|
|
meditateTicks: 0,
|
|
elements: (() => {
|
|
const elems: Record<string, ElementState> = {};
|
|
const pu = get().prestigeUpgrades;
|
|
const elemMax = computeElementMax(get());
|
|
|
|
Object.keys(ELEMENTS).forEach((k) => {
|
|
const isUnlocked = BASE_UNLOCKED_ELEMENTS.includes(k);
|
|
let startAmount = 0;
|
|
|
|
if (isUnlocked && pu.elemStart) {
|
|
startAmount = pu.elemStart * 5;
|
|
}
|
|
|
|
elems[k] = {
|
|
current: startAmount,
|
|
max: elemMax,
|
|
unlocked: isUnlocked,
|
|
};
|
|
});
|
|
return elems;
|
|
})(),
|
|
|
|
gatherMana: () => {
|
|
const state = get();
|
|
let cm = computeClickMana(state);
|
|
|
|
// Mana overflow bonus
|
|
const overflowBonus = 1 + (state.skills.manaOverflow || 0) * 0.25;
|
|
cm = Math.floor(cm * overflowBonus);
|
|
|
|
const effects = computeEffects(state.skillUpgrades || {}, state.skillTiers || {});
|
|
const max = computeMaxMana(state, effects);
|
|
|
|
// Mana Echo: 10% chance to gain double mana from clicks
|
|
const hasManaEcho = effects.specials?.has('MANA_ECHO') ?? false;
|
|
if (hasManaEcho && Math.random() < 0.1) {
|
|
cm *= 2;
|
|
}
|
|
|
|
set({
|
|
rawMana: Math.min(state.rawMana + cm, max),
|
|
totalManaGathered: state.totalManaGathered + cm,
|
|
});
|
|
},
|
|
|
|
convertMana: (element: string, amount: number = 1) => {
|
|
const state = get();
|
|
const e = state.elements[element];
|
|
if (!e?.unlocked) return;
|
|
|
|
const cost = MANA_PER_ELEMENT * amount;
|
|
if (state.rawMana < cost) return;
|
|
if (e.current >= e.max) return;
|
|
|
|
const canConvert = Math.min(
|
|
amount,
|
|
Math.floor(state.rawMana / MANA_PER_ELEMENT),
|
|
e.max - e.current
|
|
);
|
|
|
|
set({
|
|
rawMana: state.rawMana - canConvert * MANA_PER_ELEMENT,
|
|
elements: {
|
|
...state.elements,
|
|
[element]: { ...e, current: e.current + canConvert },
|
|
},
|
|
});
|
|
},
|
|
|
|
unlockElement: (element: string) => {
|
|
const state = get();
|
|
if (state.elements[element]?.unlocked) return;
|
|
|
|
const cost = 500;
|
|
if (state.rawMana < cost) return;
|
|
|
|
set({
|
|
rawMana: state.rawMana - cost,
|
|
elements: {
|
|
...state.elements,
|
|
[element]: { ...state.elements[element], unlocked: true },
|
|
},
|
|
});
|
|
},
|
|
|
|
craftComposite: (target: string) => {
|
|
const state = get();
|
|
const edef = ELEMENTS[target];
|
|
if (!edef?.recipe) return;
|
|
|
|
const recipe = edef.recipe;
|
|
const costs: Record<string, number> = {};
|
|
recipe.forEach((r) => {
|
|
costs[r] = (costs[r] || 0) + 1;
|
|
});
|
|
|
|
// Check ingredients
|
|
for (const [r, amt] of Object.entries(costs)) {
|
|
if ((state.elements[r]?.current || 0) < amt) return;
|
|
}
|
|
|
|
const newElems = { ...state.elements };
|
|
for (const [r, amt] of Object.entries(costs)) {
|
|
newElems[r] = { ...newElems[r], current: newElems[r].current - amt };
|
|
}
|
|
|
|
// Elemental crafting bonus
|
|
const craftBonus = 1 + (state.skills.elemCrafting || 0) * 0.25;
|
|
const outputAmount = Math.floor(craftBonus);
|
|
|
|
const effects = computeEffects(state.skillUpgrades || {}, state.skillTiers || {});
|
|
const elemMax = computeElementMax(state, effects);
|
|
newElems[target] = {
|
|
...(newElems[target] || { current: 0, max: elemMax, unlocked: false }),
|
|
current: (newElems[target]?.current || 0) + outputAmount,
|
|
max: elemMax,
|
|
unlocked: true,
|
|
};
|
|
|
|
set({
|
|
elements: newElems,
|
|
});
|
|
},
|
|
|
|
getMaxMana: () => computeMaxMana(get()),
|
|
getRegen: () => {
|
|
const state = get();
|
|
const effects = computeEffects(state.skillUpgrades || {}, state.skillTiers || {});
|
|
// This would need proper regen calculation
|
|
return 2;
|
|
},
|
|
getClickMana: () => computeClickMana(get()),
|
|
getMeditationMultiplier: () => {
|
|
const state = get();
|
|
const effects = computeEffects(state.skillUpgrades || {}, state.skillTiers || {});
|
|
return getMeditationBonus(state.meditateTicks, state.skills, effects.meditationEfficiency);
|
|
},
|
|
});
|
|
|
|
// Helper function to deduct spell cost
|
|
export function deductSpellCost(
|
|
cost: SpellCost,
|
|
rawMana: number,
|
|
elements: Record<string, ElementState>
|
|
): { rawMana: number; elements: Record<string, ElementState> } {
|
|
const newElements = { ...elements };
|
|
|
|
if (cost.type === 'raw') {
|
|
return { rawMana: rawMana - cost.amount, elements: newElements };
|
|
} else if (cost.element && newElements[cost.element]) {
|
|
newElements[cost.element] = {
|
|
...newElements[cost.element],
|
|
current: newElements[cost.element].current - cost.amount,
|
|
};
|
|
return { rawMana, elements: newElements };
|
|
}
|
|
|
|
return { rawMana, elements: newElements };
|
|
}
|
|
|
|
export { canAffordSpellCost };
|