513cab81a3
- Delete src/lib/game/constants/guardians.ts (the old static GUARDIANS constant) - Create src/lib/game/data/guardian-data.ts with BASE_GUARDIANS (same data, new home) - Remove GUARDIANS export from constants/index.ts - Update all 11 files that imported GUARDIANS to use getGuardianForFloor() or BASE_GUARDIANS: - useGameDerived.ts, combat-actions.ts, gameStore.ts, prestigeStore.ts - combat-utils.ts, room-utils.ts, floor-utils.ts, spire-utils.ts - SpireCombatPage.tsx, SpireHeader.tsx - Update 4 test files to use getGuardianForFloor() instead of GUARDIANS constant - guardian-encounters.ts now imports BASE_GUARDIANS from guardian-data.ts - Split room-utils.test.ts (505 lines) into room-utils.test.ts + room-utils-floor-state.test.ts
247 lines
7.9 KiB
TypeScript
Executable File
247 lines
7.9 KiB
TypeScript
Executable File
// ─── Derived Stats Hooks ───────────────────────────────────────────────────────
|
|
// Custom hooks for computing derived game stats from the store
|
|
|
|
import { useMemo } from 'react';
|
|
import { useGameStore } from '../stores/gameStore';
|
|
import { useManaStore } from '../stores/manaStore';
|
|
import { useCombatStore } from '../stores/combatStore';
|
|
import { usePrestigeStore } from '../stores/prestigeStore';
|
|
import { computeEffects } from '../effects/upgrade-effects';
|
|
import {
|
|
computeMaxMana,
|
|
computeRegen,
|
|
computeClickMana,
|
|
getMeditationBonus,
|
|
getIncursionStrength,
|
|
getFloorElement,
|
|
calcDamage,
|
|
getElementalBonus,
|
|
} from '../utils';
|
|
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';
|
|
|
|
/**
|
|
* Hook for all mana-related derived stats
|
|
*/
|
|
export function useManaStats() {
|
|
const skills = useGameStore((s) => s.skills);
|
|
const skillUpgrades = useGameStore((s) => s.skillUpgrades);
|
|
const skillTiers = useGameStore((s) => s.skillTiers);
|
|
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 upgradeEffects = useMemo(
|
|
() => computeEffects(skillUpgrades || {}, skillTiers || {}),
|
|
[skillUpgrades, skillTiers]
|
|
);
|
|
|
|
const maxMana = useMemo(
|
|
() => computeMaxMana({ skills, prestigeUpgrades, skillUpgrades, skillTiers }, upgradeEffects),
|
|
[skills, prestigeUpgrades, skillUpgrades, skillTiers, upgradeEffects]
|
|
);
|
|
|
|
const baseRegen = useMemo(
|
|
() => computeRegen({ skills, prestigeUpgrades, skillUpgrades, skillTiers }, upgradeEffects),
|
|
[skills, prestigeUpgrades, skillUpgrades, skillTiers, upgradeEffects]
|
|
);
|
|
|
|
const clickMana = useMemo(
|
|
() => computeClickMana({ skills }),
|
|
[skills]
|
|
);
|
|
|
|
const meditationMultiplier = useMemo(
|
|
() => getMeditationBonus(meditateTicks, skills, upgradeEffects.meditationEfficiency),
|
|
[meditateTicks, skills, upgradeEffects.meditationEfficiency]
|
|
);
|
|
|
|
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,
|
|
incursionStrength,
|
|
effectiveRegenWithSpecials,
|
|
manaCascadeBonus,
|
|
manaWaterfallBonus,
|
|
effectiveRegen,
|
|
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 skills = useGameStore((s) => s.skills);
|
|
const signedPacts = usePrestigeStore((s) => s.signedPacts);
|
|
const currentFloor = useCombatStore((s) => s.currentFloor);
|
|
const activeSpell = useCombatStore((s) => s.activeSpell);
|
|
const { upgradeEffects } = useManaStats();
|
|
|
|
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 pactMultiplier = useMemo(
|
|
() => computePactMultiplier({ signedPacts }),
|
|
[signedPacts]
|
|
);
|
|
|
|
const pactInsightMultiplier = useMemo(
|
|
() => computePactInsightMultiplier({ signedPacts }),
|
|
[signedPacts]
|
|
);
|
|
|
|
// DPS calculation
|
|
const dps = useMemo(() => {
|
|
if (!activeSpellDef) return 0;
|
|
|
|
const spellCastSpeed = activeSpellDef.castSpeed || 1;
|
|
const quickCastBonus = 1 + (skills.quickCast || 0) * 0.05;
|
|
const attackSpeedMult = upgradeEffects.attackSpeedMultiplier;
|
|
const totalCastSpeed = spellCastSpeed * quickCastBonus * attackSpeedMult;
|
|
|
|
const damagePerCast = calcDamage({ skills, signedPacts }, activeSpell, floorElem);
|
|
const castsPerSecond = totalCastSpeed * HOURS_PER_TICK / (TICK_MS / 1000);
|
|
|
|
return damagePerCast * castsPerSecond;
|
|
}, [activeSpellDef, skills, signedPacts, activeSpell, floorElem, upgradeEffects.attackSpeedMultiplier]);
|
|
|
|
// Damage breakdown for display
|
|
const damageBreakdown = useMemo(() => {
|
|
if (!activeSpellDef) return null;
|
|
|
|
const baseDmg = activeSpellDef.dmg;
|
|
const combatTrainBonus = (skills.combatTrain || 0) * 5;
|
|
const arcaneFuryMult = 1 + (skills.arcaneFury || 0) * 0.1;
|
|
const elemMasteryMult = 1 + (skills.elementalMastery || 0) * 0.15;
|
|
const guardianBaneMult = isGuardianFloor ? (1 + (skills.guardianBane || 0) * 0.2) : 1;
|
|
const precisionChance = (skills.precision || 0) * 0.05;
|
|
|
|
// 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({ skills, signedPacts }, activeSpell, floorElem),
|
|
};
|
|
}, [activeSpellDef, skills, signedPacts, activeSpell, floorElem, isGuardianFloor, pactMultiplier]);
|
|
|
|
return {
|
|
floorElem,
|
|
floorElemDef,
|
|
isGuardianFloor,
|
|
currentGuardian,
|
|
activeSpellDef,
|
|
pactMultiplier,
|
|
pactInsightMultiplier,
|
|
dps,
|
|
damageBreakdown,
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Hook for study-related derived stats
|
|
*/
|
|
export function useStudyStats() {
|
|
const skills = useGameStore((s) => s.skills);
|
|
const skillUpgrades = useGameStore((s) => s.skillUpgrades);
|
|
const skillTiers = useGameStore((s) => s.skillTiers);
|
|
|
|
const studySpeedMult = useMemo(
|
|
() => getStudySpeedMultiplier(skills),
|
|
[skills]
|
|
);
|
|
|
|
const studyCostMult = useMemo(
|
|
() => getStudyCostMultiplier(skills),
|
|
[skills]
|
|
);
|
|
|
|
const upgradeEffects = useMemo(
|
|
() => computeEffects(skillUpgrades || {}, skillTiers || {}),
|
|
[skillUpgrades, skillTiers]
|
|
);
|
|
|
|
const effectiveStudySpeedMult = studySpeedMult * upgradeEffects.studySpeedMultiplier;
|
|
|
|
return {
|
|
studySpeedMult,
|
|
studyCostMult,
|
|
effectiveStudySpeedMult,
|
|
hasParallelStudy: hasSpecial(upgradeEffects, SPECIAL_EFFECTS.PARALLEL_STUDY),
|
|
};
|
|
}
|