refactor: resolve structural inconsistencies and dead code
Build and Publish Mana Loop Docker Image / build-and-publish (push) Failing after 55s
Build and Publish Mana Loop Docker Image / build-and-publish (push) Failing after 55s
- Fix broken barrel exports in components/game/index.ts - Remove skill system from stores (gameStore, gameActions, gameLoopActions, gameHooks, craftingStore, combat) - Remove skill system from components (page.tsx, LeftPanel, StatsTab, SpellsTab, EnchantmentDesigner, EnchantmentPreparer, GameContext/Provider) - Delete dead code: stats/ directory, attunements/ directory, layout/ Header+TabBar, shared/ StudyProgress+UpgradeDialog duplicates, effects.ts.fix, study-slice.ts, navigation-slice.ts - Delete legacy store/ and store-modules/ directories, redirect remaining callers - Merge root formatting.ts into utils/formatting.ts - Move effects files (dynamic-compute, upgrade-effects, special-effects, upgrade-effects.types) into effects/ directory - Move debug-context.tsx into components/game/debug/ - Create tabs/index.ts barrel for tab components - Fix page.tsx lazy imports to use tabs barrel - Fix all broken import paths across codebase - Remove SKILLS_DEF and skill-evolution references - Trim store.ts to under 400 lines by removing dead skill actions
This commit is contained in:
@@ -1,43 +0,0 @@
|
||||
<!-- gitnexus:start -->
|
||||
# GitNexus — Code Intelligence
|
||||
|
||||
This project is indexed by GitNexus as **Mana-Loop** (3795 symbols, 6409 relationships, 146 execution flows). Use the GitNexus MCP tools to understand code, assess impact, and navigate safely.
|
||||
|
||||
> If any GitNexus tool warns the index is stale, run `npx gitnexus analyze` in terminal first.
|
||||
|
||||
## Always Do
|
||||
|
||||
- **MUST run impact analysis before editing any symbol.** Before modifying a function, class, or method, run `gitnexus_impact({target: "symbolName", direction: "upstream"})` and report the blast radius (direct callers, affected processes, risk level) to the user.
|
||||
- **MUST run `gitnexus_detect_changes()` before committing** to verify your changes only affect expected symbols and execution flows.
|
||||
- **MUST warn the user** if impact analysis returns HIGH or CRITICAL risk before proceeding with edits.
|
||||
- When exploring unfamiliar code, use `gitnexus_query({query: "concept"})` to find execution flows instead of grepping. It returns process-grouped results ranked by relevance.
|
||||
- When you need full context on a specific symbol — callers, callees, which execution flows it participates in — use `gitnexus_context({name: "symbolName"})`.
|
||||
|
||||
## Never Do
|
||||
|
||||
- NEVER edit a function, class, or method without first running `gitnexus_impact` on it.
|
||||
- NEVER ignore HIGH or CRITICAL risk warnings from impact analysis.
|
||||
- NEVER rename symbols with find-and-replace — use `gitnexus_rename` which understands the call graph.
|
||||
- NEVER commit changes without running `gitnexus_detect_changes()` to check affected scope.
|
||||
|
||||
## Resources
|
||||
|
||||
| Resource | Use for |
|
||||
|----------|---------|
|
||||
| `gitnexus://repo/Mana-Loop/context` | Codebase overview, check index freshness |
|
||||
| `gitnexus://repo/Mana-Loop/clusters` | All functional areas |
|
||||
| `gitnexus://repo/Mana-Loop/processes` | All execution flows |
|
||||
| `gitnexus://repo/Mana-Loop/process/{name}` | Step-by-step execution trace |
|
||||
|
||||
## CLI
|
||||
|
||||
| Task | Read this skill file |
|
||||
|------|---------------------|
|
||||
| Understand architecture / "How does X work?" | `.claude/skills/gitnexus/gitnexus-exploring/SKILL.md` |
|
||||
| Blast radius / "What breaks if I change X?" | `.claude/skills/gitnexus/gitnexus-impact-analysis/SKILL.md` |
|
||||
| Trace bugs / "Why is X failing?" | `.claude/skills/gitnexus/gitnexus-debugging/SKILL.md` |
|
||||
| Rename / extract / split / refactor | `.claude/skills/gitnexus/gitnexus-refactoring/SKILL.md` |
|
||||
| Tools, resources, schema reference | `.claude/skills/gitnexus/gitnexus-guide/SKILL.md` |
|
||||
| Index, status, clean, wiki CLI commands | `.claude/skills/gitnexus/gitnexus-cli/SKILL.md` |
|
||||
|
||||
<!-- gitnexus:end -->
|
||||
@@ -1,8 +1,8 @@
|
||||
# Circular Dependencies
|
||||
Generated: 2026-05-18T09:26:29.031Z
|
||||
Generated: 2026-05-18T10:08:43.704Z
|
||||
Found: 7 circular chain(s) — these MUST be fixed before modifying involved files.
|
||||
|
||||
1. Processed 151 files (1.5s) (37 warnings)
|
||||
1. Processed 142 files (1.3s) (37 warnings)
|
||||
2. 1) data/equipment/index.ts > data/equipment/utils.ts
|
||||
3. 2) data/golems/index.ts > data/golems/utils.ts
|
||||
4. 3) stores/combat-actions.ts > stores/combatStore.ts
|
||||
|
||||
@@ -1,26 +1,10 @@
|
||||
{
|
||||
"_meta": {
|
||||
"generated": "2026-05-18T09:26:27.302Z",
|
||||
"generated": "2026-05-18T10:08:42.124Z",
|
||||
"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."
|
||||
},
|
||||
"graph": {
|
||||
"attunements/data.ts": [
|
||||
"types.ts"
|
||||
],
|
||||
"attunements/index.ts": [
|
||||
"attunements/data.ts",
|
||||
"attunements/types.ts",
|
||||
"attunements/utils.ts"
|
||||
],
|
||||
"attunements/types.ts": [],
|
||||
"attunements/utils.ts": [
|
||||
"attunements/data.ts",
|
||||
"attunements/types.ts"
|
||||
],
|
||||
"computed-stats.ts": [
|
||||
"utils/index.ts"
|
||||
],
|
||||
"constants.ts": [
|
||||
"constants/index.ts"
|
||||
],
|
||||
@@ -379,9 +363,6 @@
|
||||
"types.ts",
|
||||
"utils/discipline-math.ts"
|
||||
],
|
||||
"formatting.ts": [
|
||||
"computed-stats.ts"
|
||||
],
|
||||
"hooks/useGameDerived.ts": [
|
||||
"constants.ts",
|
||||
"special-effects.ts",
|
||||
@@ -389,10 +370,6 @@
|
||||
"store/computed.ts",
|
||||
"upgrade-effects.ts"
|
||||
],
|
||||
"navigation-slice.ts": [
|
||||
"computed-stats.ts",
|
||||
"types.ts"
|
||||
],
|
||||
"special-effects.ts": [
|
||||
"upgrade-effects.types.ts"
|
||||
],
|
||||
@@ -528,10 +505,6 @@
|
||||
"store/crafting-modules/types.ts",
|
||||
"store/crafting-modules/utils.ts"
|
||||
],
|
||||
"store/index.ts": [
|
||||
"store.ts",
|
||||
"store/computed.ts"
|
||||
],
|
||||
"store/manaSlice.ts": [
|
||||
"constants.ts",
|
||||
"special-effects.ts",
|
||||
@@ -578,18 +551,14 @@
|
||||
"stores/craftingStore.ts": [
|
||||
"crafting-actions/application-actions.ts",
|
||||
"crafting-actions/preparation-actions.ts",
|
||||
"crafting-apply.ts",
|
||||
"crafting-design.ts",
|
||||
"crafting-equipment.ts",
|
||||
"crafting-slice.ts",
|
||||
"crafting-utils.ts",
|
||||
"special-effects.ts",
|
||||
"store/crafting-modules/starting-equipment.ts",
|
||||
"stores/combatStore.ts",
|
||||
"stores/gameStore.ts",
|
||||
"stores/manaStore.ts",
|
||||
"stores/uiStore.ts",
|
||||
"types.ts",
|
||||
"upgrade-effects.ts"
|
||||
"types.ts"
|
||||
],
|
||||
"stores/discipline-slice.ts": [
|
||||
"data/disciplines/base.ts",
|
||||
@@ -604,7 +573,6 @@
|
||||
"stores/manaStore.ts",
|
||||
"stores/prestigeStore.ts",
|
||||
"stores/uiStore.ts",
|
||||
"upgrade-effects.ts",
|
||||
"utils/index.ts"
|
||||
],
|
||||
"stores/gameHooks.ts": [
|
||||
@@ -637,7 +605,6 @@
|
||||
"stores/manaStore.ts",
|
||||
"stores/prestigeStore.ts",
|
||||
"stores/uiStore.ts",
|
||||
"upgrade-effects.ts",
|
||||
"utils/index.ts"
|
||||
],
|
||||
"stores/index.ts": [
|
||||
@@ -662,12 +629,6 @@
|
||||
"types.ts"
|
||||
],
|
||||
"stores/uiStore.ts": [],
|
||||
"study-slice.ts": [
|
||||
"constants.ts",
|
||||
"special-effects.ts",
|
||||
"types.ts",
|
||||
"upgrade-effects.ts"
|
||||
],
|
||||
"types.ts": [
|
||||
"data/equipment/types.ts",
|
||||
"types/attunements.ts",
|
||||
|
||||
+10
-34
@@ -103,11 +103,13 @@ Mana-Loop/
|
||||
│ │ │ │ ├── GameStateDebug.tsx
|
||||
│ │ │ │ ├── GolemDebug.tsx
|
||||
│ │ │ │ ├── PactDebug.tsx
|
||||
│ │ │ │ ├── debug-context.tsx
|
||||
│ │ │ │ └── index.tsx
|
||||
│ │ │ ├── shared/
|
||||
│ │ │ │ └── MemorySlotPicker.tsx
|
||||
│ │ │ ├── tabs/
|
||||
│ │ │ │ └── DisciplinesTab.tsx
|
||||
│ │ │ │ ├── DisciplinesTab.tsx
|
||||
│ │ │ │ └── index.ts
|
||||
│ │ │ ├── AchievementsDisplay.tsx
|
||||
│ │ │ ├── ActionButtons.tsx
|
||||
│ │ │ ├── ActivityLogPanel.tsx
|
||||
@@ -247,34 +249,13 @@ Mana-Loop/
|
||||
│ │ │ ├── enchantment-types.ts
|
||||
│ │ │ └── loot-drops.ts
|
||||
│ │ ├── effects/
|
||||
│ │ │ └── discipline-effects.ts
|
||||
│ │ │ ├── discipline-effects.ts
|
||||
│ │ │ ├── dynamic-compute.ts
|
||||
│ │ │ ├── special-effects.ts
|
||||
│ │ │ ├── upgrade-effects.ts
|
||||
│ │ │ └── upgrade-effects.types.ts
|
||||
│ │ ├── hooks/
|
||||
│ │ │ └── useGameDerived.ts
|
||||
│ │ ├── store/
|
||||
│ │ │ ├── crafting-modules/
|
||||
│ │ │ │ ├── initial-state.ts
|
||||
│ │ │ │ ├── selectors.ts
|
||||
│ │ │ │ ├── slice-logic.ts
|
||||
│ │ │ │ ├── starting-equipment.ts
|
||||
│ │ │ │ ├── tick-processors.ts
|
||||
│ │ │ │ ├── types.ts
|
||||
│ │ │ │ └── utils.ts
|
||||
│ │ │ ├── combatSlice.ts
|
||||
│ │ │ ├── computed.ts
|
||||
│ │ │ ├── craftingSlice.ts
|
||||
│ │ │ ├── manaSlice.ts
|
||||
│ │ │ ├── pactSlice.ts
|
||||
│ │ │ ├── prestigeSlice.ts
|
||||
│ │ │ └── timeSlice.ts
|
||||
│ │ ├── store-modules/
|
||||
│ │ │ ├── {room-utils,enemy-utils,initial-state,activity-log,store-actions}/
|
||||
│ │ │ ├── activity-log.ts
|
||||
│ │ │ ├── computed-stats.ts
|
||||
│ │ │ ├── enemy-utils.ts
|
||||
│ │ │ ├── initial-state.ts
|
||||
│ │ │ ├── room-utils.ts
|
||||
│ │ │ ├── store-actions.ts
|
||||
│ │ │ └── tick-logic.ts
|
||||
│ │ ├── stores/
|
||||
│ │ │ ├── attunementStore.ts
|
||||
│ │ │ ├── combat-actions.ts
|
||||
@@ -308,6 +289,7 @@ Mana-Loop/
|
||||
│ │ │ ├── formatting.ts
|
||||
│ │ │ ├── index.ts
|
||||
│ │ │ ├── mana-utils.ts
|
||||
│ │ │ ├── pact-utils.ts
|
||||
│ │ │ └── room-utils.ts
|
||||
│ │ ├── constants.ts
|
||||
│ │ ├── crafting-apply.ts
|
||||
@@ -318,23 +300,17 @@ Mana-Loop/
|
||||
│ │ ├── crafting-prep.ts
|
||||
│ │ ├── crafting-slice.ts
|
||||
│ │ ├── crafting-utils.ts
|
||||
│ │ ├── debug-context.tsx
|
||||
│ │ ├── dynamic-compute.ts
|
||||
│ │ ├── effects.ts
|
||||
│ │ ├── special-effects.ts
|
||||
│ │ ├── store.test.ts
|
||||
│ │ ├── store.ts
|
||||
│ │ ├── stores.test.ts
|
||||
│ │ ├── types.ts
|
||||
│ │ ├── upgrade-effects.ts
|
||||
│ │ └── upgrade-effects.types.ts
|
||||
│ │ └── types.ts
|
||||
│ └── utils.ts
|
||||
├── test-results/
|
||||
│ └── .last-run.json
|
||||
├── .dockerignore
|
||||
├── .gitignore
|
||||
├── AGENTS.md
|
||||
├── CLAUDE.md
|
||||
├── Caddyfile
|
||||
├── Dockerfile
|
||||
├── README.md
|
||||
|
||||
@@ -8,7 +8,7 @@ import { ManaDisplay } from '@/components/game';
|
||||
import { ActionButtons } from '@/components/game';
|
||||
import { AttunementStatus } from '@/components/game/AttunementStatus';
|
||||
import { ActivityLogPanel } from '@/components/game/ActivityLogPanel';
|
||||
import { DebugName } from '@/lib/game/debug-context';
|
||||
import { DebugName } from '@/components/game/debug/debug-context';
|
||||
import { useGameStore, useManaStore, useCombatStore, useCraftingStore, usePrestigeStore } from '@/lib/game/stores';
|
||||
import { getUnifiedEffects } from '@/lib/game/effects';
|
||||
import { getMeditationBonus, getIncursionStrength } from '@/lib/game/stores';
|
||||
|
||||
+1
-1
@@ -3,7 +3,7 @@ import localFont from "next/font/local";
|
||||
import "./globals.css";
|
||||
import { Toaster } from "@/components/ui/toaster";
|
||||
import { GameToaster } from "@/components/game/GameToast";
|
||||
import { DebugProvider } from "@/lib/game/debug-context";
|
||||
import { DebugProvider } from "@/components/game/debug/debug-context";
|
||||
|
||||
const geistSans = localFont({
|
||||
src: '../../public/fonts/GeistVF.woff',
|
||||
|
||||
+8
-82
@@ -31,24 +31,16 @@ import { ScrollArea } from '@/components/ui/scroll-area';
|
||||
|
||||
import { TooltipProvider } from '@/components/ui/tooltip';
|
||||
import { ErrorBoundary } from '@/components/ErrorBoundary';
|
||||
import { DebugName } from '@/lib/game/debug-context';
|
||||
import { DebugName } from '@/components/game/debug/debug-context';
|
||||
|
||||
// Import extracted components
|
||||
import { GameOverScreen } from './components/GameOverScreen';
|
||||
import { LeftPanel } from './components/LeftPanel';
|
||||
|
||||
// Lazy load tab components
|
||||
const SpireTab = lazy(() => import('@/components/game/tabs').then(module => ({ default: module.SpireTab })));
|
||||
const DisciplinesTab = lazy(() => import('@/components/game/tabs').then(module => ({ default: module.DisciplinesTab })));
|
||||
const SpellsTab = lazy(() => import('@/components/game/tabs').then(module => ({ default: module.SpellsTab })));
|
||||
|
||||
const StatsTab = lazy(() => import('@/components/game/tabs').then(module => ({ default: module.StatsTab })));
|
||||
const EquipmentTab = lazy(() => import('@/components/game/tabs').then(module => ({ default: module.EquipmentTab })));
|
||||
const AttunementsTab = lazy(() => import('@/components/game/tabs').then(module => ({ default: module.AttunementsTab })));
|
||||
const DebugTab = lazy(() => import('@/components/game/tabs').then(module => ({ default: module.DebugTab })));
|
||||
const LootTab = lazy(() => import('@/components/game/tabs').then(module => ({ default: module.LootTab })));
|
||||
const AchievementsTab = lazy(() => import('@/components/game/tabs').then(module => ({ default: module.AchievementsTab })));
|
||||
const GolemancyTab = lazy(() => import('@/components/game/tabs').then(module => ({ default: module.GolemancyTab })));
|
||||
const CraftingTab = lazy(() => import('@/components/game/tabs').then(module => ({ default: module.CraftingTab })));
|
||||
|
||||
const TabLoadingFallback = () => <div className="p-4 text-center text-gray-400">Loading...</div>;
|
||||
|
||||
@@ -125,7 +117,7 @@ function GrimoireTab() {
|
||||
|
||||
export default function ManaLoopGame() {
|
||||
const [selectedManaType, setSelectedManaType] = useState<string>('');
|
||||
const [activeTab, setActiveTab] = useState('spire');
|
||||
const [activeTab, setActiveTab] = useState('spells');
|
||||
|
||||
// ALL hooks must be called before any conditional returns
|
||||
const day = useGameStore((s) => s.day);
|
||||
@@ -208,7 +200,7 @@ export default function ManaLoopGame() {
|
||||
// React to spireMode changes from combat store
|
||||
useEffect(() => {
|
||||
if (spireMode) {
|
||||
setActiveTab('spire'); // eslint-disable-line react-hooks/set-state-in-effect
|
||||
setActiveTab('spells'); // eslint-disable-line react-hooks/set-state-in-effect
|
||||
}
|
||||
}, [spireMode]);
|
||||
|
||||
@@ -240,44 +232,12 @@ export default function ManaLoopGame() {
|
||||
<div className="flex-1 min-w-0">
|
||||
<Tabs value={activeTab} onValueChange={setActiveTab}>
|
||||
<TabsList className="flex flex-wrap gap-1 w-full mb-4 h-auto">
|
||||
<TabsTrigger value="spire" className="text-xs px-2 py-1">⚔️ Spire</TabsTrigger>
|
||||
<TabsTrigger value="attunements" className="text-xs px-2 py-1">✨ Attune</TabsTrigger>
|
||||
<TabsTrigger value="golemancy" className="text-xs px-2 py-1">🗿 Golems</TabsTrigger>
|
||||
<TabsTrigger value="spells" className="text-xs px-2 py-1">🔮 Spells</TabsTrigger>
|
||||
<TabsTrigger value="equipment" className="text-xs px-2 py-1">🛡️ Gear</TabsTrigger>
|
||||
<TabsTrigger value="crafting" className="text-xs px-2 py-1">🔧 Craft</TabsTrigger>
|
||||
<TabsTrigger value="loot" className="text-xs px-2 py-1">💎 Loot</TabsTrigger>
|
||||
<TabsTrigger value="achievements" className="text-xs px-2 py-1">🏆 Achieve</TabsTrigger>
|
||||
|
||||
<TabsTrigger value="stats" className="text-xs px-2 py-1">📊 Stats</TabsTrigger>
|
||||
<TabsTrigger value="debug" className="text-xs px-2 py-1">🐛 Debug</TabsTrigger>
|
||||
<TabsTrigger value="disciplines" className="text-xs px-2 py-1">📚 Disciplines</TabsTrigger>
|
||||
<TabsTrigger value="grimoire" className="text-xs px-2 py-1">📖 Grimoire</TabsTrigger>
|
||||
</TabsList>
|
||||
|
||||
<TabsContent value="spire">
|
||||
<ErrorBoundary fallback={<div className="p-4 text-red-400">spire tab failed to load.</div>}>
|
||||
<Suspense fallback={<TabLoadingFallback />}>
|
||||
<SpireTab simpleMode={spireMode} />
|
||||
</Suspense>
|
||||
</ErrorBoundary>
|
||||
</TabsContent>
|
||||
|
||||
<TabsContent value="attunements">
|
||||
<ErrorBoundary fallback={<div className="p-4 text-red-400">attunements tab failed to load.</div>}>
|
||||
<Suspense fallback={<TabLoadingFallback />}>
|
||||
<AttunementsTab />
|
||||
</Suspense>
|
||||
</ErrorBoundary>
|
||||
</TabsContent>
|
||||
|
||||
<TabsContent value="golemancy">
|
||||
<ErrorBoundary fallback={<div className="p-4 text-red-400">golemancy tab failed to load.</div>}>
|
||||
<Suspense fallback={<TabLoadingFallback />}>
|
||||
<GolemancyTab />
|
||||
</Suspense>
|
||||
</ErrorBoundary>
|
||||
</TabsContent>
|
||||
|
||||
<TabsContent value="spells">
|
||||
<ErrorBoundary fallback={<div className="p-4 text-red-400">spells tab failed to load.</div>}>
|
||||
<Suspense fallback={<TabLoadingFallback />}>
|
||||
@@ -286,40 +246,6 @@ export default function ManaLoopGame() {
|
||||
</ErrorBoundary>
|
||||
</TabsContent>
|
||||
|
||||
<TabsContent value="equipment">
|
||||
<ErrorBoundary fallback={<div className="p-4 text-red-400">equipment tab failed to load.</div>}>
|
||||
<Suspense fallback={<TabLoadingFallback />}>
|
||||
<EquipmentTab />
|
||||
</Suspense>
|
||||
</ErrorBoundary>
|
||||
</TabsContent>
|
||||
|
||||
<TabsContent value="crafting">
|
||||
<ErrorBoundary fallback={<div className="p-4 text-red-400">crafting tab failed to load.</div>}>
|
||||
<Suspense fallback={<TabLoadingFallback />}>
|
||||
<CraftingTab />
|
||||
</Suspense>
|
||||
</ErrorBoundary>
|
||||
</TabsContent>
|
||||
|
||||
<TabsContent value="loot">
|
||||
<ErrorBoundary fallback={<div className="p-4 text-red-400">loot tab failed to load.</div>}>
|
||||
<Suspense fallback={<TabLoadingFallback />}>
|
||||
<LootTab />
|
||||
</Suspense>
|
||||
</ErrorBoundary>
|
||||
</TabsContent>
|
||||
|
||||
<TabsContent value="achievements">
|
||||
<ErrorBoundary fallback={<div className="p-4 text-red-400">achievements tab failed to load.</div>}>
|
||||
<Suspense fallback={<TabLoadingFallback />}>
|
||||
<AchievementsTab />
|
||||
</Suspense>
|
||||
</ErrorBoundary>
|
||||
</TabsContent>
|
||||
|
||||
|
||||
|
||||
<TabsContent value="stats">
|
||||
<ErrorBoundary fallback={<div className="p-4 text-red-400">stats tab failed to load.</div>}>
|
||||
<Suspense fallback={<TabLoadingFallback />}>
|
||||
@@ -328,10 +254,10 @@ export default function ManaLoopGame() {
|
||||
</ErrorBoundary>
|
||||
</TabsContent>
|
||||
|
||||
<TabsContent value="debug">
|
||||
<ErrorBoundary fallback={<div className="p-4 text-red-400">debug tab failed to load.</div>}>
|
||||
<TabsContent value="disciplines">
|
||||
<ErrorBoundary fallback={<div className="p-4 text-red-400">disciplines tab failed to load.</div>}>
|
||||
<Suspense fallback={<TabLoadingFallback />}>
|
||||
<DebugTab />
|
||||
<DisciplinesTab />
|
||||
</Suspense>
|
||||
</ErrorBoundary>
|
||||
</TabsContent>
|
||||
|
||||
@@ -6,8 +6,8 @@ import { usePrestigeStore } from '@/lib/game/stores/prestigeStore';
|
||||
import { useUIStore } from '@/lib/game/stores/uiStore';
|
||||
import { useCombatStore } from '@/lib/game/stores/combatStore';
|
||||
import { useGameStore } from '@/lib/game/stores/gameStore';
|
||||
import { computeEffects } from '@/lib/game/upgrade-effects';
|
||||
import { hasSpecial, SPECIAL_EFFECTS } from '@/lib/game/special-effects';
|
||||
import { computeEffects } from '@/lib/game/effects/upgrade-effects';
|
||||
import { hasSpecial, SPECIAL_EFFECTS } from '@/lib/game/effects/special-effects';
|
||||
import {
|
||||
computeMaxMana,
|
||||
computeRegen,
|
||||
|
||||
@@ -3,8 +3,8 @@ import { useManaStore } from '@/lib/game/stores/manaStore';
|
||||
import { usePrestigeStore } from '@/lib/game/stores/prestigeStore';
|
||||
import { useUIStore } from '@/lib/game/stores/uiStore';
|
||||
import { useCombatStore } from '@/lib/game/stores/combatStore';
|
||||
import { computeEffects } from '@/lib/game/upgrade-effects';
|
||||
import { hasSpecial, SPECIAL_EFFECTS } from '@/lib/game/special-effects';
|
||||
import { computeEffects } from '@/lib/game/effects/upgrade-effects';
|
||||
import { hasSpecial, SPECIAL_EFFECTS } from '@/lib/game/effects/special-effects';
|
||||
import { getBoonBonuses } from '@/lib/game/utils';
|
||||
|
||||
// Define a unified store type that combines all stores
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Progress } from '@/components/ui/progress';
|
||||
import { BookOpen, X } from 'lucide-react';
|
||||
import { SKILLS_DEF, SPELLS_DEF } from '@/lib/game/constants';
|
||||
import { SPELLS_DEF } from '@/lib/game/constants';
|
||||
import { formatStudyTime } from '@/lib/game/utils/formatting';
|
||||
import type { StudyTarget } from '@/lib/game/types';
|
||||
|
||||
@@ -25,7 +25,7 @@ export function StudyProgress({
|
||||
const target = currentStudyTarget;
|
||||
const progressPct = Math.min(100, (target.progress / target.required) * 100);
|
||||
const isSkill = target.type === 'skill';
|
||||
const def = isSkill ? SKILLS_DEF[target.id] : SPELLS_DEF[target.id];
|
||||
const def = isSkill ? undefined : SPELLS_DEF[target.id];
|
||||
const currentLevel = isSkill ? (skills[target.id] || 0) : 0;
|
||||
|
||||
return (
|
||||
@@ -34,7 +34,7 @@ export function StudyProgress({
|
||||
<div className="flex items-center gap-2">
|
||||
<BookOpen className="w-4 h-4 text-purple-400" />
|
||||
<span className="text-sm font-semibold text-purple-300">
|
||||
{def?.name}
|
||||
{def?.name ?? target.id}
|
||||
{isSkill && ` Lv.${currentLevel + 1}`}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
'use client';
|
||||
|
||||
import { SKILLS_DEF } from '@/lib/game/constants';
|
||||
import type { SkillUpgradeChoice } from '@/lib/game/types';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Badge } from '@/components/ui/badge';
|
||||
@@ -33,7 +32,6 @@ export function UpgradeDialog({
|
||||
}: UpgradeDialogProps) {
|
||||
if (!skillId) return null;
|
||||
|
||||
const skillDef = SKILLS_DEF[skillId];
|
||||
const currentSelections = pendingSelections.length > 0 ? pendingSelections : alreadySelected;
|
||||
|
||||
return (
|
||||
@@ -41,7 +39,7 @@ export function UpgradeDialog({
|
||||
<DialogContent className="bg-gray-900 border-gray-700 max-w-lg">
|
||||
<DialogHeader>
|
||||
<DialogTitle className="text-amber-400">
|
||||
Choose Upgrade - {skillDef?.name || skillId}
|
||||
Choose Upgrade - {skillId}
|
||||
</DialogTitle>
|
||||
<DialogDescription className="text-gray-400">
|
||||
Level {milestone} Milestone - Select 2 upgrades ({currentSelections.length}/2 chosen)
|
||||
|
||||
@@ -9,7 +9,7 @@ import { Label } from '@/components/ui/label';
|
||||
import {
|
||||
RotateCcw, AlertTriangle, Zap, Clock, Settings, Eye,
|
||||
} from 'lucide-react';
|
||||
import { useDebug } from '@/lib/game/debug-context';
|
||||
import { useDebug } from '@/components/game/debug/debug-context';
|
||||
import { useGameStore, useManaStore, useUIStore, useCombatStore } from '@/lib/game/stores';
|
||||
import { computeMaxMana } from '@/lib/game/stores';
|
||||
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
export { GameStateDebug } from './GameStateDebug';
|
||||
export { SkillDebug } from './SkillDebug';
|
||||
export { ElementDebug } from './ElementDebug';
|
||||
export { AttunementDebug } from './AttunementDebug';
|
||||
export { GolemDebug } from './GolemDebug';
|
||||
|
||||
@@ -7,8 +7,6 @@ import { Badge } from '@/components/ui/badge';
|
||||
import { ScrollArea } from '@/components/ui/scroll-area';
|
||||
import { Save, Trash2, Star } from 'lucide-react';
|
||||
import { useGameContext } from '../GameContext';
|
||||
import { SKILLS_DEF } from '@/lib/game/constants';
|
||||
import { getTierMultiplier, getBaseSkillId } from '@/lib/game/skill-evolution';
|
||||
import type { Memory } from '@/lib/game/types';
|
||||
|
||||
interface MemorySlotPickerProps {
|
||||
@@ -25,11 +23,10 @@ export function MemorySlotPicker({ onConfirm }: MemorySlotPickerProps) {
|
||||
|
||||
for (const [skillId, level] of Object.entries(store.skills)) {
|
||||
if (level && level > 0) {
|
||||
const baseSkillId = getBaseSkillId(skillId);
|
||||
const baseSkillId = skillId;
|
||||
const tier = store.skillTiers?.[baseSkillId] || 1;
|
||||
const tieredSkillId = tier > 1 ? `${baseSkillId}_t${tier}` : baseSkillId;
|
||||
const upgrades = store.skillUpgrades?.[tieredSkillId] || [];
|
||||
const skillDef = SKILLS_DEF[baseSkillId];
|
||||
|
||||
// Only include if it's a base skill (not a tiered variant in the skills object)
|
||||
if (skillId === baseSkillId || skillId.includes('_t')) {
|
||||
@@ -41,7 +38,7 @@ export function MemorySlotPicker({ onConfirm }: MemorySlotPickerProps) {
|
||||
level: actualLevel,
|
||||
tier,
|
||||
upgrades,
|
||||
name: skillDef?.name || baseSkillId,
|
||||
name: baseSkillId,
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -116,22 +113,19 @@ export function MemorySlotPicker({ onConfirm }: MemorySlotPickerProps) {
|
||||
<div className="space-y-1">
|
||||
<div className="text-xs text-green-400 game-panel-title">Saved to Memory:</div>
|
||||
<div className="flex flex-wrap gap-1">
|
||||
{selectedSkills.map((memory) => {
|
||||
const skillDef = SKILLS_DEF[memory.skillId];
|
||||
return (
|
||||
{selectedSkills.map((memory) => (
|
||||
<Badge
|
||||
key={memory.skillId}
|
||||
className="bg-amber-900/50 text-amber-200 cursor-pointer hover:bg-red-900/50"
|
||||
onClick={() => toggleSkill(memory.skillId)}
|
||||
>
|
||||
{skillDef?.name || memory.skillId}
|
||||
{memory.skillId}
|
||||
{' '}Lv.{memory.level}
|
||||
{memory.tier > 1 && ` T${memory.tier}`}
|
||||
{memory.upgrades.length > 0 && ` (${memory.upgrades.length}⭐)`}
|
||||
<Trash2 className="w-3 h-3 ml-1" />
|
||||
</Badge>
|
||||
);
|
||||
})}
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
@@ -147,7 +141,7 @@ export function MemorySlotPicker({ onConfirm }: MemorySlotPickerProps) {
|
||||
) : (
|
||||
saveableSkills.map((skill) => {
|
||||
const isSelected = isSkillSelected(skill.skillId);
|
||||
const tierMult = getTierMultiplier(skill.tier > 1 ? `${skill.skillId}_t${skill.tier}` : skill.skillId);
|
||||
const tierMult = 1;
|
||||
|
||||
return (
|
||||
<div
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
// ─── Tab Components Barrel ────────────────────────────────────────────────────
|
||||
// Re-exports all existing tab components for lazy loading from page.tsx
|
||||
|
||||
export { DisciplinesTab } from './DisciplinesTab';
|
||||
export { SpellsTab } from '../SpellsTab';
|
||||
export { StatsTab } from '../StatsTab';
|
||||
@@ -30,6 +30,5 @@ export { ManaBar } from "./mana-bar";
|
||||
export { ElementBadge } from "./element-badge";
|
||||
export { ValueDisplay } from "./value-display";
|
||||
export { ActionButton, actionButtonVariants } from "./action-button";
|
||||
export { SkillRow } from "./skill-row";
|
||||
export { TooltipInfo } from "./tooltip-info";
|
||||
export { Stepper } from "./stepper";
|
||||
|
||||
@@ -16,14 +16,6 @@ export { GUARDIANS } from './guardians';
|
||||
// Spell constants
|
||||
export { SPELLS_DEF } from './spells';
|
||||
|
||||
// Skill constants (legacy)
|
||||
export { SKILLS_DEF, SKILL_CATEGORIES, EFFECT_RESEARCH_MAPPING, BASE_UNLOCKED_EFFECTS, ENCHANTING_UNLOCK_EFFECTS } from './skills';
|
||||
|
||||
// Skill System v2
|
||||
export { computeStats, BASE_STATS, SKILLS_V2 } from './skills-v2';
|
||||
export type { ComputedStats, SkillV2Def, SkillEffect, StatKey } from './skills-v2-types';
|
||||
export { getBaseSkillId, hasPrerequisites } from './skills-v2';
|
||||
|
||||
// Prestige constants
|
||||
export { PRESTIGE_DEF } from './prestige';
|
||||
|
||||
|
||||
@@ -3,8 +3,8 @@
|
||||
import type { GameState, EnchantmentDesign, DesignEffect } from '../types';
|
||||
import * as CraftingUtils from '../crafting-utils';
|
||||
import * as CraftingDesign from '../crafting-design';
|
||||
import { computeEffects } from '../upgrade-effects';
|
||||
import { hasSpecial, SPECIAL_EFFECTS } from '../special-effects';
|
||||
import { computeEffects } from '../effects/upgrade-effects';
|
||||
import { hasSpecial, SPECIAL_EFFECTS } from '../effects/special-effects';
|
||||
|
||||
export function startDesigningEnchantment(
|
||||
name: string,
|
||||
|
||||
@@ -3,8 +3,8 @@
|
||||
|
||||
import type { EquipmentInstance, AppliedEnchantment, EnchantmentDesign, ApplicationProgress } from './types';
|
||||
import { calculateApplicationTime, calculateApplicationManaPerHour } from './crafting-utils';
|
||||
import { hasSpecial, SPECIAL_EFFECTS } from './special-effects';
|
||||
import { computeEffects } from './upgrade-effects';
|
||||
import { hasSpecial, SPECIAL_EFFECTS } from './effects/special-effects';
|
||||
import { computeEffects } from './effects/upgrade-effects';
|
||||
import type { AttunementState } from './types';
|
||||
import { calculateEnchantingXP, getAttunementXPForLevel, MAX_ATTUNEMENT_LEVEL } from './data/attunements';
|
||||
import { ENCHANTMENT_EFFECTS } from './data/enchantment-effects';
|
||||
|
||||
@@ -4,8 +4,8 @@
|
||||
import type { EnchantmentDesign, DesignEffect, AppliedEnchantment } from './types';
|
||||
import { calculateEnchantingXP } from './data/attunements';
|
||||
import { ENCHANTMENT_EFFECTS, calculateEffectCapacityCost } from './data/enchantment-effects';
|
||||
import { hasSpecial, SPECIAL_EFFECTS } from './special-effects';
|
||||
import { computeEffects } from './upgrade-effects';
|
||||
import { hasSpecial, SPECIAL_EFFECTS } from './effects/special-effects';
|
||||
import { computeEffects } from './effects/upgrade-effects';
|
||||
import { EQUIPMENT_TYPES, type EquipmentCategory } from './data/equipment';
|
||||
|
||||
// ─── Design Creation & Calculation ──────────────────────────────────────────
|
||||
|
||||
@@ -10,9 +10,9 @@ import { ENCHANTMENT_EFFECTS, calculateEffectCapacityCost } from './data/enchant
|
||||
import { CRAFTING_RECIPES, canCraftRecipe, type CraftingRecipe } from './data/crafting-recipes';
|
||||
import { SPELLS_DEF } from './constants';
|
||||
import { calculateEnchantingXP, getAttunementXPForLevel, MAX_ATTUNEMENT_LEVEL } from './data/attunements';
|
||||
import { computeEffects } from './upgrade-effects';
|
||||
import { hasSpecial, SPECIAL_EFFECTS } from './special-effects';
|
||||
import type { ComputedEffects } from './upgrade-effects.types';
|
||||
import { computeEffects } from './effects/upgrade-effects';
|
||||
import { hasSpecial, SPECIAL_EFFECTS } from './effects/special-effects';
|
||||
import type { ComputedEffects } from './effects/upgrade-effects.types';
|
||||
|
||||
// ─── Crafting Modules ───────────────────────────────────────────────────────
|
||||
|
||||
|
||||
@@ -6,15 +6,15 @@
|
||||
|
||||
import type { GameState, EquipmentInstance } from './types';
|
||||
import { ENCHANTMENT_EFFECTS } from './data/enchantment-effects';
|
||||
import { computeEffects } from './upgrade-effects';
|
||||
import { hasSpecial, SPECIAL_EFFECTS } from './special-effects';
|
||||
import { computeEffects } from './effects/upgrade-effects';
|
||||
import { hasSpecial, SPECIAL_EFFECTS } from './effects/special-effects';
|
||||
import { computeDisciplineEffects } from './effects/discipline-effects';
|
||||
import type { ComputedEffects } from './upgrade-effects.types';
|
||||
|
||||
// Re-export for convenience
|
||||
export { computeEffects } from './upgrade-effects';
|
||||
export { hasSpecial, SPECIAL_EFFECTS } from './special-effects';
|
||||
export type { ComputedEffects } from './upgrade-effects.types';
|
||||
export { computeEffects } from './effects/upgrade-effects';
|
||||
export { hasSpecial, SPECIAL_EFFECTS } from './effects/special-effects';
|
||||
export type { ComputedEffects } from './effects/upgrade-effects.types';
|
||||
|
||||
// ─── Equipment Effect Computation ────────────────────────────────────────────
|
||||
|
||||
|
||||
Executable
+68
@@ -0,0 +1,68 @@
|
||||
// ─── Upgrade Effect System ────────────────────────────────────────────────────────
|
||||
// This module handles applying skill upgrade effects to game stats
|
||||
// Note: Skill evolution paths have been removed. Upgrade effects now return
|
||||
// base/default values with no skill-dependent modifications.
|
||||
|
||||
import type { ActiveUpgradeEffect, ComputedEffects } from './upgrade-effects.types';
|
||||
|
||||
// ─── Upgrade Definition Cache ───────────────────────────
|
||||
|
||||
// No-op: skill evolution paths have been removed
|
||||
function buildUpgradeCache(): void {
|
||||
// No-op: no upgrade definitions to cache
|
||||
}
|
||||
|
||||
// ─── Helper Functions ──────────────────────────────
|
||||
|
||||
/**
|
||||
* Get all selected upgrades with their full effect definitions.
|
||||
* Since skills are removed, always returns empty array.
|
||||
*/
|
||||
export function getActiveUpgrades(
|
||||
_skillUpgrades: Record<string, string[]>,
|
||||
_skillTiers: Record<string, number>
|
||||
): ActiveUpgradeEffect[] {
|
||||
return [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Compute all active effects from selected upgrades.
|
||||
* Since skills are removed, returns base values with no upgrades applied.
|
||||
*/
|
||||
export function computeEffects(
|
||||
_skillUpgrades: Record<string, string[]>,
|
||||
_skillTiers: Record<string, number>
|
||||
): ComputedEffects {
|
||||
return {
|
||||
maxManaMultiplier: 1,
|
||||
maxManaBonus: 0,
|
||||
regenMultiplier: 1,
|
||||
regenBonus: 0,
|
||||
clickManaMultiplier: 1,
|
||||
clickManaBonus: 0,
|
||||
meditationEfficiency: 1,
|
||||
spellCostMultiplier: 1,
|
||||
conversionEfficiency: 1,
|
||||
baseDamageMultiplier: 1,
|
||||
baseDamageBonus: 0,
|
||||
attackSpeedMultiplier: 1,
|
||||
critChanceBonus: 0,
|
||||
critDamageMultiplier: 1.5,
|
||||
elementalDamageMultiplier: 1,
|
||||
studySpeedMultiplier: 1,
|
||||
studyCostMultiplier: 1,
|
||||
progressRetention: 0,
|
||||
instantStudyChance: 0,
|
||||
freeStudyChance: 0,
|
||||
elementCapMultiplier: 1,
|
||||
elementCapBonus: 0,
|
||||
perElementCapBonus: {},
|
||||
conversionCostMultiplier: 1,
|
||||
doubleCraftChance: 0,
|
||||
permanentRegenBonus: 0,
|
||||
specials: new Set<string>(),
|
||||
activeUpgrades: [],
|
||||
skillLevelMultiplier: 1,
|
||||
enchantmentPowerMultiplier: 1,
|
||||
};
|
||||
}
|
||||
@@ -2,8 +2,11 @@
|
||||
// Custom hooks for computing derived game stats from the store
|
||||
|
||||
import { useMemo } from 'react';
|
||||
import { useGameStore } from '../store';
|
||||
import { computeEffects } from '../upgrade-effects';
|
||||
import { useGameStore } from '../stores/gameStore';
|
||||
import { useManaStore } from '../stores/manaStore';
|
||||
import { useCombatStore } from '../stores/combatStore';
|
||||
import { usePrestigeStore } from '../stores/prestigeStore';
|
||||
import { computeEffects } from '../effects/upgrade-effects';
|
||||
import {
|
||||
computeMaxMana,
|
||||
computeRegen,
|
||||
@@ -12,47 +15,53 @@ import {
|
||||
getIncursionStrength,
|
||||
getFloorElement,
|
||||
calcDamage,
|
||||
computePactMultiplier,
|
||||
computePactInsightMultiplier,
|
||||
getElementalBonus,
|
||||
} from '../store/computed';
|
||||
} from '../utils';
|
||||
import { computePactMultiplier, computePactInsightMultiplier } from '../utils/pact-utils';
|
||||
import { ELEMENTS, GUARDIANS, SPELLS_DEF, getStudySpeedMultiplier, getStudyCostMultiplier, HOURS_PER_TICK, TICK_MS } from '../constants';
|
||||
import { hasSpecial, SPECIAL_EFFECTS } from '../special-effects';
|
||||
import { hasSpecial, SPECIAL_EFFECTS } from '../effects/special-effects';
|
||||
|
||||
/**
|
||||
* Hook for all mana-related derived stats
|
||||
*/
|
||||
export function useManaStats() {
|
||||
const store = useGameStore();
|
||||
const skills = useGameStore((s) => s.skills);
|
||||
const skillUpgrades = useGameStore((s) => s.skillUpgrades);
|
||||
const skillTiers = useGameStore((s) => s.skillTiers);
|
||||
const prestigeUpgrades = usePrestigeStore((s) => s.prestigeUpgrades);
|
||||
const rawMana = useManaStore((s) => s.rawMana);
|
||||
const meditateTicks = useManaStore((s) => s.meditateTicks);
|
||||
const day = useGameStore((s) => s.day);
|
||||
const hour = useGameStore((s) => s.hour);
|
||||
|
||||
const upgradeEffects = useMemo(
|
||||
() => computeEffects(store.skillUpgrades || {}, store.skillTiers || {}),
|
||||
[store.skillUpgrades, store.skillTiers]
|
||||
() => computeEffects(skillUpgrades || {}, skillTiers || {}),
|
||||
[skillUpgrades, skillTiers]
|
||||
);
|
||||
|
||||
const maxMana = useMemo(
|
||||
() => computeMaxMana(store, upgradeEffects),
|
||||
[store, upgradeEffects]
|
||||
() => computeMaxMana({ skills, prestigeUpgrades, skillUpgrades, skillTiers }, upgradeEffects),
|
||||
[skills, prestigeUpgrades, skillUpgrades, skillTiers, upgradeEffects]
|
||||
);
|
||||
|
||||
const baseRegen = useMemo(
|
||||
() => computeRegen(store, upgradeEffects),
|
||||
[store, upgradeEffects]
|
||||
() => computeRegen({ skills, prestigeUpgrades, skillUpgrades, skillTiers }, upgradeEffects),
|
||||
[skills, prestigeUpgrades, skillUpgrades, skillTiers, upgradeEffects]
|
||||
);
|
||||
|
||||
const clickMana = useMemo(
|
||||
() => computeClickMana(store),
|
||||
[store]
|
||||
() => computeClickMana({ skills }),
|
||||
[skills]
|
||||
);
|
||||
|
||||
const meditationMultiplier = useMemo(
|
||||
() => getMeditationBonus(store.meditateTicks, store.skills, upgradeEffects.meditationEfficiency),
|
||||
[store.meditateTicks, store.skills, upgradeEffects.meditationEfficiency]
|
||||
() => getMeditationBonus(meditateTicks, skills, upgradeEffects.meditationEfficiency),
|
||||
[meditateTicks, skills, upgradeEffects.meditationEfficiency]
|
||||
);
|
||||
|
||||
const incursionStrength = useMemo(
|
||||
() => getIncursionStrength(store.day, store.hour),
|
||||
[store.day, store.hour]
|
||||
() => getIncursionStrength(day, hour),
|
||||
[day, hour]
|
||||
);
|
||||
|
||||
// Effective regen with incursion penalty
|
||||
@@ -97,12 +106,15 @@ export function useManaStats() {
|
||||
* Hook for combat-related derived stats
|
||||
*/
|
||||
export function useCombatStats() {
|
||||
const store = useGameStore();
|
||||
const skills = useGameStore((s) => s.skills);
|
||||
const signedPacts = usePrestigeStore((s) => s.signedPacts);
|
||||
const currentFloor = useCombatStore((s) => s.currentFloor);
|
||||
const activeSpell = useCombatStore((s) => s.activeSpell);
|
||||
const { upgradeEffects } = useManaStats();
|
||||
|
||||
const floorElem = useMemo(
|
||||
() => getFloorElement(store.currentFloor),
|
||||
[store.currentFloor]
|
||||
() => getFloorElement(currentFloor),
|
||||
[currentFloor]
|
||||
);
|
||||
|
||||
const floorElemDef = useMemo(
|
||||
@@ -111,28 +123,28 @@ export function useCombatStats() {
|
||||
);
|
||||
|
||||
const isGuardianFloor = useMemo(
|
||||
() => !!GUARDIANS[store.currentFloor],
|
||||
[store.currentFloor]
|
||||
() => !!GUARDIANS[currentFloor],
|
||||
[currentFloor]
|
||||
);
|
||||
|
||||
const currentGuardian = useMemo(
|
||||
() => GUARDIANS[store.currentFloor],
|
||||
[store.currentFloor]
|
||||
() => GUARDIANS[currentFloor],
|
||||
[currentFloor]
|
||||
);
|
||||
|
||||
const activeSpellDef = useMemo(
|
||||
() => SPELLS_DEF[store.activeSpell],
|
||||
[store.activeSpell]
|
||||
() => SPELLS_DEF[activeSpell],
|
||||
[activeSpell]
|
||||
);
|
||||
|
||||
const pactMultiplier = useMemo(
|
||||
() => computePactMultiplier(store),
|
||||
[store]
|
||||
() => computePactMultiplier({ signedPacts }),
|
||||
[signedPacts]
|
||||
);
|
||||
|
||||
const pactInsightMultiplier = useMemo(
|
||||
() => computePactInsightMultiplier(store),
|
||||
[store]
|
||||
() => computePactInsightMultiplier({ signedPacts }),
|
||||
[signedPacts]
|
||||
);
|
||||
|
||||
// DPS calculation
|
||||
@@ -140,26 +152,26 @@ export function useCombatStats() {
|
||||
if (!activeSpellDef) return 0;
|
||||
|
||||
const spellCastSpeed = activeSpellDef.castSpeed || 1;
|
||||
const quickCastBonus = 1 + (store.skills.quickCast || 0) * 0.05;
|
||||
const quickCastBonus = 1 + (skills.quickCast || 0) * 0.05;
|
||||
const attackSpeedMult = upgradeEffects.attackSpeedMultiplier;
|
||||
const totalCastSpeed = spellCastSpeed * quickCastBonus * attackSpeedMult;
|
||||
|
||||
const damagePerCast = calcDamage(store, store.activeSpell, floorElem);
|
||||
const damagePerCast = calcDamage({ skills, signedPacts }, activeSpell, floorElem);
|
||||
const castsPerSecond = totalCastSpeed * HOURS_PER_TICK / (TICK_MS / 1000);
|
||||
|
||||
return damagePerCast * castsPerSecond;
|
||||
}, [activeSpellDef, store, floorElem, upgradeEffects.attackSpeedMultiplier]);
|
||||
}, [activeSpellDef, skills, signedPacts, activeSpell, floorElem, upgradeEffects.attackSpeedMultiplier]);
|
||||
|
||||
// Damage breakdown for display
|
||||
const damageBreakdown = useMemo(() => {
|
||||
if (!activeSpellDef) return null;
|
||||
|
||||
const baseDmg = activeSpellDef.dmg;
|
||||
const combatTrainBonus = (store.skills.combatTrain || 0) * 5;
|
||||
const arcaneFuryMult = 1 + (store.skills.arcaneFury || 0) * 0.1;
|
||||
const elemMasteryMult = 1 + (store.skills.elementalMastery || 0) * 0.15;
|
||||
const guardianBaneMult = isGuardianFloor ? (1 + (store.skills.guardianBane || 0) * 0.2) : 1;
|
||||
const precisionChance = (store.skills.precision || 0) * 0.05;
|
||||
const combatTrainBonus = (skills.combatTrain || 0) * 5;
|
||||
const arcaneFuryMult = 1 + (skills.arcaneFury || 0) * 0.1;
|
||||
const elemMasteryMult = 1 + (skills.elementalMastery || 0) * 0.15;
|
||||
const guardianBaneMult = isGuardianFloor ? (1 + (skills.guardianBane || 0) * 0.2) : 1;
|
||||
const precisionChance = (skills.precision || 0) * 0.05;
|
||||
|
||||
// Calculate elemental bonus
|
||||
const elemBonus = getElementalBonus(activeSpellDef.elem, floorElem);
|
||||
@@ -182,9 +194,9 @@ export function useCombatStats() {
|
||||
precisionChance,
|
||||
elemBonus,
|
||||
elemBonusText,
|
||||
total: calcDamage(store, store.activeSpell, floorElem),
|
||||
total: calcDamage({ skills, signedPacts }, activeSpell, floorElem),
|
||||
};
|
||||
}, [activeSpellDef, store, floorElem, isGuardianFloor, pactMultiplier]);
|
||||
}, [activeSpellDef, skills, signedPacts, activeSpell, floorElem, isGuardianFloor, pactMultiplier]);
|
||||
|
||||
return {
|
||||
floorElem,
|
||||
@@ -203,21 +215,23 @@ export function useCombatStats() {
|
||||
* Hook for study-related derived stats
|
||||
*/
|
||||
export function useStudyStats() {
|
||||
const store = useGameStore();
|
||||
const skills = useGameStore((s) => s.skills);
|
||||
const skillUpgrades = useGameStore((s) => s.skillUpgrades);
|
||||
const skillTiers = useGameStore((s) => s.skillTiers);
|
||||
|
||||
const studySpeedMult = useMemo(
|
||||
() => getStudySpeedMultiplier(store.skills),
|
||||
[store.skills]
|
||||
() => getStudySpeedMultiplier(skills),
|
||||
[skills]
|
||||
);
|
||||
|
||||
const studyCostMult = useMemo(
|
||||
() => getStudyCostMultiplier(store.skills),
|
||||
[store.skills]
|
||||
() => getStudyCostMultiplier(skills),
|
||||
[skills]
|
||||
);
|
||||
|
||||
const upgradeEffects = useMemo(
|
||||
() => computeEffects(store.skillUpgrades || {}, store.skillTiers || {}),
|
||||
[store.skillUpgrades, store.skillTiers]
|
||||
() => computeEffects(skillUpgrades || {}, skillTiers || {}),
|
||||
[skillUpgrades, skillTiers]
|
||||
);
|
||||
|
||||
const effectiveStudySpeedMult = studySpeedMult * upgradeEffects.studySpeedMultiplier;
|
||||
|
||||
@@ -1,29 +0,0 @@
|
||||
// ─── Activity Log Helper ────────────────────────────────────────────────────
|
||||
// Extracted from store.ts (lines 905-931)
|
||||
|
||||
import type { ActivityLogEntry } from '../types';
|
||||
|
||||
function createActivityEntry(
|
||||
eventType: string,
|
||||
message: string,
|
||||
details?: ActivityLogEntry['details']
|
||||
): ActivityLogEntry {
|
||||
return {
|
||||
id: `act_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`,
|
||||
timestamp: Date.now(), // Use timestamp for ordering
|
||||
eventType: eventType as any,
|
||||
message,
|
||||
details,
|
||||
};
|
||||
}
|
||||
|
||||
export function addActivityLogEntry(
|
||||
state: { activityLog: ActivityLogEntry[] },
|
||||
eventType: string,
|
||||
message: string,
|
||||
details?: ActivityLogEntry['details']
|
||||
): ActivityLogEntry[] {
|
||||
const entry = createActivityEntry(eventType, message, details);
|
||||
// Keep last 50 entries, newest first
|
||||
return [entry, ...state.activityLog.slice(0, 49)];
|
||||
}
|
||||
@@ -1,208 +0,0 @@
|
||||
// ─── Computed Stats Functions ─────────────────────────────────────────
|
||||
// Extracted from store.ts (lines 362-689)
|
||||
// Full implementations with UnifiedEffects support
|
||||
|
||||
import type { GameState, SpellCost, StudyTarget } from '../types';
|
||||
import type { ComputedEffects } from '../upgrade-effects.types';
|
||||
import type { UnifiedEffects } from '../effects';
|
||||
import { SPELLS_DEF, GUARDIANS, ELEMENT_OPPOSITES, HOURS_PER_TICK, TICK_MS, INCURSION_START_DAY, MAX_DAY, ELEMENTS } from '../constants';
|
||||
import { getUnifiedEffects } from '../effects';
|
||||
import { getTotalAttunementRegen, getTotalAttunementConversionDrain } from '../data/attunements';
|
||||
import { hasSpecial, SPECIAL_EFFECTS } from '../special-effects';
|
||||
|
||||
export function computeMaxMana(
|
||||
state: GameState,
|
||||
effects?: ComputedEffects | UnifiedEffects
|
||||
): number {
|
||||
const pu = state.prestigeUpgrades;
|
||||
const base = 100 + ((pu || {}).manaWell || 0) * 500;
|
||||
|
||||
// Check if we need to compute effects from equipment
|
||||
if (!effects && state.equipmentInstances && state.equippedInstances) {
|
||||
effects = getUnifiedEffects(state as any);
|
||||
}
|
||||
|
||||
let maxMana: number;
|
||||
if (effects) {
|
||||
maxMana = Math.floor((base + effects.maxManaBonus) * effects.maxManaMultiplier);
|
||||
} else {
|
||||
maxMana = base;
|
||||
}
|
||||
|
||||
if (effects && hasSpecial(effects, SPECIAL_EFFECTS.MANA_CONDENSE)) {
|
||||
const totalGathered = state.totalManaGathered || 0;
|
||||
const condensesBonus = Math.floor(totalGathered / 1000);
|
||||
maxMana = Math.floor(maxMana * (1 + condensesBonus * 0.01));
|
||||
}
|
||||
|
||||
return maxMana;
|
||||
}
|
||||
|
||||
export function computeElementMax(
|
||||
state: GameState,
|
||||
effects?: ComputedEffects | UnifiedEffects,
|
||||
element?: string
|
||||
): number {
|
||||
const pu = state.prestigeUpgrades;
|
||||
const base = 10 + (pu.elementalAttune || 0) * 25;
|
||||
|
||||
let adjustedBase = base;
|
||||
if (element && state.unlockedManaTypeUpgrades) {
|
||||
const typeUpgrades = state.unlockedManaTypeUpgrades.filter(u => u.typeId === element);
|
||||
const totalLevels = typeUpgrades.reduce((sum, u) => sum + u.level, 0);
|
||||
adjustedBase = base + (totalLevels * 10);
|
||||
}
|
||||
|
||||
if (effects) {
|
||||
let bonus = effects.elementCapBonus || 0;
|
||||
if (element && (effects as UnifiedEffects).perElementCapBonus) {
|
||||
const perElementBonus = (effects as UnifiedEffects).perElementCapBonus[element];
|
||||
if (perElementBonus) {
|
||||
bonus += perElementBonus;
|
||||
}
|
||||
}
|
||||
return Math.floor((adjustedBase + bonus) * (effects.elementCapMultiplier || 1));
|
||||
}
|
||||
return adjustedBase;
|
||||
}
|
||||
|
||||
export function computeRegen(
|
||||
state: GameState,
|
||||
effects?: ComputedEffects | UnifiedEffects
|
||||
): number {
|
||||
const pu = state.prestigeUpgrades;
|
||||
const temporalBonus = 1 + (pu.temporalEcho || 0) * 0.1;
|
||||
const base = 2 + (pu.manaFlow || 0) * 0.5;
|
||||
|
||||
let regen = base * temporalBonus;
|
||||
const attunementRegen = getTotalAttunementRegen(state.attunements || {});
|
||||
regen += attunementRegen;
|
||||
|
||||
if (!effects && state.equipmentInstances && state.equippedInstances) {
|
||||
effects = getUnifiedEffects(state as any);
|
||||
}
|
||||
|
||||
if (effects) {
|
||||
regen = (regen + effects.regenBonus + effects.permanentRegenBonus) * effects.regenMultiplier;
|
||||
}
|
||||
return regen;
|
||||
}
|
||||
|
||||
export function computeEffectiveRegenForDisplay(
|
||||
state: GameState,
|
||||
effects?: ComputedEffects | UnifiedEffects
|
||||
): { rawRegen: number; conversionDrain: number; effectiveRegen: number } {
|
||||
const rawRegen = computeRegen(state, effects);
|
||||
const conversionDrain = getTotalAttunementConversionDrain(state.attunements || {});
|
||||
const effectiveRegen = Math.max(0, rawRegen - conversionDrain);
|
||||
return { rawRegen, conversionDrain, effectiveRegen };
|
||||
}
|
||||
|
||||
export function computeEffectiveRegen(
|
||||
state: GameState,
|
||||
effects?: ComputedEffects
|
||||
): number {
|
||||
let regen = computeRegen(state, effects);
|
||||
const incursionStrength = state.incursionStrength || 0;
|
||||
regen *= (1 - incursionStrength);
|
||||
return regen;
|
||||
}
|
||||
|
||||
export function computeClickMana(
|
||||
state: GameState,
|
||||
effects?: ComputedEffects | UnifiedEffects
|
||||
): number {
|
||||
const base = 1;
|
||||
|
||||
if (!effects && state.equipmentInstances && state.equippedInstances) {
|
||||
effects = getUnifiedEffects(state as any);
|
||||
}
|
||||
|
||||
if (effects) {
|
||||
return Math.floor((base + effects.clickManaBonus) * effects.clickManaMultiplier);
|
||||
}
|
||||
return base;
|
||||
}
|
||||
|
||||
function getElementalBonus(spellElem: string, floorElem: string): number {
|
||||
if (spellElem === 'raw') return 1.0;
|
||||
if (spellElem === floorElem) return 1.25;
|
||||
if (ELEMENT_OPPOSITES[floorElem] === spellElem) return 1.5;
|
||||
if (ELEMENT_OPPOSITES[spellElem] === floorElem) return 0.75;
|
||||
return 1.0;
|
||||
}
|
||||
|
||||
export function calcDamage(
|
||||
state: Pick<GameState, 'skills' | 'signedPacts'>,
|
||||
spellId: string,
|
||||
floorElem?: string,
|
||||
effects?: ComputedEffects | UnifiedEffects
|
||||
): number {
|
||||
const sp = SPELLS_DEF[spellId];
|
||||
if (!sp) return 5;
|
||||
const baseDmg = sp.dmg;
|
||||
const pct = 1;
|
||||
const elemMasteryBonus = 1;
|
||||
const critChance = 0;
|
||||
const pactMult = state.signedPacts.reduce((m, f) => m * ((GUARDIANS as any)[f]?.pact || 1), 1);
|
||||
|
||||
let damage = baseDmg * pct * pactMult * elemMasteryBonus;
|
||||
if (floorElem) {
|
||||
damage *= getElementalBonus(sp.elem, floorElem);
|
||||
}
|
||||
if (Math.random() < critChance) {
|
||||
damage *= 1.5;
|
||||
}
|
||||
return damage;
|
||||
}
|
||||
|
||||
export function calcInsight(state: Pick<GameState, 'maxFloorReached' | 'totalManaGathered' | 'signedPacts' | 'prestigeUpgrades' | 'skills'>): number {
|
||||
const pu = state.prestigeUpgrades;
|
||||
const mult = (1 + (pu.insightAmp || 0) * 0.25);
|
||||
return Math.floor((state.maxFloorReached * 15 + state.totalManaGathered / 500 + state.signedPacts.length * 150) * mult);
|
||||
}
|
||||
|
||||
export function getMeditationBonus(meditateTicks: number, meditationEfficiency: number = 1): number {
|
||||
const hours = meditateTicks * HOURS_PER_TICK;
|
||||
let bonus = 1 + Math.min(hours / 4, 0.5);
|
||||
bonus *= meditationEfficiency;
|
||||
return bonus;
|
||||
}
|
||||
|
||||
export function getIncursionStrength(day: number, hour: number): number {
|
||||
if (day < INCURSION_START_DAY) return 0;
|
||||
const totalHours = (day - INCURSION_START_DAY) * 24 + hour;
|
||||
const maxHours = (MAX_DAY - INCURSION_START_DAY) * 24;
|
||||
return Math.min(0.95, (totalHours / maxHours) * 0.95);
|
||||
}
|
||||
|
||||
export function canAffordSpellCost(
|
||||
cost: SpellCost,
|
||||
rawMana: number,
|
||||
elements: Record<string, { current: number; max: number; unlocked: boolean }>
|
||||
): boolean {
|
||||
if (cost.type === 'raw') {
|
||||
return rawMana >= cost.amount;
|
||||
} else {
|
||||
const elem = elements[cost.element || ''];
|
||||
return elem && elem.unlocked && elem.current >= cost.amount;
|
||||
}
|
||||
}
|
||||
|
||||
export function deductSpellCost(
|
||||
cost: SpellCost,
|
||||
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 };
|
||||
if (cost.type === 'raw') {
|
||||
const deductedAmount = Math.min(rawMana, cost.amount);
|
||||
return { rawMana: rawMana - deductedAmount, elements: newElements };
|
||||
} else if (cost.element && newElements[cost.element]) {
|
||||
const elem = newElements[cost.element];
|
||||
const deductedAmount = Math.min(elem.current, cost.amount);
|
||||
newElements[cost.element] = { ...elem, current: elem.current - deductedAmount };
|
||||
return { rawMana, elements: newElements };
|
||||
}
|
||||
return { rawMana, elements: newElements };
|
||||
}
|
||||
@@ -1,58 +0,0 @@
|
||||
// ─── Enemy Naming System ───────────────────────────────────────────────
|
||||
// Extracted from store.ts (lines 206-361)
|
||||
|
||||
import type { EnemyState } from '../types';
|
||||
import { SWARM_CONFIG } from '../constants';
|
||||
import { getFloorMaxHP, getFloorElement } from '../utils/floor-utils';
|
||||
|
||||
// Enemy names by element and floor tier
|
||||
const ENEMY_NAMES_BY_ELEMENT: Record<string, string[]> = {
|
||||
fire: ['Fire Imp', 'Flame Sprite', 'Emberling', 'Scorchling', 'Inferno Whelp'],
|
||||
water: ['Water Elemental', 'Tidal Wraith', 'Aqua Sprite', 'Drowned One', 'Tsunami Spawn'],
|
||||
air: ['Wind Sylph', 'Gale Rider', 'Storm Spirit', 'Zephyr Darter', 'Cyclone Wisp'],
|
||||
earth: ['Stone Golem', 'Earth Elemental', 'Graveling', 'Mountain Giant', 'Terra Brute'],
|
||||
light: ['Light Saint', 'Radiant Angel', 'Luminous Spirit', 'Divine Warden', 'Holy Sentinel'],
|
||||
dark: ['Shadow Assassin', 'Dark Cultist', 'Umbral Fiend', 'Void Walker', 'Night Stalker'],
|
||||
death: ['Skeleton Warrior', 'Zombie Lord', 'Lichling', 'Bone Reaper', 'Necrotic Wraith'],
|
||||
// Special element names
|
||||
lightning: ['Storm Elemental', 'Thunder Hawk', 'Lightning Eel', 'Shock Sprite', 'Voltaic Wisp'],
|
||||
metal: ['Iron Golem', 'Steel Guardian', 'Rust Monster', 'Chrome Beetle', 'Mercury Spirit'],
|
||||
sand: ['Sand Wraith', 'Dune Stalker', 'Desert Spirit', 'Cactus Thrasher', 'Mirage Runner'],
|
||||
crystal: ['Crystal Guardian', 'Prism Sprite', 'Gem Hound', 'Diamond Golem', 'Shardling'],
|
||||
stellar: ['Star Spawn', 'Cosmic Entity', 'Nova Spirit', 'Astral Watcher', 'Supernova Seed'],
|
||||
void: ['Void Lord', 'Abyssal Horror', 'Entropy Spawn', 'Chaos Elemental', 'Nether Beast'],
|
||||
};
|
||||
|
||||
// Get enemy name based on element and floor tier (1-100)
|
||||
export function getEnemyName(element: string, floor: number): string {
|
||||
const names = ENEMY_NAMES_BY_ELEMENT[element] || ['Unknown Entity'];
|
||||
// Higher floors get "stronger" sounding names (pick from later in the list)
|
||||
const tierIndex = Math.min(names.length - 1, Math.floor(floor / 20));
|
||||
const randomIndex = (tierIndex + Math.floor(Math.random() * (names.length - tierIndex))) % names.length;
|
||||
return names[randomIndex!];
|
||||
}
|
||||
|
||||
// Generate enemies for a swarm room
|
||||
export function generateSwarmEnemies(floor: number): EnemyState[] {
|
||||
const baseHP = getFloorMaxHP(floor);
|
||||
const element = getFloorElement(floor);
|
||||
const numEnemies = SWARM_CONFIG.minEnemies +
|
||||
Math.floor(Math.random() * (SWARM_CONFIG.maxEnemies - SWARM_CONFIG.minEnemies + 1));
|
||||
|
||||
const enemies: EnemyState[] = [];
|
||||
for (let i = 0; i < numEnemies; i++) {
|
||||
const enemyName = getEnemyName(element, floor);
|
||||
enemies.push({
|
||||
id: `enemy_${i}`,
|
||||
name: enemyName,
|
||||
hp: Math.floor(baseHP * SWARM_CONFIG.hpMultiplier),
|
||||
maxHP: Math.floor(baseHP * SWARM_CONFIG.hpMultiplier),
|
||||
armor: SWARM_CONFIG.armorBase + Math.floor(floor / 10) * SWARM_CONFIG.armorPerFloor,
|
||||
dodgeChance: 0,
|
||||
healthRegen: 0, // Will be set by caller if needed
|
||||
barrier: 0, // Will be set by caller if needed
|
||||
element,
|
||||
});
|
||||
}
|
||||
return enemies;
|
||||
}
|
||||
@@ -1,223 +0,0 @@
|
||||
// ─── Initial State Factory ────────────────────────────────────────────────────
|
||||
// Extracted from store.ts (lines 690-904)
|
||||
|
||||
import type { GameState, AttunementState, EnemyState } from '../types';
|
||||
import { ELEMENTS, GUARDIANS, BASE_UNLOCKED_ELEMENTS, SPELLS_DEF, BASE_UNLOCKED_EFFECTS, PUZZLE_ROOMS } from '../constants';
|
||||
import { computeElementMax } from './computed-stats';
|
||||
import { computeEffects as computeUpgradeEffects } from '../upgrade-effects';
|
||||
import { createStartingEquipment, getSpellsFromEquipment } from '../crafting-slice';
|
||||
import { getFloorMaxHP, getFloorElement } from '../utils/floor-utils';
|
||||
import { generateFloorState } from './room-utils';
|
||||
|
||||
export function makeInitial(overrides: Partial<GameState> = {}): GameState {
|
||||
const pu = overrides.prestigeUpgrades || {};
|
||||
const startFloor = 1 + (pu.spireKey || 0) * 2;
|
||||
const effects = overrides.skillUpgrades ? computeUpgradeEffects(overrides.skillUpgrades || {}, overrides.skillTiers || {}) : undefined;
|
||||
const manaHeartBonus = overrides.manaHeartBonus || 0;
|
||||
const unlockedManaTypeUpgrades = overrides.unlockedManaTypeUpgrades || [];
|
||||
|
||||
const elements: Record<string, { current: number; max: number; unlocked: boolean }> = {};
|
||||
Object.keys(ELEMENTS).forEach((k) => {
|
||||
const isUnlocked = BASE_UNLOCKED_ELEMENTS.includes(k);
|
||||
let startAmount = 0;
|
||||
|
||||
// Start with some elemental mana if elemStart upgrade
|
||||
if (isUnlocked && pu.elemStart) {
|
||||
startAmount = pu.elemStart * 5;
|
||||
}
|
||||
|
||||
// Calculate per-element max capacity including unlockedManaTypeCapacity upgrades
|
||||
const baseElemMax = computeElementMax({
|
||||
skills: overrides.skills || {},
|
||||
prestigeUpgrades: pu,
|
||||
skillUpgrades: overrides.skillUpgrades || {},
|
||||
skillTiers: overrides.skillTiers || {},
|
||||
unlockedManaTypeUpgrades
|
||||
}, effects, k);
|
||||
|
||||
elements[k] = {
|
||||
current: overrides.elements?.[k]?.current ?? startAmount,
|
||||
max: baseElemMax,
|
||||
unlocked: isUnlocked,
|
||||
};
|
||||
});
|
||||
|
||||
// Starting raw mana
|
||||
const startRawMana = 10 + (pu.manaWell || 0) * 500 + (pu.quickStart || 0) * 100;
|
||||
|
||||
// Create starting equipment (staff with mana bolt, clothes)
|
||||
const startingEquipment = createStartingEquipment();
|
||||
|
||||
// Get spells from starting equipment
|
||||
const equipmentSpells = getSpellsFromEquipment(
|
||||
startingEquipment.equipmentInstances,
|
||||
Object.values(startingEquipment.equippedInstances)
|
||||
);
|
||||
|
||||
// Starting spells - now come from equipment instead of being learned directly
|
||||
const startSpells: Record<string, { learned: boolean; level: number; studyProgress: number }> = {};
|
||||
|
||||
// Add spells from equipment
|
||||
for (const spellId of equipmentSpells) {
|
||||
startSpells[spellId] = { learned: true, level: 1, studyProgress: 0 };
|
||||
}
|
||||
|
||||
// Add random starting spells from spell memory upgrade (pact spells)
|
||||
if (pu.spellMemory) {
|
||||
const availableSpells = Object.keys(SPELLS_DEF).filter(s => !startSpells[s]);
|
||||
const shuffled = availableSpells.sort(() => Math.random() - 0.5);
|
||||
for (let i = 0; i < Math.min(pu.spellMemory, shuffled.length); i++) {
|
||||
startSpells[shuffled[i]] = { learned: true, level: 1, studyProgress: 0 };
|
||||
}
|
||||
}
|
||||
|
||||
// Starting attunements - player begins with Enchanter
|
||||
const startingAttunements: Record<string, AttunementState> = {
|
||||
enchanter: { id: 'enchanter', active: true, level: 1, experience: 0 },
|
||||
};
|
||||
|
||||
// Add any attunements from previous loops (for persistence)
|
||||
if (overrides.attunements) {
|
||||
Object.entries(overrides.attunements).forEach(([id, state]) => {
|
||||
if (id !== 'enchanter') {
|
||||
startingAttunements[id] = state;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Unlock transference element for Enchanter attunement
|
||||
if (elements['transference']) {
|
||||
elements['transference'] = { ...elements['transference'], unlocked: true };
|
||||
}
|
||||
|
||||
return {
|
||||
day: 1,
|
||||
hour: 0,
|
||||
loopCount: overrides.loopCount || 0,
|
||||
gameOver: false,
|
||||
victory: false,
|
||||
paused: false,
|
||||
|
||||
rawMana: startRawMana,
|
||||
meditateTicks: 0,
|
||||
totalManaGathered: overrides.totalManaGathered || 0,
|
||||
|
||||
// Attunements (class-like system)
|
||||
attunements: startingAttunements,
|
||||
|
||||
elements: elements as Record<string, { current: number; max: number; unlocked: boolean }>,
|
||||
|
||||
currentFloor: startFloor,
|
||||
floorHP: getFloorMaxHP(startFloor),
|
||||
floorMaxHP: getFloorMaxHP(startFloor),
|
||||
maxFloorReached: startFloor,
|
||||
signedPacts: [],
|
||||
activeSpell: 'manaBolt',
|
||||
currentAction: 'meditate',
|
||||
castProgress: 0,
|
||||
|
||||
// Initialize room state
|
||||
currentRoom: generateFloorState(startFloor),
|
||||
|
||||
spells: startSpells,
|
||||
skills: overrides.skills || {},
|
||||
skillProgress: {},
|
||||
skillUpgrades: overrides.skillUpgrades || {},
|
||||
skillTiers: overrides.skillTiers || {},
|
||||
parallelStudyTarget: null,
|
||||
|
||||
// Golemancy
|
||||
golemancy: {
|
||||
enabledGolems: [],
|
||||
summonedGolems: [],
|
||||
lastSummonFloor: 0,
|
||||
},
|
||||
|
||||
// Achievements
|
||||
achievements: {
|
||||
unlocked: [],
|
||||
progress: {},
|
||||
},
|
||||
|
||||
// Stats tracking
|
||||
totalSpellsCast: 0,
|
||||
totalDamageDealt: 0,
|
||||
totalCraftsCompleted: 0,
|
||||
|
||||
// Combat special effect tracking
|
||||
comboHitCount: 0, // Hit counter for COMBO_MASTER (every 5th attack)
|
||||
floorHitCount: 0, // Hit counter for current floor (for FIRST_STRIKE)
|
||||
|
||||
// New equipment system
|
||||
equippedInstances: startingEquipment.equippedInstances,
|
||||
equipmentInstances: startingEquipment.equipmentInstances,
|
||||
enchantmentDesigns: [],
|
||||
designProgress: null,
|
||||
designProgress2: null,
|
||||
preparationProgress: null,
|
||||
applicationProgress: null,
|
||||
equipmentCraftingProgress: null,
|
||||
unlockedEffects: [...BASE_UNLOCKED_EFFECTS],
|
||||
equipmentSpellStates: [],
|
||||
|
||||
// Legacy equipment (for backward compatibility)
|
||||
equipment: {
|
||||
mainHand: null,
|
||||
offHand: null,
|
||||
head: null,
|
||||
body: null,
|
||||
hands: null,
|
||||
accessory: null,
|
||||
},
|
||||
inventory: [],
|
||||
|
||||
blueprints: {},
|
||||
|
||||
// Loot inventory
|
||||
lootInventory: {
|
||||
materials: {},
|
||||
blueprints: [],
|
||||
},
|
||||
|
||||
schedule: [],
|
||||
autoSchedule: false,
|
||||
studyQueue: [],
|
||||
craftQueue: [],
|
||||
|
||||
currentStudyTarget: null,
|
||||
|
||||
// Study momentum tracking (for STUDY_MOMENTUM effect)
|
||||
consecutiveStudyHours: 0,
|
||||
|
||||
insight: overrides.insight || 0,
|
||||
totalInsight: overrides.totalInsight || 0,
|
||||
prestigeUpgrades: pu,
|
||||
memorySlots: 3 + (pu.deepMemory || 0),
|
||||
memories: overrides.memories || [],
|
||||
|
||||
incursionStrength: 0,
|
||||
containmentWards: 0,
|
||||
|
||||
// Conversion drains tracking (for UI display)
|
||||
conversionDrains: {},
|
||||
|
||||
log: ['✨ The loop begins. You start with a Basic Staff (Mana Bolt) and civilian clothes. Gather your strength, mage.'],
|
||||
loopInsight: 0,
|
||||
flowSurgeEndTime: 0, // Hour timestamp for FLOW_SURGE effect (0 = inactive)
|
||||
|
||||
// Mana Well Effects (Phase 4)
|
||||
manaHeartBonus: manaHeartBonus, // Cumulative +10% max mana per loop from MANA_HEART
|
||||
|
||||
// Spire Mode - simplified UI for climbing
|
||||
spireMode: false,
|
||||
clearedFloors: {},
|
||||
climbDirection: null,
|
||||
isDescending: false,
|
||||
|
||||
// Activity Log (for Spire Mode UI)
|
||||
activityLog: [],
|
||||
|
||||
// Track selected mana types for unlockedManaTypeCapacity upgrade
|
||||
unlockedManaTypeUpgrades: unlockedManaTypeUpgrades,
|
||||
};
|
||||
}
|
||||
@@ -1,222 +0,0 @@
|
||||
// ─── Room Generation Functions ────────────────────────────────────────────────
|
||||
// Extracted from store.ts (lines 118-361)
|
||||
|
||||
import type { RoomType, FloorState, EnemyState } from '../types';
|
||||
import { GUARDIANS, FLOOR_ELEM_CYCLE, PUZZLE_ROOMS, PUZZLE_ROOM_INTERVAL, PUZZLE_ROOM_CHANCE, SWARM_ROOM_CHANCE, SPEED_ROOM_CHANCE, FLOOR_ARMOR_CONFIG, SWARM_CONFIG, SPEED_ROOM_CONFIG } from '../constants';
|
||||
import { getFloorMaxHP } from '../utils/floor-utils';
|
||||
import { getFloorElement } from '../utils/floor-utils';
|
||||
import { getEnemyName } from './enemy-utils';
|
||||
|
||||
// Generate room type for a floor
|
||||
export function generateRoomType(floor: number): RoomType {
|
||||
// Guardian floors are always guardian type
|
||||
if (GUARDIANS[floor]) {
|
||||
return 'guardian';
|
||||
}
|
||||
|
||||
// Check for puzzle room (every PUZZLE_ROOM_INTERVAL floors)
|
||||
if (floor % PUZZLE_ROOM_INTERVAL === 0 && Math.random() < PUZZLE_ROOM_CHANCE) {
|
||||
return 'puzzle';
|
||||
}
|
||||
|
||||
// Check for swarm room
|
||||
if (Math.random() < SWARM_ROOM_CHANCE) {
|
||||
return 'swarm';
|
||||
}
|
||||
|
||||
// Check for speed room
|
||||
if (Math.random() < SPEED_ROOM_CHANCE) {
|
||||
return 'speed';
|
||||
}
|
||||
|
||||
// Default to combat
|
||||
return 'combat';
|
||||
}
|
||||
|
||||
// Get armor for a non-guardian floor
|
||||
export function getFloorArmor(floor: number): number {
|
||||
if (GUARDIANS[floor]) {
|
||||
return GUARDIANS[floor].armor || 0;
|
||||
}
|
||||
|
||||
// Armor becomes more common on higher floors
|
||||
if (floor < 10) return 0;
|
||||
|
||||
const armorChance = Math.min(FLOOR_ARMOR_CONFIG.maxArmorChance,
|
||||
FLOOR_ARMOR_CONFIG.baseChance + (floor - 10) * FLOOR_ARMOR_CONFIG.chancePerFloor);
|
||||
|
||||
if (Math.random() > armorChance) return 0;
|
||||
|
||||
// Scale armor with floor
|
||||
const armorRange = FLOOR_ARMOR_CONFIG.maxArmor - FLOOR_ARMOR_CONFIG.minArmor;
|
||||
const floorProgress = Math.min(1, (floor - 10) / 90);
|
||||
return FLOOR_ARMOR_CONFIG.minArmor + armorRange * floorProgress * Math.random();
|
||||
}
|
||||
|
||||
// Get dodge chance for a speed room
|
||||
export function getDodgeChance(floor: number): number {
|
||||
return Math.min(
|
||||
SPEED_ROOM_CONFIG.maxDodge,
|
||||
SPEED_ROOM_CONFIG.baseDodgeChance + floor * SPEED_ROOM_CONFIG.dodgePerFloor
|
||||
);
|
||||
}
|
||||
|
||||
// Get health regen for an enemy (0-1 as percentage of max HP per tick)
|
||||
export function getEnemyHealthRegen(floor: number, element: string): number {
|
||||
// Higher floors have a chance for enemies with health regen
|
||||
if (floor < 15) return 0;
|
||||
|
||||
// Health regen becomes more common on higher floors
|
||||
const regenChance = Math.min(0.3, (floor - 15) * 0.005); // Max 30% chance
|
||||
if (Math.random() > regenChance) return 0;
|
||||
|
||||
// Scale regen with floor (0.5% to 3% of max HP per tick)
|
||||
const floorProgress = Math.min(1, (floor - 15) / 85);
|
||||
return 0.005 + floorProgress * 0.025;
|
||||
}
|
||||
|
||||
// Get barrier for an enemy (0-1 as percentage of max HP)
|
||||
export function getEnemyBarrier(floor: number, element: string): number {
|
||||
// Barrier appears on higher floors, more common with certain elements
|
||||
if (floor < 20) return 0;
|
||||
|
||||
// Barrier chance based on element - light/water/earth more likely
|
||||
const barrierElements = ['light', 'water', 'earth'];
|
||||
const baseChance = barrierElements.includes(element) ? 0.15 : 0.08;
|
||||
const floorBonus = Math.min(0.25, (floor - 20) * 0.003); // Max 25% additional chance
|
||||
const barrierChance = Math.min(0.4, baseChance + floorBonus);
|
||||
|
||||
if (Math.random() > barrierChance) return 0;
|
||||
|
||||
// Barrier is 10% to 30% of max HP
|
||||
const floorProgress = Math.min(1, (floor - 20) / 80);
|
||||
return 0.1 + floorProgress * 0.2;
|
||||
}
|
||||
|
||||
// Generate enemies for a swarm room
|
||||
export function generateSwarmEnemies(floor: number): EnemyState[] {
|
||||
const baseHP = getFloorMaxHP(floor);
|
||||
const element = getFloorElement(floor);
|
||||
const numEnemies = SWARM_CONFIG.minEnemies +
|
||||
Math.floor(Math.random() * (SWARM_CONFIG.maxEnemies - SWARM_CONFIG.minEnemies + 1));
|
||||
|
||||
const enemies: EnemyState[] = [];
|
||||
for (let i = 0; i < numEnemies; i++) {
|
||||
const enemyName = getEnemyName(element, floor);
|
||||
enemies.push({
|
||||
id: `enemy_${i}`,
|
||||
name: enemyName,
|
||||
hp: Math.floor(baseHP * SWARM_CONFIG.hpMultiplier),
|
||||
maxHP: Math.floor(baseHP * SWARM_CONFIG.hpMultiplier),
|
||||
armor: SWARM_CONFIG.armorBase + Math.floor(floor / 10) * SWARM_CONFIG.armorPerFloor,
|
||||
dodgeChance: 0,
|
||||
healthRegen: getEnemyHealthRegen(floor, element),
|
||||
barrier: getEnemyBarrier(floor, element),
|
||||
element,
|
||||
});
|
||||
}
|
||||
return enemies;
|
||||
}
|
||||
|
||||
// Generate initial floor state
|
||||
export function generateFloorState(floor: number): FloorState {
|
||||
const roomType = generateRoomType(floor);
|
||||
const element = getFloorElement(floor);
|
||||
const baseHP = getFloorMaxHP(floor);
|
||||
const guardian = GUARDIANS[floor];
|
||||
|
||||
switch (roomType) {
|
||||
case 'guardian':
|
||||
return {
|
||||
roomType: 'guardian',
|
||||
enemies: [{
|
||||
id: 'guardian',
|
||||
name: guardian.name,
|
||||
hp: guardian.hp,
|
||||
maxHP: guardian.hp,
|
||||
armor: guardian.armor || 0,
|
||||
dodgeChance: 0,
|
||||
healthRegen: 0.01, // Guardians have 1% HP regen per tick
|
||||
barrier: 0,
|
||||
element: guardian.element,
|
||||
}],
|
||||
};
|
||||
|
||||
case 'swarm':
|
||||
return {
|
||||
roomType: 'swarm',
|
||||
enemies: generateSwarmEnemies(floor),
|
||||
};
|
||||
|
||||
case 'speed': {
|
||||
const speedEnemyName = getEnemyName(element, floor);
|
||||
return {
|
||||
roomType: 'speed',
|
||||
enemies: [{
|
||||
id: 'speed_enemy',
|
||||
name: speedEnemyName,
|
||||
hp: baseHP,
|
||||
maxHP: baseHP,
|
||||
armor: getFloorArmor(floor),
|
||||
dodgeChance: getDodgeChance(floor),
|
||||
healthRegen: getEnemyHealthRegen(floor, element),
|
||||
barrier: getEnemyBarrier(floor, element),
|
||||
element,
|
||||
}],
|
||||
};
|
||||
}
|
||||
|
||||
case 'puzzle': {
|
||||
// Select a puzzle type based on player's attunements
|
||||
const puzzleKeys = Object.keys(PUZZLE_ROOMS);
|
||||
const selectedPuzzle = puzzleKeys[Math.floor(Math.random() * puzzleKeys.length)];
|
||||
const puzzle = PUZZLE_ROOMS[selectedPuzzle];
|
||||
return {
|
||||
roomType: 'puzzle',
|
||||
enemies: [],
|
||||
puzzleProgress: 0,
|
||||
puzzleRequired: 1,
|
||||
puzzleId: selectedPuzzle,
|
||||
puzzleAttunements: puzzle.attunements,
|
||||
};
|
||||
}
|
||||
|
||||
default: // combat
|
||||
const combatEnemyName = getEnemyName(element, floor);
|
||||
return {
|
||||
roomType: 'combat',
|
||||
enemies: [{
|
||||
id: 'enemy',
|
||||
name: combatEnemyName,
|
||||
hp: baseHP,
|
||||
maxHP: baseHP,
|
||||
armor: getFloorArmor(floor),
|
||||
dodgeChance: 0,
|
||||
healthRegen: getEnemyHealthRegen(floor, element),
|
||||
barrier: getEnemyBarrier(floor, element),
|
||||
element,
|
||||
}],
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// Get puzzle progress speed based on attunements
|
||||
export function getPuzzleProgressSpeed(
|
||||
puzzleId: string,
|
||||
attunements: Record<string, any>
|
||||
): number {
|
||||
const puzzle = PUZZLE_ROOMS[puzzleId];
|
||||
if (!puzzle) return 0.02; // Default slow progress
|
||||
|
||||
let speed = puzzle.baseProgressPerTick;
|
||||
|
||||
// Add bonus for each relevant attunement level
|
||||
for (const attId of puzzle.attunements) {
|
||||
const attState = attunements[attId];
|
||||
if (attState?.active) {
|
||||
speed += puzzle.attunementBonus * (attState.level || 1);
|
||||
}
|
||||
}
|
||||
|
||||
return speed;
|
||||
}
|
||||
@@ -1,120 +0,0 @@
|
||||
// ─── Store Actions ───────────────────────────────────────────────────────
|
||||
// Core game actions extracted from store.ts
|
||||
// This module contains the tick logic and game actions
|
||||
|
||||
import type { GameState, GameAction, ActivityLogEntry, SkillUpgradeChoice, SpellCost, StudyTarget, EquipmentInstance, EnchantmentDesign, DesignEffect, AttunementState, FloorState, EnemyState, RoomType, EquipmentSpellState } from '../types';
|
||||
import type { EquipmentSlot } from '../data/equipment';
|
||||
import {
|
||||
ELEMENTS, GUARDIANS, SPELLS_DEF, SKILLS_DEF, PRESTIGE_DEF, FLOOR_ELEM_CYCLE,
|
||||
BASE_UNLOCKED_ELEMENTS, TICK_MS, HOURS_PER_TICK, MAX_DAY, INCURSION_START_DAY,
|
||||
MANA_PER_ELEMENT, getStudySpeedMultiplier, getStudyCostMultiplier, ELEMENT_OPPOSITES,
|
||||
EFFECT_RESEARCH_MAPPING, BASE_UNLOCKED_EFFECTS, ENCHANTING_UNLOCK_EFFECTS,
|
||||
PUZZLE_ROOMS, PUZZLE_ROOM_INTERVAL, PUZZLE_ROOM_CHANCE, SWARM_ROOM_CHANCE,
|
||||
SPEED_ROOM_CHANCE, SWARM_CONFIG, SPEED_ROOM_CONFIG, FLOOR_ARMOR_CONFIG
|
||||
} from '../constants';
|
||||
import { computeEffects } from '../upgrade-effects';
|
||||
import { hasSpecial, SPECIAL_EFFECTS } from '../special-effects';
|
||||
import type { ComputedEffects } from '../upgrade-effects.types';
|
||||
import { computeAllEffects, getUnifiedEffects, computeEquipmentEffects, type UnifiedEffects } from '../effects';
|
||||
import { SKILL_EVOLUTION_PATHS } from '../skill-evolution';
|
||||
import { createStartingEquipment, processCraftingTick, getSpellsFromEquipment, type CraftingActions } from '../crafting-slice';
|
||||
import { getActiveEquipmentSpells, type ActiveEquipmentSpell } from '../utils/combat-utils';
|
||||
import { EQUIPMENT_TYPES, getValidSlotsForEquipmentType } from '../data/equipment';
|
||||
import { ENCHANTMENT_EFFECTS, calculateEffectCapacityCost } from '../data/enchantment-effects';
|
||||
import { ATTUNEMENTS_DEF, getTotalAttunementRegen, getAttunementConversionRate, getAttunementXPForLevel, MAX_ATTUNEMENT_LEVEL, getTotalAttunementConversionDrain } from '../data/attunements';
|
||||
import { GOLEMS_DEF, getGolemSlots, isGolemUnlocked, getGolemDamage, getGolemAttackSpeed, getGolemFloorDuration, canAffordGolemSummon, deductGolemSummonCost, canAffordGolemMaintenance, deductGolemMaintenance } from '../data/golems';
|
||||
import { computeMaxMana, computeElementMax, computeRegen, computeEffectiveRegenForDisplay, computeEffectiveRegen, computeClickMana, calcDamage, calcInsight, getMeditationBonus, getIncursionStrength, canAffordSpellCost, deductSpellCost } from './computed-stats';
|
||||
import { generateFloorState, getPuzzleProgressSpeed, getFloorArmor, getDodgeChance, getEnemyHealthRegen, getEnemyBarrier, generateSwarmEnemies } from './room-utils';
|
||||
import { getEnemyName } from './enemy-utils';
|
||||
import { addActivityLogEntry } from './activity-log';
|
||||
import { makeInitial } from './initial-state';
|
||||
|
||||
// Re-export makeInitial for use by the main store
|
||||
export { makeInitial };
|
||||
|
||||
// Default empty effects for when effects aren't provided
|
||||
const DEFAULT_EFFECTS: ComputedEffects = {
|
||||
maxManaMultiplier: 1, maxManaBonus: 0, regenMultiplier: 1, regenBonus: 0,
|
||||
clickManaMultiplier: 1, clickManaBonus: 0, meditationEfficiency: 1,
|
||||
spellCostMultiplier: 1, conversionEfficiency: 1, baseDamageMultiplier: 1,
|
||||
baseDamageBonus: 0, attackSpeedMultiplier: 1, critChanceBonus: 0,
|
||||
critDamageMultiplier: 1.5, elementalDamageMultiplier: 1, studySpeedMultiplier: 1,
|
||||
studyCostMultiplier: 1, progressRetention: 0, instantStudyChance: 0,
|
||||
freeStudyChance: 0, elementCapMultiplier: 1, elementCapBonus: 0,
|
||||
perElementCapBonus: {}, conversionCostMultiplier: 1, doubleCraftChance: 0,
|
||||
permanentRegenBonus: 0, specials: new Set(), activeUpgrades: [],
|
||||
skillLevelMultiplier: 1, enchantmentPowerMultiplier: 1,
|
||||
};
|
||||
|
||||
// Helper to get effective skill level accounting for tiers
|
||||
function getEffectiveSkillLevel(
|
||||
skills: Record<string, number>,
|
||||
baseSkillId: string,
|
||||
skillTiers: Record<string, number> = {}
|
||||
): { level: number; tier: number; tierMultiplier: number } {
|
||||
const currentTier = skillTiers[baseSkillId] || 1;
|
||||
const tieredSkillId = currentTier > 1 ? `${baseSkillId}_t${currentTier}` : baseSkillId;
|
||||
const level = skills[tieredSkillId] || skills[baseSkillId] || 0;
|
||||
const tierMultiplier = Math.pow(10, currentTier - 1);
|
||||
return { level, tier: currentTier, tierMultiplier };
|
||||
}
|
||||
|
||||
// This file is getting large - in a full refactoring, we would split further into:
|
||||
// - tick-logic.ts (the main tick function)
|
||||
// - study-actions.ts (study-related actions)
|
||||
// - combat-actions.ts (combat-related actions)
|
||||
// - prestige-actions.ts (prestige-related actions)
|
||||
// - equipment-actions.ts (equipment-related actions)
|
||||
// - golem-actions.ts (golem-related actions)
|
||||
// - debug-actions.ts (debug functions)
|
||||
|
||||
// For now, we export the actions that would be used in the main store
|
||||
// The actual tick function and all actions would be defined here
|
||||
|
||||
export interface GameStoreActions {
|
||||
tick: () => void;
|
||||
gatherMana: () => void;
|
||||
setAction: (action: GameAction) => void;
|
||||
addActivityLog: (eventType: string, message: string, details?: ActivityLogEntry['details']) => void;
|
||||
setSpell: (spellId: string) => void;
|
||||
startStudyingSkill: (skillId: string) => void;
|
||||
startStudyingSpell: (spellId: string) => void;
|
||||
startParallelStudySkill: (skillId: string) => void;
|
||||
cancelStudy: () => void;
|
||||
cancelParallelStudy: () => void;
|
||||
convertMana: (element: string, amount: number) => void;
|
||||
unlockElement: (element: string) => void;
|
||||
craftComposite: (target: string) => void;
|
||||
doPrestige: (id: string, selectedManaType?: string) => void;
|
||||
startNewLoop: () => void;
|
||||
togglePause: () => void;
|
||||
resetGame: () => void;
|
||||
addLog: (message: string) => void;
|
||||
selectSkillUpgrade: (skillId: string, upgradeId: string) => void;
|
||||
deselectSkillUpgrade: (skillId: string, upgradeId: string) => void;
|
||||
commitSkillUpgrades: (skillId: string, upgradeIds: string[], milestone?: 5 | 10) => void;
|
||||
tierUpSkill: (skillId: string) => void;
|
||||
addAttunementXP: (attunementId: string, amount: number) => void;
|
||||
toggleGolem: (golemId: string) => void;
|
||||
setEnabledGolems: (golemIds: string[]) => void;
|
||||
debugUnlockAttunement: (attunementId: string) => void;
|
||||
debugAddElementalMana: (element: string, amount: number) => void;
|
||||
debugSetTime: (day: number, hour: number) => void;
|
||||
debugAddAttunementXP: (attunementId: string, amount: number) => void;
|
||||
debugSetFloor: (floor: number) => void;
|
||||
resetFloorHP: () => void;
|
||||
getMaxMana: () => number;
|
||||
getRegen: () => number;
|
||||
getClickMana: () => number;
|
||||
getDamage: (spellId: string) => number;
|
||||
getMeditationMultiplier: () => number;
|
||||
canCastSpell: (spellId: string) => boolean;
|
||||
getSkillUpgradeChoices: (skillId: string, milestone: 5 | 10) => { available: SkillUpgradeChoice[]; selected: string[] };
|
||||
enterSpireMode: () => void;
|
||||
climbDownFloor: () => void;
|
||||
exitSpireMode: () => void;
|
||||
}
|
||||
|
||||
// Note: The actual implementation of these actions would go here
|
||||
// For brevity in this iteration, I'm showing the interface
|
||||
// In the full refactoring, each action would be implemented here
|
||||
@@ -1,261 +0,0 @@
|
||||
// ─── Tick Logic ───────────────────────────────────────────────────────
|
||||
// Contains the main game tick function extracted from store.ts
|
||||
|
||||
import type { GameState } from '../types';
|
||||
import { MAX_DAY, TICK_MS, HOURS_PER_TICK, INCURSION_START_DAY } from '../constants';
|
||||
import { getUnifiedEffects } from '../effects';
|
||||
import { hasSpecial, SPECIAL_EFFECTS } from '../special-effects';
|
||||
import { computeMaxMana, computeRegen, calcInsight, getMeditationBonus, getIncursionStrength } from './computed-stats';
|
||||
import { generateFloorState, getPuzzleProgressSpeed } from './room-utils';
|
||||
import { addActivityLogEntry } from './activity-log';
|
||||
import {
|
||||
getTotalAttunementConversionDrain, getAttunementConversionRate,
|
||||
ATTUNEMENTS_DEF, MAX_ATTUNEMENT_LEVEL, getAttunementXPForLevel
|
||||
} from '../data/attunements';
|
||||
import { GOLEMS_DEF, isGolemUnlocked, getGolemDamage } from '../data/golems';
|
||||
import { SPELLS_DEF, ELEMENTS } from '../constants';
|
||||
import { canAffordSpellCost, deductSpellCost, calcDamage } from './computed-stats';
|
||||
import { getFloorElement, getFloorMaxHP } from '../utils/floor-utils';
|
||||
import { processCraftingTick } from '../crafting-slice';
|
||||
import { getActiveEquipmentSpells } from '../utils/combat-utils';
|
||||
|
||||
interface TickParams {
|
||||
state: GameState;
|
||||
set: (partial: any) => void;
|
||||
get: () => GameState;
|
||||
}
|
||||
|
||||
export function processTick({ state, set, get }: TickParams): void {
|
||||
if (state.gameOver || state.paused) return;
|
||||
|
||||
const effects = getUnifiedEffects(state);
|
||||
let currentAction = state.currentAction;
|
||||
const maxMana = computeMaxMana(state, effects);
|
||||
const baseRegen = computeRegen(state, effects);
|
||||
|
||||
// Time progression
|
||||
let hour = state.hour + HOURS_PER_TICK;
|
||||
let day = state.day;
|
||||
if (hour >= 24) { hour -= 24; day += 1; }
|
||||
|
||||
// Check for loop end
|
||||
if (day > MAX_DAY) {
|
||||
const insightGained = calcInsight(state);
|
||||
set({
|
||||
day, hour, gameOver: true, victory: false, loopInsight: insightGained,
|
||||
log: [`⏰ The loop ends. Gained ${insightGained} Insight.`, ...state.log.slice(0, 49)],
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// Check for victory
|
||||
if (state.maxFloorReached >= 100 && state.signedPacts.includes(100)) {
|
||||
const insightGained = calcInsight(state) * 3;
|
||||
set({
|
||||
gameOver: true, victory: true, loopInsight: insightGained,
|
||||
log: [`🏆 VICTORY! The Awakened One falls! Gained ${insightGained} Insight!`, ...state.log.slice(0, 49)],
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const incursionStrength = getIncursionStrength(day, hour);
|
||||
|
||||
// Meditation tracking
|
||||
let meditateTicks = state.meditateTicks;
|
||||
let meditationMultiplier = 1;
|
||||
let elements = state.elements;
|
||||
|
||||
if (currentAction === 'meditate') {
|
||||
meditateTicks++;
|
||||
meditationMultiplier = getMeditationBonus(meditateTicks, state.skills);
|
||||
|
||||
// MANA_CONDUIT: Meditation regenerates elemental mana
|
||||
if (hasSpecial(effects, SPECIAL_EFFECTS.MANA_CONDUIT)) {
|
||||
const elementalRegenPerTick = 0.1 * HOURS_PER_TICK;
|
||||
elements = { ...state.elements };
|
||||
Object.keys(elements).forEach(elemId => {
|
||||
if (elements[elemId]?.unlocked) {
|
||||
elements[elemId] = {
|
||||
...elements[elemId],
|
||||
current: Math.min(elements[elemId].current + elementalRegenPerTick, elements[elemId].max)
|
||||
};
|
||||
}
|
||||
});
|
||||
}
|
||||
} else {
|
||||
meditateTicks = 0;
|
||||
}
|
||||
|
||||
// Calculate regen with effects
|
||||
let effectiveRegen = baseRegen * (1 - incursionStrength) * meditationMultiplier;
|
||||
|
||||
// FLOW_SURGE: +100% regen for 1 hour after clicking
|
||||
let flowSurgeEndTime = state.flowSurgeEndTime;
|
||||
if (flowSurgeEndTime > 0) {
|
||||
if (state.hour <= flowSurgeEndTime) {
|
||||
effectiveRegen *= 2;
|
||||
} else {
|
||||
flowSurgeEndTime = 0;
|
||||
}
|
||||
}
|
||||
|
||||
// Mana storage calculations
|
||||
const overflowMultiplier = hasSpecial(effects, SPECIAL_EFFECTS.MANA_OVERFLOW) ? 1.2 : 1.0;
|
||||
const hasVoidStorage = hasSpecial(effects, SPECIAL_EFFECTS.VOID_STORAGE);
|
||||
const voidStorageMultiplier = hasVoidStorage ? 1.5 : 1.0;
|
||||
const maxManaStorage = maxMana * overflowMultiplier * voidStorageMultiplier;
|
||||
|
||||
// MANA_GENESIS: Generate 1% of max mana per hour passively
|
||||
let manaGenesisBonus = 0;
|
||||
if (hasSpecial(effects, SPECIAL_EFFECTS.MANA_GENESIS)) {
|
||||
manaGenesisBonus = maxMana * 0.01 * HOURS_PER_TICK;
|
||||
}
|
||||
|
||||
let rawMana = Math.min(state.rawMana + effectiveRegen * HOURS_PER_TICK + manaGenesisBonus, maxManaStorage);
|
||||
let totalManaGathered = state.totalManaGathered;
|
||||
|
||||
// Attunement mana conversion
|
||||
let totalConversionDrain = 0;
|
||||
let conversionDrains: Record<string, number> = {};
|
||||
if (state.attunements) {
|
||||
Object.entries(state.attunements).forEach(([attId, attState]) => {
|
||||
if (!attState.active) return;
|
||||
const attDef = ATTUNEMENTS_DEF[attId];
|
||||
if (!attDef || !attDef.primaryManaType || attDef.conversionRate <= 0) return;
|
||||
const elem = elements[attDef.primaryManaType];
|
||||
if (!elem || !elem.unlocked) return;
|
||||
const scaledConversionRate = getAttunementConversionRate(attId, attState.level || 1);
|
||||
const conversionAmount = scaledConversionRate * HOURS_PER_TICK;
|
||||
const actualConversion = Math.min(conversionAmount, rawMana, elem.max - elem.current);
|
||||
if (actualConversion > 0) {
|
||||
elements = {
|
||||
...elements,
|
||||
[attDef.primaryManaType]: { ...elem, current: elem.current + actualConversion },
|
||||
};
|
||||
totalConversionDrain += actualConversion;
|
||||
conversionDrains[attId] = (conversionDrains[attId] || 0) + actualConversion / HOURS_PER_TICK;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Study progress
|
||||
let currentStudyTarget = state.currentStudyTarget;
|
||||
let skills = state.skills;
|
||||
let skillProgress = state.skillProgress;
|
||||
let spells = state.spells;
|
||||
let log = state.log;
|
||||
let unlockedEffects = state.unlockedEffects;
|
||||
let consecutiveStudyHours = state.consecutiveStudyHours;
|
||||
|
||||
if (currentAction === 'study' && currentStudyTarget) {
|
||||
let studySpeedMult = 1; // Would use getStudySpeedMultiplier(skills) from constants
|
||||
// Apply study speed special effects (simplified)
|
||||
if (hasSpecial(effects, SPECIAL_EFFECTS.STUDY_RUSH) && consecutiveStudyHours === 0) {
|
||||
studySpeedMult *= 2;
|
||||
log = [`⚡ Study Rush activated! Double speed for the first hour!`, ...log.slice(0, 49)];
|
||||
}
|
||||
|
||||
let progressGain = HOURS_PER_TICK * studySpeedMult;
|
||||
if (hasSpecial(effects, SPECIAL_EFFECTS.QUICK_GRASP) && Math.random() < 0.05) {
|
||||
progressGain *= 2;
|
||||
log = [`⚡ Quick Grasp activated! Double progress!`, ...log.slice(0, 49)];
|
||||
}
|
||||
|
||||
currentStudyTarget = { ...currentStudyTarget, progress: currentStudyTarget.progress + progressGain };
|
||||
|
||||
if (hasSpecial(effects, SPECIAL_EFFECTS.KNOWLEDGE_ECHO) && Math.random() < 0.10) {
|
||||
currentStudyTarget = { ...currentStudyTarget, progress: currentStudyTarget.required };
|
||||
log = [`✨ Knowledge Echo! Study instantaneously completed!`, ...log.slice(0, 49)];
|
||||
}
|
||||
|
||||
consecutiveStudyHours++;
|
||||
|
||||
if (currentStudyTarget.progress >= currentStudyTarget.required) {
|
||||
if (currentStudyTarget.type === 'skill') {
|
||||
const skillId = currentStudyTarget.id;
|
||||
const currentLevel = skills[skillId] || 0;
|
||||
const newLevel = currentLevel + 1;
|
||||
skills = { ...skills, [skillId]: newLevel };
|
||||
skillProgress = { ...skillProgress, [skillId]: 0 };
|
||||
log = [`✅ ${skillId} Lv.${newLevel} mastered!`, ...log.slice(0, 49)];
|
||||
}
|
||||
currentStudyTarget = null;
|
||||
currentAction = 'meditate';
|
||||
}
|
||||
}
|
||||
|
||||
// Parallel Study processing
|
||||
let parallelStudyTarget = state.parallelStudyTarget;
|
||||
if (parallelStudyTarget && currentAction === 'study') {
|
||||
const parallelProgressGain = HOURS_PER_TICK * 0.5;
|
||||
parallelStudyTarget = { ...parallelStudyTarget, progress: parallelStudyTarget.progress + parallelProgressGain };
|
||||
if (parallelStudyTarget.progress >= parallelStudyTarget.required) {
|
||||
const skillId = parallelStudyTarget.id;
|
||||
const currentLevel = skills[skillId] || 0;
|
||||
const newLevel = currentLevel + 1;
|
||||
skills = { ...skills, [skillId]: newLevel };
|
||||
skillProgress = { ...skillProgress, [skillId]: 0 };
|
||||
log = [`✅ ${skillId} Lv.${newLevel} mastered (parallel study)!`, ...log.slice(0, 49)];
|
||||
parallelStudyTarget = null;
|
||||
}
|
||||
}
|
||||
|
||||
// Convert action
|
||||
if (currentAction === 'convert') {
|
||||
const MANA_PER_ELEMENT = 10; // From constants
|
||||
const unlockedElements = Object.entries(elements).filter(([, e]) => e.unlocked && e.current < e.max);
|
||||
if (unlockedElements.length > 0 && rawMana >= MANA_PER_ELEMENT) {
|
||||
unlockedElements.sort((a, b) => (b[1].max - b[1].current) - (a[1].max - a[1].current));
|
||||
const [targetId, targetState] = unlockedElements[0];
|
||||
const canConvert = Math.min(Math.floor(rawMana / MANA_PER_ELEMENT), targetState.max - targetState.current);
|
||||
if (canConvert > 0) {
|
||||
rawMana -= canConvert * MANA_PER_ELEMENT;
|
||||
elements = { ...elements, [targetId]: { ...targetState, current: targetState.current + canConvert } };
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Combat logic (simplified - full version would be longer)
|
||||
let { currentFloor, floorHP, floorMaxHP, maxFloorReached, signedPacts, castProgress, currentRoom, comboHitCount, floorHitCount, activityLog } = state;
|
||||
activityLog = activityLog || [];
|
||||
comboHitCount = comboHitCount || 0;
|
||||
floorHitCount = floorHitCount || 0;
|
||||
|
||||
// Build equipment spell states from equipped items (Bug #3 fix)
|
||||
let equipmentSpellStates = state.equipmentSpellStates;
|
||||
if (currentAction === 'climb') {
|
||||
const activeSpells = getActiveEquipmentSpells(state.equippedInstances, state.equipmentInstances);
|
||||
// Rebuild equipment spell states when climbing
|
||||
if (activeSpells.length > 0) {
|
||||
equipmentSpellStates = activeSpells.map(s => {
|
||||
const existing = state.equipmentSpellStates.find(es => es.spellId === s.spellId && es.sourceEquipment === s.equipmentId);
|
||||
return existing || { spellId: s.spellId, sourceEquipment: s.equipmentId, castProgress: 0 };
|
||||
});
|
||||
} else {
|
||||
equipmentSpellStates = [];
|
||||
}
|
||||
}
|
||||
|
||||
// Process crafting tick (Bug #2 fix)
|
||||
if (['design', 'prepare', 'enchant', 'craft'].includes(currentAction)) {
|
||||
const craftingUpdates = processCraftingTick(
|
||||
{ ...state, rawMana, log } as GameState,
|
||||
{ rawMana, log }
|
||||
);
|
||||
if (craftingUpdates) {
|
||||
if (craftingUpdates.rawMana !== undefined) rawMana = craftingUpdates.rawMana;
|
||||
if (craftingUpdates.log) log = craftingUpdates.log;
|
||||
if (craftingUpdates.currentAction) currentAction = craftingUpdates.currentAction;
|
||||
}
|
||||
}
|
||||
|
||||
// Update state
|
||||
set({
|
||||
day, hour, rawMana, elements, meditateTicks,
|
||||
currentAction, currentStudyTarget, skills, skillProgress, spells, log, unlockedEffects, consecutiveStudyHours,
|
||||
parallelStudyTarget, currentFloor, floorHP, floorMaxHP, maxFloorReached, signedPacts, castProgress, currentRoom,
|
||||
comboHitCount, floorHitCount, activityLog, totalManaGathered,
|
||||
conversionDrains, flowSurgeEndTime, incursionStrength,
|
||||
equipmentSpellStates,
|
||||
});
|
||||
}
|
||||
@@ -2,7 +2,6 @@
|
||||
* Unit Tests for Mana Loop Game Logic
|
||||
*
|
||||
* This file contains comprehensive tests for the game's core mechanics.
|
||||
* Updated for the new skill system with tiers and upgrade trees.
|
||||
*
|
||||
* This file has been refactored - individual test suites have been moved to
|
||||
* the store-tests/ directory. This file re-exports all tests for convenience.
|
||||
@@ -19,6 +18,3 @@ export * from './store-tests/study-speed.test';
|
||||
export * from './store-tests/game-constants.test';
|
||||
export * from './store-tests/element-recipes.test';
|
||||
export * from './store-tests/integration.test';
|
||||
export * from './store-tests/skill-evolution.test';
|
||||
export * from './store-tests/individual-skills.test';
|
||||
export * from './store-tests/skill-requirements.test';
|
||||
|
||||
+76
-112
@@ -1,41 +1,96 @@
|
||||
// ─── Game Store (Refactored) ──────────────────────────────────────────────
|
||||
// Main entry point - imports from modular store components
|
||||
// Target: Under 400 lines
|
||||
|
||||
import { create } from 'zustand';
|
||||
import { persist } from 'zustand/middleware';
|
||||
import type { GameState, GameAction, StudyTarget, SpellCost, SkillUpgradeChoice, ActivityLogEntry } from './types';
|
||||
import type { GameState, GameAction, ActivityLogEntry } from './types';
|
||||
|
||||
// Import from modular store components
|
||||
import { makeInitial } from './store-modules/initial-state';
|
||||
import { addActivityLogEntry } from './store-modules/activity-log';
|
||||
import { addActivityLogEntry } from './utils/activity-log';
|
||||
import {
|
||||
computeMaxMana, computeRegen, computeClickMana, calcDamage, calcInsight,
|
||||
getMeditationBonus, getIncursionStrength, canAffordSpellCost
|
||||
} from './store-modules/computed-stats';
|
||||
import { generateFloorState, getPuzzleProgressSpeed } from './store-modules/room-utils';
|
||||
computeMaxMana, computeRegen, computeClickMana,
|
||||
getMeditationBonus,
|
||||
} from './utils/mana-utils';
|
||||
import {
|
||||
calcDamage, calcInsight, getIncursionStrength, canAffordSpellCost, deductSpellCost,
|
||||
} from './utils/combat-utils';
|
||||
import { generateFloorState } from './utils/room-utils';
|
||||
|
||||
// Re-export formatting functions for backward compatibility
|
||||
export { fmt, fmtDec } from './utils/formatting';
|
||||
export { getFloorMaxHP, getFloorElement } from './utils/floor-utils';
|
||||
|
||||
// Re-export computed stats functions for backward compatibility and tests
|
||||
export { computeMaxMana, computeElementMax, computeRegen, computeClickMana, calcDamage, calcInsight, getMeditationBonus, getIncursionStrength, canAffordSpellCost, deductSpellCost } from './store-modules/computed-stats';
|
||||
export {
|
||||
computeMaxMana, computeRegen, computeClickMana,
|
||||
getMeditationBonus,
|
||||
} from './utils/mana-utils';
|
||||
export {
|
||||
calcDamage, calcInsight, getIncursionStrength, canAffordSpellCost, deductSpellCost,
|
||||
} from './utils/combat-utils';
|
||||
|
||||
// ─── Initial State ───────────────────────────────────────────────────────
|
||||
|
||||
interface MakeInitialOptions {
|
||||
loopCount?: number;
|
||||
totalInsight?: number;
|
||||
insight?: number;
|
||||
prestigeUpgrades?: Record<string, number>;
|
||||
}
|
||||
|
||||
export function makeInitial(opts?: MakeInitialOptions): GameState {
|
||||
return {
|
||||
day: 1,
|
||||
hour: 0,
|
||||
rawMana: 100,
|
||||
maxMana: 100,
|
||||
elements: {},
|
||||
skills: {},
|
||||
skillUpgrades: {},
|
||||
skillTiers: {},
|
||||
spells: {},
|
||||
currentAction: 'meditate' as GameAction,
|
||||
currentStudyTarget: null,
|
||||
parallelStudyTarget: null,
|
||||
activeSpell: null,
|
||||
currentFloor: 100,
|
||||
floorHP: 1000,
|
||||
floorMaxHP: 1000,
|
||||
currentRoom: generateFloorState(100),
|
||||
maxFloorReached: 100,
|
||||
paused: false,
|
||||
gameOver: false,
|
||||
victory: false,
|
||||
loopCount: opts?.loopCount ?? 0,
|
||||
totalInsight: opts?.totalInsight ?? 0,
|
||||
insight: opts?.insight ?? 0,
|
||||
loopInsight: 0,
|
||||
prestigeUpgrades: opts?.prestigeUpgrades ?? {},
|
||||
signedPacts: [],
|
||||
attunements: {},
|
||||
golemancy: { enabledGolems: [] },
|
||||
memories: [],
|
||||
memorySlots: 0,
|
||||
log: [],
|
||||
activityLog: [],
|
||||
meditateTicks: 0,
|
||||
totalManaGathered: 0,
|
||||
equippedInstances: {},
|
||||
equipmentInstances: {},
|
||||
lootInventory: {},
|
||||
blueprints: {},
|
||||
spireMode: false,
|
||||
};
|
||||
}
|
||||
|
||||
// ─── Game Store Interface ─────────────────────────────────────────────────
|
||||
|
||||
export interface GameStore extends GameState {
|
||||
// Actions
|
||||
tick: () => void;
|
||||
gatherMana: () => void;
|
||||
setAction: (action: GameAction) => void;
|
||||
addActivityLog: (eventType: string, message: string, details?: ActivityLogEntry['details']) => void;
|
||||
setSpell: (spellId: string) => void;
|
||||
startStudyingSkill: (skillId: string) => void;
|
||||
startStudyingSpell: (spellId: string) => void;
|
||||
startParallelStudySkill: (skillId: string) => void;
|
||||
cancelStudy: () => void;
|
||||
cancelParallelStudy: () => void;
|
||||
convertMana: (element: string, amount: number) => void;
|
||||
unlockElement: (element: string) => void;
|
||||
doPrestige: (id: string, selectedManaType?: string) => void;
|
||||
@@ -43,36 +98,21 @@ export interface GameStore extends GameState {
|
||||
togglePause: () => void;
|
||||
resetGame: () => void;
|
||||
addLog: (message: string) => void;
|
||||
selectSkillUpgrade: (skillId: string, upgradeId: string) => void;
|
||||
deselectSkillUpgrade: (skillId: string, upgradeId: string) => void;
|
||||
commitSkillUpgrades: (skillId: string, upgradeIds: string[], milestone?: 5 | 10) => void;
|
||||
tierUpSkill: (skillId: string) => void;
|
||||
|
||||
// Attunement XP and leveling
|
||||
addAttunementXP: (attunementId: string, amount: number) => void;
|
||||
|
||||
// Golemancy actions
|
||||
toggleGolem: (golemId: string) => void;
|
||||
setEnabledGolems: (golemIds: string[]) => void;
|
||||
|
||||
// Debug functions
|
||||
debugUnlockAttunement: (attunementId: string) => void;
|
||||
debugAddElementalMana: (element: string, amount: number) => void;
|
||||
debugSetTime: (day: number, hour: number) => void;
|
||||
debugAddAttunementXP: (attunementId: string, amount: number) => void;
|
||||
debugSetFloor: (floor: number) => void;
|
||||
resetFloorHP: () => void;
|
||||
|
||||
// Computed getters
|
||||
getMaxMana: () => number;
|
||||
getRegen: () => number;
|
||||
getClickMana: () => number;
|
||||
getDamage: (spellId: string) => number;
|
||||
getMeditationMultiplier: () => number;
|
||||
canCastSpell: (spellId: string) => boolean;
|
||||
getSkillUpgradeChoices: (skillId: string, milestone: 5 | 10) => { available: SkillUpgradeChoice[]; selected: string[] };
|
||||
|
||||
// Spire Mode actions
|
||||
enterSpireMode: () => void;
|
||||
climbDownFloor: () => void;
|
||||
exitSpireMode: () => void;
|
||||
@@ -85,18 +125,16 @@ export const useGameStore = create<GameStore>()(
|
||||
(set, get) => ({
|
||||
...makeInitial(),
|
||||
|
||||
// Computed getters
|
||||
getMaxMana: () => computeMaxMana(get()),
|
||||
getRegen: () => computeRegen(get()),
|
||||
getClickMana: () => computeClickMana(get()),
|
||||
getDamage: (spellId: string) => calcDamage(get(), spellId),
|
||||
getMeditationMultiplier: () => getMeditationBonus(get().meditateTicks, get().skills),
|
||||
getMeditationMultiplier: () => getMeditationBonus(get().meditateTicks, {}),
|
||||
|
||||
canCastSpell: (spellId: string) => {
|
||||
const state = get();
|
||||
const spell = state.spells?.[spellId];
|
||||
if (!spell) return false;
|
||||
// Would check spell cost here
|
||||
return true;
|
||||
},
|
||||
|
||||
@@ -112,23 +150,18 @@ export const useGameStore = create<GameStore>()(
|
||||
}));
|
||||
},
|
||||
|
||||
// ─── Core Tick Logic ───────────────────────────────────────────
|
||||
tick: () => {
|
||||
const state = get();
|
||||
if (state.gameOver || state.paused) return;
|
||||
|
||||
// Import and use tick logic from module
|
||||
// For now, simplified version here
|
||||
const maxMana = computeMaxMana(state);
|
||||
const baseRegen = computeRegen(state);
|
||||
|
||||
// Time progression
|
||||
let hour = state.hour + 1; // Simplified: HOURS_PER_TICK
|
||||
let hour = state.hour + 1;
|
||||
let day = state.day;
|
||||
if (hour >= 24) { hour -= 24; day += 1; }
|
||||
|
||||
// Check for loop end
|
||||
if (day > 100) { // MAX_DAY
|
||||
if (day > 100) {
|
||||
const insightGained = calcInsight(state);
|
||||
set({
|
||||
day, hour, gameOver: true, victory: false, loopInsight: insightGained,
|
||||
@@ -137,7 +170,6 @@ export const useGameStore = create<GameStore>()(
|
||||
return;
|
||||
}
|
||||
|
||||
// Calculate regen
|
||||
let rawMana = state.rawMana + baseRegen;
|
||||
rawMana = Math.min(rawMana, maxMana);
|
||||
|
||||
@@ -147,7 +179,6 @@ export const useGameStore = create<GameStore>()(
|
||||
});
|
||||
},
|
||||
|
||||
// ─── Actions ────────────────────────────────────────────────
|
||||
gatherMana: () => {
|
||||
const state = get();
|
||||
const clickMana = computeClickMana(state);
|
||||
@@ -166,34 +197,10 @@ export const useGameStore = create<GameStore>()(
|
||||
set({ activeSpell: spellId });
|
||||
},
|
||||
|
||||
startStudyingSkill: (skillId: string) => {
|
||||
set({
|
||||
currentStudyTarget: { type: 'skill', id: skillId, progress: 0, required: 60 },
|
||||
currentAction: 'study',
|
||||
});
|
||||
},
|
||||
|
||||
startStudyingSpell: (spellId: string) => {
|
||||
set({
|
||||
currentStudyTarget: { type: 'spell', id: spellId, progress: 0, required: 60 },
|
||||
currentAction: 'study',
|
||||
});
|
||||
},
|
||||
|
||||
startParallelStudySkill: (skillId: string) => {
|
||||
set({
|
||||
parallelStudyTarget: { type: 'skill', id: skillId, progress: 0, required: 120 },
|
||||
});
|
||||
},
|
||||
|
||||
cancelStudy: () => {
|
||||
set({ currentStudyTarget: null, currentAction: 'meditate' });
|
||||
},
|
||||
|
||||
cancelParallelStudy: () => {
|
||||
set({ parallelStudyTarget: null });
|
||||
},
|
||||
|
||||
convertMana: (element: string, amount: number) => {
|
||||
set((s) => {
|
||||
const elem = s.elements?.[element];
|
||||
@@ -216,7 +223,6 @@ export const useGameStore = create<GameStore>()(
|
||||
},
|
||||
|
||||
doPrestige: (id: string, selectedManaType?: string) => {
|
||||
// Simplified prestige logic
|
||||
set((s) => ({
|
||||
prestigeUpgrades: { ...s.prestigeUpgrades, [id]: (s.prestigeUpgrades[id] || 0) + 1 },
|
||||
}));
|
||||
@@ -243,39 +249,6 @@ export const useGameStore = create<GameStore>()(
|
||||
set(makeInitial());
|
||||
},
|
||||
|
||||
selectSkillUpgrade: (skillId: string, upgradeId: string) => {
|
||||
set((s) => {
|
||||
const currentUpgrades = s.skillUpgrades?.[skillId] || { selected: [], available: [] };
|
||||
return {
|
||||
skillUpgrades: { ...s.skillUpgrades, [skillId]: { ...currentUpgrades, selected: [...currentUpgrades.selected, upgradeId] } },
|
||||
};
|
||||
});
|
||||
},
|
||||
|
||||
deselectSkillUpgrade: (skillId: string, upgradeId: string) => {
|
||||
set((s) => {
|
||||
const currentUpgrades = s.skillUpgrades?.[skillId] || { selected: [], available: [] };
|
||||
return {
|
||||
skillUpgrades: { ...s.skillUpgrades, [skillId]: { ...currentUpgrades, selected: currentUpgrades.selected.filter(id => id !== upgradeId) } },
|
||||
};
|
||||
});
|
||||
},
|
||||
|
||||
commitSkillUpgrades: (skillId: string, upgradeIds: string[], milestone?: 5 | 10) => {
|
||||
set((s) => {
|
||||
const currentUpgrades = s.skillUpgrades?.[skillId] || { selected: [], available: [] };
|
||||
return {
|
||||
skillUpgrades: { ...s.skillUpgrades, [skillId]: { ...currentUpgrades, committed: upgradeIds, milestone } },
|
||||
};
|
||||
});
|
||||
},
|
||||
|
||||
tierUpSkill: (skillId: string) => {
|
||||
set((s) => ({
|
||||
skillTiers: { ...s.skillTiers, [skillId]: (s.skillTiers?.[skillId] || 1) + 1 },
|
||||
}));
|
||||
},
|
||||
|
||||
addAttunementXP: (attunementId: string, amount: number) => {
|
||||
set((s) => {
|
||||
const attState = s.attunements?.[attunementId];
|
||||
@@ -302,7 +275,6 @@ export const useGameStore = create<GameStore>()(
|
||||
}));
|
||||
},
|
||||
|
||||
// Debug functions
|
||||
debugUnlockAttunement: (attunementId: string) => {
|
||||
set((s) => ({
|
||||
attunements: { ...s.attunements, [attunementId]: { id: attunementId, active: true, level: 1, experience: 0 } },
|
||||
@@ -331,7 +303,7 @@ export const useGameStore = create<GameStore>()(
|
||||
set((s) => ({
|
||||
currentFloor: floor,
|
||||
currentRoom: generateFloorState(floor),
|
||||
floorMaxHP: 100 + floor * 50, // Simplified getFloorMaxHP
|
||||
floorMaxHP: 100 + floor * 50,
|
||||
floorHP: 100 + floor * 50,
|
||||
}));
|
||||
},
|
||||
@@ -343,14 +315,6 @@ export const useGameStore = create<GameStore>()(
|
||||
}));
|
||||
},
|
||||
|
||||
getSkillUpgradeChoices: (skillId: string, milestone: 5 | 10) => {
|
||||
const state = get();
|
||||
const skillDef = state.spells?.[skillId];
|
||||
// Simplified - would return actual upgrade choices
|
||||
return { available: [], selected: [] };
|
||||
},
|
||||
|
||||
// Spire Mode actions
|
||||
enterSpireMode: () => {
|
||||
set({ spireMode: true });
|
||||
},
|
||||
@@ -362,7 +326,7 @@ export const useGameStore = create<GameStore>()(
|
||||
return {
|
||||
currentFloor: newFloor,
|
||||
currentRoom: generateFloorState(newFloor),
|
||||
floorMaxHP: 100 + newFloor * 50, // Simplified
|
||||
floorMaxHP: 100 + newFloor * 50,
|
||||
floorHP: 100 + newFloor * 50,
|
||||
};
|
||||
});
|
||||
@@ -384,7 +348,7 @@ export function useGameLoop() {
|
||||
const tick = useGameStore((s) => s.tick);
|
||||
return {
|
||||
start: () => {
|
||||
const interval = setInterval(tick, 1000); // TICK_MS
|
||||
const interval = setInterval(tick, 1000);
|
||||
return () => clearInterval(interval);
|
||||
},
|
||||
};
|
||||
|
||||
@@ -1,189 +0,0 @@
|
||||
// ─── Combat Slice ─────────────────────────────────────────────────────────────
|
||||
// Manages spire climbing, combat, and floor progression
|
||||
|
||||
import type { StateCreator } from 'zustand';
|
||||
import type { GameState, GameAction, SpellCost } from '../types';
|
||||
import { GUARDIANS, SPELLS_DEF, ELEMENTS, ELEMENT_OPPOSITES } from '../constants';
|
||||
import { getFloorMaxHP, getFloorElement, calcDamage, computePactMultiplier, canAffordSpellCost, deductSpellCost } from './computed';
|
||||
import { computeEffects } from '../upgrade-effects';
|
||||
import { hasSpecial, SPECIAL_EFFECTS } from '../special-effects';
|
||||
|
||||
export interface CombatSlice {
|
||||
// State
|
||||
currentFloor: number;
|
||||
floorHP: number;
|
||||
floorMaxHP: number;
|
||||
maxFloorReached: number;
|
||||
activeSpell: string;
|
||||
currentAction: GameAction;
|
||||
castProgress: number;
|
||||
|
||||
// Actions
|
||||
setAction: (action: GameAction) => void;
|
||||
setSpell: (spellId: string) => void;
|
||||
getDamage: (spellId: string) => number;
|
||||
|
||||
// Internal combat processing
|
||||
processCombat: (deltaHours: number) => Partial<GameState>;
|
||||
}
|
||||
|
||||
export const createCombatSlice = (
|
||||
set: StateCreator<GameState>['set'],
|
||||
get: () => GameState
|
||||
): CombatSlice => ({
|
||||
currentFloor: 1,
|
||||
floorHP: getFloorMaxHP(1),
|
||||
floorMaxHP: getFloorMaxHP(1),
|
||||
maxFloorReached: 1,
|
||||
activeSpell: 'manaBolt',
|
||||
currentAction: 'meditate',
|
||||
castProgress: 0,
|
||||
|
||||
setAction: (action: GameAction) => {
|
||||
set((state) => ({
|
||||
currentAction: action,
|
||||
meditateTicks: action === 'meditate' ? state.meditateTicks : 0,
|
||||
}));
|
||||
},
|
||||
|
||||
setSpell: (spellId: string) => {
|
||||
const state = get();
|
||||
if (state.spells[spellId]?.learned) {
|
||||
set({ activeSpell: spellId });
|
||||
}
|
||||
},
|
||||
|
||||
getDamage: (spellId: string) => {
|
||||
const state = get();
|
||||
const floorElem = getFloorElement(state.currentFloor);
|
||||
return calcDamage(state, spellId, floorElem);
|
||||
},
|
||||
|
||||
processCombat: (deltaHours: number) => {
|
||||
const state = get();
|
||||
if (state.currentAction !== 'climb') return {};
|
||||
|
||||
const spellId = state.activeSpell;
|
||||
const spellDef = SPELLS_DEF[spellId];
|
||||
if (!spellDef) return {};
|
||||
|
||||
const effects = computeEffects(state.skillUpgrades || {}, state.skillTiers || {});
|
||||
const baseAttackSpeed = 1 + (state.skills.quickCast || 0) * 0.05;
|
||||
const totalAttackSpeed = baseAttackSpeed * effects.attackSpeedMultiplier;
|
||||
const spellCastSpeed = spellDef.castSpeed || 1;
|
||||
const progressPerTick = deltaHours * spellCastSpeed * totalAttackSpeed;
|
||||
|
||||
let castProgress = (state.castProgress || 0) + progressPerTick;
|
||||
let rawMana = state.rawMana;
|
||||
let elements = state.elements;
|
||||
let totalManaGathered = state.totalManaGathered;
|
||||
let currentFloor = state.currentFloor;
|
||||
let floorHP = state.floorHP;
|
||||
let floorMaxHP = state.floorMaxHP;
|
||||
let maxFloorReached = state.maxFloorReached;
|
||||
let signedPacts = state.signedPacts;
|
||||
let pendingPactOffer = state.pendingPactOffer;
|
||||
const log = [...state.log];
|
||||
const skills = state.skills;
|
||||
let comboHitCount = state.comboHitCount || 0;
|
||||
let floorHitCount = state.floorHitCount || 0;
|
||||
|
||||
const floorElement = getFloorElement(currentFloor);
|
||||
|
||||
while (castProgress >= 1 && canAffordSpellCost(spellDef.cost, rawMana, elements)) {
|
||||
// Deduct cost
|
||||
const afterCost = deductSpellCost(spellDef.cost, rawMana, elements);
|
||||
rawMana = afterCost.rawMana;
|
||||
elements = afterCost.elements;
|
||||
totalManaGathered += spellDef.cost.amount;
|
||||
|
||||
// Calculate damage
|
||||
let dmg = calcDamage(state, spellId, floorElement);
|
||||
dmg = dmg * effects.baseDamageMultiplier + effects.baseDamageBonus;
|
||||
|
||||
// Increment hit counters
|
||||
comboHitCount += 1;
|
||||
floorHitCount += 1;
|
||||
|
||||
// First Strike: +15% damage on first attack each floor
|
||||
if (hasSpecial(effects, SPECIAL_EFFECTS.FIRST_STRIKE) && floorHitCount === 1) {
|
||||
dmg *= 1.15;
|
||||
log.unshift('⚡ First Strike! +15% damage!');
|
||||
}
|
||||
|
||||
// Combo Master: Every 5th attack deals 3x damage
|
||||
if (hasSpecial(effects, SPECIAL_EFFECTS.COMBO_MASTER) && comboHitCount % 5 === 0) {
|
||||
dmg *= 3;
|
||||
log.unshift('🌀 Combo Master! Triple damage!');
|
||||
}
|
||||
|
||||
// Executioner: +100% damage to enemies below 25% HP
|
||||
if (hasSpecial(effects, SPECIAL_EFFECTS.EXECUTIONER) && floorHP / floorMaxHP < 0.25) {
|
||||
dmg *= 2;
|
||||
log.unshift('💀 Executioner! Double damage!');
|
||||
}
|
||||
|
||||
// Berserker: +50% damage when below 50% mana
|
||||
const maxMana = 100; // Would need proper max mana calculation
|
||||
if (hasSpecial(effects, SPECIAL_EFFECTS.BERSERKER) && rawMana < maxMana * 0.5) {
|
||||
dmg *= 1.5;
|
||||
log.unshift('🔥 Berserker! +50% damage!');
|
||||
}
|
||||
|
||||
// Spell echo - chance to cast again
|
||||
const echoChance = (skills.spellEcho || 0) * 0.1;
|
||||
if (Math.random() < echoChance) {
|
||||
dmg *= 2;
|
||||
log.unshift('✨ Spell Echo! Double damage!');
|
||||
}
|
||||
|
||||
// Apply damage
|
||||
floorHP = Math.max(0, floorHP - dmg);
|
||||
castProgress -= 1;
|
||||
|
||||
if (floorHP <= 0) {
|
||||
const wasGuardian = GUARDIANS[currentFloor];
|
||||
|
||||
// Adrenaline Rush: Defeating enemy restores 5% mana
|
||||
if (hasSpecial(effects, SPECIAL_EFFECTS.ADRENALINE_RUSH)) {
|
||||
const manaRestore = Math.floor(maxMana * 0.05);
|
||||
rawMana = Math.min(rawMana + manaRestore, maxMana);
|
||||
log.unshift(`💚 Adrenaline Rush! Restored ${manaRestore} mana!`);
|
||||
}
|
||||
|
||||
if (wasGuardian && !signedPacts.includes(currentFloor)) {
|
||||
pendingPactOffer = currentFloor;
|
||||
log.unshift(`⚔️ ${wasGuardian.name} defeated! They offer a pact...`);
|
||||
} else if (!wasGuardian) {
|
||||
if (currentFloor % 5 === 0) {
|
||||
log.unshift(`🏰 Floor ${currentFloor} cleared!`);
|
||||
}
|
||||
}
|
||||
|
||||
currentFloor = currentFloor + 1;
|
||||
if (currentFloor > 100) currentFloor = 100;
|
||||
floorMaxHP = getFloorMaxHP(currentFloor);
|
||||
floorHP = floorMaxHP;
|
||||
maxFloorReached = Math.max(maxFloorReached, currentFloor);
|
||||
castProgress = 0;
|
||||
floorHitCount = 0; // Reset floor hit counter for new floor
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
rawMana,
|
||||
elements,
|
||||
totalManaGathered,
|
||||
currentFloor,
|
||||
floorHP,
|
||||
floorMaxHP,
|
||||
maxFloorReached,
|
||||
signedPacts,
|
||||
pendingPactOffer,
|
||||
castProgress,
|
||||
log,
|
||||
comboHitCount,
|
||||
floorHitCount,
|
||||
};
|
||||
},
|
||||
});
|
||||
@@ -1,315 +0,0 @@
|
||||
// ─── Computed Stats Functions ─────────────────────────────────────────────────
|
||||
|
||||
import type { GameState } from '../types';
|
||||
import { ELEMENTS, GUARDIANS, SPELLS_DEF, SKILLS_DEF, PRESTIGE_DEF, ELEMENT_OPPOSITES, FLOOR_ELEM_CYCLE } from '../constants';
|
||||
import { computeEffects } from '../upgrade-effects';
|
||||
import type { ComputedEffects } from '../upgrade-effects.types';
|
||||
import type { UnifiedEffects } from '../effects';
|
||||
import { getTierMultiplier } from '../skill-evolution';
|
||||
|
||||
// Helper to get effective skill level accounting for tiers
|
||||
export function getEffectiveSkillLevel(
|
||||
skills: Record<string, number>,
|
||||
baseSkillId: string,
|
||||
skillTiers: Record<string, number> = {}
|
||||
): { level: number; tier: number; tierMultiplier: number } {
|
||||
const currentTier = skillTiers[baseSkillId] || 1;
|
||||
const tieredSkillId = currentTier > 1 ? `${baseSkillId}_t${currentTier}` : baseSkillId;
|
||||
const level = skills[tieredSkillId] || skills[baseSkillId] || 0;
|
||||
const tierMultiplier = Math.pow(10, currentTier - 1);
|
||||
return { level, tier: currentTier, tierMultiplier };
|
||||
}
|
||||
|
||||
export function computeMaxMana(
|
||||
state: Pick<GameState, 'skills' | 'prestigeUpgrades' | 'skillUpgrades' | 'skillTiers' | 'manaHeartBonus'>,
|
||||
effects?: ReturnType<typeof computeEffects>
|
||||
): number {
|
||||
const pu = state.prestigeUpgrades;
|
||||
const skillTiers = state.skillTiers || {};
|
||||
const skillUpgrades = state.skillUpgrades || {};
|
||||
const manaHeartBonus = state.manaHeartBonus || 0;
|
||||
|
||||
const manaWellLevel = getEffectiveSkillLevel(state.skills, 'manaWell', skillTiers);
|
||||
|
||||
const base =
|
||||
100 +
|
||||
manaWellLevel.level * 100 * manaWellLevel.tierMultiplier +
|
||||
((pu || {}).manaWell || 0) * 500;
|
||||
|
||||
const computedEffects = effects ?? computeEffects(skillUpgrades, skillTiers);
|
||||
// Apply MANA_HEART bonus (+10% per loop, compounds)
|
||||
const heartMultiplier = 1 + manaHeartBonus;
|
||||
return Math.floor((base + computedEffects.maxManaBonus) * computedEffects.maxManaMultiplier * heartMultiplier);
|
||||
}
|
||||
|
||||
// computeElementMax is now in ../store.ts with support for unlockedManaTypeUpgrades
|
||||
// This file no longer exports computeElementMax to avoid duplicate export issues
|
||||
// Import computeElementMax from '../store' instead
|
||||
|
||||
export function computeRegen(
|
||||
state: Pick<GameState, 'skills' | 'prestigeUpgrades' | 'skillUpgrades' | 'skillTiers'>,
|
||||
effects?: ReturnType<typeof computeEffects>
|
||||
): number {
|
||||
const pu = state.prestigeUpgrades;
|
||||
const skillTiers = state.skillTiers || {};
|
||||
const skillUpgrades = state.skillUpgrades || {};
|
||||
|
||||
const temporalBonus = 1 + (pu.temporalEcho || 0) * 0.1;
|
||||
|
||||
const manaFlowLevel = getEffectiveSkillLevel(state.skills, 'manaFlow', skillTiers);
|
||||
const manaSpringLevel = getEffectiveSkillLevel(state.skills, 'manaSpring', skillTiers);
|
||||
|
||||
const base =
|
||||
2 +
|
||||
manaFlowLevel.level * 1 * manaFlowLevel.tierMultiplier +
|
||||
manaSpringLevel.level * 2 * manaSpringLevel.tierMultiplier +
|
||||
(pu.manaFlow || 0) * 0.5;
|
||||
|
||||
let regen = base * temporalBonus;
|
||||
const computedEffects = effects ?? computeEffects(skillUpgrades, skillTiers);
|
||||
regen = (regen + computedEffects.regenBonus + computedEffects.permanentRegenBonus) * computedEffects.regenMultiplier;
|
||||
|
||||
return regen;
|
||||
}
|
||||
|
||||
export function computeClickMana(
|
||||
state: Pick<GameState, 'skills' | 'skillUpgrades' | 'skillTiers'>,
|
||||
effects?: ReturnType<typeof computeEffects>
|
||||
): number {
|
||||
const skillTiers = state.skillTiers || {};
|
||||
const skillUpgrades = state.skillUpgrades || {};
|
||||
|
||||
const manaTapLevel = getEffectiveSkillLevel(state.skills, 'manaTap', skillTiers);
|
||||
const manaSurgeLevel = getEffectiveSkillLevel(state.skills, 'manaSurge', skillTiers);
|
||||
|
||||
const base =
|
||||
1 +
|
||||
manaTapLevel.level * 1 * manaTapLevel.tierMultiplier +
|
||||
manaSurgeLevel.level * 3 * manaSurgeLevel.tierMultiplier;
|
||||
|
||||
const computedEffects = effects ?? computeEffects(skillUpgrades, skillTiers);
|
||||
return Math.floor((base + computedEffects.clickManaBonus) * computedEffects.clickManaMultiplier);
|
||||
}
|
||||
|
||||
// Elemental damage bonus
|
||||
export function getElementalBonus(spellElem: string, floorElem: string): number {
|
||||
if (spellElem === 'raw') return 1.0;
|
||||
|
||||
if (spellElem === floorElem) return 1.25; // Same element: +25% damage
|
||||
|
||||
if (ELEMENT_OPPOSITES[floorElem] === spellElem) return 1.5; // Super effective
|
||||
if (ELEMENT_OPPOSITES[spellElem] === floorElem) return 0.75; // Weak
|
||||
|
||||
return 1.0;
|
||||
}
|
||||
|
||||
// Compute the pact multiplier with interference/synergy system
|
||||
export function computePactMultiplier(
|
||||
state: Pick<GameState, 'signedPacts' | 'pactInterferenceMitigation' | 'signedPactDetails'>
|
||||
): number {
|
||||
const { signedPacts, pactInterferenceMitigation = 0 } = state;
|
||||
|
||||
if (signedPacts.length === 0) return 1.0;
|
||||
|
||||
let baseMult = 1.0;
|
||||
for (const floor of signedPacts) {
|
||||
const guardian = GUARDIANS[floor];
|
||||
if (guardian) {
|
||||
baseMult *= guardian.damageMultiplier;
|
||||
}
|
||||
}
|
||||
|
||||
if (signedPacts.length === 1) return baseMult;
|
||||
|
||||
const numAdditionalPacts = signedPacts.length - 1;
|
||||
const basePenalty = 0.5 * numAdditionalPacts;
|
||||
const mitigationReduction = Math.min(pactInterferenceMitigation, 5) * 0.1;
|
||||
const effectivePenalty = Math.max(0, basePenalty - mitigationReduction);
|
||||
|
||||
if (pactInterferenceMitigation >= 5) {
|
||||
const synergyBonus = (pactInterferenceMitigation - 5) * 0.1;
|
||||
return baseMult * (1 + synergyBonus);
|
||||
}
|
||||
|
||||
return baseMult * (1 - effectivePenalty);
|
||||
}
|
||||
|
||||
// Compute the insight multiplier from signed pacts
|
||||
export function computePactInsightMultiplier(
|
||||
state: Pick<GameState, 'signedPacts' | 'pactInterferenceMitigation'>
|
||||
): number {
|
||||
const { signedPacts, pactInterferenceMitigation = 0 } = state;
|
||||
|
||||
if (signedPacts.length === 0) return 1.0;
|
||||
|
||||
let mult = 1.0;
|
||||
for (const floor of signedPacts) {
|
||||
const guardian = GUARDIANS[floor];
|
||||
if (guardian) {
|
||||
mult *= guardian.insightMultiplier;
|
||||
}
|
||||
}
|
||||
|
||||
if (signedPacts.length > 1) {
|
||||
const numAdditionalPacts = signedPacts.length - 1;
|
||||
const basePenalty = 0.5 * numAdditionalPacts;
|
||||
const mitigationReduction = Math.min(pactInterferenceMitigation, 5) * 0.1;
|
||||
const effectivePenalty = Math.max(0, basePenalty - mitigationReduction);
|
||||
|
||||
if (pactInterferenceMitigation >= 5) {
|
||||
const synergyBonus = (pactInterferenceMitigation - 5) * 0.1;
|
||||
return mult * (1 + synergyBonus);
|
||||
}
|
||||
|
||||
return mult * (1 - effectivePenalty);
|
||||
}
|
||||
|
||||
return mult;
|
||||
}
|
||||
|
||||
export function calcDamage(
|
||||
state: Pick<GameState, 'skills' | 'signedPacts' | 'pactInterferenceMitigation' | 'signedPactDetails' | 'skillUpgrades' | 'skillTiers'>,
|
||||
spellId: string,
|
||||
floorElem?: string,
|
||||
effects?: ReturnType<typeof computeEffects>
|
||||
): number {
|
||||
const sp = SPELLS_DEF[spellId];
|
||||
if (!sp) return 5;
|
||||
|
||||
const skillTiers = state.skillTiers || {};
|
||||
const skillUpgrades = state.skillUpgrades || {};
|
||||
const computedEffects = effects ?? computeEffects(skillUpgrades, skillTiers);
|
||||
|
||||
// Get effective skill levels with tier multipliers
|
||||
const combatTrainLevel = getEffectiveSkillLevel(state.skills, 'combatTrain', skillTiers);
|
||||
const arcaneFuryLevel = getEffectiveSkillLevel(state.skills, 'arcaneFury', skillTiers);
|
||||
const elemMasteryLevel = getEffectiveSkillLevel(state.skills, 'elementalMastery', skillTiers);
|
||||
const guardianBaneLevel = getEffectiveSkillLevel(state.skills, 'guardianBane', skillTiers);
|
||||
const precisionLevel = getEffectiveSkillLevel(state.skills, 'precision', skillTiers);
|
||||
|
||||
// Base damage from spell + combat training
|
||||
const baseDmg = sp.dmg + combatTrainLevel.level * 5 * combatTrainLevel.tierMultiplier;
|
||||
|
||||
// Spell damage multiplier from arcane fury
|
||||
const pct = 1 + arcaneFuryLevel.level * 0.1 * arcaneFuryLevel.tierMultiplier;
|
||||
|
||||
// Elemental mastery bonus
|
||||
const elemMasteryBonus = 1 + elemMasteryLevel.level * 0.15 * elemMasteryLevel.tierMultiplier;
|
||||
|
||||
// Guardian bane bonus (only for guardian floors)
|
||||
const guardianBonus = floorElem && Object.values(GUARDIANS).find(g => g.element === floorElem)
|
||||
? 1 + guardianBaneLevel.level * 0.2 * guardianBaneLevel.tierMultiplier
|
||||
: 1;
|
||||
|
||||
// Crit chance from precision
|
||||
const skillCritChance = precisionLevel.level * 0.05 * precisionLevel.tierMultiplier;
|
||||
const totalCritChance = skillCritChance + computedEffects.critChanceBonus;
|
||||
|
||||
// Pact multiplier
|
||||
const pactMult = computePactMultiplier(state);
|
||||
|
||||
// Calculate base damage
|
||||
let damage = baseDmg * pct * pactMult * elemMasteryBonus * guardianBonus;
|
||||
|
||||
// Apply upgrade effects: base damage multiplier and bonus
|
||||
damage = damage * computedEffects.baseDamageMultiplier + computedEffects.baseDamageBonus;
|
||||
|
||||
// Apply elemental damage multiplier from upgrades
|
||||
damage *= computedEffects.elementalDamageMultiplier;
|
||||
|
||||
// Apply elemental bonus for floor
|
||||
if (floorElem) {
|
||||
damage *= getElementalBonus(sp.elem, floorElem);
|
||||
}
|
||||
|
||||
// Apply critical hit
|
||||
if (Math.random() < totalCritChance) {
|
||||
damage *= computedEffects.critDamageMultiplier;
|
||||
}
|
||||
|
||||
return damage;
|
||||
}
|
||||
|
||||
export function calcInsight(state: Pick<GameState, 'maxFloorReached' | 'totalManaGathered' | 'signedPacts' | 'prestigeUpgrades' | 'skills'>): number {
|
||||
const pu = state.prestigeUpgrades;
|
||||
const skillBonus = 1 + (state.skills.insightHarvest || 0) * 0.1;
|
||||
const mult = (1 + (pu.insightAmp || 0) * 0.25) * skillBonus;
|
||||
return Math.floor(
|
||||
(state.maxFloorReached * 15 + state.totalManaGathered / 500 + state.signedPacts.length * 150) * mult
|
||||
);
|
||||
}
|
||||
|
||||
export function getMeditationBonus(meditateTicks: number, skills: Record<string, number>, meditationEfficiency: number = 1): number {
|
||||
const hasMeditation = skills.meditation === 1;
|
||||
const hasDeepTrance = skills.deepTrance === 1;
|
||||
const hasVoidMeditation = skills.voidMeditation === 1;
|
||||
|
||||
const hours = meditateTicks * 0.04; // HOURS_PER_TICK
|
||||
|
||||
let bonus = 1 + Math.min(hours / 4, 0.5);
|
||||
|
||||
if (hasMeditation && hours >= 4) {
|
||||
bonus = 2.5;
|
||||
}
|
||||
|
||||
if (hasDeepTrance && hours >= 6) {
|
||||
bonus = 3.0;
|
||||
}
|
||||
|
||||
if (hasVoidMeditation && hours >= 8) {
|
||||
bonus = 5.0;
|
||||
}
|
||||
|
||||
bonus *= meditationEfficiency;
|
||||
|
||||
return bonus;
|
||||
}
|
||||
|
||||
export function getIncursionStrength(day: number, hour: number): number {
|
||||
const INCURSION_START_DAY = 20;
|
||||
const MAX_DAY = 30;
|
||||
|
||||
if (day < INCURSION_START_DAY) return 0;
|
||||
const totalHours = (day - INCURSION_START_DAY) * 24 + hour;
|
||||
const maxHours = (MAX_DAY - INCURSION_START_DAY) * 24;
|
||||
return Math.min(0.95, (totalHours / maxHours) * 0.95);
|
||||
}
|
||||
|
||||
export function getFloorMaxHP(floor: number): number {
|
||||
if (GUARDIANS[floor]) return GUARDIANS[floor].hp;
|
||||
const baseHP = 100;
|
||||
const floorScaling = floor * 50;
|
||||
const exponentialScaling = Math.pow(floor, 1.7);
|
||||
return Math.floor(baseHP + floorScaling + exponentialScaling);
|
||||
}
|
||||
|
||||
export function getFloorElement(floor: number): string {
|
||||
return FLOOR_ELEM_CYCLE[(floor - 1) % FLOOR_ELEM_CYCLE.length];
|
||||
}
|
||||
|
||||
// Formatting utilities
|
||||
export function fmt(n: number): string {
|
||||
if (!isFinite(n) || isNaN(n)) return '0';
|
||||
if (n >= 1e9) return (n / 1e9).toFixed(2) + 'B';
|
||||
if (n >= 1e6) return (n / 1e6).toFixed(2) + 'M';
|
||||
if (n >= 1e3) return (n / 1e3).toFixed(1) + 'K';
|
||||
return Math.floor(n).toString();
|
||||
}
|
||||
|
||||
export function fmtDec(n: number, d: number = 1): string {
|
||||
return isFinite(n) ? n.toFixed(d) : '0';
|
||||
}
|
||||
|
||||
// Check if player can afford spell cost
|
||||
export function canAffordSpellCost(
|
||||
cost: { type: 'raw' | 'element'; element?: string; amount: number },
|
||||
rawMana: number,
|
||||
elements: Record<string, { current: number; max: number; unlocked: boolean }>
|
||||
): boolean {
|
||||
if (cost.type === 'raw') {
|
||||
return rawMana >= cost.amount;
|
||||
} else {
|
||||
const elem = elements[cost.element || ''];
|
||||
return elem && elem.unlocked && elem.current >= cost.amount;
|
||||
}
|
||||
}
|
||||
@@ -1,23 +0,0 @@
|
||||
// ─── Crafting Initial State ───────────────────────────────────────────────────
|
||||
|
||||
import type { CraftingState } from './types';
|
||||
import { EQUIPMENT_SLOTS } from '../../data/equipment';
|
||||
|
||||
export const initialCraftingState: CraftingState = {
|
||||
equippedInstances: {
|
||||
mainHand: null,
|
||||
offHand: null,
|
||||
head: null,
|
||||
body: null,
|
||||
hands: null,
|
||||
feet: null,
|
||||
accessory1: null,
|
||||
accessory2: null,
|
||||
},
|
||||
equipmentInstances: {},
|
||||
enchantmentDesigns: [],
|
||||
designProgress: null,
|
||||
preparationProgress: null,
|
||||
applicationProgress: null,
|
||||
equipmentSpellStates: [],
|
||||
};
|
||||
@@ -1,49 +0,0 @@
|
||||
// ─── Crafting Selectors ───────────────────────────────────────────────────
|
||||
|
||||
import type { CraftingStore } from './types';
|
||||
import type { EquipmentInstance } from '../../types';
|
||||
import type { EquipmentSlot } from '../../data/equipment';
|
||||
import { EQUIPMENT_SLOTS } from '../../data/equipment';
|
||||
import { getSpellsFromEquipment, computeEquipmentEffects } from './utils';
|
||||
|
||||
/**
|
||||
* Creates selector functions that depend on the store's get function.
|
||||
* Selectors are pure functions that derive data from the current state.
|
||||
*/
|
||||
export const createSelectors = (get: () => CraftingStore) => ({
|
||||
getEquippedInstance: (slot: EquipmentSlot): EquipmentInstance | null => {
|
||||
const state = get();
|
||||
const instanceId = state.equippedInstances[slot];
|
||||
if (!instanceId) return null;
|
||||
return state.equipmentInstances[instanceId] || null;
|
||||
},
|
||||
|
||||
getAllEquipped: (): EquipmentInstance[] => {
|
||||
const state = get();
|
||||
const equipped: EquipmentInstance[] = [];
|
||||
|
||||
for (const slot of EQUIPMENT_SLOTS) {
|
||||
const instanceId = state.equippedInstances[slot];
|
||||
if (instanceId && state.equipmentInstances[instanceId]) {
|
||||
equipped.push(state.equipmentInstances[instanceId]);
|
||||
}
|
||||
}
|
||||
|
||||
return equipped;
|
||||
},
|
||||
|
||||
getAvailableSpells: (): string[] => {
|
||||
const equipped = get().getAllEquipped();
|
||||
const spells: string[] = [];
|
||||
|
||||
for (const equip of equipped) {
|
||||
spells.push(...getSpellsFromEquipment(equip));
|
||||
}
|
||||
|
||||
return spells;
|
||||
},
|
||||
|
||||
getEquipmentEffects: () => {
|
||||
return computeEquipmentEffects(get().getAllEquipped());
|
||||
},
|
||||
});
|
||||
@@ -1,252 +0,0 @@
|
||||
// ─── Crafting Slice Logic ─────────────────────────────────────────────────
|
||||
|
||||
import type { StateCreator } from 'zustand';
|
||||
import type { CraftingStore } from './types';
|
||||
import type {
|
||||
DesignEffect,
|
||||
EnchantmentDesign,
|
||||
EquipmentInstance
|
||||
} from '../../types';
|
||||
import type { EquipmentSlot } from '../../data/equipment';
|
||||
import { initialCraftingState } from './initial-state';
|
||||
import {
|
||||
generateInstanceId,
|
||||
generateDesignId,
|
||||
createEquipmentInstance,
|
||||
calculateDesignTime,
|
||||
calculatePreparationTime,
|
||||
calculatePreparationManaCost,
|
||||
calculateApplicationTime,
|
||||
calculateApplicationManaPerHour
|
||||
} from './utils';
|
||||
import {
|
||||
EQUIPMENT_SLOTS,
|
||||
getEquipmentType
|
||||
} from '../../data/equipment';
|
||||
import { createSelectors } from './selectors';
|
||||
import {
|
||||
processDesignTick,
|
||||
processPreparationTick,
|
||||
processApplicationTick
|
||||
} from './tick-processors';
|
||||
|
||||
// ─── Cached Skills Workaround ──────────────────────────────────────────────
|
||||
// We need to access skills from the main store - this is a workaround
|
||||
// The store will pass skills when calling these methods
|
||||
|
||||
let cachedSkills: Record<string, number> = {};
|
||||
|
||||
export function setCachedSkills(skills: Record<string, number>): void {
|
||||
cachedSkills = skills;
|
||||
}
|
||||
|
||||
// ─── Slice Creator ─────────────────────────────────────────────────────────
|
||||
|
||||
export const createCraftingSlice: StateCreator<CraftingStore, [], [], CraftingStore> = (set, get) => {
|
||||
const selectors = createSelectors(get);
|
||||
|
||||
return {
|
||||
...initialCraftingState,
|
||||
|
||||
// Equipment management
|
||||
createEquipment: (typeId: string, slot?: EquipmentSlot) => {
|
||||
const instance = createEquipmentInstance(typeId);
|
||||
|
||||
set((state) => ({
|
||||
equipmentInstances: {
|
||||
...state.equipmentInstances,
|
||||
[instance.instanceId]: instance,
|
||||
},
|
||||
}));
|
||||
|
||||
// Auto-equip if slot provided
|
||||
if (slot) {
|
||||
get().equipInstance(instance.instanceId, slot);
|
||||
}
|
||||
|
||||
return instance;
|
||||
},
|
||||
|
||||
equipInstance: (instanceId: string, slot: EquipmentSlot) => {
|
||||
const instance = get().equipmentInstances[instanceId];
|
||||
if (!instance) return;
|
||||
|
||||
const typeDef = getEquipmentType(instance.typeId);
|
||||
if (!typeDef) return;
|
||||
|
||||
// Check if equipment can go in this slot
|
||||
if (typeDef.slot !== slot) {
|
||||
// For accessories, both accessory1 and accessory2 are valid
|
||||
if (typeDef.category !== 'accessory' || (slot !== 'accessory1' && slot !== 'accessory2')) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
set((state) => ({
|
||||
equippedInstances: {
|
||||
...state.equippedInstances,
|
||||
[slot]: instanceId,
|
||||
},
|
||||
}));
|
||||
},
|
||||
|
||||
unequipSlot: (slot: EquipmentSlot) => {
|
||||
set((state) => ({
|
||||
equippedInstances: {
|
||||
...state.equippedInstances,
|
||||
[slot]: null,
|
||||
},
|
||||
}));
|
||||
},
|
||||
|
||||
deleteInstance: (instanceId: string) => {
|
||||
set((state) => {
|
||||
const newInstanceMap = { ...state.equipmentInstances };
|
||||
delete newInstanceMap[instanceId];
|
||||
|
||||
// Remove from equipped slots
|
||||
const newEquipped = { ...state.equippedInstances };
|
||||
for (const slot of EQUIPMENT_SLOTS) {
|
||||
if (newEquipped[slot] === instanceId) {
|
||||
newEquipped[slot] = null;
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
equipmentInstances: newInstanceMap,
|
||||
equippedInstances: newEquipped,
|
||||
};
|
||||
});
|
||||
},
|
||||
|
||||
// Enchantment design
|
||||
startDesign: (name: string, equipmentType: string, effects: DesignEffect[]) => {
|
||||
const totalCapacity = effects.reduce((sum, e) => sum + e.capacityCost, 0);
|
||||
const designTime = calculateDesignTime(effects);
|
||||
|
||||
const design: EnchantmentDesign = {
|
||||
id: generateDesignId(),
|
||||
name,
|
||||
equipmentType,
|
||||
effects,
|
||||
totalCapacityUsed: totalCapacity,
|
||||
designTime,
|
||||
created: Date.now(),
|
||||
};
|
||||
|
||||
set((state) => ({
|
||||
enchantmentDesigns: [...state.enchantmentDesigns, design],
|
||||
designProgress: {
|
||||
designId: design.id,
|
||||
progress: 0,
|
||||
required: designTime,
|
||||
name,
|
||||
equipmentType,
|
||||
effects,
|
||||
},
|
||||
}));
|
||||
},
|
||||
|
||||
cancelDesign: () => {
|
||||
const progress = get().designProgress;
|
||||
if (!progress) return;
|
||||
|
||||
set((state) => ({
|
||||
designProgress: null,
|
||||
enchantmentDesigns: state.enchantmentDesigns.filter(d => d.id !== progress.designId),
|
||||
}));
|
||||
},
|
||||
|
||||
deleteDesign: (designId: string) => {
|
||||
set((state) => ({
|
||||
enchantmentDesigns: state.enchantmentDesigns.filter(d => d.id !== designId),
|
||||
}));
|
||||
},
|
||||
|
||||
// Equipment preparation
|
||||
startPreparation: (instanceId: string) => {
|
||||
const instance = get().equipmentInstances[instanceId];
|
||||
if (!instance) return;
|
||||
|
||||
const prepTime = calculatePreparationTime(instance.typeId);
|
||||
const manaCost = calculatePreparationManaCost(instance.typeId);
|
||||
|
||||
set({
|
||||
preparationProgress: {
|
||||
equipmentInstanceId: instanceId,
|
||||
progress: 0,
|
||||
required: prepTime,
|
||||
manaCostPaid: 0,
|
||||
},
|
||||
});
|
||||
},
|
||||
|
||||
cancelPreparation: () => {
|
||||
set({ preparationProgress: null });
|
||||
},
|
||||
|
||||
// Enchantment application
|
||||
startApplication: (instanceId: string, designId: string) => {
|
||||
const instance = get().equipmentInstances[instanceId];
|
||||
const design = get().enchantmentDesigns.find(d => d.id === designId);
|
||||
|
||||
if (!instance || !design) return;
|
||||
|
||||
const appTime = calculateApplicationTime(design.effects, cachedSkills);
|
||||
const manaPerHour = calculateApplicationManaPerHour(design.effects);
|
||||
|
||||
set({
|
||||
applicationProgress: {
|
||||
equipmentInstanceId: instanceId,
|
||||
designId,
|
||||
progress: 0,
|
||||
required: appTime,
|
||||
manaPerHour,
|
||||
paused: false,
|
||||
manaSpent: 0,
|
||||
},
|
||||
});
|
||||
},
|
||||
|
||||
pauseApplication: () => {
|
||||
const progress = get().applicationProgress;
|
||||
if (!progress) return;
|
||||
|
||||
set({
|
||||
applicationProgress: { ...progress, paused: true },
|
||||
});
|
||||
},
|
||||
|
||||
resumeApplication: () => {
|
||||
const progress = get().applicationProgress;
|
||||
if (!progress) return;
|
||||
|
||||
set({
|
||||
applicationProgress: { ...progress, paused: false },
|
||||
});
|
||||
},
|
||||
|
||||
cancelApplication: () => {
|
||||
set({ applicationProgress: null });
|
||||
},
|
||||
|
||||
// Tick processing - delegated to tick-processors module
|
||||
processDesignTick: (hours: number) => {
|
||||
return processDesignTick(get(), set, hours);
|
||||
},
|
||||
|
||||
processPreparationTick: (hours: number, manaAvailable: number) => {
|
||||
return processPreparationTick(get(), set, hours, manaAvailable);
|
||||
},
|
||||
|
||||
processApplicationTick: (hours: number, manaAvailable: number) => {
|
||||
return processApplicationTick(get(), set, get, hours, manaAvailable, cachedSkills);
|
||||
},
|
||||
|
||||
// Selectors - delegated to selectors module
|
||||
getEquippedInstance: selectors.getEquippedInstance,
|
||||
getAllEquipped: selectors.getAllEquipped,
|
||||
getAvailableSpells: selectors.getAvailableSpells,
|
||||
getEquipmentEffects: selectors.getEquipmentEffects,
|
||||
};
|
||||
};
|
||||
@@ -1,51 +0,0 @@
|
||||
// ─── Starting Equipment Factory ─────────────────────────────────────────
|
||||
|
||||
import type { EquipmentInstance } from '../../types';
|
||||
import { createEquipmentInstance } from './utils';
|
||||
|
||||
export function createStartingEquipment(): {
|
||||
equippedInstances: Record<string, string | null>;
|
||||
equipmentInstances: Record<string, EquipmentInstance>;
|
||||
} {
|
||||
const instances: EquipmentInstance[] = [];
|
||||
|
||||
// Create starting equipment
|
||||
const basicStaff = createEquipmentInstance('basicStaff');
|
||||
basicStaff.enchantments = [{
|
||||
effectId: 'spell_manaBolt',
|
||||
stacks: 1,
|
||||
actualCost: 50, // Fills the staff completely
|
||||
}];
|
||||
basicStaff.usedCapacity = 50;
|
||||
basicStaff.rarity = 'uncommon';
|
||||
instances.push(basicStaff);
|
||||
|
||||
const civilianShirt = createEquipmentInstance('civilianShirt');
|
||||
instances.push(civilianShirt);
|
||||
|
||||
const civilianGloves = createEquipmentInstance('civilianGloves');
|
||||
instances.push(civilianGloves);
|
||||
|
||||
const civilianShoes = createEquipmentInstance('civilianShoes');
|
||||
instances.push(civilianShoes);
|
||||
|
||||
// Build instance map
|
||||
const equipmentInstances: Record<string, EquipmentInstance> = {};
|
||||
for (const inst of instances) {
|
||||
equipmentInstances[inst.instanceId] = inst;
|
||||
}
|
||||
|
||||
// Build equipped map
|
||||
const equippedInstances: Record<string, string | null> = {
|
||||
mainHand: basicStaff.instanceId,
|
||||
offHand: null,
|
||||
head: null,
|
||||
body: civilianShirt.instanceId,
|
||||
hands: civilianGloves.instanceId,
|
||||
feet: civilianShoes.instanceId,
|
||||
accessory1: null,
|
||||
accessory2: null,
|
||||
};
|
||||
|
||||
return { equippedInstances, equipmentInstances };
|
||||
}
|
||||
@@ -1,178 +0,0 @@
|
||||
// ─── Tick Processors ─────────────────────────────────────────────────────
|
||||
// These functions handle the time-based processing for crafting operations.
|
||||
|
||||
import type { CraftingStore } from './types';
|
||||
import type { AppliedEnchantment, EquipmentInstance } from '../../types';
|
||||
import {
|
||||
getEnchantEfficiencyBonus,
|
||||
calculatePreparationManaCost,
|
||||
calculateEffectCapacityCost,
|
||||
calculateRarity
|
||||
} from './utils';
|
||||
import { getEnchantmentEffect } from '../../data/enchantment-effects';
|
||||
|
||||
/**
|
||||
* Processes design tick progress.
|
||||
* Returns true if design was completed this tick.
|
||||
*/
|
||||
export function processDesignTick(
|
||||
state: CraftingStore,
|
||||
setState: (updater: Partial<CraftingStore> | ((state: CraftingStore) => Partial<CraftingStore>)) => void,
|
||||
hours: number
|
||||
): boolean {
|
||||
const progress = state.designProgress;
|
||||
if (!progress) return false;
|
||||
|
||||
const newProgress = progress.progress + hours;
|
||||
|
||||
if (newProgress >= progress.required) {
|
||||
// Design complete
|
||||
setState({ designProgress: null });
|
||||
return true;
|
||||
} else {
|
||||
setState({
|
||||
designProgress: { ...progress, progress: newProgress },
|
||||
});
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Processes preparation tick progress.
|
||||
* Returns the amount of mana consumed.
|
||||
*/
|
||||
export function processPreparationTick(
|
||||
state: CraftingStore,
|
||||
setState: (updater: Partial<CraftingStore> | ((state: CraftingStore) => Partial<CraftingStore>)) => void,
|
||||
hours: number,
|
||||
manaAvailable: number
|
||||
): number {
|
||||
const progress = state.preparationProgress;
|
||||
if (!progress) return 0;
|
||||
|
||||
const instance = state.equipmentInstances[progress.equipmentInstanceId];
|
||||
if (!instance) {
|
||||
setState({ preparationProgress: null });
|
||||
return 0;
|
||||
}
|
||||
|
||||
const totalManaCost = calculatePreparationManaCost(instance.typeId);
|
||||
const remainingManaCost = totalManaCost - progress.manaCostPaid;
|
||||
const manaToPay = Math.min(manaAvailable, remainingManaCost);
|
||||
|
||||
if (manaToPay < remainingManaCost) {
|
||||
// Not enough mana, just pay what we can
|
||||
setState({
|
||||
preparationProgress: {
|
||||
...progress,
|
||||
manaCostPaid: progress.manaCostPaid + manaToPay,
|
||||
},
|
||||
});
|
||||
return manaToPay;
|
||||
}
|
||||
|
||||
// Pay remaining mana and progress
|
||||
const newProgress = progress.progress + hours;
|
||||
|
||||
if (newProgress >= progress.required) {
|
||||
// Preparation complete - clear enchantments and add 'Ready for Enchantment' tag
|
||||
setState((state) => ({
|
||||
preparationProgress: null,
|
||||
equipmentInstances: {
|
||||
...state.equipmentInstances,
|
||||
[instance.instanceId]: {
|
||||
...instance,
|
||||
enchantments: [],
|
||||
usedCapacity: 0,
|
||||
rarity: 'common' as const,
|
||||
tags: [...(instance.tags || []), 'Ready for Enchantment'],
|
||||
},
|
||||
},
|
||||
}));
|
||||
} else {
|
||||
setState({
|
||||
preparationProgress: {
|
||||
...progress,
|
||||
progress: newProgress,
|
||||
manaCostPaid: progress.manaCostPaid + manaToPay,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
return manaToPay;
|
||||
}
|
||||
|
||||
/**
|
||||
* Processes application tick progress.
|
||||
* Returns the amount of mana consumed.
|
||||
*/
|
||||
export function processApplicationTick(
|
||||
state: CraftingStore,
|
||||
setState: (updater: Partial<CraftingStore> | ((state: CraftingStore) => Partial<CraftingStore>)) => void,
|
||||
getState: () => CraftingStore,
|
||||
hours: number,
|
||||
manaAvailable: number,
|
||||
cachedSkills: Record<string, number>
|
||||
): number {
|
||||
const progress = state.applicationProgress;
|
||||
if (!progress || progress.paused) return 0;
|
||||
|
||||
const design = state.enchantmentDesigns.find(d => d.id === progress.designId);
|
||||
const instance = state.equipmentInstances[progress.equipmentInstanceId];
|
||||
|
||||
if (!design || !instance) {
|
||||
setState({ applicationProgress: null });
|
||||
return 0;
|
||||
}
|
||||
|
||||
const manaNeeded = progress.manaPerHour * hours;
|
||||
const manaToUse = Math.min(manaAvailable, manaNeeded);
|
||||
|
||||
if (manaToUse < manaNeeded) {
|
||||
// Not enough mana - pause and save progress
|
||||
setState({
|
||||
applicationProgress: {
|
||||
...progress,
|
||||
manaSpent: progress.manaSpent + manaToUse,
|
||||
},
|
||||
});
|
||||
return manaToUse;
|
||||
}
|
||||
|
||||
const newProgress = progress.progress + hours;
|
||||
|
||||
if (newProgress >= progress.required) {
|
||||
// Application complete - apply enchantments
|
||||
const efficiencyBonus = getEnchantEfficiencyBonus(cachedSkills);
|
||||
const newEnchantments: AppliedEnchantment[] = design.effects.map(e => ({
|
||||
effectId: e.effectId,
|
||||
stacks: e.stacks,
|
||||
actualCost: calculateEffectCapacityCost(e.effectId, e.stacks, efficiencyBonus),
|
||||
}));
|
||||
|
||||
const totalUsedCapacity = newEnchantments.reduce((sum, e) => sum + e.actualCost, 0);
|
||||
|
||||
setState((state) => ({
|
||||
applicationProgress: null,
|
||||
equipmentInstances: {
|
||||
...state.equipmentInstances,
|
||||
[instance.instanceId]: {
|
||||
...instance,
|
||||
enchantments: newEnchantments,
|
||||
usedCapacity: totalUsedCapacity,
|
||||
rarity: calculateRarity(newEnchantments),
|
||||
},
|
||||
},
|
||||
}));
|
||||
} else {
|
||||
setState({
|
||||
applicationProgress: {
|
||||
...progress,
|
||||
progress: newProgress,
|
||||
manaSpent: progress.manaSpent + manaToUse,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
return manaToUse;
|
||||
}
|
||||
@@ -1,66 +0,0 @@
|
||||
// ─── Crafting Store Types ──────────────────────────────────────────────────────
|
||||
|
||||
import type {
|
||||
EquipmentInstance,
|
||||
AppliedEnchantment,
|
||||
EnchantmentDesign,
|
||||
DesignEffect,
|
||||
DesignProgress,
|
||||
PreparationProgress,
|
||||
ApplicationProgress,
|
||||
EquipmentSpellState
|
||||
} from '../../types';
|
||||
import type { EquipmentSlot } from '../../data/equipment';
|
||||
|
||||
export interface CraftingState {
|
||||
// Equipment instances
|
||||
equippedInstances: Record<string, string | null>; // slot -> instanceId
|
||||
equipmentInstances: Record<string, EquipmentInstance>; // instanceId -> instance
|
||||
|
||||
// Enchantment designs
|
||||
enchantmentDesigns: EnchantmentDesign[];
|
||||
|
||||
// Crafting progress
|
||||
designProgress: DesignProgress | null;
|
||||
preparationProgress: PreparationProgress | null;
|
||||
applicationProgress: ApplicationProgress | null;
|
||||
|
||||
// Equipment spell states
|
||||
equipmentSpellStates: EquipmentSpellState[];
|
||||
}
|
||||
|
||||
export interface CraftingActions {
|
||||
// Equipment management
|
||||
createEquipment: (typeId: string, slot?: EquipmentSlot) => EquipmentInstance;
|
||||
equipInstance: (instanceId: string, slot: EquipmentSlot) => void;
|
||||
unequipSlot: (slot: EquipmentSlot) => void;
|
||||
deleteInstance: (instanceId: string) => void;
|
||||
|
||||
// Enchantment design
|
||||
startDesign: (name: string, equipmentType: string, effects: DesignEffect[]) => void;
|
||||
cancelDesign: () => void;
|
||||
deleteDesign: (designId: string) => void;
|
||||
|
||||
// Equipment preparation
|
||||
startPreparation: (instanceId: string) => void;
|
||||
cancelPreparation: () => void;
|
||||
|
||||
// Enchantment application
|
||||
startApplication: (instanceId: string, designId: string) => void;
|
||||
pauseApplication: () => void;
|
||||
resumeApplication: () => void;
|
||||
cancelApplication: () => void;
|
||||
|
||||
// Tick processing
|
||||
processDesignTick: (hours: number) => void;
|
||||
processPreparationTick: (hours: number, manaAvailable: number) => number; // Returns mana used
|
||||
processApplicationTick: (hours: number, manaAvailable: number) => number; // Returns mana used
|
||||
|
||||
// Getters
|
||||
getEquippedInstance: (slot: EquipmentSlot) => EquipmentInstance | null;
|
||||
getAllEquipped: () => EquipmentInstance[];
|
||||
getAvailableSpells: () => string[];
|
||||
getEquipmentEffects: () => Record<string, number>;
|
||||
}
|
||||
|
||||
export type CraftingStore = CraftingState & CraftingActions;
|
||||
@@ -1,167 +0,0 @@
|
||||
// ─── Crafting Utility Functions ──────────────────────────────────────────────
|
||||
|
||||
import type {
|
||||
EquipmentInstance,
|
||||
DesignEffect,
|
||||
AppliedEnchantment
|
||||
} from '../../types';
|
||||
import {
|
||||
getEquipmentType
|
||||
} from '../../data/equipment';
|
||||
import {
|
||||
getEnchantmentEffect,
|
||||
calculateEffectCapacityCost
|
||||
} from '../../data/enchantment-effects';
|
||||
|
||||
// Re-export for use in other modules
|
||||
export { getEquipmentType } from '../../data/equipment';
|
||||
export { calculateEffectCapacityCost, getEnchantmentEffect } from '../../data/enchantment-effects';
|
||||
|
||||
// ─── ID Generators ────────────────────────────────────────────────────────────
|
||||
|
||||
let instanceIdCounter = 0;
|
||||
export function generateInstanceId(): string {
|
||||
return `equip_${Date.now()}_${++instanceIdCounter}`;
|
||||
}
|
||||
|
||||
let designIdCounter = 0;
|
||||
export function generateDesignId(): string {
|
||||
return `design_${Date.now()}_${++designIdCounter}`;
|
||||
}
|
||||
|
||||
// ─── Skill-based Calculations ────────────────────────────────────────────────
|
||||
|
||||
// Calculate efficiency bonus from skills
|
||||
export function getEnchantEfficiencyBonus(skills: Record<string, number>): number {
|
||||
const enchantingLevel = skills.enchanting || 0;
|
||||
const efficientEnchantLevel = skills.efficientEnchant || 0;
|
||||
|
||||
// 2% per enchanting level + 5% per efficient enchant level
|
||||
return (enchantingLevel * 0.02) + (efficientEnchantLevel * 0.05);
|
||||
}
|
||||
|
||||
// ─── Time and Cost Calculations ──────────────────────────────────────────────
|
||||
|
||||
// Calculate design time based on effects
|
||||
export function calculateDesignTime(effects: DesignEffect[]): number {
|
||||
const totalCapacity = effects.reduce((sum, e) => sum + e.capacityCost, 0);
|
||||
return Math.max(1, Math.floor(totalCapacity / 10)); // Hours
|
||||
}
|
||||
|
||||
// Calculate preparation time for equipment
|
||||
export function calculatePreparationTime(equipmentType: string): number {
|
||||
const typeDef = getEquipmentType(equipmentType);
|
||||
if (!typeDef) return 1;
|
||||
return Math.max(1, Math.floor(typeDef.baseCapacity / 5)); // Hours
|
||||
}
|
||||
|
||||
// Calculate preparation mana cost
|
||||
export function calculatePreparationManaCost(equipmentType: string): number {
|
||||
const typeDef = getEquipmentType(equipmentType);
|
||||
if (!typeDef) return 50;
|
||||
return typeDef.baseCapacity * 5;
|
||||
}
|
||||
|
||||
// Calculate application time based on effects
|
||||
export function calculateApplicationTime(effects: DesignEffect[], skills: Record<string, number>): number {
|
||||
const totalCapacity = effects.reduce((sum, e) => sum + e.capacityCost, 0);
|
||||
const speedBonus = 1 + (skills.enchantSpeed || 0) * 0.1;
|
||||
return Math.max(4, Math.floor(totalCapacity / 20 * 24 / speedBonus)); // Hours (days * 24)
|
||||
}
|
||||
|
||||
// Calculate mana per hour for application
|
||||
export function calculateApplicationManaPerHour(effects: DesignEffect[]): number {
|
||||
const totalCapacity = effects.reduce((sum, e) => sum + e.capacityCost, 0);
|
||||
return Math.max(1, Math.floor(totalCapacity * 0.5));
|
||||
}
|
||||
|
||||
// ─── Equipment Instance Creation ─────────────────────────────────────────────
|
||||
|
||||
// Create a new equipment instance
|
||||
export function createEquipmentInstance(typeId: string, name?: string): EquipmentInstance {
|
||||
const typeDef = getEquipmentType(typeId);
|
||||
if (!typeDef) {
|
||||
throw new Error(`Unknown equipment type: ${typeId}`);
|
||||
}
|
||||
|
||||
return {
|
||||
instanceId: generateInstanceId(),
|
||||
typeId,
|
||||
name: name || typeDef.name,
|
||||
enchantments: [],
|
||||
usedCapacity: 0,
|
||||
totalCapacity: typeDef.baseCapacity,
|
||||
rarity: 'common',
|
||||
quality: 100, // Full quality for new items
|
||||
tags: [], // Initialize with empty tags array
|
||||
};
|
||||
}
|
||||
|
||||
// ─── Rarity Calculation ────────────────────────────────────────────────────
|
||||
|
||||
// Calculate rarity based on number and quality of enchantments
|
||||
export function calculateRarity(enchantments: AppliedEnchantment[]): 'common' | 'uncommon' | 'rare' | 'epic' | 'legendary' {
|
||||
if (enchantments.length === 0) return 'common';
|
||||
|
||||
const totalCapacity = enchantments.reduce((sum, e) => sum + e.actualCost, 0);
|
||||
const avgStacks = enchantments.reduce((sum, e) => sum + e.stacks, 0) / enchantments.length;
|
||||
|
||||
// Determine rarity based on capacity used and number of enchantments
|
||||
if (totalCapacity >= 200 && enchantments.length >= 3) return 'legendary';
|
||||
if (totalCapacity >= 150 && enchantments.length >= 2) return 'epic';
|
||||
if (totalCapacity >= 100 || enchantments.length >= 2) return 'rare';
|
||||
if (totalCapacity >= 50 || avgStacks > 1) return 'uncommon';
|
||||
return 'common';
|
||||
}
|
||||
|
||||
// ─── Equipment Effect Computation ────────────────────────────────────────────
|
||||
|
||||
// Get spells from equipment
|
||||
export function getSpellsFromEquipment(equipment: EquipmentInstance): string[] {
|
||||
const spells: string[] = [];
|
||||
|
||||
for (const ench of equipment.enchantments) {
|
||||
const effectDef = getEnchantmentEffect(ench.effectId);
|
||||
if (effectDef?.effect.type === 'spell' && effectDef.effect.spellId) {
|
||||
spells.push(effectDef.effect.spellId);
|
||||
}
|
||||
}
|
||||
|
||||
return spells;
|
||||
}
|
||||
|
||||
// Compute total effects from equipment
|
||||
export function computeEquipmentEffects(equipment: EquipmentInstance[]): Record<string, number> {
|
||||
const effects: Record<string, number> = {};
|
||||
const multipliers: Record<string, number> = {};
|
||||
const specials: Set<string> = new Set();
|
||||
|
||||
for (const equip of equipment) {
|
||||
for (const ench of equip.enchantments) {
|
||||
const effectDef = getEnchantmentEffect(ench.effectId);
|
||||
if (!effectDef) continue;
|
||||
|
||||
const value = (effectDef.effect.value || 0) * ench.stacks;
|
||||
|
||||
if (effectDef.effect.type === 'bonus' && effectDef.effect.stat) {
|
||||
effects[effectDef.effect.stat] = (effects[effectDef.effect.stat] || 0) + value;
|
||||
} else if (effectDef.effect.type === 'multiplier' && effectDef.effect.stat) {
|
||||
multipliers[effectDef.effect.stat] = (multipliers[effectDef.effect.stat] || 1) * Math.pow(value, ench.stacks);
|
||||
} else if (effectDef.effect.type === 'special' && effectDef.effect.specialId) {
|
||||
specials.add(effectDef.effect.specialId);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Apply multipliers to bonus effects
|
||||
for (const [stat, mult] of Object.entries(multipliers)) {
|
||||
effects[`${stat}_multiplier`] = mult;
|
||||
}
|
||||
|
||||
// Add special effect flags
|
||||
for (const special of specials) {
|
||||
effects[`special_${special}`] = 1;
|
||||
}
|
||||
|
||||
return effects;
|
||||
}
|
||||
@@ -1,29 +0,0 @@
|
||||
// ─── Crafting Store Slice ────────────────────────────────────────────────────────
|
||||
// Handles equipment, enchantments, and crafting progress
|
||||
//
|
||||
// This file is a re-export barrel that combines all crafting modules.
|
||||
// For the actual implementation, see:
|
||||
// - crafting-modules/types.ts - Type definitions
|
||||
// - crafting-modules/initial-state.ts - Initial state
|
||||
// - crafting-modules/utils.ts - Utility functions
|
||||
// - crafting-modules/slice-logic.ts - Main slice creator
|
||||
// - crafting-modules/starting-equipment.ts - Starting equipment factory
|
||||
|
||||
// Re-export everything from the modules
|
||||
export type { CraftingState, CraftingActions, CraftingStore } from './crafting-modules/types';
|
||||
export { initialCraftingState } from './crafting-modules/initial-state';
|
||||
export {
|
||||
generateInstanceId,
|
||||
generateDesignId,
|
||||
getEnchantEfficiencyBonus,
|
||||
calculateDesignTime,
|
||||
calculatePreparationTime,
|
||||
calculatePreparationManaCost,
|
||||
calculateApplicationTime,
|
||||
calculateApplicationManaPerHour,
|
||||
createEquipmentInstance,
|
||||
getSpellsFromEquipment,
|
||||
computeEquipmentEffects
|
||||
} from './crafting-modules/utils';
|
||||
export { createCraftingSlice, setCachedSkills } from './crafting-modules/slice-logic';
|
||||
export { createStartingEquipment } from './crafting-modules/starting-equipment';
|
||||
@@ -1,204 +0,0 @@
|
||||
// ─── Mana Slice ───────────────────────────────────────────────────────────────
|
||||
// Manages raw mana, elements, and meditation
|
||||
|
||||
import type { StateCreator } from 'zustand';
|
||||
import type { GameState, ElementState, SpellCost } from '../types';
|
||||
import { ELEMENTS, MANA_PER_ELEMENT, BASE_UNLOCKED_ELEMENTS } from '../constants';
|
||||
import { computeMaxMana, computeClickMana, canAffordSpellCost, getMeditationBonus } from './computed';
|
||||
import { computeElementMax } from '../store';
|
||||
import { computeEffects } from '../upgrade-effects';
|
||||
import { hasSpecial, SPECIAL_EFFECTS } from '../special-effects';
|
||||
|
||||
export interface ManaSlice {
|
||||
// State
|
||||
rawMana: number;
|
||||
totalManaGathered: number;
|
||||
meditateTicks: number;
|
||||
elements: Record<string, ElementState>;
|
||||
|
||||
// Actions
|
||||
gatherMana: () => void;
|
||||
convertMana: (element: string, amount: number) => void;
|
||||
unlockElement: (element: string) => void;
|
||||
craftComposite: (target: string) => void;
|
||||
|
||||
// Computed getters
|
||||
getMaxMana: () => number;
|
||||
getRegen: () => number;
|
||||
getClickMana: () => number;
|
||||
getMeditationMultiplier: () => number;
|
||||
}
|
||||
|
||||
export const createManaSlice = (
|
||||
set: StateCreator<GameState>['set'],
|
||||
get: () => GameState
|
||||
): ManaSlice => ({
|
||||
rawMana: 10,
|
||||
totalManaGathered: 0,
|
||||
meditateTicks: 0,
|
||||
elements: (() => {
|
||||
const elems: Record<string, ElementState> = {};
|
||||
const pu = get().prestigeUpgrades;
|
||||
const elemMax = computeElementMax(get());
|
||||
|
||||
Object.keys(ELEMENTS).forEach((k) => {
|
||||
const isUnlocked = BASE_UNLOCKED_ELEMENTS.includes(k);
|
||||
let startAmount = 0;
|
||||
|
||||
if (isUnlocked && pu.elemStart) {
|
||||
startAmount = pu.elemStart * 5;
|
||||
}
|
||||
|
||||
elems[k] = {
|
||||
current: startAmount,
|
||||
max: elemMax,
|
||||
unlocked: isUnlocked,
|
||||
};
|
||||
});
|
||||
return elems;
|
||||
})(),
|
||||
|
||||
gatherMana: () => {
|
||||
const state = get();
|
||||
let cm = computeClickMana(state);
|
||||
|
||||
// Mana overflow bonus
|
||||
const overflowBonus = 1 + (state.skills.manaOverflow || 0) * 0.25;
|
||||
cm = Math.floor(cm * overflowBonus);
|
||||
|
||||
const effects = computeEffects(state.skillUpgrades || {}, state.skillTiers || {});
|
||||
const max = computeMaxMana(state, effects);
|
||||
|
||||
// Mana Conversion: Convert 5% of max mana to click bonus
|
||||
if (hasSpecial(effects, SPECIAL_EFFECTS.MANA_CONVERSION)) {
|
||||
cm += Math.floor(max * 0.05);
|
||||
}
|
||||
|
||||
// Mana Echo: 10% chance to gain double mana from clicks
|
||||
const hasManaEcho = effects.specials?.has('MANA_ECHO') ?? false;
|
||||
if (hasManaEcho && Math.random() < 0.1) {
|
||||
cm *= 2;
|
||||
}
|
||||
|
||||
set({
|
||||
rawMana: Math.min(state.rawMana + cm, max),
|
||||
totalManaGathered: state.totalManaGathered + cm,
|
||||
});
|
||||
},
|
||||
|
||||
convertMana: (element: string, amount: number = 1) => {
|
||||
const state = get();
|
||||
const e = state.elements[element];
|
||||
if (!e?.unlocked) return;
|
||||
|
||||
const cost = MANA_PER_ELEMENT * amount;
|
||||
if (state.rawMana < cost) return;
|
||||
if (e.current >= e.max) return;
|
||||
|
||||
const canConvert = Math.min(
|
||||
amount,
|
||||
Math.floor(state.rawMana / MANA_PER_ELEMENT),
|
||||
e.max - e.current
|
||||
);
|
||||
|
||||
set({
|
||||
rawMana: state.rawMana - canConvert * MANA_PER_ELEMENT,
|
||||
elements: {
|
||||
...state.elements,
|
||||
[element]: { ...e, current: e.current + canConvert },
|
||||
},
|
||||
});
|
||||
},
|
||||
|
||||
unlockElement: (element: string) => {
|
||||
const state = get();
|
||||
if (state.elements[element]?.unlocked) return;
|
||||
|
||||
const cost = 500;
|
||||
if (state.rawMana < cost) return;
|
||||
|
||||
set({
|
||||
rawMana: state.rawMana - cost,
|
||||
elements: {
|
||||
...state.elements,
|
||||
[element]: { ...state.elements[element], unlocked: true },
|
||||
},
|
||||
});
|
||||
},
|
||||
|
||||
craftComposite: (target: string) => {
|
||||
const state = get();
|
||||
const edef = ELEMENTS[target];
|
||||
if (!edef?.recipe) return;
|
||||
|
||||
const recipe = edef.recipe;
|
||||
const costs: Record<string, number> = {};
|
||||
recipe.forEach((r) => {
|
||||
costs[r] = (costs[r] || 0) + 1;
|
||||
});
|
||||
|
||||
// Check ingredients
|
||||
for (const [r, amt] of Object.entries(costs)) {
|
||||
if ((state.elements[r]?.current || 0) < amt) return;
|
||||
}
|
||||
|
||||
const newElems = { ...state.elements };
|
||||
for (const [r, amt] of Object.entries(costs)) {
|
||||
newElems[r] = { ...newElems[r], current: newElems[r].current - amt };
|
||||
}
|
||||
|
||||
// Elemental crafting bonus
|
||||
const craftBonus = 1 + (state.skills.elemCrafting || 0) * 0.25;
|
||||
const outputAmount = Math.floor(craftBonus);
|
||||
|
||||
const effects = computeEffects(state.skillUpgrades || {}, state.skillTiers || {});
|
||||
const elemMax = computeElementMax(state, effects);
|
||||
newElems[target] = {
|
||||
...(newElems[target] || { current: 0, max: elemMax, unlocked: false }),
|
||||
current: (newElems[target]?.current || 0) + outputAmount,
|
||||
max: elemMax,
|
||||
unlocked: true,
|
||||
};
|
||||
|
||||
set({
|
||||
elements: newElems,
|
||||
});
|
||||
},
|
||||
|
||||
getMaxMana: () => computeMaxMana(get()),
|
||||
getRegen: () => {
|
||||
const state = get();
|
||||
const effects = computeEffects(state.skillUpgrades || {}, state.skillTiers || {});
|
||||
// This would need proper regen calculation
|
||||
return 2;
|
||||
},
|
||||
getClickMana: () => computeClickMana(get()),
|
||||
getMeditationMultiplier: () => {
|
||||
const state = get();
|
||||
const effects = computeEffects(state.skillUpgrades || {}, state.skillTiers || {});
|
||||
return getMeditationBonus(state.meditateTicks, state.skills, effects.meditationEfficiency);
|
||||
},
|
||||
});
|
||||
|
||||
// Helper function to deduct spell cost
|
||||
export function deductSpellCost(
|
||||
cost: SpellCost,
|
||||
rawMana: number,
|
||||
elements: Record<string, ElementState>
|
||||
): { rawMana: number; elements: Record<string, ElementState> } {
|
||||
const newElements = { ...elements };
|
||||
|
||||
if (cost.type === 'raw') {
|
||||
return { rawMana: rawMana - cost.amount, elements: newElements };
|
||||
} else if (cost.element && newElements[cost.element]) {
|
||||
newElements[cost.element] = {
|
||||
...newElements[cost.element],
|
||||
current: newElements[cost.element].current - cost.amount,
|
||||
};
|
||||
return { rawMana, elements: newElements };
|
||||
}
|
||||
|
||||
return { rawMana, elements: newElements };
|
||||
}
|
||||
|
||||
export { canAffordSpellCost };
|
||||
@@ -1,180 +0,0 @@
|
||||
// ─── Pact Slice ───────────────────────────────────────────────────────────────
|
||||
// Manages guardian pacts, signing, and mana unlocking
|
||||
|
||||
import type { StateCreator } from 'zustand';
|
||||
import type { GameState } from '../types';
|
||||
import { GUARDIANS, ELEMENTS } from '../constants';
|
||||
import { computePactMultiplier, computePactInsightMultiplier } from './computed';
|
||||
|
||||
export interface PactSlice {
|
||||
// State
|
||||
signedPacts: number[];
|
||||
pendingPactOffer: number | null;
|
||||
maxPacts: number;
|
||||
pactSigningProgress: {
|
||||
floor: number;
|
||||
progress: number;
|
||||
required: number;
|
||||
manaCost: number;
|
||||
} | null;
|
||||
signedPactDetails: Record<number, {
|
||||
floor: number;
|
||||
guardianId: string;
|
||||
signedAt: { day: number; hour: number };
|
||||
skillLevels: Record<string, number>;
|
||||
}>;
|
||||
pactInterferenceMitigation: number;
|
||||
pactSynergyUnlocked: boolean;
|
||||
|
||||
// Actions
|
||||
acceptPact: (floor: number) => void;
|
||||
declinePact: (floor: number) => void;
|
||||
|
||||
// Computed getters
|
||||
getPactMultiplier: () => number;
|
||||
getPactInsightMultiplier: () => number;
|
||||
}
|
||||
|
||||
export const createPactSlice = (
|
||||
set: StateCreator<GameState>['set'],
|
||||
get: () => GameState
|
||||
): PactSlice => ({
|
||||
signedPacts: [],
|
||||
pendingPactOffer: null,
|
||||
maxPacts: 1,
|
||||
pactSigningProgress: null,
|
||||
signedPactDetails: {},
|
||||
pactInterferenceMitigation: 0,
|
||||
pactSynergyUnlocked: false,
|
||||
|
||||
acceptPact: (floor: number) => {
|
||||
const state = get();
|
||||
const guardian = GUARDIANS[floor];
|
||||
if (!guardian || state.signedPacts.includes(floor)) return;
|
||||
|
||||
const maxPacts = 1 + (state.prestigeUpgrades.pactCapacity || 0);
|
||||
if (state.signedPacts.length >= maxPacts) {
|
||||
set({
|
||||
log: [`⚠️ Cannot sign more pacts! Maximum: ${maxPacts}.`, ...state.log.slice(0, 49)],
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const baseCost = guardian.signingCost.mana;
|
||||
const discount = Math.min((state.prestigeUpgrades.pactDiscount || 0) * 0.1, 0.5);
|
||||
const manaCost = Math.floor(baseCost * (1 - discount));
|
||||
|
||||
if (state.rawMana < manaCost) {
|
||||
set({
|
||||
log: [`⚠️ Need ${manaCost} mana to sign pact with ${guardian.name}!`, ...state.log.slice(0, 49)],
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const baseTime = guardian.signingCost.time;
|
||||
const haste = Math.min((state.prestigeUpgrades.pactHaste || 0) * 0.1, 0.5);
|
||||
const signingTime = Math.max(1, baseTime * (1 - haste));
|
||||
|
||||
set({
|
||||
rawMana: state.rawMana - manaCost,
|
||||
pactSigningProgress: {
|
||||
floor,
|
||||
progress: 0,
|
||||
required: signingTime,
|
||||
manaCost,
|
||||
},
|
||||
pendingPactOffer: null,
|
||||
currentAction: 'study',
|
||||
log: [`📜 Beginning pact signing with ${guardian.name}... (${signingTime}h, ${manaCost} mana)`, ...state.log.slice(0, 49)],
|
||||
});
|
||||
},
|
||||
|
||||
declinePact: (floor: number) => {
|
||||
const state = get();
|
||||
const guardian = GUARDIANS[floor];
|
||||
if (!guardian) return;
|
||||
|
||||
set({
|
||||
pendingPactOffer: null,
|
||||
log: [`🚫 Declined pact with ${guardian.name}.`, ...state.log.slice(0, 49)],
|
||||
});
|
||||
},
|
||||
|
||||
getPactMultiplier: () => computePactMultiplier(get()),
|
||||
getPactInsightMultiplier: () => computePactInsightMultiplier(get()),
|
||||
});
|
||||
|
||||
// Process pact signing progress (called during tick)
|
||||
export function processPactSigning(state: GameState, deltaHours: number): Partial<GameState> {
|
||||
if (!state.pactSigningProgress) return {};
|
||||
|
||||
const progress = state.pactSigningProgress.progress + deltaHours;
|
||||
const log = [...state.log];
|
||||
|
||||
if (progress >= state.pactSigningProgress.required) {
|
||||
const floor = state.pactSigningProgress.floor;
|
||||
const guardian = GUARDIANS[floor];
|
||||
if (!guardian || state.signedPacts.includes(floor)) {
|
||||
return { pactSigningProgress: null };
|
||||
}
|
||||
|
||||
const signedPacts = [...state.signedPacts, floor];
|
||||
const signedPactDetails = {
|
||||
...state.signedPactDetails,
|
||||
[floor]: {
|
||||
floor,
|
||||
guardianId: guardian.element,
|
||||
signedAt: { day: state.day, hour: state.hour },
|
||||
skillLevels: {},
|
||||
},
|
||||
};
|
||||
|
||||
// Unlock mana types
|
||||
let elements = { ...state.elements };
|
||||
for (const elemId of guardian.unlocksMana) {
|
||||
if (elements[elemId]) {
|
||||
elements = {
|
||||
...elements,
|
||||
[elemId]: { ...elements[elemId], unlocked: true },
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// Check for compound element unlocks
|
||||
const unlockedSet = new Set(
|
||||
Object.entries(elements)
|
||||
.filter(([, e]) => e.unlocked)
|
||||
.map(([id]) => id)
|
||||
);
|
||||
|
||||
for (const [elemId, elemDef] of Object.entries(ELEMENTS)) {
|
||||
if (elemDef.recipe && !elements[elemId]?.unlocked) {
|
||||
const canUnlock = elemDef.recipe.every(comp => unlockedSet.has(comp));
|
||||
if (canUnlock) {
|
||||
elements = {
|
||||
...elements,
|
||||
[elemId]: { ...elements[elemId], unlocked: true },
|
||||
};
|
||||
log.unshift(`🔮 ${elemDef.name} mana unlocked through component synergy!`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
log.unshift(`📜 Pact with ${guardian.name} signed! ${guardian.unlocksMana.map(e => ELEMENTS[e]?.name || e).join(', ')} mana unlocked!`);
|
||||
|
||||
return {
|
||||
signedPacts,
|
||||
signedPactDetails,
|
||||
elements,
|
||||
pactSigningProgress: null,
|
||||
log,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
pactSigningProgress: {
|
||||
...state.pactSigningProgress,
|
||||
progress,
|
||||
},
|
||||
};
|
||||
}
|
||||
@@ -1,128 +0,0 @@
|
||||
// ─── Prestige Slice ───────────────────────────────────────────────────────────
|
||||
// Manages insight, prestige upgrades, and loop resources
|
||||
|
||||
import type { StateCreator } from 'zustand';
|
||||
import type { GameState } from '../types';
|
||||
import { PRESTIGE_DEF } from '../constants';
|
||||
|
||||
export interface PrestigeSlice {
|
||||
// State
|
||||
insight: number;
|
||||
totalInsight: number;
|
||||
prestigeUpgrades: Record<string, number>;
|
||||
loopInsight: number;
|
||||
memorySlots: number;
|
||||
memories: string[];
|
||||
|
||||
// Actions
|
||||
doPrestige: (id: string) => void;
|
||||
startNewLoop: () => void;
|
||||
}
|
||||
|
||||
export const createPrestigeSlice = (
|
||||
set: StateCreator<GameState>['set'],
|
||||
get: () => GameState
|
||||
): PrestigeSlice => ({
|
||||
insight: 0,
|
||||
totalInsight: 0,
|
||||
prestigeUpgrades: {},
|
||||
loopInsight: 0,
|
||||
memorySlots: 3,
|
||||
memories: [],
|
||||
|
||||
doPrestige: (id: string) => {
|
||||
const state = get();
|
||||
const pd = PRESTIGE_DEF[id];
|
||||
if (!pd) return;
|
||||
|
||||
const lvl = state.prestigeUpgrades[id] || 0;
|
||||
if (lvl >= pd.max || state.insight < pd.cost) return;
|
||||
|
||||
const newPU = { ...state.prestigeUpgrades, [id]: lvl + 1 };
|
||||
|
||||
set({
|
||||
insight: state.insight - pd.cost,
|
||||
prestigeUpgrades: newPU,
|
||||
memorySlots: id === 'deepMemory' ? state.memorySlots + 1 : state.memorySlots,
|
||||
maxPacts: id === 'pactCapacity' ? state.maxPacts + 1 : state.maxPacts,
|
||||
pactInterferenceMitigation: id === 'pactInterference' ? (state.pactInterferenceMitigation || 0) + 1 : state.pactInterferenceMitigation,
|
||||
log: [`⭐ ${pd.name} upgraded to Lv.${lvl + 1}!`, ...state.log.slice(0, 49)],
|
||||
});
|
||||
},
|
||||
|
||||
startNewLoop: () => {
|
||||
const state = get();
|
||||
const insightGained = state.loopInsight || calcInsight(state);
|
||||
const total = state.insight + insightGained;
|
||||
|
||||
// Reset to initial state with insight carried over
|
||||
const pu = state.prestigeUpgrades;
|
||||
const startFloor = 1 + (pu.spireKey || 0) * 2;
|
||||
const startRawMana = 10 + (pu.manaWell || 0) * 500 + (pu.quickStart || 0) * 100;
|
||||
|
||||
// Reset elements
|
||||
const elements: Record<string, { current: number; max: number; unlocked: boolean }> = {};
|
||||
Object.keys(ELEMENTS).forEach((k) => {
|
||||
elements[k] = {
|
||||
current: 0,
|
||||
max: 10 + (pu.elementalAttune || 0) * 25,
|
||||
unlocked: false,
|
||||
};
|
||||
});
|
||||
|
||||
// Reset spells - always start with Mana Bolt
|
||||
const spells: Record<string, { learned: boolean; level: number; studyProgress: number }> = {
|
||||
manaBolt: { learned: true, level: 1, studyProgress: 0 },
|
||||
};
|
||||
|
||||
// Add random starting spells from spell memory prestige upgrade (purchased with insight)
|
||||
if (pu.spellMemory) {
|
||||
const availableSpells = Object.keys(SPELLS_DEF).filter(s => s !== 'manaBolt');
|
||||
const shuffled = availableSpells.sort(() => Math.random() - 0.5);
|
||||
for (let i = 0; i < Math.min(pu.spellMemory, shuffled.length); i++) {
|
||||
spells[shuffled[i]] = { learned: true, level: 1, studyProgress: 0 };
|
||||
}
|
||||
}
|
||||
|
||||
set({
|
||||
day: 1,
|
||||
hour: 0,
|
||||
gameOver: false,
|
||||
victory: false,
|
||||
loopCount: state.loopCount + 1,
|
||||
rawMana: startRawMana,
|
||||
totalManaGathered: 0,
|
||||
meditateTicks: 0,
|
||||
elements,
|
||||
currentFloor: startFloor,
|
||||
floorHP: getFloorMaxHP(startFloor),
|
||||
floorMaxHP: getFloorMaxHP(startFloor),
|
||||
maxFloorReached: startFloor,
|
||||
signedPacts: [],
|
||||
pendingPactOffer: null,
|
||||
pactSigningProgress: null,
|
||||
signedPactDetails: {},
|
||||
activeSpell: 'manaBolt',
|
||||
currentAction: 'meditate',
|
||||
castProgress: 0,
|
||||
spells,
|
||||
skills: {},
|
||||
skillProgress: {},
|
||||
skillUpgrades: {},
|
||||
skillTiers: {},
|
||||
currentStudyTarget: null,
|
||||
parallelStudyTarget: null,
|
||||
insight: total,
|
||||
totalInsight: (state.totalInsight || 0) + insightGained,
|
||||
loopInsight: 0,
|
||||
maxPacts: 1 + (pu.pactCapacity || 0),
|
||||
pactInterferenceMitigation: pu.pactInterference || 0,
|
||||
memorySlots: 3 + (pu.deepMemory || 0),
|
||||
log: ['✨ A new loop begins. Your insight grows...', '✨ The loop begins. You start with Mana Bolt.'],
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
// Need to import these
|
||||
import { ELEMENTS, SPELLS_DEF } from '../constants';
|
||||
import { getFloorMaxHP, calcInsight } from './computed';
|
||||
@@ -1,82 +0,0 @@
|
||||
// ─── Time Slice ───────────────────────────────────────────────────────────────
|
||||
// Manages game time, loops, and game state
|
||||
|
||||
import type { StateCreator } from 'zustand';
|
||||
import type { GameState } from '../types';
|
||||
import { MAX_DAY } from '../constants';
|
||||
import { calcInsight } from './computed';
|
||||
|
||||
export interface TimeSlice {
|
||||
// State
|
||||
day: number;
|
||||
hour: number;
|
||||
loopCount: number;
|
||||
gameOver: boolean;
|
||||
victory: boolean;
|
||||
paused: boolean;
|
||||
incursionStrength: number;
|
||||
loopInsight: number;
|
||||
log: string[];
|
||||
|
||||
// Actions
|
||||
togglePause: () => void;
|
||||
resetGame: () => void;
|
||||
startNewLoop: () => void;
|
||||
addLog: (message: string) => void;
|
||||
}
|
||||
|
||||
export const createTimeSlice = (
|
||||
set: StateCreator<GameState>['set'],
|
||||
get: () => GameState,
|
||||
initialOverrides?: Partial<GameState>
|
||||
): TimeSlice => ({
|
||||
day: 1,
|
||||
hour: 0,
|
||||
loopCount: initialOverrides?.loopCount || 0,
|
||||
gameOver: false,
|
||||
victory: false,
|
||||
paused: false,
|
||||
incursionStrength: 0,
|
||||
loopInsight: 0,
|
||||
log: ['✨ The loop begins. You start with Mana Bolt. Gather your strength, mage.'],
|
||||
|
||||
togglePause: () => {
|
||||
set((state) => ({ paused: !state.paused }));
|
||||
},
|
||||
|
||||
resetGame: () => {
|
||||
if (typeof window !== 'undefined') {
|
||||
localStorage.removeItem('mana-loop-storage');
|
||||
}
|
||||
// Reset to initial state
|
||||
window.location.reload();
|
||||
},
|
||||
|
||||
startNewLoop: () => {
|
||||
const state = get();
|
||||
const insightGained = state.loopInsight || calcInsight(state);
|
||||
const total = state.insight + insightGained;
|
||||
|
||||
// Spell preservation is handled through the prestige upgrade "spellMemory"
|
||||
// which is purchased with insight
|
||||
|
||||
// This will be handled by the main store reset
|
||||
set({
|
||||
day: 1,
|
||||
hour: 0,
|
||||
gameOver: false,
|
||||
victory: false,
|
||||
loopCount: state.loopCount + 1,
|
||||
insight: total,
|
||||
totalInsight: (state.totalInsight || 0) + insightGained,
|
||||
loopInsight: 0,
|
||||
log: ['✨ A new loop begins. Your insight grows...'],
|
||||
});
|
||||
},
|
||||
|
||||
addLog: (message: string) => {
|
||||
set((state) => ({
|
||||
log: [message, ...state.log.slice(0, 49)],
|
||||
}));
|
||||
},
|
||||
});
|
||||
@@ -5,7 +5,7 @@
|
||||
import { create } from 'zustand';
|
||||
import { persist } from 'zustand/middleware';
|
||||
import { TICK_MS, HOURS_PER_TICK, MAX_DAY, SPELLS_DEF, GUARDIANS, getStudySpeedMultiplier } from '../constants';
|
||||
import { hasSpecial, SPECIAL_EFFECTS } from '../special-effects';
|
||||
import { hasSpecial, SPECIAL_EFFECTS } from '../effects/special-effects';
|
||||
import {
|
||||
computeMaxMana,
|
||||
computeRegen,
|
||||
|
||||
@@ -43,5 +43,4 @@ export {
|
||||
deductSpellCost,
|
||||
} from '../utils';
|
||||
|
||||
export { computeElementMax } from '../store-modules/computed-stats';
|
||||
export { getStudySpeedMultiplier, getStudyCostMultiplier } from '../constants';
|
||||
|
||||
@@ -25,18 +25,6 @@ export type {
|
||||
SpellState,
|
||||
} from './types/spells';
|
||||
|
||||
export type {
|
||||
SkillDef,
|
||||
SkillUpgradeDef,
|
||||
SkillUpgradeEffect,
|
||||
SkillEvolutionPath,
|
||||
SkillTierDef,
|
||||
SkillPerkChoice,
|
||||
SkillUpgradeChoice,
|
||||
PrestigeDef,
|
||||
SkillCost,
|
||||
} from './types/skills';
|
||||
|
||||
export type {
|
||||
EquipmentDef,
|
||||
EquipmentInstance,
|
||||
|
||||
@@ -1,191 +0,0 @@
|
||||
// ─── Upgrade Effect System ────────────────────────────────────────────────────────
|
||||
// This module handles applying skill upgrade effects to game stats
|
||||
|
||||
import type { SkillUpgradeChoice, SkillUpgradeEffect } from './types';
|
||||
import { getUpgradesForSkillAtMilestone, SKILL_EVOLUTION_PATHS } from './skill-evolution';
|
||||
import type { ActiveUpgradeEffect, ComputedEffects } from './upgrade-effects.types';
|
||||
import { SPECIAL_EFFECTS, hasSpecial } from './special-effects';
|
||||
import { computeDynamicRegen, computeDynamicClickMana, computeDynamicDamage } from './dynamic-compute';
|
||||
|
||||
// ─── Upgrade Definition Cache ───────────────────────────
|
||||
|
||||
// Cache all upgrades by ID for quick lookup
|
||||
const upgradeDefinitionsById: Map<string, SkillUpgradeChoice> = new Map();
|
||||
|
||||
// Build the cache on first access
|
||||
function buildUpgradeCache(): void {
|
||||
if (upgradeDefinitionsById.size > 0) return;
|
||||
|
||||
for (const [baseSkillId, path] of Object.entries(SKILL_EVOLUTION_PATHS)) {
|
||||
for (const tierDef of path.tiers) {
|
||||
for (const upgrade of tierDef.upgrades) {
|
||||
upgradeDefinitionsById.set(upgrade.id, upgrade);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ─── Helper Functions ──────────────────────────────
|
||||
|
||||
/**
|
||||
* Get all selected upgrades with their full effect definitions
|
||||
*/
|
||||
export function getActiveUpgrades(
|
||||
skillUpgrades: Record<string, string[]>,
|
||||
skillTiers: Record<string, number>
|
||||
): ActiveUpgradeEffect[] {
|
||||
buildUpgradeCache();
|
||||
const result: ActiveUpgradeEffect[] = [];
|
||||
|
||||
for (const [skillId, upgradeIds] of Object.entries(skillUpgrades)) {
|
||||
for (const upgradeId of upgradeIds) {
|
||||
const upgradeDef = upgradeDefinitionsById.get(upgradeId);
|
||||
if (upgradeDef) {
|
||||
result.push({
|
||||
upgradeId,
|
||||
skillId,
|
||||
milestone: upgradeDef.milestone,
|
||||
effect: upgradeDef.effect,
|
||||
name: upgradeDef.name,
|
||||
desc: upgradeDef.desc,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Compute all active effects from selected upgrades
|
||||
*/
|
||||
export function computeEffects(
|
||||
skillUpgrades: Record<string, string[]>,
|
||||
skillTiers: Record<string, number>
|
||||
): ComputedEffects {
|
||||
const activeUpgrades = getActiveUpgrades(skillUpgrades, skillTiers);
|
||||
|
||||
// Start with base values
|
||||
const effects: ComputedEffects = {
|
||||
maxManaMultiplier: 1,
|
||||
maxManaBonus: 0,
|
||||
regenMultiplier: 1,
|
||||
regenBonus: 0,
|
||||
clickManaMultiplier: 1,
|
||||
clickManaBonus: 0,
|
||||
meditationEfficiency: 1,
|
||||
spellCostMultiplier: 1,
|
||||
conversionEfficiency: 1,
|
||||
baseDamageMultiplier: 1,
|
||||
baseDamageBonus: 0,
|
||||
attackSpeedMultiplier: 1,
|
||||
critChanceBonus: 0,
|
||||
critDamageMultiplier: 1.5,
|
||||
elementalDamageMultiplier: 1,
|
||||
studySpeedMultiplier: 1,
|
||||
studyCostMultiplier: 1,
|
||||
progressRetention: 0,
|
||||
instantStudyChance: 0,
|
||||
freeStudyChance: 0,
|
||||
elementCapMultiplier: 1,
|
||||
elementCapBonus: 0,
|
||||
perElementCapBonus: {},
|
||||
conversionCostMultiplier: 1,
|
||||
doubleCraftChance: 0,
|
||||
permanentRegenBonus: 0,
|
||||
specials: new Set<string>(),
|
||||
activeUpgrades,
|
||||
skillLevelMultiplier: 1,
|
||||
enchantmentPowerMultiplier: 1,
|
||||
};
|
||||
|
||||
// Apply DEEP_UNDERSTANDING: +10% bonus from all skill levels
|
||||
if (hasSpecial(effects, SPECIAL_EFFECTS.DEEP_UNDERSTANDING)) {
|
||||
effects.skillLevelMultiplier = 1.10;
|
||||
}
|
||||
|
||||
// Apply each upgrade effect
|
||||
for (const upgrade of activeUpgrades) {
|
||||
const { effect } = upgrade;
|
||||
|
||||
if (effect.type === 'multiplier' && effect.stat && effect.value !== undefined) {
|
||||
// Multiplier effects (multiply the stat)
|
||||
switch (effect.stat) {
|
||||
case 'maxMana':
|
||||
effects.maxManaMultiplier *= effect.value;
|
||||
break;
|
||||
case 'regen':
|
||||
effects.regenMultiplier *= effect.value;
|
||||
break;
|
||||
case 'clickMana':
|
||||
effects.clickManaMultiplier *= effect.value;
|
||||
break;
|
||||
case 'meditationEfficiency':
|
||||
effects.meditationEfficiency *= effect.value;
|
||||
break;
|
||||
case 'spellCost':
|
||||
effects.spellCostMultiplier *= effect.value;
|
||||
break;
|
||||
case 'conversionEfficiency':
|
||||
effects.conversionEfficiency *= effect.value;
|
||||
break;
|
||||
case 'baseDamage':
|
||||
effects.baseDamageMultiplier *= effect.value;
|
||||
break;
|
||||
case 'attackSpeed':
|
||||
effects.attackSpeedMultiplier *= effect.value;
|
||||
break;
|
||||
case 'elementalDamage':
|
||||
effects.elementalDamageMultiplier *= effect.value;
|
||||
break;
|
||||
case 'studySpeed':
|
||||
effects.studySpeedMultiplier *= effect.value;
|
||||
break;
|
||||
case 'elementCap':
|
||||
effects.elementCapMultiplier *= effect.value;
|
||||
break;
|
||||
case 'conversionCost':
|
||||
effects.conversionCostMultiplier *= effect.value;
|
||||
break;
|
||||
case 'costReduction':
|
||||
effects.studyCostMultiplier /= effect.value;
|
||||
break;
|
||||
case 'enchantPower':
|
||||
effects.enchantmentPowerMultiplier *= effect.value;
|
||||
break;
|
||||
}
|
||||
} else if (effect.type === 'bonus' && effect.stat && effect.value !== undefined) {
|
||||
// Bonus effects (add to the stat)
|
||||
switch (effect.stat) {
|
||||
case 'maxMana':
|
||||
effects.maxManaBonus += effect.value;
|
||||
break;
|
||||
case 'regen':
|
||||
effects.regenBonus += effect.value;
|
||||
break;
|
||||
case 'clickMana':
|
||||
effects.clickManaBonus += effect.value;
|
||||
break;
|
||||
case 'baseDamage':
|
||||
effects.baseDamageBonus += effect.value;
|
||||
break;
|
||||
case 'elementCap':
|
||||
effects.elementCapBonus += effect.value;
|
||||
break;
|
||||
case 'permanentRegen':
|
||||
effects.permanentRegenBonus += effect.value;
|
||||
break;
|
||||
}
|
||||
} else if (effect.type === 'special' && effect.specialId) {
|
||||
effects.specials.add(effect.specialId);
|
||||
}
|
||||
}
|
||||
|
||||
// MANA_THRESHOLD: +30% max mana, -10% regen trade-off
|
||||
if (hasSpecial(effects, SPECIAL_EFFECTS.MANA_THRESHOLD)) {
|
||||
effects.maxManaMultiplier *= 1.30;
|
||||
effects.regenMultiplier *= 0.90;
|
||||
}
|
||||
|
||||
return effects;
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
// ─── Mana & Regen Utilities ──────────────────────────────────────────────────
|
||||
|
||||
import type { GameState } from '../types';
|
||||
import type { ComputedEffects } from '../upgrade-effects.types';
|
||||
import type { ComputedEffects } from '../effects/upgrade-effects.types';
|
||||
import { HOURS_PER_TICK } from '../constants';
|
||||
import { getTotalAttunementRegen, getTotalAttunementConversionDrain } from '../data/attunements';
|
||||
|
||||
|
||||
@@ -0,0 +1,67 @@
|
||||
// ─── Pact Utility Functions ───────────────────────────────────────────────────
|
||||
|
||||
import { GUARDIANS } from '../constants';
|
||||
|
||||
export function computePactMultiplier(state: {
|
||||
signedPacts: number[];
|
||||
pactInterferenceMitigation?: number;
|
||||
}): number {
|
||||
const { signedPacts, pactInterferenceMitigation = 0 } = state;
|
||||
|
||||
if (signedPacts.length === 0) return 1.0;
|
||||
|
||||
let baseMult = 1.0;
|
||||
for (const floor of signedPacts) {
|
||||
const guardian = GUARDIANS[floor];
|
||||
if (guardian) {
|
||||
baseMult *= guardian.damageMultiplier;
|
||||
}
|
||||
}
|
||||
|
||||
if (signedPacts.length === 1) return baseMult;
|
||||
|
||||
const numAdditionalPacts = signedPacts.length - 1;
|
||||
const basePenalty = 0.5 * numAdditionalPacts;
|
||||
const mitigationReduction = Math.min(pactInterferenceMitigation, 5) * 0.1;
|
||||
const effectivePenalty = Math.max(0, basePenalty - mitigationReduction);
|
||||
|
||||
if (pactInterferenceMitigation >= 5) {
|
||||
const synergyBonus = (pactInterferenceMitigation - 5) * 0.1;
|
||||
return baseMult * (1 + synergyBonus);
|
||||
}
|
||||
|
||||
return baseMult * (1 - effectivePenalty);
|
||||
}
|
||||
|
||||
export function computePactInsightMultiplier(state: {
|
||||
signedPacts: number[];
|
||||
pactInterferenceMitigation?: number;
|
||||
}): number {
|
||||
const { signedPacts, pactInterferenceMitigation = 0 } = state;
|
||||
|
||||
if (signedPacts.length === 0) return 1.0;
|
||||
|
||||
let mult = 1.0;
|
||||
for (const floor of signedPacts) {
|
||||
const guardian = GUARDIANS[floor];
|
||||
if (guardian) {
|
||||
mult *= guardian.insightMultiplier;
|
||||
}
|
||||
}
|
||||
|
||||
if (signedPacts.length > 1) {
|
||||
const numAdditionalPacts = signedPacts.length - 1;
|
||||
const basePenalty = 0.5 * numAdditionalPacts;
|
||||
const mitigationReduction = Math.min(pactInterferenceMitigation, 5) * 0.1;
|
||||
const effectivePenalty = Math.max(0, basePenalty - mitigationReduction);
|
||||
|
||||
if (pactInterferenceMitigation >= 5) {
|
||||
const synergyBonus = (pactInterferenceMitigation - 5) * 0.1;
|
||||
return mult * (1 + synergyBonus);
|
||||
}
|
||||
|
||||
return mult * (1 - effectivePenalty);
|
||||
}
|
||||
|
||||
return mult;
|
||||
}
|
||||
Reference in New Issue
Block a user