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
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:
@@ -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.
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
@@ -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';
|
||||||
|
|||||||
@@ -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}.`],
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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 };
|
||||||
|
|||||||
@@ -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 }>,
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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' });
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|||||||
@@ -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,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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';
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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 });
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
Reference in New Issue
Block a user