refactor: split bloated state types into State + Actions interfaces (issue #102)
Build and Publish Mana Loop Docker Image / build-and-publish (push) Successful in 1m19s

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