diff --git a/docs/circular-deps.txt b/docs/circular-deps.txt index 6ce663a..a4c81c3 100644 --- a/docs/circular-deps.txt +++ b/docs/circular-deps.txt @@ -1,10 +1,11 @@ # Circular Dependencies -Generated: 2026-05-20T16:38:29.616Z -Found: 3 circular chain(s) — these MUST be fixed before modifying involved files. +Generated: 2026-05-20T17:48:45.265Z +Found: 4 circular chain(s) — these MUST be fixed before modifying involved files. -1. Processed 125 files (1.4s) (3 warnings) +1. Processed 126 files (1.4s) (3 warnings) 2. 1) stores/gameStore.ts > stores/gameActions.ts 3. 2) stores/gameStore.ts > stores/gameLoopActions.ts +4. 3) stores/gameStore.ts > stores/tick-pipeline.ts ## How to fix 1. Identify which import in the chain can be extracted to a shared types/utils file. diff --git a/docs/dependency-graph.json b/docs/dependency-graph.json index 49ae9a1..2d86217 100644 --- a/docs/dependency-graph.json +++ b/docs/dependency-graph.json @@ -1,6 +1,6 @@ { "_meta": { - "generated": "2026-05-20T16:38:28.025Z", + "generated": "2026-05-20T17:48:43.703Z", "description": "Import dependency graph for src/lib/game. Keys are files, values are arrays of files they import.", "usage": "To find what a file affects, search for its path in the VALUES. To find what a file depends on, look at its KEY entry." }, @@ -120,7 +120,7 @@ ], "crafting-actions/preparation-actions.ts": [ "crafting-prep.ts", - "types.ts" + "stores/craftingStore.types.ts" ], "crafting-apply.ts": [ "crafting-utils.ts", @@ -403,8 +403,6 @@ "constants.ts", "effects/discipline-effects.ts", "stores/combat-state.types.ts", - "stores/discipline-slice.ts", - "stores/prestigeStore.ts", "types.ts", "utils/index.ts" ], @@ -493,6 +491,7 @@ "stores/gameLoopActions.ts", "stores/manaStore.ts", "stores/prestigeStore.ts", + "stores/tick-pipeline.ts", "stores/uiStore.ts", "utils/index.ts" ], @@ -519,6 +518,16 @@ "constants.ts", "types.ts" ], + "stores/tick-pipeline.ts": [ + "stores/attunementStore.ts", + "stores/combat-state.types.ts", + "stores/craftingStore.types.ts", + "stores/discipline-slice.ts", + "stores/gameStore.ts", + "stores/manaStore.ts", + "stores/prestigeStore.ts", + "stores/uiStore.ts" + ], "stores/uiStore.ts": [], "types.ts": [ "data/equipment/types.ts", diff --git a/src/components/game/tabs/DisciplinesTab.tsx b/src/components/game/tabs/DisciplinesTab.tsx index 347491e..efebf56 100644 --- a/src/components/game/tabs/DisciplinesTab.tsx +++ b/src/components/game/tabs/DisciplinesTab.tsx @@ -8,6 +8,8 @@ import { invokerDisciplines } from '@/lib/game/data/disciplines/invoker'; import { calculateStatBonus, calculateManaDrain } from '@/lib/game/utils/discipline-math'; import clsx from 'clsx'; +// ─── Attunement Tabs ───────────────────────────────────────────────────────── + interface AttunementTab { key: string; label: string; @@ -21,7 +23,9 @@ const ATTUNEMENT_TABS: AttunementTab[] = [ { key: 'invoker', label: 'Invoker', items: invokerDisciplines }, ]; -interface DisciplineCardProps { +// ─── Discipline Card Props (split from monolithic 15-field interface) ──────── + +export interface DisciplineCardDefinition { id: string; name: string; description: string; @@ -33,32 +37,36 @@ interface DisciplineCardProps { drainBase: number; difficultyFactor: number; scalingFactor: number; +} + +export interface DisciplineCardRuntime { xp: number; paused: boolean; concurrentLimit: number; +} + +export interface DisciplineCardCallbacks { onToggle: (id: string, paused: boolean) => void; } -const DisciplineCard: React.FC = ({ - id, - name, - description, - perkThresholds, - perkValues, - perkTypes, - statBonus, - baseValue, - drainBase, - difficultyFactor, - scalingFactor, - xp, - paused, - concurrentLimit, - onToggle, -}) => { +interface DisciplineCardProps { + definition: DisciplineCardDefinition; + runtime: DisciplineCardRuntime; + callbacks: DisciplineCardCallbacks; +} + +// ─── Discipline Card Component ─────────────────────────────────────────────── + +const DisciplineCard: React.FC = ({ definition, runtime, callbacks }) => { + const { + id, name, description, perkThresholds, perkValues, perkTypes, + statBonus, baseValue, drainBase, difficultyFactor, scalingFactor, + } = definition; + const { xp, paused: isPaused, concurrentLimit } = runtime; + const { onToggle } = callbacks; + const displayXp = xp; const progressPercent = Math.min(displayXp / Math.max(1, concurrentLimit * 100), 100); - const isPaused = paused; const activeStatBonus = calculateStatBonus(baseValue, displayXp, scalingFactor); const estimatedDrain = calculateManaDrain(drainBase, displayXp, difficultyFactor); @@ -134,6 +142,8 @@ const DisciplineCard: React.FC = ({ ); }; +// ─── Disciplines Tab ───────────────────────────────────────────────────────── + export const DisciplinesTab: React.FC = () => { const activeIds = useDisciplineStore((s) => s.activeIds); const concurrentLimit = useDisciplineStore((s) => s.concurrentLimit); @@ -194,21 +204,27 @@ export const DisciplinesTab: React.FC = () => { return ( p.threshold)} - perkValues={disc.perks?.map((p) => p.value)} - perkTypes={disc.perks?.map((p) => p.type)} - statBonus={disc.statBonus.stat} - baseValue={disc.statBonus.baseValue} - drainBase={disc.drainBase} - difficultyFactor={disc.difficultyFactor} - scalingFactor={disc.scalingFactor} - xp={discState.xp} - paused={discState.paused} - concurrentLimit={concurrentLimit} - onToggle={handleToggle} + definition={{ + id: disc.id, + name: disc.name, + description: disc.description, + perkThresholds: disc.perks?.map((p) => p.threshold), + perkValues: disc.perks?.map((p) => p.value), + perkTypes: disc.perks?.map((p) => p.type), + statBonus: disc.statBonus.stat, + baseValue: disc.statBonus.baseValue, + drainBase: disc.drainBase, + difficultyFactor: disc.difficultyFactor, + scalingFactor: disc.scalingFactor, + }} + runtime={{ + xp: discState.xp, + paused: discState.paused, + concurrentLimit, + }} + callbacks={{ + onToggle: handleToggle, + }} /> ); })} diff --git a/src/components/game/tabs/EquipmentTab.tsx b/src/components/game/tabs/EquipmentTab.tsx index 06fabd4..599d2d0 100644 --- a/src/components/game/tabs/EquipmentTab.tsx +++ b/src/components/game/tabs/EquipmentTab.tsx @@ -2,7 +2,6 @@ import { useState, useEffect, useCallback, useMemo } from 'react'; import { useCraftingStore } from '@/lib/game/stores/craftingStore'; -import { equipItem, unequipItem, deleteEquipmentInstance } from '@/lib/game/crafting-actions/equipment-actions'; import type { EquipmentSlot } from '@/lib/game/types'; import { DebugName } from '@/components/game/debug/debug-context'; import { EquipmentSlotGrid } from './EquipmentTab/EquipmentSlotGrid'; diff --git a/src/lib/game/__tests__/mana-utils.test.ts b/src/lib/game/__tests__/mana-utils.test.ts index 42bf8f3..0d97ea6 100644 --- a/src/lib/game/__tests__/mana-utils.test.ts +++ b/src/lib/game/__tests__/mana-utils.test.ts @@ -170,27 +170,27 @@ describe('computeRegen', () => { describe('computeClickMana', () => { it('should return 1 with no skills', () => { - const result = computeClickMana({ skills: {} }); + const result = computeClickMana({}); expect(result).toBe(1); }); it('should add 1 per manaTap skill level', () => { - const result = computeClickMana({ skills: { manaTap: 3 } }); + const result = computeClickMana({ manaTap: 3 }); expect(result).toBe(4); // 1 + 3 }); it('should add 3 per manaSurge skill level', () => { - const result = computeClickMana({ skills: { manaSurge: 2 } }); + const result = computeClickMana({ manaSurge: 2 }); expect(result).toBe(7); // 1 + 6 }); it('should combine manaTap and manaSurge', () => { - const result = computeClickMana({ skills: { manaTap: 2, manaSurge: 1 } }); + const result = computeClickMana({ manaTap: 2, manaSurge: 1 }); expect(result).toBe(6); // 1 + 2 + 3 }); it('should add discipline click multiplier', () => { - const result = computeClickMana({ skills: {} }, { + const result = computeClickMana({}, { bonuses: { clickManaMultiplier: 5 }, multipliers: {}, }); diff --git a/src/lib/game/crafting-actions/application-actions.ts b/src/lib/game/crafting-actions/application-actions.ts index f338288..ec3bc17 100644 --- a/src/lib/game/crafting-actions/application-actions.ts +++ b/src/lib/game/crafting-actions/application-actions.ts @@ -1,13 +1,19 @@ // ─── Enchantment Application Actions ──────────────────────────────────────── -import type { GameState } from '../types'; +import type { CraftingState } from '../stores/craftingStore.types'; +import type { GameAction } from '../types'; import * as CraftingApply from '../crafting-apply'; +/** + * Start applying an enchantment design to an equipment instance. + * Note: currentAction must be passed from the combat store. + */ export function startApplying( equipmentInstanceId: string, designId: string, - get: () => GameState, - set: (fn: (state: GameState) => Partial) => void + get: () => CraftingState, + set: (partial: Partial) => void, + currentAction: GameAction, ): boolean { const state = get(); const instance = state.equipmentInstances[equipmentInstanceId]; @@ -16,57 +22,53 @@ export function startApplying( const validation = CraftingApply.canApplyEnchantment( instance, design, - state.currentAction + currentAction ); if (!validation.canApply) return false; - set(() => ({ - currentAction: 'enchant' as const, + set({ applicationProgress: CraftingApply.initializeApplicationProgress( equipmentInstanceId, designId, design! ), - })); + }); return true; } export function pauseApplication( - get: () => GameState, - set: (fn: (state: GameState) => Partial) => void + get: () => CraftingState, + set: (partial: Partial) => void ) { - set((state) => { - if (!state.applicationProgress) return {}; - return { - applicationProgress: { - ...state.applicationProgress, - paused: true, - }, - }; + const state = get(); + if (!state.applicationProgress) return; + set({ + applicationProgress: { + ...state.applicationProgress, + paused: true, + }, }); } export function resumeApplication( - get: () => GameState, - set: (fn: (state: GameState) => Partial) => void + get: () => CraftingState, + set: (partial: Partial) => void ) { - set((state) => { - if (!state.applicationProgress) return {}; - return { - applicationProgress: { - ...state.applicationProgress, - paused: false, - }, - }; + const state = get(); + if (!state.applicationProgress) return; + set({ + applicationProgress: { + ...state.applicationProgress, + paused: false, + }, }); } export function cancelApplication( - set: (fn: (state: GameState) => Partial) => void + set: (partial: Partial) => void ) { - set(() => ({ - currentAction: 'meditate' as const, + set({ applicationProgress: null, - })); + }); } diff --git a/src/lib/game/crafting-actions/computed-getters.ts b/src/lib/game/crafting-actions/computed-getters.ts index 163bff6..e1a134d 100644 --- a/src/lib/game/crafting-actions/computed-getters.ts +++ b/src/lib/game/crafting-actions/computed-getters.ts @@ -1,9 +1,9 @@ // ─── Computed Getters ────────────────────────────────────────────────────── -import type { GameState } from '../types'; +import type { CraftingState } from '../stores/craftingStore.types'; import { ENCHANTMENT_EFFECTS } from '../data/enchantment-effects'; -export function getEquipmentSpells(get: () => GameState): string[] { +export function getEquipmentSpells(get: () => CraftingState): string[] { const state = get(); const spells: string[] = []; @@ -23,7 +23,7 @@ export function getEquipmentSpells(get: () => GameState): string[] { return [...new Set(spells)]; } -export function getEquipmentEffects(get: () => GameState): Record { +export function getEquipmentEffects(get: () => CraftingState): Record { const state = get(); const effects: Record = {}; @@ -47,7 +47,7 @@ export function getEquipmentEffects(get: () => GameState): Record GameState + get: () => CraftingState ): number { const state = get(); const instance = state.equipmentInstances[instanceId]; diff --git a/src/lib/game/crafting-actions/crafting-equipment-actions.ts b/src/lib/game/crafting-actions/crafting-equipment-actions.ts index ed29198..99c95ec 100644 --- a/src/lib/game/crafting-actions/crafting-equipment-actions.ts +++ b/src/lib/game/crafting-actions/crafting-equipment-actions.ts @@ -2,16 +2,16 @@ // Note: The main implementation is now in craftingStore.ts // These wrappers are kept for backward compatibility but are no longer used directly -import type { GameState } from '../types'; +import type { CraftingState } from '../stores/craftingStore.types'; +import type { GameAction } from '../types'; import * as CraftingEquipment from '../crafting-equipment'; -// Wrapper functions kept for backward compatibility -// The actual implementation is in craftingStore.ts using CraftingEquipment functions directly - export function startCraftingEquipment( blueprintId: string, - get: () => GameState, - set: (fn: (state: GameState) => Partial) => void + get: () => CraftingState, + set: (fn: (state: CraftingState) => Partial) => void, + rawMana: number, + currentAction: GameAction, ): boolean { const state = get(); @@ -19,8 +19,8 @@ export function startCraftingEquipment( blueprintId, state.lootInventory.blueprints.includes(blueprintId), state.lootInventory.materials, - state.rawMana, - state.currentAction + rawMana, + currentAction ); if (!check.canCraft) return false; @@ -28,16 +28,14 @@ export function startCraftingEquipment( const result = CraftingEquipment.initializeEquipmentCrafting( blueprintId, state.lootInventory.materials, - state.rawMana + rawMana ); - set((state) => ({ + set((s) => ({ lootInventory: { - ...state.lootInventory, + ...s.lootInventory, materials: result.newMaterials, }, - rawMana: state.rawMana - result.manaCost, - currentAction: 'craft' as const, equipmentCraftingProgress: result.progress, })); @@ -45,12 +43,12 @@ export function startCraftingEquipment( } export function cancelEquipmentCrafting( - get: () => GameState, - set: (fn: (state: GameState) => Partial) => void + get: () => CraftingState, + set: (fn: (state: CraftingState) => Partial) => void ) { set((state) => { const progress = state.equipmentCraftingProgress; - if (!progress) return { currentAction: 'meditate' as const, equipmentCraftingProgress: null }; + if (!progress) return { equipmentCraftingProgress: null }; const cancelResult = CraftingEquipment.cancelEquipmentCrafting( progress.blueprintId, @@ -58,10 +56,8 @@ export function cancelEquipmentCrafting( ); return { - currentAction: 'meditate' as const, equipmentCraftingProgress: null, - rawMana: state.rawMana + cancelResult.manaRefund, - log: [cancelResult.logMessage, ...state.log.slice(0, 49)], + log: [cancelResult.logMessage], }; }); } @@ -69,8 +65,8 @@ export function cancelEquipmentCrafting( export function deleteMaterial( materialId: string, amount: number, - get: () => GameState, - set: (fn: (state: GameState) => Partial) => void + get: () => CraftingState, + set: (fn: (state: CraftingState) => Partial) => void ) { set((state) => { const newMaterials = { ...state.lootInventory.materials }; @@ -88,7 +84,7 @@ export function deleteMaterial( ...state.lootInventory, materials: newMaterials, }, - log: [`🗑️ Deleted ${amount}x ${materialId}.`, ...state.log.slice(0, 49)], + log: [`🗑️ Deleted ${amount}x ${materialId}.`], }; }); } diff --git a/src/lib/game/crafting-actions/design-actions.ts b/src/lib/game/crafting-actions/design-actions.ts index 9bed0ae..78b24d8 100644 --- a/src/lib/game/crafting-actions/design-actions.ts +++ b/src/lib/game/crafting-actions/design-actions.ts @@ -1,20 +1,28 @@ // ─── Enchantment Design Actions ──────────────────────────────────────────── -import type { GameState, EnchantmentDesign, DesignEffect, DesignProgress } from '../types'; +import type { CraftingState } from '../stores/craftingStore.types'; +import type { EnchantmentDesign, DesignEffect } from '../types'; import * as CraftingUtils from '../crafting-utils'; import * as CraftingDesign from '../crafting-design'; import { computeEffects } from '../effects/upgrade-effects'; import { hasSpecial, SPECIAL_EFFECTS } from '../effects/special-effects'; +export interface DesignActionsParams { + skills: Record; + skillUpgrades: Record; + skillTiers: Record; +} + export function startDesigningEnchantment( name: string, equipmentTypeId: string, effects: DesignEffect[], - get: () => GameState, - set: (fn: (state: GameState) => Partial) => void + params: DesignActionsParams, + get: () => CraftingState, + set: (fn: (state: CraftingState) => Partial) => void ): boolean { const state = get(); - const enchantingLevel = state.skills.enchanting || 0; + const enchantingLevel = params.skills.enchanting || 0; const validation = CraftingDesign.validateDesignEffects( effects, equipmentTypeId, @@ -25,21 +33,20 @@ export function startDesigningEnchantment( const equipType = CraftingUtils.getEquipmentType(equipmentTypeId); if (!equipType) return false; - const efficiencyBonus = ((state.skillUpgrades || {})['efficientEnchant'] || [])?.length * 0.05 || 0; + const efficiencyBonus = ((params.skillUpgrades || {})['efficientEnchant'] || [])?.length * 0.05 || 0; const totalCapacityCost = CraftingDesign.calculateDesignCapacityCost(effects, efficiencyBonus); if (totalCapacityCost > equipType.baseCapacity) { return false; } - const computedEffects = computeEffects(state.skillUpgrades || {}, state.skillTiers || {}); + const computedEffects = computeEffects(params.skillUpgrades || {}, params.skillTiers || {}); const hasEnchantMastery = hasSpecial(computedEffects, SPECIAL_EFFECTS.ENCHANT_MASTERY); - let updates: Partial = {}; + let updates: Partial = {}; if (!state.designProgress) { updates = { - currentAction: 'design' as const, designProgress: { designId: CraftingUtils.generateDesignId(), progress: 0, @@ -69,15 +76,14 @@ export function startDesigningEnchantment( } export function cancelDesign( - get: () => GameState, - set: (fn: (state: GameState) => Partial) => void + get: () => CraftingState, + set: (fn: (state: CraftingState) => Partial) => void ) { const state = get(); if (state.designProgress2 && !state.designProgress) { set(() => ({ designProgress2: null })); } else { set(() => ({ - currentAction: 'meditate' as const, designProgress: null, })); } @@ -85,27 +91,26 @@ export function cancelDesign( export function saveDesign( design: EnchantmentDesign, - get: () => GameState, - set: (fn: (state: GameState) => Partial) => void + get: () => CraftingState, + set: (fn: (state: CraftingState) => Partial) => void ) { const state = get(); if (state.designProgress2 && state.designProgress2.designId === design.id) { - set((state) => ({ - enchantmentDesigns: [...state.enchantmentDesigns, design], + set((s) => ({ + enchantmentDesigns: [...s.enchantmentDesigns, design], designProgress2: null, })); } else { - set((state) => ({ - enchantmentDesigns: [...state.enchantmentDesigns, design], + set((s) => ({ + enchantmentDesigns: [...s.enchantmentDesigns, design], designProgress: null, - currentAction: 'meditate' as const, })); } } export function deleteDesign( designId: string, - set: (fn: (state: GameState) => Partial) => void + set: (fn: (state: CraftingState) => Partial) => void ) { set((state) => ({ enchantmentDesigns: state.enchantmentDesigns.filter(d => d.id !== designId), diff --git a/src/lib/game/crafting-actions/disenchant-actions.ts b/src/lib/game/crafting-actions/disenchant-actions.ts index 6cbc341..c779638 100644 --- a/src/lib/game/crafting-actions/disenchant-actions.ts +++ b/src/lib/game/crafting-actions/disenchant-actions.ts @@ -1,11 +1,11 @@ // ─── Disenchanting Actions ───────────────────────────────────────────────── -import type { GameState, EquipmentInstance } from '../types'; +import type { CraftingState } from '../stores/craftingStore.types'; export function disenchantEquipment( instanceId: string, - get: () => GameState, - set: (fn: (state: GameState) => Partial) => void + get: () => CraftingState, + set: (fn: (state: CraftingState) => Partial) => void ) { const state = get(); const instance = state.equipmentInstances[instanceId]; @@ -19,16 +19,15 @@ export function disenchantEquipment( totalRecovered += Math.floor(ench.actualCost * recoveryRate); } - set((state) => ({ - rawMana: state.rawMana + totalRecovered, + set((s) => ({ equipmentInstances: { - ...state.equipmentInstances, + ...s.equipmentInstances, [instanceId]: { ...instance, enchantments: [], usedCapacity: 0, }, }, - log: [`✨ Disenchanted ${instance.name}, recovered ${totalRecovered} mana.`, ...state.log.slice(0, 49)], + log: [`✨ Disenchanted ${instance.name}, recovered ${totalRecovered} mana.`], })); } diff --git a/src/lib/game/crafting-actions/equipment-actions.ts b/src/lib/game/crafting-actions/equipment-actions.ts index ea0fd02..4e9629f 100644 --- a/src/lib/game/crafting-actions/equipment-actions.ts +++ b/src/lib/game/crafting-actions/equipment-actions.ts @@ -1,12 +1,13 @@ // ─── Equipment Management Actions ──────────────────────────────────────────── -import type { GameState, EquipmentInstance, EquipmentSlot } from '../types'; +import type { CraftingState } from '../stores/craftingStore.types'; +import type { EquipmentInstance, EquipmentSlot } from '../types'; import * as CraftingUtils from '../crafting-utils'; // Create equipment instance export function createEquipmentInstance( typeId: string, - set: (fn: (state: GameState) => Partial) => void + set: (fn: (state: CraftingState) => Partial) => void ): string | null { const type = CraftingUtils.getEquipmentType(typeId); if (!type) return null; @@ -38,8 +39,8 @@ export function createEquipmentInstance( export function equipItem( instanceId: string, slot: EquipmentSlot, - get: () => GameState, - set: (fn: (state: GameState) => Partial) => void + get: () => CraftingState, + set: (fn: (state: CraftingState) => Partial) => void ): boolean { const state = get(); const instance = state.equipmentInstances[instanceId]; @@ -69,7 +70,7 @@ export function equipItem( // Unequip item export function unequipItem( slot: EquipmentSlot, - set: (fn: (state: GameState) => Partial) => void + set: (fn: (state: CraftingState) => Partial) => void ) { set((state) => ({ equippedInstances: { @@ -82,8 +83,8 @@ export function unequipItem( // Delete equipment instance export function deleteEquipmentInstance( instanceId: string, - get: () => GameState, - set: (fn: (state: GameState) => Partial) => void + get: () => CraftingState, + set: (fn: (state: CraftingState) => Partial) => void ) { const state = get(); let newEquipped = { ...state.equippedInstances }; diff --git a/src/lib/game/stores/combat-actions.ts b/src/lib/game/stores/combat-actions.ts index 045acbb..47c4088 100644 --- a/src/lib/game/stores/combat-actions.ts +++ b/src/lib/game/stores/combat-actions.ts @@ -3,7 +3,7 @@ // All external data (signedPacts, etc.) is passed in as parameters. import { SPELLS_DEF, GUARDIANS, HOURS_PER_TICK } from '../constants'; -import type { CombatState } from './combat-state.types'; +import type { CombatStore, CombatState } from './combat-state.types'; import type { SpellState } from '../types'; import { getFloorMaxHP, getFloorElement, calcDamage, canAffordSpellCost, deductSpellCost } from '../utils'; import { computeDisciplineEffects } from '../effects/discipline-effects'; @@ -22,7 +22,7 @@ export interface CombatTickResult { } export function processCombatTick( - get: () => CombatState, + get: () => CombatStore, set: (state: Partial) => void, rawMana: number, elements: Record, diff --git a/src/lib/game/stores/combat-state.types.ts b/src/lib/game/stores/combat-state.types.ts index c56b12f..da40832 100644 --- a/src/lib/game/stores/combat-state.types.ts +++ b/src/lib/game/stores/combat-state.types.ts @@ -3,6 +3,8 @@ import type { GameAction, SpellState, FloorState, GolemancyState, ActivityLogEntry, AchievementState, EquipmentSpellState, ActivityEventType } from '../types'; +// ─── Combat State (data only) ───────────────────────────────────────────────── + export interface CombatState { // Floor state currentFloor: number; @@ -49,8 +51,11 @@ export interface CombatState { totalSpellsCast: number; totalDamageDealt: number; totalCraftsCompleted: number; +} - // Actions +// ─── Combat Actions ─────────────────────────────────────────────────────────── + +export interface CombatActions { setCurrentFloor: (floor: number) => void; advanceFloor: () => void; setFloorHP: (hp: number) => void; @@ -119,5 +124,8 @@ export interface CombatState { // Debug helpers debugSetFloor: (floor: number) => void; resetFloorHP: () => void; - debugSetTime: (day: number, hour: number) => void; } + +// ─── Combined Combat Store Type ─────────────────────────────────────────────── + +export type CombatStore = CombatState & CombatActions; diff --git a/src/lib/game/stores/combatStore.ts b/src/lib/game/stores/combatStore.ts index 7004641..c5dfa3c 100755 --- a/src/lib/game/stores/combatStore.ts +++ b/src/lib/game/stores/combatStore.ts @@ -9,9 +9,9 @@ import { usePrestigeStore } from './prestigeStore'; import { generateFloorState } from '../utils/room-utils'; import { addActivityLogEntry } from '../utils/activity-log'; import { processCombatTick, makeInitialSpells } from './combat-actions'; -import type { CombatState } from './combat-state.types'; +import type { CombatStore } from './combat-state.types'; -export const useCombatStore = create()( +export const useCombatStore = create()( persist( (set, get) => ({ currentFloor: 1, diff --git a/src/lib/game/stores/craftingStore.ts b/src/lib/game/stores/craftingStore.ts index 34706f8..2ec7859 100644 --- a/src/lib/game/stores/craftingStore.ts +++ b/src/lib/game/stores/craftingStore.ts @@ -183,24 +183,26 @@ export const useCraftingStore = create()( // Enchantment application actions startApplying: (equipmentInstanceId, designId) => { + const currentAction = useCombatStore.getState().currentAction; return ApplicationActions.startApplying( equipmentInstanceId, designId, get, - set + set as unknown as (partial: Partial) => void, + currentAction ); }, pauseApplication: () => { - ApplicationActions.pauseApplication(get, set); + ApplicationActions.pauseApplication(get, set as unknown as (partial: Partial) => void); }, resumeApplication: () => { - ApplicationActions.resumeApplication(get, set); + ApplicationActions.resumeApplication(get, set as unknown as (partial: Partial) => void); }, cancelApplication: () => { - ApplicationActions.cancelApplication(set); + ApplicationActions.cancelApplication(set as unknown as (partial: Partial) => void); useCombatStore.setState({ currentAction: 'meditate' }); }, diff --git a/src/lib/game/stores/gameActions.ts b/src/lib/game/stores/gameActions.ts index 3122398..f54ab2a 100644 --- a/src/lib/game/stores/gameActions.ts +++ b/src/lib/game/stores/gameActions.ts @@ -39,7 +39,7 @@ export const createGatherMana = () => () => { // Compute click mana with discipline bonuses (mana-channeling → clickManaMultiplier) const cm = computeClickMana( - { skills: {} }, + {}, disciplineEffects, ); diff --git a/src/lib/game/stores/gameHooks.ts b/src/lib/game/stores/gameHooks.ts index 1bd78bc..fb30cba 100644 --- a/src/lib/game/stores/gameHooks.ts +++ b/src/lib/game/stores/gameHooks.ts @@ -76,9 +76,7 @@ export function useManaStats() { disciplineEffects, ); - const clickMana = computeClickMana({ - skills: {}, - }, disciplineEffects); + const clickMana = computeClickMana({}, disciplineEffects); const meditationMultiplier = getMeditationBonus(meditateTicks, {}, upgradeEffects.meditationEfficiency); const incursionStrength = getIncursionStrength(day, hour); diff --git a/src/lib/game/stores/index.ts b/src/lib/game/stores/index.ts index 9ecf8ff..25b5ee6 100755 --- a/src/lib/game/stores/index.ts +++ b/src/lib/game/stores/index.ts @@ -6,13 +6,13 @@ export { useUIStore } from './uiStore'; export type { UIState } from './uiStore'; export { usePrestigeStore } from './prestigeStore'; -export type { PrestigeState } from './prestigeStore'; +export type { PrestigeState, PrestigeActions, PrestigeStore } from './prestigeStore'; export { useManaStore, makeInitialElements } from './manaStore'; -export type { ManaState } from './manaStore'; +export type { ManaState, ManaActions, ManaStore } from './manaStore'; export { useCombatStore, makeInitialSpells } from './combatStore'; -export type { CombatState } from './combat-state.types'; +export type { CombatState, CombatActions, CombatStore } from './combat-state.types'; export { useCraftingStore } from './craftingStore'; export type { CraftingState, CraftingActions } from './craftingStore.types'; @@ -21,6 +21,7 @@ export { useAttunementStore } from './attunementStore'; export type { AttunementStoreState } from './attunementStore'; export { useDisciplineStore } from './discipline-slice'; +export type { DisciplineStoreState, DisciplineStoreActions, DisciplineStore } from './discipline-slice'; export { useGameStore } from './gameStore'; export { useGameLoop } from './gameHooks'; diff --git a/src/lib/game/stores/manaStore.ts b/src/lib/game/stores/manaStore.ts index 789ebd1..c965694 100755 --- a/src/lib/game/stores/manaStore.ts +++ b/src/lib/game/stores/manaStore.ts @@ -6,23 +6,28 @@ import { persist } from 'zustand/middleware'; import { ELEMENTS, MANA_PER_ELEMENT, BASE_UNLOCKED_ELEMENTS } from '../constants'; import type { ElementState } from '../types'; +// ─── Mana State (data only) ───────────────────────────────────────────────── + export interface ManaState { rawMana: number; meditateTicks: number; totalManaGathered: number; elements: Record; - - // Actions +} + +// ─── Mana Actions ──────────────────────────────────────────────────────────── + +export interface ManaActions { setRawMana: (amount: number) => void; addRawMana: (amount: number, maxMana: number) => void; spendRawMana: (amount: number) => boolean; gatherMana: (amount: number, maxMana: number) => void; - + // Meditation setMeditateTicks: (ticks: number) => void; incrementMeditateTicks: () => void; resetMeditateTicks: () => void; - + // Elements convertMana: (element: string, amount: number) => boolean; unlockElement: (element: string, cost: number) => boolean; @@ -30,10 +35,10 @@ export interface ManaState { spendElementMana: (element: string, amount: number) => boolean; setElementMax: (max: number) => void; craftComposite: (target: string, recipe: string[]) => boolean; - + // Helper for gameStore coordination processConvertAction: (rawMana: number) => { rawMana: number; elements: Record } | null; - + // Reset resetMana: ( prestigeUpgrades: Record, @@ -43,7 +48,11 @@ export interface ManaState { ) => void; } -export const useManaStore = create()( +// ─── Combined Mana Store Type ──────────────────────────────────────────────── + +export type ManaStore = ManaState & ManaActions; + +export const useManaStore = create()( persist( (set, get) => ({ rawMana: 10, @@ -59,62 +68,62 @@ export const useManaStore = create()( } ]) ) as Record, - + setRawMana: (amount: number) => { set({ rawMana: Math.max(0, amount) }); }, - + addRawMana: (amount: number, maxMana: number) => { set((state) => ({ rawMana: Math.min(state.rawMana + amount, maxMana), totalManaGathered: state.totalManaGathered + amount, })); }, - + spendRawMana: (amount: number) => { const state = get(); if (state.rawMana < amount) return false; - + set({ rawMana: state.rawMana - amount }); return true; }, - + gatherMana: (amount: number, maxMana: number) => { set((state) => ({ rawMana: Math.min(state.rawMana + amount, maxMana), totalManaGathered: state.totalManaGathered + amount, })); }, - + setMeditateTicks: (ticks: number) => { set({ meditateTicks: ticks }); }, - + incrementMeditateTicks: () => { set((state) => ({ meditateTicks: state.meditateTicks + 1 })); }, - + resetMeditateTicks: () => { set({ meditateTicks: 0 }); }, - + convertMana: (element: string, amount: number) => { const state = get(); const elem = state.elements[element]; if (!elem?.unlocked) return false; - + const cost = MANA_PER_ELEMENT * amount; if (state.rawMana < cost) return false; if (elem.current >= elem.max) return false; - + const canConvert = Math.min( amount, Math.floor(state.rawMana / MANA_PER_ELEMENT), elem.max - elem.current ); - + if (canConvert <= 0) return false; - + set({ rawMana: state.rawMana - canConvert * MANA_PER_ELEMENT, elements: { @@ -122,15 +131,15 @@ export const useManaStore = create()( [element]: { ...elem, current: elem.current + canConvert }, }, }); - + return true; }, - + unlockElement: (element: string, cost: number) => { const state = get(); if (state.elements[element]?.unlocked) return false; if (state.rawMana < cost) return false; - + set({ rawMana: state.rawMana - cost, elements: { @@ -138,15 +147,15 @@ export const useManaStore = create()( [element]: { ...state.elements[element], unlocked: true }, }, }); - + return true; }, - + addElementMana: (element: string, amount: number, max: number) => { set((state) => { const elem = state.elements[element]; if (!elem) return state; - + return { elements: { ...state.elements, @@ -158,22 +167,22 @@ export const useManaStore = create()( }; }); }, - + spendElementMana: (element: string, amount: number) => { const state = get(); const elem = state.elements[element]; if (!elem || elem.current < amount) return false; - + set({ elements: { ...state.elements, [element]: { ...elem, current: elem.current - amount }, }, }); - + return true; }, - + setElementMax: (max: number) => { set((state) => ({ elements: Object.fromEntries( @@ -181,21 +190,21 @@ export const useManaStore = create()( ) as Record, })); }, - + craftComposite: (target: string, recipe: string[]) => { const state = get(); - + // Count required ingredients const costs: Record = {}; 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 false; } - + // Deduct ingredients const newElems = { ...state.elements }; for (const [r, amt] of Object.entries(costs)) { @@ -204,7 +213,7 @@ export const useManaStore = create()( current: newElems[r].current - amt, }; } - + // Add crafted element const targetElem = newElems[target]; newElems[target] = { @@ -212,38 +221,38 @@ export const useManaStore = create()( current: (targetElem?.current || 0) + 1, unlocked: true, }; - + set({ elements: newElems }); return true; }, - + processConvertAction: (rawMana: number) => { 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; - + 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 ); - + if (canConvert <= 0) return null; - + rawMana -= canConvert * 100; const updatedElements = { ...elements, [targetId]: { ...targetState, current: targetState.current + canConvert } }; - + return { rawMana, elements: updatedElements }; }, - + resetMana: ( prestigeUpgrades: Record, skills: Record = {}, @@ -253,7 +262,7 @@ export const useManaStore = create()( const elementMax = 10 + (prestigeUpgrades.elemMax || 0) * 5; const startingMana = 10 + (prestigeUpgrades.manaStart || 0) * 10; const elements = makeInitialElements(elementMax, prestigeUpgrades); - + set({ rawMana: startingMana, meditateTicks: 0, @@ -279,7 +288,7 @@ export function makeInitialElements( prestigeUpgrades: Record = {} ): Record { const elemStart = (prestigeUpgrades.elemStart || 0) * 5; - + const elements: Record = {}; Object.keys(ELEMENTS).forEach(k => { const isUnlocked = BASE_UNLOCKED_ELEMENTS.includes(k); @@ -289,6 +298,6 @@ export function makeInitialElements( unlocked: isUnlocked, }; }); - + return elements; } diff --git a/src/lib/game/stores/prestigeStore.ts b/src/lib/game/stores/prestigeStore.ts index 42a5ce8..6449d54 100755 --- a/src/lib/game/stores/prestigeStore.ts +++ b/src/lib/game/stores/prestigeStore.ts @@ -6,23 +6,25 @@ import { persist } from 'zustand/middleware'; import type { Memory } from '../types'; import { GUARDIANS, PRESTIGE_DEF } from '../constants'; +// ─── Prestige State (data only) ────────────────────────────────────────────── + export interface PrestigeState { // Loop counter loopCount: number; - + // Insight insight: number; totalInsight: number; - loopInsight: number; // Insight earned at end of current loop - + loopInsight: number; + // Prestige upgrades prestigeUpgrades: Record; memorySlots: number; pactSlots: number; - + // Memories (skills preserved across loops) memories: Memory[]; - + // Guardian pacts defeatedGuardians: number[]; signedPacts: number[]; @@ -34,8 +36,11 @@ export interface PrestigeState { }>; pactRitualFloor: number | null; pactRitualProgress: number; - - // Actions +} + +// ─── Prestige Actions ──────────────────────────────────────────────────────── + +export interface PrestigeActions { doPrestige: (id: string) => boolean; addMemory: (memory: Memory) => void; removeMemory: (skillId: string) => void; @@ -46,7 +51,7 @@ export interface PrestigeState { updatePactRitualProgress: (hours: number) => void; removePact: (floor: number) => void; defeatGuardian: (floor: number) => void; - + // Methods called by gameStore addSignedPact: (floor: number) => void; removeDefeatedGuardian: (floor: number) => void; @@ -59,14 +64,14 @@ export interface PrestigeState { memories: Memory[], memorySlots: number ) => void; - + // Loop management startNewLoop: (insightGained: number) => void; setLoopInsight: (insight: number) => void; - + // Reset resetPrestige: () => void; - + // Debug helpers debugSetSignedPacts: (pacts: number[]) => void; debugSetPactDetails: (details: Record) => void; } -const initialState = { +// ─── Combined Prestige Store Type ──────────────────────────────────────────── + +export type PrestigeStore = PrestigeState & PrestigeActions; + +// ─── Initial State ─────────────────────────────────────────────────────────── + +const initialState: PrestigeState = { loopCount: 0, insight: 0, totalInsight: 0, loopInsight: 0, - prestigeUpgrades: {} as Record, + prestigeUpgrades: {}, memorySlots: 3, pactSlots: 1, - memories: [] as Memory[], - defeatedGuardians: [] as number[], - signedPacts: [] as number[], - signedPactDetails: {} as Record; - }>, - pactRitualFloor: null as number | null, + memories: [], + defeatedGuardians: [], + signedPacts: [], + signedPactDetails: {}, + pactRitualFloor: null, pactRitualProgress: 0, }; -export const usePrestigeStore = create()( +export const usePrestigeStore = create()( persist( (set, get) => ({ ...initialState, - + doPrestige: (id: string) => { const state = get(); const pd = PRESTIGE_DEF[id]; if (!pd) return false; - + const lvl = state.prestigeUpgrades[id] || 0; if (lvl >= pd.max || state.insight < pd.cost) return false; - + const newPU = { ...state.prestigeUpgrades, [id]: lvl + 1 }; set({ insight: state.insight - pd.cost, @@ -120,114 +126,114 @@ export const usePrestigeStore = create()( }); return true; }, - + addMemory: (memory: Memory) => { const state = get(); if (state.memories.length >= state.memorySlots) return; if (state.memories.some(m => m.skillId === memory.skillId)) return; - + set({ memories: [...state.memories, memory] }); }, - + removeMemory: (skillId: string) => { set((state) => ({ memories: state.memories.filter(m => m.skillId !== skillId), })); }, - + clearMemories: () => { set({ memories: [] }); }, - + startPactRitual: (floor: number, rawMana: number) => { const state = get(); const guardian = GUARDIANS[floor]; if (!guardian) return false; - + if (!state.defeatedGuardians.includes(floor)) return false; if (state.signedPacts.includes(floor)) return false; if (state.signedPacts.length >= state.pactSlots) return false; if (rawMana < guardian.pactCost) return false; if (state.pactRitualFloor !== null) return false; - + set({ pactRitualFloor: floor, pactRitualProgress: 0, }); return true; }, - + cancelPactRitual: () => { set({ pactRitualFloor: null, pactRitualProgress: 0, }); }, - + completePactRitual: (addLog: (msg: string) => void) => { const state = get(); if (state.pactRitualFloor === null) return; - + const guardian = GUARDIANS[state.pactRitualFloor]; if (!guardian) return; - + set({ signedPacts: [...state.signedPacts, state.pactRitualFloor], defeatedGuardians: state.defeatedGuardians.filter(f => f !== state.pactRitualFloor), pactRitualFloor: null, pactRitualProgress: 0, }); - + addLog(`📜 Pact signed with ${guardian.name}! You have gained their boons.`); }, - + updatePactRitualProgress: (hours: number) => { set((state) => ({ pactRitualProgress: state.pactRitualProgress + hours, })); }, - + removePact: (floor: number) => { set((state) => ({ signedPacts: state.signedPacts.filter(f => f !== floor), })); }, - + defeatGuardian: (floor: number) => { const state = get(); if (state.defeatedGuardians.includes(floor) || state.signedPacts.includes(floor)) return; - + set({ defeatedGuardians: [...state.defeatedGuardians, floor], }); }, - + addSignedPact: (floor: number) => { const state = get(); if (state.signedPacts.includes(floor)) return; set({ signedPacts: [...state.signedPacts, floor] }); }, - + removeDefeatedGuardian: (floor: number) => { set((state) => ({ defeatedGuardians: state.defeatedGuardians.filter(f => f !== floor), })); }, - + setPactRitualFloor: (floor: number | null) => { set({ pactRitualFloor: floor, pactRitualProgress: 0 }); }, - + addDefeatedGuardian: (floor: number) => { const state = get(); if (state.defeatedGuardians.includes(floor) || state.signedPacts.includes(floor)) return; set({ defeatedGuardians: [...state.defeatedGuardians, floor] }); }, - + incrementLoopCount: () => { set((state) => ({ loopCount: state.loopCount + 1 })); }, - + resetPrestigeForNewLoop: ( totalInsight: number, prestigeUpgrades: Record, @@ -247,7 +253,7 @@ export const usePrestigeStore = create()( loopInsight: 0, }); }, - + startNewLoop: (insightGained: number) => { const state = get(); set({ @@ -262,15 +268,15 @@ export const usePrestigeStore = create()( pactRitualProgress: 0, }); }, - + setLoopInsight: (insight: number) => { set({ loopInsight: insight }); }, - + resetPrestige: () => { set(initialState); }, - + // Debug helpers debugSetSignedPacts: (pacts: number[]) => { set({ signedPacts: pacts }); diff --git a/src/lib/game/types.ts b/src/lib/game/types.ts index 2bf7a3d..1a9dae5 100755 --- a/src/lib/game/types.ts +++ b/src/lib/game/types.ts @@ -51,12 +51,15 @@ export type { StudyTarget, SummonedGolem, GolemancyState, - GameState, GameActionType, ActivityEventType, ActivityLogEntry, } from './types/game'; +export type { PrestigeDef } from './types/game'; + +export type { EquipmentSlot } from './types/equipmentSlot'; + // ─── New: Memory Type Definition ───────────────────────────────────────────── export interface Memory { skillId: string; diff --git a/src/lib/game/types/index.ts b/src/lib/game/types/index.ts index c188794..09f8c9b 100644 --- a/src/lib/game/types/index.ts +++ b/src/lib/game/types/index.ts @@ -30,7 +30,7 @@ export type { // Equipment slot type (canonical) export type { EquipmentSlot } from './equipmentSlot'; -// Game state types +// Game state types (non-monolithic) export type { RoomType, EnemyState, @@ -42,7 +42,6 @@ export type { StudyTarget, SummonedGolem, GolemancyState, - GameState, GameActionType, ActivityEventType, ActivityLogEntry, diff --git a/src/lib/game/utils/combat-utils.ts b/src/lib/game/utils/combat-utils.ts index 3146b1a..d3a6371 100644 --- a/src/lib/game/utils/combat-utils.ts +++ b/src/lib/game/utils/combat-utils.ts @@ -1,34 +1,59 @@ // ─── Combat Utilities ──────────────────────────────────────────────────────── -import type { GameState, SpellCost, EquipmentInstance } from '../types'; +import type { SpellCost, EquipmentInstance } from '../types'; import type { DisciplineBonuses } from './mana-utils'; import { GUARDIANS, SPELLS_DEF, ELEMENT_OPPOSITES, INCURSION_START_DAY, MAX_DAY } from '../constants'; import { ENCHANTMENT_EFFECTS } from '../data/enchantment-effects'; +// ─── Damage Calculation Params ────────────────────────────────────────────── + +export interface DamageCalcParams { + skills: Record; + signedPacts: number[]; +} + +// ─── Insight Calculation Params ───────────────────────────────────────────── + +export interface InsightCalcParams { + maxFloorReached: number; + totalManaGathered: number; + signedPacts: number[]; + prestigeUpgrades: Record; + skills: Record; +} + +// ─── DPS Calculation Params ───────────────────────────────────────────────── + +export interface DPSCalcParams { + skills: Record; + signedPacts: number[]; + equippedInstances: Record; + equipmentInstances: Record; + spells: Record; + prestigeUpgrades: Record; +} + // ─── Elemental Damage Bonus ────────────────────────────────────────────────── // Elemental damage bonus: +50% if spell element opposes floor element (super effective) // -25% if spell element matches its own opposite (weak) export function getElementalBonus(spellElem: string, floorElem: string): number { if (spellElem === 'raw') return 1.0; // Raw mana has no elemental bonus - + if (spellElem === floorElem) return 1.25; // Same element: +25% damage - + // Check for super effective first: spell is the opposite of floor - // e.g., casting water (opposite of fire) at fire floor = super effective if (ELEMENT_OPPOSITES[floorElem] === spellElem) return 1.5; // Super effective: +50% damage - + // Check for weak: spell's opposite matches floor - // e.g., casting fire (whose opposite is water) at water floor = weak if (ELEMENT_OPPOSITES[spellElem] === floorElem) return 0.75; // Weak: -25% damage - + return 1.0; // Neutral } // ─── Boon Bonuses ───────────────────────────────────────────────────────────── -// Helper to calculate total boon bonuses from signed pacts -export function getBoonBonuses(signedPacts: number[]): { +export interface BoonBonuses { maxMana: number; manaRegen: number; castingSpeed: number; @@ -41,8 +66,11 @@ export function getBoonBonuses(signedPacts: number[]): { insightGain: number; studySpeed: number; prestigeInsight: number; -} { - const bonuses = { +} + +// Helper to calculate total boon bonuses from signed pacts +export function getBoonBonuses(signedPacts: number[]): BoonBonuses { + const bonuses: BoonBonuses = { maxMana: 0, manaRegen: 0, castingSpeed: 0, @@ -56,11 +84,11 @@ export function getBoonBonuses(signedPacts: number[]): { studySpeed: 0, prestigeInsight: 0, }; - + for (const floor of signedPacts) { const guardian = GUARDIANS[floor]; if (!guardian) continue; - + for (const boon of guardian.boons) { switch (boon.type) { case 'maxMana': @@ -102,14 +130,14 @@ export function getBoonBonuses(signedPacts: number[]): { } } } - + return bonuses; } // ─── Damage Calculation ─────────────────────────────────────────────────────── export function calcDamage( - state: Pick, + state: DamageCalcParams, spellId: string, floorElem?: string, discipline?: DisciplineBonuses, @@ -118,18 +146,18 @@ export function calcDamage( if (!sp) return 5; const skills = state.skills; - // Base damage: spell base + skill bonus + discipline bonus (spell-casting → baseDamageBonus) + // Base damage: spell base + skill bonus + discipline bonus const discBaseDmg = discipline?.bonuses?.baseDamageBonus || 0; const baseDmg = sp.dmg + (skills.combatTrain || 0) * 5 + discBaseDmg; - // Percentage multiplier: skill arcaneFury + discipline void-manipulation (baseDamageMultiplier) + // Percentage multiplier const discDmgMult = discipline?.bonuses?.baseDamageMultiplier || 0; const pct = 1 + (skills.arcaneFury || 0) * 0.1 + discDmgMult; // Elemental mastery bonus const elemMasteryBonus = 1 + (skills.elementalMastery || 0) * 0.15; - // Guardian bane bonus - check if current floor has a guardian with matching element + // Guardian bane bonus const isGuardianFloor = floorElem && Object.values(GUARDIANS).some(g => g.element === floorElem); const guardianBonus = isGuardianFloor ? 1 + (skills.guardianBane || 0) * 0.2 @@ -163,20 +191,20 @@ export function calcDamage( // ─── Insight Calculation ────────────────────────────────────────────────────── -export function calcInsight(state: Pick, discipline?: DisciplineBonuses): number { +export function calcInsight(state: InsightCalcParams, discipline?: DisciplineBonuses): number { const pu = state.prestigeUpgrades; const discInsightBonus = discipline?.bonuses?.insightGainBonus || 0; const skillBonus = 1 + (state.skills.insightHarvest || 0) * 0.1 + discInsightBonus; - + // Get boon bonuses for insight gain const boons = getBoonBonuses(state.signedPacts); const boonInsightMult = 1 + boons.insightGain / 100; - + const mult = (1 + (pu.insightAmp || 0) * 0.25) * skillBonus * boonInsightMult; - + // Add prestigeInsight bonus per loop const prestigeInsightBonus = boons.prestigeInsight; - + return Math.floor( (state.maxFloorReached * 15 + state.totalManaGathered / 500 + state.signedPacts.length * 150 + prestigeInsightBonus) * mult ); @@ -195,8 +223,8 @@ export function getIncursionStrength(day: number, hour: number): number { // Check if player can afford spell cost export function canAffordSpellCost( - cost: SpellCost, - rawMana: number, + cost: SpellCost, + rawMana: number, elements: Record ): boolean { if (cost.type === 'raw') { @@ -214,14 +242,12 @@ export function deductSpellCost( elements: Record ): { rawMana: number; elements: Record } { const newElements = { ...elements }; - + if (cost.type === 'raw') { - // Don't allow rawMana to go below zero const deductedAmount = Math.min(rawMana, cost.amount); return { rawMana: rawMana - deductedAmount, elements: newElements }; } else if (cost.element && newElements[cost.element]) { const elem = newElements[cost.element]; - // Don't allow elemental mana to go below zero const deductedAmount = Math.min(elem.current, cost.amount); newElements[cost.element] = { ...elem, @@ -229,7 +255,7 @@ export function deductSpellCost( }; return { rawMana, elements: newElements }; } - + return { rawMana, elements: newElements }; } @@ -248,15 +274,14 @@ export function getActiveEquipmentSpells( ): ActiveEquipmentSpell[] { const equippedIds = Object.values(equippedInstances || {}).filter((id): id is string => id !== null); const spells: ActiveEquipmentSpell[] = []; - + for (const id of equippedIds) { const instance = equipmentInstances[id]; if (!instance) continue; - + for (const ench of instance.enchantments) { const effectDef = ENCHANTMENT_EFFECTS[ench.effectId]; if (effectDef?.effect.type === 'spell' && effectDef.effect.spellId) { - // Check if we already have this spell from this equipment const exists = spells.some(s => s.spellId === effectDef.effect.spellId && s.equipmentId === id); if (!exists) { spells.push({ spellId: effectDef.effect.spellId, equipmentId: id }); @@ -264,7 +289,7 @@ export function getActiveEquipmentSpells( } } } - + return spells; } @@ -272,7 +297,7 @@ export function getActiveEquipmentSpells( // Compute total DPS from all sources (spells, equipment, etc.) export function getTotalDPS( - state: Pick, + state: DPSCalcParams, upgradeEffects: { spellDamageBonus?: number; attackSpeedMultiplier?: number; [key: string]: unknown }, floorElem?: string, discipline?: DisciplineBonuses, @@ -289,23 +314,22 @@ export function getTotalDPS( // Calculate damage per cast const damage = calcDamage(state, spellId, floorElem, discipline); - + // Get cast speed (spells per second) - // Base cast time is 1 second, modified by casting speed bonuses const baseCastTime = spellDef.baseCastTime || 1.0; const castingSpeedBonus = 1 + (state.skills.castingSpeed || 0) * 0.1; const equipmentAttackSpeed = upgradeEffects.attackSpeedMultiplier || 1; const castTime = baseCastTime / (castingSpeedBonus * equipmentAttackSpeed); - + // DPS for this spell const spellDPS = damage / castTime; totalDPS += spellDPS; } - + // Add equipment DPS bonuses from upgrade effects if (upgradeEffects.spellDamageBonus) { totalDPS *= (1 + upgradeEffects.spellDamageBonus / 100); } - + return totalDPS; } diff --git a/src/lib/game/utils/mana-utils.ts b/src/lib/game/utils/mana-utils.ts index 03b8798..108e175 100644 --- a/src/lib/game/utils/mana-utils.ts +++ b/src/lib/game/utils/mana-utils.ts @@ -1,6 +1,6 @@ // ─── Mana & Regen Utilities ────────────────────────────────────────────────── -import type { GameState } from '../types'; +import type { AttunementState } from '../types'; import type { ComputedEffects } from '../effects/upgrade-effects.types'; import { HOURS_PER_TICK } from '../constants'; import { getTotalAttunementRegen, getTotalAttunementConversionDrain } from '../data/attunements'; @@ -10,8 +10,28 @@ export interface DisciplineBonuses { multipliers: Record; } +// ─── Mana Params ──────────────────────────────────────────────────────────── + +export interface ManaComputeParams { + skills: Record; + prestigeUpgrades: Record; + skillUpgrades?: Record; + skillTiers?: Record; +} + +export interface RegenComputeParams extends ManaComputeParams { + attunements: Record; +} + +export interface EffectiveRegenParams extends RegenComputeParams { + rawMana: number; + incursionStrength: number; +} + +// ─── Max Mana ──────────────────────────────────────────────────────────────── + export function computeMaxMana( - state: Pick, + state: Pick & Partial>, effects?: ComputedEffects, discipline?: DisciplineBonuses, ): number { @@ -22,17 +42,16 @@ export function computeMaxMana( ((pu || {}).manaWell || 0) * 500 + (discipline?.bonuses?.maxManaBonus || 0); - // Apply upgrade effects if provided if (effects) { return Math.floor((base + effects.maxManaBonus) * effects.maxManaMultiplier); } return base; } -// computeElementMax has been removed — element max is computed in manaStore.ts +// ─── Regen ──────────────────────────────────────────────────────────────────── export function computeRegen( - state: Pick, + state: Pick & Partial>, effects?: ComputedEffects, discipline?: DisciplineBonuses, ): number { @@ -63,55 +82,48 @@ export function computeRegen( return regen; } -// Compute the effective regen (raw regen minus conversion drains) for display purposes +// ─── Effective Regen for Display ────────────────────────────────────────────── + export function computeEffectiveRegenForDisplay( - state: Pick, + state: Pick, effects?: ComputedEffects, discipline?: DisciplineBonuses, ): { rawRegen: number; conversionDrain: number; effectiveRegen: number } { - // Get the full raw regen (without conversion drain) const rawRegen = computeRegen(state, effects, discipline); - - // Calculate conversion drain const conversionDrain = getTotalAttunementConversionDrain(state.attunements || {}); - - // Effective regen is what actually increases raw mana const effectiveRegen = Math.max(0, rawRegen - conversionDrain); return { rawRegen, conversionDrain, effectiveRegen }; } -/** - * Compute regen with dynamic special effects (needs current mana, max mana, incursion) - */ +// ─── Effective Regen (dynamic) ──────────────────────────────────────────────── + export function computeEffectiveRegen( - state: Pick, + state: Pick & { rawMana: number; incursionStrength: number }, effects?: ComputedEffects, discipline?: DisciplineBonuses, ): number { - // Base regen from existing function let regen = computeRegen(state, effects, discipline); - const incursionStrength = state.incursionStrength || 0; - - // Apply incursion penalty regen *= (1 - incursionStrength); - return regen; } +// ─── Click Mana ─────────────────────────────────────────────────────────────── + export function computeClickMana( - state: Pick, + skills: Record, discipline?: DisciplineBonuses, ): number { - const skillTap = ((state.skills || {}).manaTap || 0) * 1; - const skillSurge = ((state.skills || {}).manaSurge || 0) * 3; + const skillTap = (skills.manaTap || 0) * 1; + const skillSurge = (skills.manaSurge || 0) * 3; const discClickMult = discipline?.bonuses?.clickManaMultiplier || 0; return 1 + skillTap + skillSurge + discClickMult; } -// Meditation bonus now affects regen rate directly +// ─── Meditation Bonus ───────────────────────────────────────────────────────── + export function getMeditationBonus(meditateTicks: number, skills: Record, meditationEfficiency: number = 1): number { const hasMeditation = skills.meditation === 1; const hasDeepTrance = skills.deepTrance === 1; @@ -137,7 +149,7 @@ export function getMeditationBonus(meditateTicks: number, skills: Record