Files
Mana-Loop/src/lib/game/hooks/useGameDerived.ts
T
n8n-gitea 9476e92a4b
Build and Publish Mana Loop Docker Image / build-and-publish (push) Failing after 1m1s
fix: resolve 7 medium-priority bugs from audit #372
- #371: Replace Math.random() with seeded PRNG in getSpireEnemyArmor/Barrier
- #370: Add mana refund when cancelling pact ritual in cancelPactRitual
- #367: Add ENCHANT_MASTERY check for design slot 2 in crafting store
- #364: Fix useGameDerived to read crafting data from useCraftingStore
- #363: Clamp recovery room regen delta to prevent negative mana loss
- #365: Add shield/barrier/healthRegen fields to all procedural guardians
- #362: Refactor enchanting tick pipeline to return writes instead of direct store calls

Extracted procedural guardian generators into guardian-procedural.ts to stay under 400-line limit.

All 1158 tests pass.
2026-06-11 11:37:06 +02:00

276 lines
9.0 KiB
TypeScript

// ─── Derived Stats Hooks ───────────────────────────────────────────────────────
// Custom hooks for computing derived game stats from the store
import { useMemo } from 'react';
import { useGameStore } from '../stores/gameStore';
import { useCraftingStore } from '../stores/craftingStore';
import { useManaStore } from '../stores/manaStore';
import { useCombatStore } from '../stores/combatStore';
import { usePrestigeStore } from '../stores/prestigeStore';
import { useAttunementStore } from '../stores/attunementStore';
import { computeEffects } from '../effects/upgrade-effects';
import {
computeMaxMana,
computeRegen,
computeClickMana,
getMeditationBonus,
getIncursionStrength,
getFloorElement,
calcDamage,
getElementalBonus,
getBoonBonuses,
} from '../utils';
import { computeEquipmentEffects } from '../effects';
import { computePactMultiplier, computePactInsightMultiplier } from '../utils/pact-utils';
import { ELEMENTS, SPELLS_DEF, getStudySpeedMultiplier, getStudyCostMultiplier, HOURS_PER_TICK, TICK_MS } from '../constants';
import { getGuardianForFloor } from '../data/guardian-encounters';
import { hasSpecial, SPECIAL_EFFECTS } from '../effects/special-effects';
import { computeDisciplineEffects } from '../effects/discipline-effects';
/**
* Hook for all mana-related derived stats
*/
export function useManaStats() {
const prestigeUpgrades = usePrestigeStore((s) => s.prestigeUpgrades);
const _rawMana = useManaStore((s) => s.rawMana);
const meditateTicks = useManaStore((s) => s.meditateTicks);
const day = useGameStore((s) => s.day);
const hour = useGameStore((s) => s.hour);
const attunements = useAttunementStore((s) => s.attunements);
const disciplineEffects = useMemo(
() => computeDisciplineEffects(),
[]
);
const upgradeEffects = useMemo(
() => computeEffects({}, {}),
[]
);
const maxMana = useMemo(
() => computeMaxMana({ prestigeUpgrades }, upgradeEffects, disciplineEffects),
[prestigeUpgrades, upgradeEffects, disciplineEffects]
);
const baseRegen = useMemo(
() => computeRegen({ prestigeUpgrades, attunements } as any, upgradeEffects),
[prestigeUpgrades, upgradeEffects, attunements]
);
const clickMana = useMemo(
() => computeClickMana(),
[]
);
const meditationCap = 5.0 + disciplineEffects.meditationCapBonus;
const meditationMultiplier = useMemo(
() => getMeditationBonus(meditateTicks, upgradeEffects.meditationEfficiency, disciplineEffects.meditationCapBonus),
[meditateTicks, upgradeEffects.meditationEfficiency, disciplineEffects.meditationCapBonus]
);
const incursionStrength = useMemo(
() => getIncursionStrength(day, hour),
[day, hour]
);
// Effective regen with incursion penalty
const effectiveRegenWithSpecials = baseRegen * (1 - incursionStrength);
// Mana Cascade bonus
const manaCascadeBonus = hasSpecial(upgradeEffects, SPECIAL_EFFECTS.MANA_CASCADE)
? Math.floor(maxMana / 100) * 0.1
: 0;
// Mana Waterfall bonus
const manaWaterfallBonus = hasSpecial(upgradeEffects, SPECIAL_EFFECTS.MANA_WATERFALL)
? Math.floor(maxMana / 100) * 0.25
: 0;
// Final effective regen
const effectiveRegen = (effectiveRegenWithSpecials + manaCascadeBonus + manaWaterfallBonus) * meditationMultiplier;
return {
upgradeEffects,
maxMana,
baseRegen,
clickMana,
meditationMultiplier,
meditationCap,
incursionStrength,
effectiveRegenWithSpecials,
manaCascadeBonus,
manaWaterfallBonus,
effectiveRegen,
disciplineMaxManaBonus: disciplineEffects.bonuses.maxManaBonus || 0,
hasSteadyStream: hasSpecial(upgradeEffects, SPECIAL_EFFECTS.STEADY_STREAM),
hasManaTorrent: hasSpecial(upgradeEffects, SPECIAL_EFFECTS.MANA_TORRENT),
hasDesperateWells: hasSpecial(upgradeEffects, SPECIAL_EFFECTS.DESPERATE_WELLS),
hasManaEcho: hasSpecial(upgradeEffects, SPECIAL_EFFECTS.MANA_ECHO),
hasManaWaterfall: hasSpecial(upgradeEffects, SPECIAL_EFFECTS.MANA_WATERFALL),
hasFlowSurge: hasSpecial(upgradeEffects, SPECIAL_EFFECTS.FLOW_SURGE),
hasManaOverflow: hasSpecial(upgradeEffects, SPECIAL_EFFECTS.MANA_OVERFLOW),
hasEternalFlow: hasSpecial(upgradeEffects, SPECIAL_EFFECTS.ETERNAL_FLOW),
};
}
/**
* Hook for combat-related derived stats
*/
export function useCombatStats() {
const signedPacts = usePrestigeStore((s) => s.signedPacts);
const currentFloor = useCombatStore((s) => s.currentFloor);
const activeSpell = useCombatStore((s) => s.activeSpell);
const { upgradeEffects } = useManaStats();
const equipmentInstances = useCraftingStore((s) => s.equipmentInstances);
const equippedInstances = useCraftingStore((s) => s.equippedInstances);
const floorElem = useMemo(
() => getFloorElement(currentFloor),
[currentFloor]
);
const floorElemDef = useMemo(
() => ELEMENTS[floorElem],
[floorElem]
);
const isGuardianFloor = useMemo(
() => !!getGuardianForFloor(currentFloor),
[currentFloor]
);
const currentGuardian = useMemo(
() => getGuardianForFloor(currentFloor),
[currentFloor]
);
const activeSpellDef = useMemo(
() => SPELLS_DEF[activeSpell],
[activeSpell]
);
const pactInterferenceMitigation = usePrestigeStore((s) => s.prestigeUpgrades.pactInterferenceMitigation || 0);
const pactMultiplier = useMemo(
() => computePactMultiplier({ signedPacts, pactInterferenceMitigation }),
[signedPacts, pactInterferenceMitigation]
);
const pactInsightMultiplier = useMemo(
() => computePactInsightMultiplier({ signedPacts, pactInterferenceMitigation }),
[signedPacts, pactInterferenceMitigation]
);
// DPS calculation
const dps = useMemo(() => {
if (!activeSpellDef) return 0;
const spellCastSpeed = activeSpellDef.castSpeed || 1;
const quickCastBonus = 1;
const attackSpeedMult = upgradeEffects.attackSpeedMultiplier;
const totalCastSpeed = spellCastSpeed * quickCastBonus * attackSpeedMult;
const damagePerCast = calcDamage({ signedPacts }, activeSpell, floorElem);
const castsPerSecond = totalCastSpeed * HOURS_PER_TICK / (TICK_MS / 1000);
return damagePerCast * castsPerSecond;
}, [activeSpellDef, signedPacts, activeSpell, floorElem, upgradeEffects.attackSpeedMultiplier]);
// Damage breakdown for display
const damageBreakdown = useMemo(() => {
if (!activeSpellDef) return null;
const baseDmg = activeSpellDef.dmg;
const combatTrainBonus = 0;
const arcaneFuryMult = 1;
const elemMasteryMult = 1;
const guardianBaneMult = 1;
const precisionChance = 0;
// Calculate elemental bonus
const elemBonus = getElementalBonus(activeSpellDef.elem, floorElem);
let elemBonusText = '';
if (activeSpellDef.elem !== 'raw' && floorElem) {
if (activeSpellDef.elem === floorElem) {
elemBonusText = '+25% same element';
} else if (elemBonus === 1.5) {
elemBonusText = '+50% super effective';
}
}
return {
base: baseDmg,
combatTrainBonus,
arcaneFuryMult,
elemMasteryMult,
guardianBaneMult,
pactMult: pactMultiplier,
precisionChance,
elemBonus,
elemBonusText,
total: calcDamage({ signedPacts }, activeSpell, floorElem),
};
}, [activeSpellDef, signedPacts, activeSpell, floorElem, isGuardianFloor, pactMultiplier]);
// Crit chance: sum equipment bonus (decimal) + pact boon bonus (percentage points) + upgrade bonus (decimal)
const critChance = useMemo(() => {
const equipEffects = computeEquipmentEffects(equipmentInstances, equippedInstances);
const equipCritBonus = equipEffects.bonuses.critChance || 0;
const boons = getBoonBonuses(signedPacts);
const boonCritChance = boons.critChance / 100; // Convert percentage points to decimal
const upgradeCritBonus = upgradeEffects.critChanceBonus || 0;
return equipCritBonus + boonCritChance + upgradeCritBonus;
}, [equipmentInstances, equippedInstances, signedPacts, upgradeEffects.critChanceBonus]);
// Crit damage: base multiplier (1.5) + pact boon bonus (percentage points)
const critDamage = useMemo(() => {
const boons = getBoonBonuses(signedPacts);
return (upgradeEffects.critDamageMultiplier || 1.5) + boons.critDamage / 100;
}, [signedPacts, upgradeEffects.critDamageMultiplier]);
return {
floorElem,
floorElemDef,
isGuardianFloor,
currentGuardian,
activeSpellDef,
pactMultiplier,
pactInsightMultiplier,
dps,
damageBreakdown,
critChance,
critDamage,
};
}
/**
* Hook for study-related derived stats
*/
export function useStudyStats() {
const studySpeedMult = useMemo(
() => getStudySpeedMultiplier(),
[]
);
const studyCostMult = useMemo(
() => getStudyCostMultiplier(),
[]
);
const upgradeEffects = useMemo(
() => computeEffects(),
[]
);
const effectiveStudySpeedMult = studySpeedMult * upgradeEffects.studySpeedMultiplier;
return {
studySpeedMult,
studyCostMult,
effectiveStudySpeedMult,
hasParallelStudy: hasSpecial(upgradeEffects, SPECIAL_EFFECTS.PARALLEL_STUDY),
};
}