refactor: cleanup codebase — remove hydration guards, extract constants, fix bugs
Build and Publish Mana Loop Docker Image / build-and-publish (push) Successful in 1m20s
Build and Publish Mana Loop Docker Image / build-and-publish (push) Successful in 1m20s
This commit is contained in:
+241
-260
@@ -82,277 +82,258 @@ export const useGameStore = create<GameCoordinatorStore>()(
|
||||
|
||||
if (ctx.ui.gameOver || ctx.ui.paused) return;
|
||||
|
||||
// ── Phase 2: Compute — derive all updates ───────────────────────────
|
||||
const writes: TickWrites = { logs: [] };
|
||||
const addLog = (msg: string) => writes.logs.push(msg);
|
||||
|
||||
// Compute equipment and discipline effects
|
||||
const equipmentEffects = computeEquipmentEffects(
|
||||
ctx.crafting.equipmentInstances || {},
|
||||
ctx.crafting.equippedInstances || {}
|
||||
);
|
||||
const disciplineEffects = computeDisciplineEffects();
|
||||
const allSpecials = new Set<string>([
|
||||
...equipmentEffects.specials,
|
||||
...disciplineEffects.specials,
|
||||
]);
|
||||
const effects = { specials: allSpecials } as ComputedEffects;
|
||||
|
||||
const maxMana = computeMaxMana(
|
||||
{ skills: {}, prestigeUpgrades: ctx.prestige.prestigeUpgrades, skillUpgrades: {}, skillTiers: {} },
|
||||
undefined,
|
||||
disciplineEffects,
|
||||
);
|
||||
const baseRegen = computeRegen(
|
||||
{ skills: {}, prestigeUpgrades: ctx.prestige.prestigeUpgrades, skillUpgrades: {}, skillTiers: {}, attunements: {} },
|
||||
undefined,
|
||||
disciplineEffects,
|
||||
);
|
||||
|
||||
// Time progression
|
||||
let hour = ctx.game.hour + HOURS_PER_TICK;
|
||||
let day = ctx.game.day;
|
||||
if (hour >= 24) {
|
||||
hour -= 24;
|
||||
day += 1;
|
||||
}
|
||||
|
||||
// Check for loop end
|
||||
if (day > MAX_DAY) {
|
||||
const insightGained = calcInsight({
|
||||
maxFloorReached: ctx.combat.maxFloorReached,
|
||||
totalManaGathered: ctx.mana.totalManaGathered,
|
||||
signedPacts: ctx.prestige.signedPacts,
|
||||
prestigeUpgrades: ctx.prestige.prestigeUpgrades,
|
||||
skills: {},
|
||||
}, disciplineEffects);
|
||||
|
||||
addLog(`⏰ The loop ends. Gained ${insightGained} Insight.`);
|
||||
writes.ui = { ...(writes.ui || {}), gameOver: true, victory: false };
|
||||
writes.prestige = { ...(writes.prestige || {}), loopInsight: insightGained };
|
||||
writes.game = { day, hour };
|
||||
applyTickWrites(writes, {
|
||||
// Shared setters object — used by every applyTickWrites call below
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const storeSetters = {
|
||||
setGame: set,
|
||||
setUI: (w) => useUIStore.setState(w),
|
||||
setPrestige: (w) => usePrestigeStore.setState(w),
|
||||
setMana: (w) => useManaStore.setState(w),
|
||||
setCombat: (w) => useCombatStore.setState(w),
|
||||
setCrafting: (w) => useCraftingStore.setState(w),
|
||||
setAttunement: (w) => useAttunementStore.setState(w),
|
||||
setDiscipline: (w) => useDisciplineStore.setState(w),
|
||||
addLogs: (msgs) => msgs.forEach((m) => useUIStore.getState().addLog(m)),
|
||||
});
|
||||
return;
|
||||
}
|
||||
setUI: (w: any) => useUIStore.setState(w),
|
||||
setPrestige: (w: any) => usePrestigeStore.setState(w),
|
||||
setMana: (w: any) => useManaStore.setState(w),
|
||||
setCombat: (w: any) => useCombatStore.setState(w),
|
||||
setCrafting: (w: any) => useCraftingStore.setState(w),
|
||||
setAttunement: (w: any) => useAttunementStore.setState(w),
|
||||
setDiscipline: (w: any) => useDisciplineStore.setState(w),
|
||||
addLogs: (msgs: string[]) => msgs.forEach((m) => useUIStore.getState().addLog(m)),
|
||||
};
|
||||
|
||||
// Check for victory
|
||||
if (ctx.combat.maxFloorReached >= 100 && ctx.prestige.signedPacts.includes(100)) {
|
||||
const insightGained = calcInsight({
|
||||
maxFloorReached: ctx.combat.maxFloorReached,
|
||||
totalManaGathered: ctx.mana.totalManaGathered,
|
||||
signedPacts: ctx.prestige.signedPacts,
|
||||
prestigeUpgrades: ctx.prestige.prestigeUpgrades,
|
||||
skills: {},
|
||||
}, disciplineEffects) * 3;
|
||||
// ── Phase 2: Compute — derive all updates ───────────────────────────
|
||||
const writes: TickWrites = { logs: [] };
|
||||
const addLog = (msg: string) => writes.logs.push(msg);
|
||||
|
||||
addLog(`🏆 VICTORY! The Awakened One falls! Gained ${insightGained} Insight!`);
|
||||
writes.ui = { ...(writes.ui || {}), gameOver: true, victory: true };
|
||||
writes.prestige = { ...(writes.prestige || {}), loopInsight: insightGained };
|
||||
applyTickWrites(writes, {
|
||||
setGame: set,
|
||||
setUI: (w) => useUIStore.setState(w),
|
||||
setPrestige: (w) => usePrestigeStore.setState(w),
|
||||
setMana: (w) => useManaStore.setState(w),
|
||||
setCombat: (w) => useCombatStore.setState(w),
|
||||
setCrafting: (w) => useCraftingStore.setState(w),
|
||||
setAttunement: (w) => useAttunementStore.setState(w),
|
||||
setDiscipline: (w) => useDisciplineStore.setState(w),
|
||||
addLogs: (msgs) => msgs.forEach((m) => useUIStore.getState().addLog(m)),
|
||||
});
|
||||
return;
|
||||
}
|
||||
// Compute equipment and discipline effects
|
||||
const equipmentEffects = computeEquipmentEffects(
|
||||
ctx.crafting.equipmentInstances || {},
|
||||
ctx.crafting.equippedInstances || {}
|
||||
);
|
||||
const disciplineEffects = computeDisciplineEffects();
|
||||
const allSpecials = new Set<string>([
|
||||
...equipmentEffects.specials,
|
||||
...disciplineEffects.specials,
|
||||
]);
|
||||
const effects = { specials: allSpecials } as ComputedEffects;
|
||||
|
||||
// Incursion
|
||||
const incursionStrength = getIncursionStrength(day, hour);
|
||||
|
||||
// Meditation bonus tracking
|
||||
let meditateTicks = ctx.mana.meditateTicks;
|
||||
let meditationMultiplier = 1;
|
||||
|
||||
if (ctx.combat.currentAction === 'meditate') {
|
||||
meditateTicks++;
|
||||
meditationMultiplier = getMeditationBonus(meditateTicks, {}, 1);
|
||||
} else {
|
||||
meditateTicks = 0;
|
||||
}
|
||||
|
||||
// Calculate total attunement conversion per tick
|
||||
let totalConversionPerTick = 0;
|
||||
Object.entries(ctx.attunement.attunements).forEach(([id, state]) => {
|
||||
if (!state.active) return;
|
||||
const def = ATTUNEMENTS_DEF[id];
|
||||
if (!def || def.conversionRate <= 0 || !def.primaryManaType) return;
|
||||
const scaledRate = getAttunementConversionRate(id, state.level || 1);
|
||||
totalConversionPerTick += scaledRate * HOURS_PER_TICK;
|
||||
});
|
||||
|
||||
// Calculate effective regen
|
||||
const effectiveRegen = Math.max(0, baseRegen * (1 - incursionStrength) * meditationMultiplier - totalConversionPerTick);
|
||||
|
||||
// Mana regeneration
|
||||
let rawMana = Math.min(ctx.mana.rawMana + effectiveRegen * HOURS_PER_TICK, maxMana);
|
||||
let elements = { ...ctx.mana.elements };
|
||||
|
||||
// Apply attunement conversion
|
||||
Object.entries(ctx.attunement.attunements).forEach(([id, state]) => {
|
||||
if (!state.active) return;
|
||||
const def = ATTUNEMENTS_DEF[id];
|
||||
if (!def || def.conversionRate <= 0 || !def.primaryManaType) return;
|
||||
const scaledRate = getAttunementConversionRate(id, state.level || 1);
|
||||
const conversionThisTick = scaledRate * HOURS_PER_TICK;
|
||||
if (elements[def.primaryManaType]) {
|
||||
elements[def.primaryManaType].current = Math.min(
|
||||
elements[def.primaryManaType].max,
|
||||
elements[def.primaryManaType].current + conversionThisTick
|
||||
);
|
||||
}
|
||||
});
|
||||
let totalManaGathered = ctx.mana.totalManaGathered;
|
||||
|
||||
// Convert action — delegate to manaStore
|
||||
if (ctx.combat.currentAction === 'convert') {
|
||||
const convertResult = useManaStore.getState().processConvertAction(rawMana);
|
||||
if (convertResult) {
|
||||
rawMana = convertResult.rawMana;
|
||||
elements = convertResult.elements;
|
||||
}
|
||||
}
|
||||
|
||||
// Pact ritual progress
|
||||
if (ctx.prestige.pactRitualFloor !== null) {
|
||||
const guardian = getGuardianForFloor(ctx.prestige.pactRitualFloor);
|
||||
if (guardian) {
|
||||
const pactAffinityBonus = 1 - (ctx.prestige.prestigeUpgrades.pactAffinity || 0) * 0.1;
|
||||
const requiredTime = guardian.pactTime * pactAffinityBonus;
|
||||
const newProgress = ctx.prestige.pactRitualProgress + HOURS_PER_TICK;
|
||||
|
||||
if (newProgress >= requiredTime) {
|
||||
addLog(`📜 Pact signed with ${guardian.name}! You have gained their boons.`);
|
||||
writes.prestige = {
|
||||
...(writes.prestige || {}),
|
||||
signedPacts: [...ctx.prestige.signedPacts, ctx.prestige.pactRitualFloor],
|
||||
defeatedGuardians: ctx.prestige.defeatedGuardians.filter(f => f !== ctx.prestige.pactRitualFloor),
|
||||
pactRitualFloor: null,
|
||||
pactRitualProgress: 0,
|
||||
};
|
||||
} else {
|
||||
writes.prestige = {
|
||||
...(writes.prestige || {}),
|
||||
pactRitualProgress: newProgress,
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Discipline tick — process active disciplines (XP accrual + mana drain)
|
||||
const disciplineResult = useDisciplineStore.getState().processTick({
|
||||
rawMana,
|
||||
elements,
|
||||
});
|
||||
rawMana = disciplineResult.rawMana;
|
||||
elements = disciplineResult.elements;
|
||||
|
||||
// Apply per-element regen from discipline effects (regen_{element})
|
||||
for (const [key, value] of Object.entries(disciplineEffects.bonuses)) {
|
||||
if (key.startsWith('regen_') && key !== 'regenBonus') {
|
||||
const element = key.replace('regen_', '');
|
||||
if (elements[element]) {
|
||||
elements[element] = {
|
||||
...elements[element],
|
||||
current: Math.min(
|
||||
elements[element].max,
|
||||
elements[element].current + value * HOURS_PER_TICK,
|
||||
),
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Unlock enchantment effects from newly unlocked discipline perks
|
||||
if (disciplineResult.unlockedEffects.length > 0) {
|
||||
useCraftingStore.getState().unlockEffects(disciplineResult.unlockedEffects);
|
||||
for (const effectId of disciplineResult.unlockedEffects) {
|
||||
addLog(`✨ Discipline insight unlocked: ${effectId}`);
|
||||
}
|
||||
}
|
||||
|
||||
// Combat — delegate to combatStore
|
||||
if (ctx.combat.currentAction === 'climb') {
|
||||
const combatResult = useCombatStore.getState().processCombatTick(
|
||||
rawMana,
|
||||
elements,
|
||||
maxMana,
|
||||
1,
|
||||
(floor, wasGuardian) => {
|
||||
if (wasGuardian) {
|
||||
const defeatedGuardian = getGuardianForFloor(floor);
|
||||
addLog(`\u2694\ufe0f ${defeatedGuardian?.name || 'Guardian'} defeated! Visit the Grimoire to sign a pact.`);
|
||||
} else if (floor % 5 === 0) {
|
||||
addLog(`🏰 Floor ${floor} cleared!`);
|
||||
}
|
||||
},
|
||||
(damage) => {
|
||||
let dmg = damage;
|
||||
if (hasSpecial(effects, SPECIAL_EFFECTS.EXECUTIONER) && ctx.combat.floorHP / ctx.combat.floorMaxHP < 0.25) {
|
||||
dmg *= 2;
|
||||
}
|
||||
if (hasSpecial(effects, SPECIAL_EFFECTS.BERSERKER) && rawMana < maxMana * 0.5) {
|
||||
dmg *= 1.5;
|
||||
}
|
||||
return { rawMana, elements, modifiedDamage: dmg };
|
||||
},
|
||||
ctx.prestige.signedPacts,
|
||||
const maxMana = computeMaxMana(
|
||||
{ skills: {}, prestigeUpgrades: ctx.prestige.prestigeUpgrades, skillUpgrades: {}, skillTiers: {} },
|
||||
undefined,
|
||||
disciplineEffects,
|
||||
);
|
||||
const baseRegen = computeRegen(
|
||||
{ skills: {}, prestigeUpgrades: ctx.prestige.prestigeUpgrades, skillUpgrades: {}, skillTiers: {}, attunements: {} },
|
||||
undefined,
|
||||
disciplineEffects,
|
||||
);
|
||||
|
||||
rawMana = combatResult.rawMana;
|
||||
elements = combatResult.elements;
|
||||
totalManaGathered += combatResult.totalManaGathered || 0;
|
||||
|
||||
if (combatResult.logMessages) {
|
||||
combatResult.logMessages.forEach(msg => addLog(msg));
|
||||
// Time progression
|
||||
let hour = ctx.game.hour + HOURS_PER_TICK;
|
||||
let day = ctx.game.day;
|
||||
if (hour >= 24) {
|
||||
hour -= 24;
|
||||
day += 1;
|
||||
}
|
||||
|
||||
writes.combat = {
|
||||
...(writes.combat || {}),
|
||||
currentFloor: combatResult.currentFloor,
|
||||
floorHP: combatResult.floorHP,
|
||||
floorMaxHP: combatResult.floorMaxHP,
|
||||
maxFloorReached: combatResult.maxFloorReached,
|
||||
castProgress: combatResult.castProgress,
|
||||
equipmentSpellStates: combatResult.equipmentSpellStates,
|
||||
// Shared insight params — reused for both loop-end and victory
|
||||
const insightParams = {
|
||||
maxFloorReached: ctx.combat.maxFloorReached,
|
||||
totalManaGathered: ctx.mana.totalManaGathered,
|
||||
signedPacts: ctx.prestige.signedPacts,
|
||||
prestigeUpgrades: ctx.prestige.prestigeUpgrades,
|
||||
skills: {} as Record<string, number>,
|
||||
};
|
||||
}
|
||||
|
||||
// ── Phase 3: Write — batch all state updates ─────────────────────────
|
||||
writes.game = { day, hour, incursionStrength };
|
||||
writes.mana = {
|
||||
rawMana,
|
||||
meditateTicks,
|
||||
totalManaGathered,
|
||||
elements,
|
||||
};
|
||||
// Check for loop end
|
||||
if (day > MAX_DAY) {
|
||||
const insightGained = calcInsight(insightParams, disciplineEffects);
|
||||
|
||||
applyTickWrites(writes, {
|
||||
setGame: set,
|
||||
setUI: (w) => useUIStore.setState(w),
|
||||
setPrestige: (w) => usePrestigeStore.setState(w),
|
||||
setMana: (w) => useManaStore.setState(w),
|
||||
setCombat: (w) => useCombatStore.setState(w),
|
||||
setCrafting: (w) => useCraftingStore.setState(w),
|
||||
setAttunement: (w) => useAttunementStore.setState(w),
|
||||
setDiscipline: (w) => useDisciplineStore.setState(w),
|
||||
addLogs: (msgs) => msgs.forEach((m) => useUIStore.getState().addLog(m)),
|
||||
});
|
||||
addLog(`⏰ The loop ends. Gained ${insightGained} Insight.`);
|
||||
writes.ui = { ...(writes.ui || {}), gameOver: true, victory: false };
|
||||
writes.prestige = { ...(writes.prestige || {}), loopInsight: insightGained };
|
||||
writes.game = { day, hour };
|
||||
applyTickWrites(writes, storeSetters);
|
||||
return;
|
||||
}
|
||||
|
||||
// Check for victory (3× insight multiplier)
|
||||
if (ctx.combat.maxFloorReached >= 100 && ctx.prestige.signedPacts.includes(100)) {
|
||||
const insightGained = calcInsight(insightParams, disciplineEffects) * 3;
|
||||
|
||||
addLog(`🏆 VICTORY! The Awakened One falls! Gained ${insightGained} Insight!`);
|
||||
writes.ui = { ...(writes.ui || {}), gameOver: true, victory: true };
|
||||
writes.prestige = { ...(writes.prestige || {}), loopInsight: insightGained };
|
||||
applyTickWrites(writes, storeSetters);
|
||||
return;
|
||||
}
|
||||
|
||||
// Incursion
|
||||
const incursionStrength = getIncursionStrength(day, hour);
|
||||
|
||||
// Meditation bonus tracking
|
||||
let meditateTicks = ctx.mana.meditateTicks;
|
||||
let meditationMultiplier = 1;
|
||||
|
||||
if (ctx.combat.currentAction === 'meditate') {
|
||||
meditateTicks++;
|
||||
meditationMultiplier = getMeditationBonus(meditateTicks, {}, 1);
|
||||
} else {
|
||||
meditateTicks = 0;
|
||||
}
|
||||
|
||||
// Calculate total attunement conversion per tick
|
||||
let totalConversionPerTick = 0;
|
||||
Object.entries(ctx.attunement.attunements).forEach(([id, state]) => {
|
||||
if (!state.active) return;
|
||||
const def = ATTUNEMENTS_DEF[id];
|
||||
if (!def || def.conversionRate <= 0 || !def.primaryManaType) return;
|
||||
const scaledRate = getAttunementConversionRate(id, state.level || 1);
|
||||
totalConversionPerTick += scaledRate * HOURS_PER_TICK;
|
||||
});
|
||||
|
||||
// Calculate effective regen
|
||||
const effectiveRegen = Math.max(0, baseRegen * (1 - incursionStrength) * meditationMultiplier - totalConversionPerTick);
|
||||
|
||||
// Mana regeneration
|
||||
let rawMana = Math.min(ctx.mana.rawMana + effectiveRegen * HOURS_PER_TICK, maxMana);
|
||||
let elements = { ...ctx.mana.elements };
|
||||
|
||||
// Apply attunement conversion
|
||||
Object.entries(ctx.attunement.attunements).forEach(([id, state]) => {
|
||||
if (!state.active) return;
|
||||
const def = ATTUNEMENTS_DEF[id];
|
||||
if (!def || def.conversionRate <= 0 || !def.primaryManaType) return;
|
||||
const scaledRate = getAttunementConversionRate(id, state.level || 1);
|
||||
const conversionThisTick = scaledRate * HOURS_PER_TICK;
|
||||
if (elements[def.primaryManaType]) {
|
||||
elements[def.primaryManaType].current = Math.min(
|
||||
elements[def.primaryManaType].max,
|
||||
elements[def.primaryManaType].current + conversionThisTick
|
||||
);
|
||||
}
|
||||
});
|
||||
let totalManaGathered = ctx.mana.totalManaGathered;
|
||||
|
||||
// Convert action — delegate to manaStore
|
||||
if (ctx.combat.currentAction === 'convert') {
|
||||
const convertResult = useManaStore.getState().processConvertAction(rawMana);
|
||||
if (convertResult) {
|
||||
rawMana = convertResult.rawMana;
|
||||
elements = convertResult.elements;
|
||||
}
|
||||
}
|
||||
|
||||
// Pact ritual progress
|
||||
if (ctx.prestige.pactRitualFloor !== null) {
|
||||
const guardian = getGuardianForFloor(ctx.prestige.pactRitualFloor);
|
||||
if (guardian) {
|
||||
const pactAffinityBonus = 1 - (ctx.prestige.prestigeUpgrades.pactAffinity || 0) * 0.1;
|
||||
const requiredTime = guardian.pactTime * pactAffinityBonus;
|
||||
const newProgress = ctx.prestige.pactRitualProgress + HOURS_PER_TICK;
|
||||
|
||||
if (newProgress >= requiredTime) {
|
||||
addLog(`📜 Pact signed with ${guardian.name}! You have gained their boons.`);
|
||||
writes.prestige = {
|
||||
...(writes.prestige || {}),
|
||||
signedPacts: [...ctx.prestige.signedPacts, ctx.prestige.pactRitualFloor],
|
||||
defeatedGuardians: ctx.prestige.defeatedGuardians.filter(f => f !== ctx.prestige.pactRitualFloor),
|
||||
pactRitualFloor: null,
|
||||
pactRitualProgress: 0,
|
||||
};
|
||||
} else {
|
||||
writes.prestige = {
|
||||
...(writes.prestige || {}),
|
||||
pactRitualProgress: newProgress,
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Discipline tick — process active disciplines (XP accrual + mana drain)
|
||||
const disciplineResult = useDisciplineStore.getState().processTick({
|
||||
rawMana,
|
||||
elements,
|
||||
});
|
||||
rawMana = disciplineResult.rawMana;
|
||||
elements = disciplineResult.elements;
|
||||
|
||||
// Apply per-element regen from discipline effects (regen_{element})
|
||||
for (const [key, value] of Object.entries(disciplineEffects.bonuses)) {
|
||||
if (key.startsWith('regen_') && key !== 'regenBonus') {
|
||||
const element = key.replace('regen_', '');
|
||||
if (elements[element]) {
|
||||
elements[element] = {
|
||||
...elements[element],
|
||||
current: Math.min(
|
||||
elements[element].max,
|
||||
elements[element].current + value * HOURS_PER_TICK,
|
||||
),
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Unlock enchantment effects from newly unlocked discipline perks
|
||||
if (disciplineResult.unlockedEffects.length > 0) {
|
||||
useCraftingStore.getState().unlockEffects(disciplineResult.unlockedEffects);
|
||||
for (const effectId of disciplineResult.unlockedEffects) {
|
||||
addLog(`✨ Discipline insight unlocked: ${effectId}`);
|
||||
}
|
||||
}
|
||||
|
||||
// Combat — delegate to combatStore
|
||||
if (ctx.combat.currentAction === 'climb') {
|
||||
const combatResult = useCombatStore.getState().processCombatTick(
|
||||
rawMana,
|
||||
elements,
|
||||
maxMana,
|
||||
1,
|
||||
(floor, wasGuardian) => {
|
||||
if (wasGuardian) {
|
||||
const defeatedGuardian = getGuardianForFloor(floor);
|
||||
addLog(`\u2694\ufe0f ${defeatedGuardian?.name || 'Guardian'} defeated! Visit the Grimoire to sign a pact.`);
|
||||
} else if (floor % 5 === 0) {
|
||||
addLog(`🏰 Floor ${floor} cleared!`);
|
||||
}
|
||||
},
|
||||
(damage) => {
|
||||
let dmg = damage;
|
||||
if (hasSpecial(effects, SPECIAL_EFFECTS.EXECUTIONER) && ctx.combat.floorHP / ctx.combat.floorMaxHP < 0.25) {
|
||||
dmg *= 2;
|
||||
}
|
||||
if (hasSpecial(effects, SPECIAL_EFFECTS.BERSERKER) && rawMana < maxMana * 0.5) {
|
||||
dmg *= 1.5;
|
||||
}
|
||||
return { rawMana, elements, modifiedDamage: dmg };
|
||||
},
|
||||
ctx.prestige.signedPacts,
|
||||
);
|
||||
|
||||
rawMana = combatResult.rawMana;
|
||||
elements = combatResult.elements;
|
||||
totalManaGathered += combatResult.totalManaGathered || 0;
|
||||
|
||||
if (combatResult.logMessages) {
|
||||
combatResult.logMessages.forEach(msg => addLog(msg));
|
||||
}
|
||||
|
||||
writes.combat = {
|
||||
...(writes.combat || {}),
|
||||
currentFloor: combatResult.currentFloor,
|
||||
floorHP: combatResult.floorHP,
|
||||
floorMaxHP: combatResult.floorMaxHP,
|
||||
maxFloorReached: combatResult.maxFloorReached,
|
||||
castProgress: combatResult.castProgress,
|
||||
equipmentSpellStates: combatResult.equipmentSpellStates,
|
||||
};
|
||||
}
|
||||
|
||||
// ── Phase 3: Write — batch all state updates ─────────────────────────
|
||||
writes.game = { day, hour, incursionStrength };
|
||||
writes.mana = {
|
||||
rawMana,
|
||||
meditateTicks,
|
||||
totalManaGathered,
|
||||
elements,
|
||||
};
|
||||
|
||||
applyTickWrites(writes, storeSetters);
|
||||
} catch (error: unknown) {
|
||||
// Log error to UI store if available, otherwise console error
|
||||
try {
|
||||
|
||||
@@ -86,7 +86,6 @@ export const useManaStore = create<ManaStore>()(
|
||||
spendRawMana: (amount: number) => {
|
||||
const state = get();
|
||||
if (state.rawMana < amount) return false;
|
||||
|
||||
set({ rawMana: state.rawMana - amount });
|
||||
return true;
|
||||
},
|
||||
@@ -98,71 +97,35 @@ export const useManaStore = create<ManaStore>()(
|
||||
}));
|
||||
},
|
||||
|
||||
setMeditateTicks: (ticks: number) => {
|
||||
set({ meditateTicks: ticks });
|
||||
},
|
||||
|
||||
incrementMeditateTicks: () => {
|
||||
set((state) => ({ meditateTicks: state.meditateTicks + 1 }));
|
||||
},
|
||||
|
||||
resetMeditateTicks: () => {
|
||||
set({ meditateTicks: 0 });
|
||||
},
|
||||
setMeditateTicks: (ticks: number) => set({ meditateTicks: ticks }),
|
||||
incrementMeditateTicks: () => set((s) => ({ meditateTicks: s.meditateTicks + 1 })),
|
||||
resetMeditateTicks: () => set({ meditateTicks: 0 }),
|
||||
|
||||
convertMana: (element: string, amount: number) => {
|
||||
const state = get();
|
||||
const elem = state.elements[element];
|
||||
if (!elem?.unlocked) {
|
||||
return fail(ErrorCode.ELEMENT_NOT_UNLOCKED, `Element ${element} is not unlocked`);
|
||||
}
|
||||
if (!elem?.unlocked) return fail(ErrorCode.ELEMENT_NOT_UNLOCKED, `Element ${element} is not unlocked`);
|
||||
|
||||
const cost = MANA_PER_ELEMENT * amount;
|
||||
if (state.rawMana < cost) {
|
||||
return fail(ErrorCode.INSUFFICIENT_MANA, `Need ${cost} raw mana, have ${state.rawMana}`);
|
||||
}
|
||||
if (elem.current >= elem.max) {
|
||||
return fail(ErrorCode.ELEMENT_MAX_CAPACITY, `Element ${element} is at max capacity`);
|
||||
}
|
||||
if (state.rawMana < cost) return fail(ErrorCode.INSUFFICIENT_MANA, `Need ${cost} raw mana, have ${state.rawMana}`);
|
||||
if (elem.current >= elem.max) return fail(ErrorCode.ELEMENT_MAX_CAPACITY, `Element ${element} is at max capacity`);
|
||||
|
||||
const canConvert = Math.min(
|
||||
amount,
|
||||
Math.floor(state.rawMana / MANA_PER_ELEMENT),
|
||||
elem.max - elem.current
|
||||
);
|
||||
|
||||
if (canConvert <= 0) {
|
||||
return fail(ErrorCode.INVALID_INPUT, 'Cannot convert 0 or negative amount');
|
||||
}
|
||||
const canConvert = Math.min(amount, Math.floor(state.rawMana / MANA_PER_ELEMENT), elem.max - elem.current);
|
||||
if (canConvert <= 0) return fail(ErrorCode.INVALID_INPUT, 'Cannot convert 0 or negative amount');
|
||||
|
||||
set({
|
||||
rawMana: state.rawMana - canConvert * MANA_PER_ELEMENT,
|
||||
elements: {
|
||||
...state.elements,
|
||||
[element]: { ...elem, current: elem.current + canConvert },
|
||||
},
|
||||
elements: { ...state.elements, [element]: { ...elem, current: elem.current + canConvert } },
|
||||
});
|
||||
|
||||
return ok({ converted: canConvert });
|
||||
},
|
||||
|
||||
unlockElement: (element: string, cost: number) => {
|
||||
const state = get();
|
||||
if (state.elements[element]?.unlocked) {
|
||||
return fail(ErrorCode.INVALID_INPUT, `Element ${element} is already unlocked`);
|
||||
}
|
||||
if (state.rawMana < cost) {
|
||||
return fail(ErrorCode.INSUFFICIENT_MANA, `Need ${cost} raw mana, have ${state.rawMana}`);
|
||||
}
|
||||
|
||||
set({
|
||||
rawMana: state.rawMana - cost,
|
||||
elements: {
|
||||
...state.elements,
|
||||
[element]: { ...state.elements[element], unlocked: true },
|
||||
},
|
||||
});
|
||||
if (state.elements[element]?.unlocked) return fail(ErrorCode.INVALID_INPUT, `Element ${element} is already unlocked`);
|
||||
if (state.rawMana < cost) return fail(ErrorCode.INSUFFICIENT_MANA, `Need ${cost} raw mana, have ${state.rawMana}`);
|
||||
|
||||
set({ rawMana: state.rawMana - cost, elements: { ...state.elements, [element]: { ...state.elements[element], unlocked: true } } });
|
||||
return okVoid();
|
||||
},
|
||||
|
||||
@@ -170,15 +133,8 @@ export const useManaStore = create<ManaStore>()(
|
||||
set((state) => {
|
||||
const elem = state.elements[element];
|
||||
if (!elem) return state;
|
||||
|
||||
return {
|
||||
elements: {
|
||||
...state.elements,
|
||||
[element]: {
|
||||
...elem,
|
||||
current: Math.min(elem.current + amount, max),
|
||||
},
|
||||
},
|
||||
elements: { ...state.elements, [element]: { ...elem, current: Math.min(elem.current + amount, max) } },
|
||||
};
|
||||
});
|
||||
},
|
||||
@@ -186,64 +142,35 @@ export const useManaStore = create<ManaStore>()(
|
||||
spendElementMana: (element: string, amount: number) => {
|
||||
const state = get();
|
||||
const elem = state.elements[element];
|
||||
if (!elem) {
|
||||
return fail(ErrorCode.INVALID_ELEMENT, `Element ${element} does not exist`);
|
||||
}
|
||||
if (elem.current < amount) {
|
||||
return fail(ErrorCode.INSUFFICIENT_MANA, `Need ${amount} ${element} mana, have ${elem.current}`);
|
||||
}
|
||||
|
||||
set({
|
||||
elements: {
|
||||
...state.elements,
|
||||
[element]: { ...elem, current: elem.current - amount },
|
||||
},
|
||||
});
|
||||
if (!elem) return fail(ErrorCode.INVALID_ELEMENT, `Element ${element} does not exist`);
|
||||
if (elem.current < amount) return fail(ErrorCode.INSUFFICIENT_MANA, `Need ${amount} ${element} mana, have ${elem.current}`);
|
||||
|
||||
set({ elements: { ...state.elements, [element]: { ...elem, current: elem.current - amount } } });
|
||||
return okVoid();
|
||||
},
|
||||
|
||||
setElementMax: (max: number) => {
|
||||
set((state) => ({
|
||||
elements: Object.fromEntries(
|
||||
Object.entries(state.elements).map(([k, v]) => [k, { ...v, max }])
|
||||
) as Record<string, ElementState>,
|
||||
elements: Object.fromEntries(Object.entries(state.elements).map(([k, v]) => [k, { ...v, max }])) as Record<string, ElementState>,
|
||||
}));
|
||||
},
|
||||
|
||||
craftComposite: (target: string, recipe: string[]) => {
|
||||
const state = get();
|
||||
|
||||
// Count required ingredients
|
||||
const costs: Record<string, number> = {};
|
||||
recipe.forEach(r => {
|
||||
costs[r] = (costs[r] || 0) + 1;
|
||||
});
|
||||
recipe.forEach(r => { costs[r] = (costs[r] || 0) + 1; });
|
||||
|
||||
// Check if we have all ingredients
|
||||
for (const [r, amt] of Object.entries(costs)) {
|
||||
if ((state.elements[r]?.current || 0) < amt) {
|
||||
return fail(ErrorCode.INSUFFICIENT_MANA, `Need ${amt} ${r} mana, have ${state.elements[r]?.current || 0}`);
|
||||
}
|
||||
if ((state.elements[r]?.current || 0) < amt) return fail(ErrorCode.INSUFFICIENT_MANA, `Need ${amt} ${r} mana, have ${state.elements[r]?.current || 0}`);
|
||||
}
|
||||
|
||||
// Deduct ingredients
|
||||
const newElems = { ...state.elements };
|
||||
for (const [r, amt] of Object.entries(costs)) {
|
||||
newElems[r] = {
|
||||
...newElems[r],
|
||||
current: newElems[r].current - amt,
|
||||
};
|
||||
newElems[r] = { ...newElems[r], current: newElems[r].current - amt };
|
||||
}
|
||||
|
||||
// Add crafted element
|
||||
const targetElem = newElems[target];
|
||||
newElems[target] = {
|
||||
...(targetElem || { current: 0, max: 10, unlocked: false }),
|
||||
current: (targetElem?.current || 0) + 1,
|
||||
unlocked: true,
|
||||
};
|
||||
|
||||
newElems[target] = { ...(targetElem || { current: 0, max: 10, unlocked: false }), current: (targetElem?.current || 0) + 1, unlocked: true };
|
||||
set({ elements: newElems });
|
||||
return okVoid();
|
||||
},
|
||||
@@ -252,27 +179,16 @@ export const useManaStore = create<ManaStore>()(
|
||||
const state = get();
|
||||
const elements = { ...state.elements };
|
||||
|
||||
const unlockedElements = Object.entries(elements)
|
||||
.filter(([, e]) => e.unlocked && e.current < e.max);
|
||||
|
||||
if (unlockedElements.length === 0 || rawMana < 100) return null;
|
||||
const unlockedElements = Object.entries(elements).filter(([, e]) => e.unlocked && e.current < e.max);
|
||||
if (unlockedElements.length === 0 || rawMana < MANA_PER_ELEMENT) return null;
|
||||
|
||||
unlockedElements.sort((a, b) => (b[1].max - b[1].current) - (a[1].max - a[1].current));
|
||||
const [targetId, targetState] = unlockedElements[0];
|
||||
const canConvert = Math.min(
|
||||
Math.floor(rawMana / 100),
|
||||
targetState.max - targetState.current
|
||||
);
|
||||
|
||||
const canConvert = Math.min(Math.floor(rawMana / MANA_PER_ELEMENT), targetState.max - targetState.current);
|
||||
if (canConvert <= 0) return null;
|
||||
|
||||
rawMana -= canConvert * 100;
|
||||
const updatedElements = {
|
||||
...elements,
|
||||
[targetId]: { ...targetState, current: targetState.current + canConvert }
|
||||
};
|
||||
|
||||
return { rawMana, elements: updatedElements };
|
||||
rawMana -= canConvert * MANA_PER_ELEMENT;
|
||||
return { rawMana, elements: { ...elements, [targetId]: { ...targetState, current: targetState.current + canConvert } } };
|
||||
},
|
||||
|
||||
resetMana: (
|
||||
@@ -283,24 +199,13 @@ export const useManaStore = create<ManaStore>()(
|
||||
) => {
|
||||
const elementMax = 10 + (prestigeUpgrades.elemMax || 0) * 5;
|
||||
const startingMana = 10 + (prestigeUpgrades.manaStart || 0) * 10;
|
||||
const elements = makeInitialElements(elementMax, prestigeUpgrades);
|
||||
|
||||
set({
|
||||
rawMana: startingMana,
|
||||
meditateTicks: 0,
|
||||
totalManaGathered: 0,
|
||||
elements,
|
||||
});
|
||||
set({ rawMana: startingMana, meditateTicks: 0, totalManaGathered: 0, elements: makeInitialElements(elementMax, prestigeUpgrades) });
|
||||
},
|
||||
}),
|
||||
{
|
||||
storage: createSafeStorage(),
|
||||
name: 'mana-loop-mana',
|
||||
partialize: (state) => ({
|
||||
rawMana: state.rawMana,
|
||||
totalManaGathered: state.totalManaGathered,
|
||||
elements: state.elements,
|
||||
}),
|
||||
partialize: (state) => ({ rawMana: state.rawMana, totalManaGathered: state.totalManaGathered, elements: state.elements }),
|
||||
}
|
||||
)
|
||||
);
|
||||
@@ -311,16 +216,10 @@ export function makeInitialElements(
|
||||
prestigeUpgrades: Record<string, number> = {}
|
||||
): Record<string, ElementState> {
|
||||
const elemStart = (prestigeUpgrades.elemStart || 0) * 5;
|
||||
|
||||
const elements: Record<string, ElementState> = {};
|
||||
Object.keys(ELEMENTS).forEach(k => {
|
||||
for (const k of Object.keys(ELEMENTS)) {
|
||||
const isUnlocked = BASE_UNLOCKED_ELEMENTS.includes(k);
|
||||
elements[k] = {
|
||||
current: isUnlocked ? elemStart : 0,
|
||||
max: elementMax,
|
||||
unlocked: isUnlocked,
|
||||
};
|
||||
});
|
||||
|
||||
elements[k] = { current: isUnlocked ? elemStart : 0, max: elementMax, unlocked: isUnlocked };
|
||||
}
|
||||
return elements;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user