feat: implement Active Disciplines system
Build and Publish Mana Loop Docker Image / build-and-publish (push) Failing after 31s
Build and Publish Mana Loop Docker Image / build-and-publish (push) Failing after 31s
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
# Circular Dependencies
|
||||
Generated: 2026-05-16T09:20:16.770Z
|
||||
Generated: 2026-05-16T09:52:18.323Z
|
||||
Found: 7 circular chain(s) — these MUST be fixed before modifying involved files.
|
||||
|
||||
1. Processed 138 files (1.3s) (36 warnings)
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"_meta": {
|
||||
"generated": "2026-05-16T09:20:15.230Z",
|
||||
"generated": "2026-05-16T09:52:16.800Z",
|
||||
"description": "Import dependency graph for src/lib/game. Keys are files, values are arrays of files they import.",
|
||||
"usage": "To find what a file affects, search for its path in the VALUES. To find what a file depends on, look at its KEY entry."
|
||||
},
|
||||
|
||||
@@ -120,6 +120,8 @@ Mana-Loop/
|
||||
│ │ │ │ ├── StudyStatsSection.tsx
|
||||
│ │ │ │ ├── UpgradeEffectsSection.tsx
|
||||
│ │ │ │ └── index.tsx
|
||||
│ │ │ ├── tabs/
|
||||
│ │ │ │ └── DisciplinesTab.tsx
|
||||
│ │ │ ├── AchievementsDisplay.tsx
|
||||
│ │ │ ├── ActionButtons.tsx
|
||||
│ │ │ ├── ActivityLogPanel.tsx
|
||||
@@ -211,6 +213,15 @@ Mana-Loop/
|
||||
│ │ │ ├── index.ts
|
||||
│ │ │ └── preparation-actions.ts
|
||||
│ │ ├── data/
|
||||
│ │ │ ├── disciplines/
|
||||
│ │ │ │ ├── base-disciplines.ts
|
||||
│ │ │ │ ├── base.ts
|
||||
│ │ │ │ ├── enchanter-disciplines.ts
|
||||
│ │ │ │ ├── enchanter.ts
|
||||
│ │ │ │ ├── fabricator-disciplines.ts
|
||||
│ │ │ │ ├── fabricator.ts
|
||||
│ │ │ │ ├── invoker-disciplines.ts
|
||||
│ │ │ │ └── invoker.ts
|
||||
│ │ │ ├── enchantments/
|
||||
│ │ │ │ ├── spell-effects/
|
||||
│ │ │ │ │ ├── basic-spells.ts
|
||||
@@ -254,6 +265,8 @@ Mana-Loop/
|
||||
│ │ │ ├── enchantment-effects.ts
|
||||
│ │ │ ├── enchantment-types.ts
|
||||
│ │ │ └── loot-drops.ts
|
||||
│ │ ├── effects/
|
||||
│ │ │ └── discipline-effects.ts
|
||||
│ │ ├── hooks/
|
||||
│ │ │ └── useGameDerived.ts
|
||||
│ │ ├── store/
|
||||
@@ -287,6 +300,7 @@ Mana-Loop/
|
||||
│ │ │ ├── combat-actions.ts
|
||||
│ │ │ ├── combatStore.ts
|
||||
│ │ │ ├── craftingStore.ts
|
||||
│ │ │ ├── discipline-slice.ts
|
||||
│ │ │ ├── gameActions.ts
|
||||
│ │ │ ├── gameHooks.ts
|
||||
│ │ │ ├── gameLoopActions.ts
|
||||
@@ -298,6 +312,7 @@ Mana-Loop/
|
||||
│ │ │ └── uiStore.ts
|
||||
│ │ ├── types/
|
||||
│ │ │ ├── attunements.ts
|
||||
│ │ │ ├── disciplines.ts
|
||||
│ │ │ ├── elements.ts
|
||||
│ │ │ ├── equipment.ts
|
||||
│ │ │ ├── game.ts
|
||||
@@ -306,6 +321,7 @@ Mana-Loop/
|
||||
│ │ ├── utils/
|
||||
│ │ │ ├── activity-log.ts
|
||||
│ │ │ ├── combat-utils.ts
|
||||
│ │ │ ├── discipline-math.ts
|
||||
│ │ │ ├── enemy-utils.ts
|
||||
│ │ │ ├── floor-utils.ts
|
||||
│ │ │ ├── formatting.ts
|
||||
|
||||
@@ -0,0 +1,251 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { useDisciplineStore } from '@/lib/game/stores/discipline-slice';
|
||||
import type { DisciplineDefinition } from '@/types/disciplines';
|
||||
import baseDisciplines from '../data/disciplines/base';
|
||||
import enchanterDisciplines from '../data/disciplines/enchanter';
|
||||
import fabricatorDisciplines from '../data/disciplines/fabricator';
|
||||
import invokerDisciplines from '../data/disciplines/invoker';
|
||||
import { calculateStatBonus, calculateManaDrain } from '@/lib/game/utils/discipline-math';
|
||||
import { useRef } from 'react';
|
||||
import clsx from 'clsx';
|
||||
|
||||
export const DisciplinesTab: React.FC = () => {
|
||||
const store = useDisciplineStore();
|
||||
const { disciplines, activeIds, concurrentLimit } = store;
|
||||
|
||||
const [mounted, setMounted] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
setMounted(true);
|
||||
}, []);
|
||||
|
||||
const allDisciplines: DisciplineDefinition[] = [
|
||||
...baseDisciplines,
|
||||
...enchanterDisciplines,
|
||||
...fabricatorDisciplines,
|
||||
...invokerDisciplines,
|
||||
];
|
||||
|
||||
// Group disciplines by attunement for tab rendering
|
||||
const attunementTabs: {
|
||||
label: string;
|
||||
items: DisciplineDefinition[];
|
||||
}[] = [
|
||||
{ label: 'Base', items: baseDisciplines },
|
||||
{ label: 'Enchanter', items: enchanterDisciplines },
|
||||
{ label: 'Fabricator', items: fabricatorDisciplines },
|
||||
{ label: 'Invoker', items: invokerDisciplines },
|
||||
];
|
||||
|
||||
// Helper to render a single discipline card
|
||||
const DisciplineCard: React.FC<{
|
||||
id: string;
|
||||
name: string;
|
||||
description: string;
|
||||
xp: number;
|
||||
perkThresholds?: number[];
|
||||
perkValues?: number[];
|
||||
perkTypes?: string[];
|
||||
statBonus: string;
|
||||
baseValue: number;
|
||||
drainBase: number;
|
||||
difficultyFactor: number;
|
||||
scalingFactor: number;
|
||||
}> = ({
|
||||
id,
|
||||
name,
|
||||
description,
|
||||
xp,
|
||||
perkThresholds,
|
||||
perkValues,
|
||||
perkTypes,
|
||||
statBonus,
|
||||
baseValue,
|
||||
drainBase,
|
||||
difficultyFactor,
|
||||
scalingFactor,
|
||||
}) => {
|
||||
if (!mounted) return null;
|
||||
|
||||
const state = useDisciplineStore().getState();
|
||||
const currentDisc = state.disciplines[id] ?? { xp: 0, paused: true };
|
||||
const isActive = activeIds.includes(id);
|
||||
const canActivate = concurrentLimit > activeIds.filter(a => state.disciplines[a]?.paused !== true).length;
|
||||
|
||||
// Calculate displayed stats
|
||||
const displayXp = currentDisc.xp;
|
||||
const progressPercent = Math.min(displayXp / Math.max(1, (concurrentLimit * 100) ?? 1), 100);
|
||||
const isPaused = currentDisc.paused;
|
||||
const hasPendingPerk = perkThresholds?.some((t, i) => displayXp >= t && perkTypes?.[i] !== 'capped');
|
||||
|
||||
const activeStatBonus = calculateStatBonus(
|
||||
parseInt(baseValue) || 0,
|
||||
displayXp,
|
||||
scalingFactor
|
||||
);
|
||||
|
||||
// Simple visual for drain per tick
|
||||
const estimatedDrain = calculateManaDrain(
|
||||
drainBase,
|
||||
displayXp,
|
||||
difficultyFactor
|
||||
);
|
||||
|
||||
// Determine unlocked perks
|
||||
const unlockedPerks = perkTypes?.reduce<string[]>((acc, typ, idx) => {
|
||||
if (typ === 'once' || typ === 'infinite') {
|
||||
if (displayXp >= perkThresholds?.[idx] ?? 0) {
|
||||
acc.push(`${typ}-${idx}`);
|
||||
}
|
||||
} else if (typ === 'capped') {
|
||||
const tier = Math.max(0, Math.floor((displayXp - perkThresholds?.[idx] ?? 0) / perkValues?.[idx] ?? 1) + 1);
|
||||
if (tier > 0) acc.push(`${typ}-${idx}`);
|
||||
}
|
||||
return acc;
|
||||
}, []);
|
||||
|
||||
// Helper to decide button action
|
||||
const toggleAction = () => {
|
||||
if (isPaused) {
|
||||
// Resume – activate
|
||||
const storeDispatch = useDisciplineStore().getState().activate as any;
|
||||
storeDispatch(id);
|
||||
} else {
|
||||
// Pause – deactivate
|
||||
const storeDispatch = useDisciplineStore().getState().deactivate as any;
|
||||
storeDispatch(id);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div key={id} className="border rounded-lg p-4 shadow-sm space-y-3">
|
||||
<h3 className="text-lg font-medium">{name}</h3>
|
||||
<p className="text-sm text-gray-400">{description}</p>
|
||||
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-xs font-mono whitespace-nowrap">{Math.round(progressPercent)}%</span>
|
||||
<div className="flex-1 bg-gray-200 rounded-full overflow-hidden h-3">
|
||||
<div
|
||||
className={`bg-blue-500 transition-all duration-300 ${
|
||||
activeStatBonus > 0 ? 'bg-green-500' : 'bg-red-500'
|
||||
}`}
|
||||
style={{ width: `${progressPercent}%` }}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="text-sm text-gray-400">
|
||||
<strong>Drain:</strong> {estimatedDrain.toFixed(1)} ✦{' '}
|
||||
<strong>XP:</strong> {displayXp}
|
||||
</div>
|
||||
|
||||
{/* Bonus display */}
|
||||
<div className="mt-2 text-sm">
|
||||
<strong>Stat Bonus:</strong> {activeStatBonus.toFixed(2)} on {statBonus}
|
||||
</div>
|
||||
|
||||
{/* Perks */}
|
||||
<div className="mt-2">
|
||||
<strong>Perks:</strong>
|
||||
<ul className="mt-1 list-disc list-inside space-y-1 text-xs">
|
||||
{unlockedPerks?.map((p) => (
|
||||
<li key={p} className="text-green-500">{p.replace(/-([0-9]+)$/, ' $1')}</li>
|
||||
)) : (
|
||||
<li className="text-gray-400">—locked—</li>
|
||||
)}
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
{/* Action button */}
|
||||
<div className="mt-4 flex justify-end">
|
||||
<button
|
||||
onClick={toggleAction}
|
||||
className={clsx(
|
||||
'rounded px-3 py-1 text-sm font-medium',
|
||||
isPaused
|
||||
? 'bg-yellow-600 text-white hover:bg-yellow-500'
|
||||
: 'bg-blue-600 text-white hover:bg-blue-500',
|
||||
)}
|
||||
>
|
||||
{isPaused ? 'Activate' : 'Pause'}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
if (!mounted) {
|
||||
return (
|
||||
<div className="flex items-center justify-center p-8 text-gray-500">
|
||||
Loading disciplines…
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="mt-6">
|
||||
{/* Tab bar */}
|
||||
<div className="flex gap-2 mb-4">
|
||||
{attunementTabs.map((tab) => {
|
||||
const isActiveTab = store
|
||||
.getState()
|
||||
.activeAttunement === tab.label.toLowerCase();
|
||||
return (
|
||||
<button
|
||||
key={tab.label}
|
||||
onClick={() =>
|
||||
// Here you could dispatch an action to switch tabs if needed
|
||||
// For simplicity, we just render the tabs
|
||||
console.log(`Switch to ${tab.label}`);
|
||||
}
|
||||
className={clsx('rounded px-3 py-1', {
|
||||
'bg-blue-600 text-white': tab.label === 'Base', // highlight first for demo
|
||||
'text-gray-600': tab.label !== 'Base',
|
||||
})}
|
||||
>
|
||||
{tab.label}
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
|
||||
{/* Discipline cards */}
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-4">
|
||||
{attunementTabs
|
||||
.map((tab) =>
|
||||
tab.items.map((disc) => {
|
||||
const state = useDisciplineStore().getState();
|
||||
const discState = state.disciplines[disc.id] ?? { xp: 0, paused: true };
|
||||
const isActive = activeIds.includes(disc.id);
|
||||
const isVisible = attunementTabs.find((t) => t.label === tab.label)?.items === tab.items;
|
||||
return isVisible && (
|
||||
<DisciplineCard
|
||||
key={disc.id}
|
||||
id={disc.id}
|
||||
name={disc.name}
|
||||
description={disc.description}
|
||||
xp={discState.xp}
|
||||
perkThresholds={disc.perks?.map((p) => p.threshold)}
|
||||
perkValues={disc.perks?.map((p) => p.value)}
|
||||
perkTypes={disc.perks?.map((p) => p.type)}
|
||||
statBonus={disc.statBonus}
|
||||
baseValue={disc.statBonus.baseValue?.toString() ?? '0'}
|
||||
drainBase={disc.drainBase}
|
||||
difficultyFactor={disc.difficultyFactor}
|
||||
scalingFactor={disc.scalingFactor}
|
||||
/>
|
||||
);
|
||||
})
|
||||
)
|
||||
.flat()
|
||||
}
|
||||
</div>
|
||||
|
||||
{/* Summary info */}
|
||||
<div className="mt-4 flex flex-col sm:flex-row gap-2 text-sm text-gray-500">
|
||||
<div>Active Disciple{activeIds.length}{activeIds.length === 1 ? '' : 's'} / {concurrentLimit}</div>
|
||||
<div>Concurrent Limit: {concurrentLimit}</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,34 @@
|
||||
type DisciplineDefinition = {
|
||||
name: string;
|
||||
attunement: DisciplinesAttunementType;
|
||||
manaType: ManaType;
|
||||
baseCost: number;
|
||||
description: string;
|
||||
requires?: DisciplineDefinition[];
|
||||
};
|
||||
|
||||
enum DisciplinesAttunementType {
|
||||
base,
|
||||
enchanter,
|
||||
fabricator,
|
||||
invoker
|
||||
};
|
||||
|
||||
export const baseDisciplines: DisciplineDefinition[] = [
|
||||
{
|
||||
name: "Embercraft",
|
||||
attunement: DisciplinesAttunementType.base,
|
||||
manaType: "fire",
|
||||
baseCost: 10,
|
||||
description: "Basic flame projection with autocrit on combustion explosion",
|
||||
requires: []
|
||||
},
|
||||
{
|
||||
name: "Earthbind",
|
||||
attunement: DisciplinesAttunementType.base,
|
||||
manaType: "earth",
|
||||
baseCost: 12,
|
||||
description: "Basic mana chains with passive ground stability",
|
||||
requires: []
|
||||
}
|
||||
];
|
||||
@@ -0,0 +1,56 @@
|
||||
// ─── Base Disciplines ─────────────────────────────────────────────────────────
|
||||
// Disciplines available to all attunements
|
||||
|
||||
import type { DisciplineDefinition } from '../../types/disciplines';
|
||||
|
||||
export const baseDisciplines: DisciplineDefinition[] = [
|
||||
{
|
||||
id: 'raw-mastery',
|
||||
name: 'Raw Mana Mastery',
|
||||
attunement: 'base',
|
||||
manaType: 'raw',
|
||||
baseCost: 5,
|
||||
description: 'Learn to harness raw mana more efficiently.',
|
||||
statBonus: { stat: 'maxManaBonus', baseValue: 10 },
|
||||
difficultyFactor: 100,
|
||||
scalingFactor: 50,
|
||||
drainBase: 1,
|
||||
perks: [
|
||||
{
|
||||
id: 'raw-mastery-1',
|
||||
type: 'once',
|
||||
threshold: 100,
|
||||
value: 0,
|
||||
description: '+50 Max Mana',
|
||||
},
|
||||
{
|
||||
id: 'raw-mastery-2',
|
||||
type: 'infinite',
|
||||
threshold: 500,
|
||||
value: 100,
|
||||
description: 'Every 100 XP: +25 Max Mana',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'elemental-attunement',
|
||||
name: 'Elemental Attunement',
|
||||
attunement: 'base',
|
||||
manaType: 'fire',
|
||||
baseCost: 10,
|
||||
description: 'Begin focusing raw mana into fire.',
|
||||
statBonus: { stat: 'elementCap_fire', baseValue: 5 },
|
||||
difficultyFactor: 150,
|
||||
scalingFactor: 75,
|
||||
drainBase: 2,
|
||||
perks: [
|
||||
{
|
||||
id: 'elem-attunement-1',
|
||||
type: 'once',
|
||||
threshold: 200,
|
||||
value: 0,
|
||||
description: '+10 Fire Capacity',
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
@@ -0,0 +1,34 @@
|
||||
type DisciplineDefinition = {
|
||||
name: string;
|
||||
attunement: DisciplinesAttunementType;
|
||||
manaType: ManaType;
|
||||
baseCost: number;
|
||||
description: string;
|
||||
requires?: DisciplineDefinition[];
|
||||
};
|
||||
|
||||
enum DisciplinesAttunementType {
|
||||
base,
|
||||
enchanter,
|
||||
fabricator,
|
||||
invoker
|
||||
};
|
||||
|
||||
export const enchanterDisciplines: DisciplineDefinition[] = [
|
||||
{
|
||||
name: "Soulforge",
|
||||
attunement: DisciplinesAttunementType.enchanter,
|
||||
manaType: "light",
|
||||
baseCost: 25,
|
||||
description: "Mana chains that create permanent elemental storage nodes",
|
||||
requires: [{name: "Embercraft"}]
|
||||
},
|
||||
{
|
||||
name: "Mana Prism",
|
||||
attunement: DisciplinesAttunementType.enchanter,
|
||||
manaType: "light",
|
||||
baseCost: 30,
|
||||
description: "Prismatic mana focusing that reflexes attacks as fixed ratio",
|
||||
requires: [{name: "Soulforge"}]
|
||||
}
|
||||
];
|
||||
@@ -0,0 +1,56 @@
|
||||
// ─── Enchanter Discipline Files ──────────────────────────────────────────────
|
||||
// Attunement-focused disciplines for Enchanter role
|
||||
|
||||
import type { DisciplineDefinition } from '../../types/disciplines';
|
||||
|
||||
export const enchanterDisciplines: DisciplineDefinition[] = [
|
||||
{
|
||||
id: 'enchant-crafting',
|
||||
name: 'Enchantment Crafting',
|
||||
attunement: 'enchanter',
|
||||
manaType: 'transference',
|
||||
baseCost: 8,
|
||||
description: 'Improve your ability to apply enchantments to equipment.',
|
||||
statBonus: { stat: 'enchantPower', baseValue: 8 },
|
||||
difficultyFactor: 120,
|
||||
scalingFactor: 60,
|
||||
drainBase: 3,
|
||||
perks: [
|
||||
{
|
||||
id: 'enchant-1',
|
||||
type: 'infinite',
|
||||
threshold: 150,
|
||||
value: 5,
|
||||
description: '+5 Enchantment Power (stacks with skill tiers)',
|
||||
},
|
||||
{
|
||||
id: 'enchant-2',
|
||||
type: 'capped',
|
||||
threshold: 300,
|
||||
value: 20,
|
||||
description: 'Double enchantment duration at 300 XP',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'mana-channeling',
|
||||
name: 'Mana Channeling',
|
||||
attunement: 'enchanter',
|
||||
manaType: 'lightning',
|
||||
baseCost: 12,
|
||||
description: 'Use lightning to transfer mana to equipment.',
|
||||
statBonus: { stat: 'clickManaMultiplier', baseValue: 0.3 },
|
||||
difficultyFactor: 180,
|
||||
scalingFactor: 90,
|
||||
drainBase: 5,
|
||||
perks: [
|
||||
{
|
||||
id: 'channel-1',
|
||||
type: 'once',
|
||||
threshold: 250,
|
||||
value: 0,
|
||||
description: 'Unlock lightning mana boosting',
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
@@ -0,0 +1,21 @@
|
||||
import type { DisciplineDefinition } from '../../types/disciplines';
|
||||
|
||||
const fabricatorDisciplines: DisciplineDefinition[] = [
|
||||
{
|
||||
name: 'Metalworking',
|
||||
attunement: 'fabricator',
|
||||
manaType: 'metal',
|
||||
baseCosts: { mana: 28, time: 7 },
|
||||
description: 'Increase metal equipment crafting speed',
|
||||
thresholds: { xp: 140, interval: 70 }
|
||||
},
|
||||
{
|
||||
name: 'Crystal Shaping',
|
||||
attunement: 'fabricator',
|
||||
manaType: 'crystal',
|
||||
baseCosts: { mana: 30, time: 8 },
|
||||
description: 'Increase crystal equipment durability',
|
||||
thresholds: { xp: 160, interval: 80 }
|
||||
}
|
||||
];
|
||||
export default fabricatorDisciplines;
|
||||
@@ -0,0 +1,56 @@
|
||||
// ─── Fabricator Discipline Files ──────────────────────────────────────────────
|
||||
// Attunement-focused disciplines for Fabricator role
|
||||
|
||||
import type { DisciplineDefinition } from '../../types/disciplines';
|
||||
|
||||
export const fabricatorDisciplines: DisciplineDefinition[] = [
|
||||
{
|
||||
id: 'golem-crafting',
|
||||
name: 'Golem Crafting',
|
||||
attunement: 'fabricator',
|
||||
manaType: 'earth',
|
||||
baseCost: 10,
|
||||
description: 'Improve your ability to craft and maintain golems.',
|
||||
statBonus: { stat: 'golemCapacity', baseValue: 2 },
|
||||
difficultyFactor: 150,
|
||||
scalingFactor: 80,
|
||||
drainBase: 4,
|
||||
perks: [
|
||||
{
|
||||
id: 'golem-1',
|
||||
type: 'once',
|
||||
threshold: 200,
|
||||
value: 0,
|
||||
description: 'Unlock golem summoning',
|
||||
},
|
||||
{
|
||||
id: 'golem-2',
|
||||
type: 'capped',
|
||||
threshold: 500,
|
||||
value: 5,
|
||||
description: 'Double golem capacity at 500 XP',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'crafting-efficiency',
|
||||
name: 'Crafting Efficiency',
|
||||
attunement: 'fabricator',
|
||||
manaType: 'sand',
|
||||
baseCost: 12,
|
||||
description: 'Reduce material costs for crafting.',
|
||||
statBonus: { stat: 'craftingCostReduction', baseValue: 15 },
|
||||
difficultyFactor: 180,
|
||||
scalingFactor: 90,
|
||||
drainBase: 6,
|
||||
perks: [
|
||||
{
|
||||
id: 'efficiency-1',
|
||||
type: 'once',
|
||||
threshold: 300,
|
||||
value: 0,
|
||||
description: 'Unlock reduced crafting costs',
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
@@ -0,0 +1,21 @@
|
||||
import type { DisciplineDefinition } from '../../types/disciplines';
|
||||
|
||||
const invokerDisciplines: DisciplineDefinition[] = [
|
||||
{
|
||||
name: 'Lightning Surge',
|
||||
attunement: 'invoker',
|
||||
manaType: 'lightning',
|
||||
baseCost: 30,
|
||||
description: 'Boost lightning spell damage',
|
||||
thresholds: { xp: 150, interval: 75 }
|
||||
},
|
||||
{
|
||||
name: 'Void Echo',
|
||||
attunement: 'invoker',
|
||||
manaType: 'void',
|
||||
baseCost: 35,
|
||||
description: 'Increase void spell cast speed',
|
||||
thresholds: { xp: 180, interval: 90 }
|
||||
}
|
||||
];
|
||||
export default invokerDisciplines;
|
||||
@@ -0,0 +1,56 @@
|
||||
// ─── Invoker Discipline Files ─────────────────────────────────────────────────
|
||||
// Attunement-focused disciplines for Invoker role
|
||||
|
||||
import type { DisciplineDefinition } from '../../types/disciplines';
|
||||
|
||||
export const invokerDisciplines: DisciplineDefinition[] = [
|
||||
{
|
||||
id: 'spell-casting',
|
||||
name: 'Spell Casting',
|
||||
attunement: 'invoker',
|
||||
manaType: 'light',
|
||||
baseCost: 10,
|
||||
description: 'Improve spell power and effectiveness.',
|
||||
statBonus: { stat: 'baseDamageBonus', baseValue: 6 },
|
||||
difficultyFactor: 130,
|
||||
scalingFactor: 65,
|
||||
drainBase: 3,
|
||||
perks: [
|
||||
{
|
||||
id: 'spell-1',
|
||||
type: 'once',
|
||||
threshold: 200,
|
||||
value: 0,
|
||||
description: '+10 Base Damage',
|
||||
},
|
||||
{
|
||||
id: 'spell-2',
|
||||
type: 'infinite',
|
||||
threshold: 400,
|
||||
value: 30,
|
||||
description: 'Every 300 XP: +5 Base Damage',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'void-manipulation',
|
||||
name: 'Void Manipulation',
|
||||
attunement: 'invoker',
|
||||
manaType: 'void',
|
||||
baseCost: 15,
|
||||
description: 'Master the exotic void mana for devastating effects.',
|
||||
statBonus: { stat: 'baseDamageMultiplier', baseValue: 0.15 },
|
||||
difficultyFactor: 200,
|
||||
scalingFactor: 100,
|
||||
drainBase: 7,
|
||||
perks: [
|
||||
{
|
||||
id: 'void-1',
|
||||
type: 'once',
|
||||
threshold: 300,
|
||||
value: 0,
|
||||
description: 'Unlock void damage multiplier',
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
+34
-97
@@ -1,13 +1,14 @@
|
||||
// ─── Unified Effect System ─────────────────────────────────────────────────
|
||||
// This module consolidates ALL effect sources into a single computation:
|
||||
// - Discipline effects (from active disciplines)
|
||||
// - Skill upgrade effects (from milestone upgrades)
|
||||
// - Equipment enchantment effects (from enchanted gear)
|
||||
// - Direct skill bonuses (from skill levels)
|
||||
|
||||
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 { computeDisciplineEffects } from './effects/discipline-effects';
|
||||
import type { ComputedEffects } from './upgrade-effects.types';
|
||||
|
||||
// Re-export for convenience
|
||||
@@ -17,10 +18,6 @@ export type { ComputedEffects } from './upgrade-effects.types';
|
||||
|
||||
// ─── Equipment Effect Computation ────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* Compute all effects from equipped enchantments
|
||||
* @param enchantmentPowerMultiplier - Multiplier applied to all enchantment effect values (default 1.0)
|
||||
*/
|
||||
export function computeEquipmentEffects(
|
||||
equipmentInstances: Record<string, EquipmentInstance>,
|
||||
equippedInstances: Record<string, string | null>,
|
||||
@@ -34,24 +31,18 @@ export function computeEquipmentEffects(
|
||||
const multipliers: Record<string, number> = {};
|
||||
const specials = new Set<string>();
|
||||
|
||||
// Iterate through all equipped items
|
||||
for (const instanceId of Object.values(equippedInstances || {})) {
|
||||
if (!instanceId) continue;
|
||||
const instance = equipmentInstances[instanceId];
|
||||
if (!instance) continue;
|
||||
|
||||
// Process each enchantment on the item
|
||||
for (const ench of instance.enchantments) {
|
||||
const effectDef = ENCHANTMENT_EFFECTS[ench.effectId];
|
||||
if (!effectDef) continue;
|
||||
|
||||
const { effect } = effectDef;
|
||||
|
||||
if (effect.type === 'bonus' && effect.stat && effect.value) {
|
||||
// Bonus effects add to the stat
|
||||
// Apply enchantmentPowerMultiplier to the effect value
|
||||
const adjustedValue = effect.value * enchantmentPowerMultiplier;
|
||||
// Handle per-element capacity bonuses (stat format: elementCap_fire, elementCap_water, etc.)
|
||||
if (effect.stat.startsWith('elementCap_')) {
|
||||
const element = effect.stat.replace('elementCap_', '');
|
||||
bonuses[`elementCap_${element}`] = (bonuses[`elementCap_${element}`] || 0) + adjustedValue * ench.stacks;
|
||||
@@ -59,15 +50,9 @@ export function computeEquipmentEffects(
|
||||
bonuses[effect.stat] = (bonuses[effect.stat] || 0) + adjustedValue * ench.stacks;
|
||||
}
|
||||
} else if (effect.type === 'multiplier' && effect.stat && effect.value) {
|
||||
// Multiplier effects multiply together
|
||||
// For multipliers, we need to track them separately and apply as product
|
||||
// Apply enchantmentPowerMultiplier to the effect value
|
||||
const adjustedValue = effect.value * enchantmentPowerMultiplier;
|
||||
const key = effect.stat;
|
||||
if (!multipliers[key]) {
|
||||
multipliers[key] = 1;
|
||||
}
|
||||
// Each stack applies the multiplier
|
||||
if (!multipliers[key]) multipliers[key] = 1;
|
||||
for (let i = 0; i < ench.stacks; i++) {
|
||||
multipliers[key] *= adjustedValue;
|
||||
}
|
||||
@@ -80,35 +65,34 @@ export function computeEquipmentEffects(
|
||||
return { bonuses, multipliers, specials };
|
||||
}
|
||||
|
||||
// ─── Discipline Effects Integration ──────────────────────────────────────────
|
||||
|
||||
export function getDisciplineEffects(state: GameState) {
|
||||
return computeDisciplineEffects(state);
|
||||
}
|
||||
|
||||
// ─── Unified Computed Effects ─────────────────────────────────────────────────
|
||||
|
||||
export interface UnifiedEffects extends ComputedEffects {
|
||||
// Equipment bonuses
|
||||
equipmentBonuses: Record<string, number>;
|
||||
equipmentMultipliers: Record<string, number>;
|
||||
equipmentSpecials: Set<string>;
|
||||
disciplineBonuses: Record<string, number>;
|
||||
disciplineMultipliers: Record<string, number>;
|
||||
disciplineSpecials: Set<string>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Compute all effects from all sources: skill upgrades + equipment enchantments
|
||||
*/
|
||||
export function computeAllEffects(
|
||||
skillUpgrades: Record<string, string[]>,
|
||||
skillTiers: Record<string, number>,
|
||||
equipmentInstances: Record<string, EquipmentInstance>,
|
||||
equippedInstances: Record<string, string | null>
|
||||
equippedInstances: Record<string, string | null>,
|
||||
gameState: GameState
|
||||
): UnifiedEffects {
|
||||
// Get skill upgrade effects
|
||||
const upgradeEffects = computeEffects(skillUpgrades, skillTiers);
|
||||
const equipmentEffects = computeEquipmentEffects(equipmentInstances, equippedInstances, upgradeEffects.enchantmentPowerMultiplier);
|
||||
const disciplineEffects = getDisciplineEffects(gameState);
|
||||
|
||||
// Get equipment effects, applying the enchantment power multiplier
|
||||
const equipmentEffects = computeEquipmentEffects(
|
||||
equipmentInstances,
|
||||
equippedInstances,
|
||||
upgradeEffects.enchantmentPowerMultiplier
|
||||
);
|
||||
|
||||
// Extract per-element capacity bonuses from equipment effects
|
||||
const perElementCapBonus: Record<string, number> = { ...upgradeEffects.perElementCapBonus };
|
||||
for (const [key, value] of Object.entries(equipmentEffects.bonuses)) {
|
||||
if (key.startsWith('elementCap_')) {
|
||||
@@ -117,92 +101,65 @@ export function computeAllEffects(
|
||||
}
|
||||
}
|
||||
|
||||
// Merge the effects
|
||||
const merged: UnifiedEffects = {
|
||||
...upgradeEffects,
|
||||
// Merge equipment bonuses with upgrade bonuses
|
||||
maxManaBonus: upgradeEffects.maxManaBonus + (equipmentEffects.bonuses.maxMana || 0),
|
||||
regenBonus: upgradeEffects.regenBonus + (equipmentEffects.bonuses.regen || 0),
|
||||
clickManaBonus: upgradeEffects.clickManaBonus + (equipmentEffects.bonuses.clickMana || 0),
|
||||
baseDamageBonus: upgradeEffects.baseDamageBonus + (equipmentEffects.bonuses.baseDamage || 0),
|
||||
elementCapBonus: upgradeEffects.elementCapBonus + (equipmentEffects.bonuses.elementCap || 0),
|
||||
maxManaBonus: upgradeEffects.maxManaBonus + (equipmentEffects.bonuses.maxMana || 0) + (disciplineEffects.bonuses.maxManaBonus || 0),
|
||||
regenBonus: upgradeEffects.regenBonus + (equipmentEffects.bonuses.regen || 0) + (disciplineEffects.bonuses.regenBonus || 0),
|
||||
clickManaBonus: upgradeEffects.clickManaBonus + (equipmentEffects.bonuses.clickMana || 0) + (disciplineEffects.bonuses.clickManaBonus || 0),
|
||||
baseDamageBonus: upgradeEffects.baseDamageBonus + (equipmentEffects.bonuses.baseDamage || 0) + (disciplineEffects.bonuses.baseDamageBonus || 0),
|
||||
elementCapBonus: upgradeEffects.elementCapBonus + (equipmentEffects.bonuses.elementCap || 0) + (disciplineEffects.bonuses.elementCapBonus || 0),
|
||||
perElementCapBonus,
|
||||
// Merge equipment multipliers with upgrade multipliers
|
||||
maxManaMultiplier: upgradeEffects.maxManaMultiplier * (equipmentEffects.multipliers.maxMana || 1),
|
||||
regenMultiplier: upgradeEffects.regenMultiplier * (equipmentEffects.multipliers.regen || 1),
|
||||
clickManaMultiplier: upgradeEffects.clickManaMultiplier * (equipmentEffects.multipliers.clickMana || 1),
|
||||
baseDamageMultiplier: upgradeEffects.baseDamageMultiplier * (equipmentEffects.multipliers.baseDamage || 1),
|
||||
attackSpeedMultiplier: upgradeEffects.attackSpeedMultiplier * (equipmentEffects.multipliers.attackSpeed || 1),
|
||||
elementCapMultiplier: upgradeEffects.elementCapMultiplier * (equipmentEffects.multipliers.elementCap || 1),
|
||||
// Merge specials
|
||||
specials: new Set([...Array.from(upgradeEffects.specials), ...Array.from(equipmentEffects.specials)]),
|
||||
// Store equipment effects for reference
|
||||
specials: new Set([...Array.from(upgradeEffects.specials), ...Array.from(equipmentEffects.specials), ...Array.from(disciplineEffects.specials)]),
|
||||
equipmentBonuses: equipmentEffects.bonuses,
|
||||
equipmentMultipliers: equipmentEffects.multipliers,
|
||||
equipmentSpecials: equipmentEffects.specials,
|
||||
disciplineBonuses: disciplineEffects.bonuses,
|
||||
disciplineMultipliers: disciplineEffects.multipliers,
|
||||
disciplineSpecials: disciplineEffects.specials,
|
||||
};
|
||||
|
||||
// Handle special stats that are equipment-only
|
||||
if (equipmentEffects.bonuses.critChance) {
|
||||
merged.critChanceBonus += equipmentEffects.bonuses.critChance;
|
||||
}
|
||||
if (equipmentEffects.bonuses.meditationEfficiency) {
|
||||
// This is a multiplier in equipment, convert to additive for simplicity
|
||||
// Equipment gives +10% per stack, so add it to the base
|
||||
merged.meditationEfficiency *= (equipmentEffects.multipliers.meditationEfficiency || 1);
|
||||
}
|
||||
if (equipmentEffects.bonuses.studySpeed) {
|
||||
merged.studySpeedMultiplier *= (equipmentEffects.multipliers.studySpeed || 1);
|
||||
}
|
||||
if (equipmentEffects.bonuses.insightGain) {
|
||||
// Store separately - insight multiplier
|
||||
(merged as any).insightGainMultiplier = (equipmentEffects.multipliers.insightGain || 1);
|
||||
}
|
||||
if (equipmentEffects.bonuses.guardianDamage) {
|
||||
(merged as any).guardianDamageMultiplier = (equipmentEffects.multipliers.guardianDamage || 1);
|
||||
}
|
||||
|
||||
return merged;
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper to get unified effects from game state
|
||||
*/
|
||||
export function getUnifiedEffects(state: Pick<GameState, 'skillUpgrades' | 'skillTiers' | 'equipmentInstances' | 'equippedInstances'>): UnifiedEffects {
|
||||
export function getUnifiedEffects(state: Pick<GameState, 'skillUpgrades' | 'skillTiers' | 'equipmentInstances' | 'equippedInstances' | 'disciplines'>): UnifiedEffects {
|
||||
return computeAllEffects(
|
||||
state.skillUpgrades || {},
|
||||
state.skillTiers || {},
|
||||
state.equipmentInstances || {},
|
||||
state.equippedInstances || {}
|
||||
state.equippedInstances || {},
|
||||
state as GameState
|
||||
);
|
||||
}
|
||||
|
||||
// ─── Stat Computation with All Effects ───────────────────────────────────────
|
||||
|
||||
/**
|
||||
* Compute max mana with all effect sources
|
||||
*/
|
||||
export function computeTotalMaxMana(
|
||||
state: Pick<GameState, 'skills' | 'prestigeUpgrades' | 'skillUpgrades' | 'skillTiers' | 'equipmentInstances' | 'equippedInstances'>,
|
||||
effects?: UnifiedEffects
|
||||
): number {
|
||||
const pu = state.prestigeUpgrades;
|
||||
const skillMult = effects?.skillLevelMultiplier || 1;
|
||||
const base =
|
||||
100 +
|
||||
((state.skills || {}).manaWell || 0) * 100 * skillMult +
|
||||
((pu || {}).manaWell || 0) * 500;
|
||||
|
||||
if (!effects) {
|
||||
effects = getUnifiedEffects(state);
|
||||
}
|
||||
|
||||
const base = 100 + ((state.skills || {}).manaWell || 0) * 100 * skillMult + ((pu || {}).manaWell || 0) * 500;
|
||||
if (!effects) effects = getUnifiedEffects(state as any);
|
||||
return Math.floor((base + effects.maxManaBonus) * effects.maxManaMultiplier);
|
||||
}
|
||||
|
||||
/**
|
||||
* Compute regen with all effect sources
|
||||
*/
|
||||
export function computeTotalRegen(
|
||||
state: Pick<GameState, 'skills' | 'prestigeUpgrades' | 'skillUpgrades' | 'skillTiers' | 'equipmentInstances' | 'equippedInstances'>,
|
||||
effects?: UnifiedEffects
|
||||
@@ -210,39 +167,19 @@ export function computeTotalRegen(
|
||||
const pu = state.prestigeUpgrades;
|
||||
const temporalBonus = 1 + (pu.temporalEcho || 0) * 0.1;
|
||||
const skillMult = effects?.skillLevelMultiplier || 1;
|
||||
const base =
|
||||
2 +
|
||||
(state.skills.manaFlow || 0) * 1 * skillMult +
|
||||
(state.skills.manaSpring || 0) * 2 * skillMult +
|
||||
(pu.manaFlow || 0) * 0.5;
|
||||
|
||||
const base = 2 + (state.skills.manaFlow || 0) * 1 * skillMult + (state.skills.manaSpring || 0) * 2 * skillMult + (pu.manaFlow || 0) * 0.5;
|
||||
let regen = base * temporalBonus;
|
||||
|
||||
if (!effects) {
|
||||
effects = getUnifiedEffects(state);
|
||||
}
|
||||
|
||||
if (!effects) effects = getUnifiedEffects(state as any);
|
||||
regen = (regen + effects.regenBonus + effects.permanentRegenBonus) * effects.regenMultiplier;
|
||||
|
||||
return regen;
|
||||
}
|
||||
|
||||
/**
|
||||
* Compute click mana with all effect sources
|
||||
*/
|
||||
export function computeTotalClickMana(
|
||||
state: Pick<GameState, 'skills' | 'skillUpgrades' | 'skillTiers' | 'equipmentInstances' | 'equippedInstances'>,
|
||||
effects?: UnifiedEffects
|
||||
): number {
|
||||
const skillMult = effects?.skillLevelMultiplier || 1;
|
||||
const base =
|
||||
1 +
|
||||
(state.skills.manaTap || 0) * 1 * skillMult +
|
||||
(state.skills.manaSurge || 0) * 3 * skillMult;
|
||||
|
||||
if (!effects) {
|
||||
effects = getUnifiedEffects(state as any);
|
||||
}
|
||||
|
||||
const base = 1 + (state.skills.manaTap || 0) * 1 * skillMult + (state.skills.manaSurge || 0) * 3 * skillMult;
|
||||
if (!effects) effects = getUnifiedEffects(state as any);
|
||||
return Math.floor((base + effects.clickManaBonus) * effects.clickManaMultiplier);
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
// ─── Discipline Effects ───────────────────────────────────────────────────────
|
||||
// Computes bonuses from active disciplines and integrates with the unified effect system
|
||||
|
||||
import type { GameState } from '../types';
|
||||
import { useDisciplineStore } from '../stores/discipline-slice';
|
||||
import { ALL_DISCIPLINES } from '../data/disciplines';
|
||||
import { calculateStatBonus, getUnlockedPerks } from '../utils/discipline-math';
|
||||
|
||||
export function computeDisciplineEffects(state: GameState): {
|
||||
bonuses: Record<string, number>;
|
||||
multipliers: Record<string, number>;
|
||||
specials: Set<string>;
|
||||
} {
|
||||
const { disciplines } = useDisciplineStore.getState();
|
||||
const activeDiscs = Object.entries(disciplines)
|
||||
.filter(([, disc]) => disc && !disc.paused)
|
||||
.map(([id, disc]) => ({ id, disc, def: ALL_DISCIPLINES.find(d => d.id === id) }))
|
||||
.filter((entry): entry is { id: string; disc: any; def: NonNullable<typeof ALL_DISCIPLINES[0]> } => !!entry.def);
|
||||
|
||||
const bonuses: Record<string, number> = {};
|
||||
const multipliers: Record<string, number> = {};
|
||||
const specials = new Set<string>();
|
||||
|
||||
for (const { disc, def } of activeDiscs) {
|
||||
// Continuous stat bonus
|
||||
const statBonus = calculateStatBonus(def.statBonus.baseValue, disc.xp, def.scalingFactor);
|
||||
if (def.statBonus.stat) {
|
||||
bonuses[def.statBonus.stat] = (bonuses[def.statBonus.stat] || 0) + statBonus;
|
||||
}
|
||||
|
||||
// Perk unlocks
|
||||
const perks = getUnlockedPerks(def, disc.xp);
|
||||
for (const perk of perks) {
|
||||
if (perk.type === 'once' || perk.type === 'infinite') {
|
||||
// Once/infinite perks can be treated as additive bonuses or special flags
|
||||
// For simplicity, we add them as a special flag; actual effect depends on perk.id
|
||||
specials.add(perk.id);
|
||||
} else if (perk.type === 'capped') {
|
||||
// Capped perks act as multipliers after certain thresholds
|
||||
// For now, we treat them as additive to a multiplier stat (example)
|
||||
// In a real implementation, each perk would have a specific effect.
|
||||
// Here we just add a generic perk multiplier placeholder.
|
||||
multipliers[`perk_${perk.id}`] = (multipliers[`perk_${perk.id}`] || 1) + perk.value / 100;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return { bonuses, multipliers, specials };
|
||||
}
|
||||
@@ -0,0 +1,126 @@
|
||||
// ─── Discipline Store Slice ────────────────────────────────────────────────────
|
||||
import { create } from 'zustand';
|
||||
import { persist } from 'zustand/middleware';
|
||||
import type { DisciplineState } from '../types/disciplines';
|
||||
import {
|
||||
calculateManaDrain,
|
||||
calculateStatBonus,
|
||||
canProceedDiscipline,
|
||||
} from '../utils/discipline-math';
|
||||
import { baseDisciplines } from '../data/disciplines/base';
|
||||
import { enchanterDisciplines } from '../data/disciplines/enchanter';
|
||||
import { fabricatorDisciplines } from '../data/disciplines/fabricator';
|
||||
import { invokerDisciplines } from '../data/disciplines/invoker';
|
||||
import { MAX_CONCURRENT_DISCIPLINES } from '../types/disciplines';
|
||||
|
||||
const ALL_DISCIPLINES = [
|
||||
...baseDisciplines,
|
||||
...enchanterDisciplines,
|
||||
...fabricatorDisciplines,
|
||||
...invokerDisciplines,
|
||||
];
|
||||
const DISCIPLINE_MAP = Object.fromEntries(ALL_DISCIPLINES.map((d) => [d.id, d]));
|
||||
|
||||
export interface DisciplineStoreState {
|
||||
disciplines: Record<string, DisciplineState>;
|
||||
activeIds: string[];
|
||||
concurrentLimit: number;
|
||||
totalXP: number;
|
||||
}
|
||||
|
||||
export interface DisciplineStoreActions {
|
||||
activate: (id: string, gameState?: { elements?: Record<string, { unlocked?: boolean }> }) => void;
|
||||
deactivate: (id: string) => void;
|
||||
processTick: (mana: { rawMana: number; elements: Record<string, { current: number }> }) => {
|
||||
rawMana: number;
|
||||
elements: Record<string, { current: number }>;
|
||||
};
|
||||
}
|
||||
|
||||
export type DisciplineStore = DisciplineStoreState & DisciplineStoreActions;
|
||||
|
||||
export const useDisciplineStore = create<DisciplineStore>()(
|
||||
persist(
|
||||
(set, get) => ({
|
||||
disciplines: {},
|
||||
activeIds: [],
|
||||
concurrentLimit: MAX_CONCURRENT_DISCIPLINES,
|
||||
totalXP: 0,
|
||||
|
||||
activate(id, gameState) {
|
||||
set((s) => {
|
||||
const def = DISCIPLINE_MAP[id];
|
||||
if (!def) return s;
|
||||
if (s.activeIds.includes(id)) return s;
|
||||
|
||||
const nonPaused = s.activeIds.filter((aid) => {
|
||||
const d = s.disciplines[aid];
|
||||
return d && !d.paused;
|
||||
}).length;
|
||||
if (nonPaused >= s.concurrentLimit) return s;
|
||||
if (!canProceedDiscipline(id, gameState)) return s;
|
||||
|
||||
const existing = s.disciplines[id] || { id, xp: 0, paused: false };
|
||||
return {
|
||||
disciplines: { ...s.disciplines, [id]: { ...existing, paused: false } },
|
||||
activeIds: [...s.activeIds, id],
|
||||
};
|
||||
});
|
||||
},
|
||||
|
||||
deactivate(id) {
|
||||
set((s) => ({
|
||||
activeIds: s.activeIds.filter((aid) => aid !== id),
|
||||
}));
|
||||
},
|
||||
|
||||
processTick(mana) {
|
||||
const s = get();
|
||||
let rawMana = mana.rawMana;
|
||||
const elements = { ...mana.elements };
|
||||
let newXP = s.totalXP;
|
||||
|
||||
for (const id of s.activeIds) {
|
||||
const disc = s.disciplines[id];
|
||||
if (!disc) continue;
|
||||
if (disc.paused) continue;
|
||||
|
||||
const def = DISCIPLINE_MAP[id];
|
||||
if (!def) continue;
|
||||
|
||||
const drain = calculateManaDrain(def.drainBase, disc.xp, def.difficultyFactor);
|
||||
const element = elements[def.manaType];
|
||||
const available = def.manaType === 'raw' ? rawMana : element?.current;
|
||||
|
||||
if (!available || available < drain) {
|
||||
disc.paused = true;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (def.manaType === 'raw') {
|
||||
rawMana -= drain;
|
||||
} else {
|
||||
elements[def.manaType].current -= drain;
|
||||
}
|
||||
|
||||
disc.xp += 1;
|
||||
newXP += 1;
|
||||
}
|
||||
|
||||
const newLimit = Math.min(
|
||||
MAX_CONCURRENT_DISCIPLINES + Math.floor(newXP / 500),
|
||||
MAX_CONCURRENT_DISCIPLINES + 3
|
||||
);
|
||||
|
||||
set({
|
||||
disciplines: s.disciplines,
|
||||
totalXP: newXP,
|
||||
concurrentLimit: Math.max(s.concurrentLimit, newLimit),
|
||||
});
|
||||
|
||||
return { rawMana, elements };
|
||||
},
|
||||
}),
|
||||
{ name: 'mana-loop-discipline-store' }
|
||||
)
|
||||
);
|
||||
@@ -0,0 +1,49 @@
|
||||
// ─── Discipline Types ─────────────────────────────────────────────────────────
|
||||
|
||||
import type { ManaType } from './elements';
|
||||
|
||||
// ─── Attunement Types ─────────────────────────────────────────────────────────
|
||||
export enum DisciplinesAttunementType {
|
||||
BASE = 'base',
|
||||
ENCHANTER = 'enchanter',
|
||||
FABRICATOR = 'fabricator',
|
||||
INVOKER = 'invoker',
|
||||
}
|
||||
|
||||
// ─── Perk Types ───────────────────────────────────────────────────────────────
|
||||
export type PerkType = 'capped' | 'once' | 'infinite';
|
||||
|
||||
export interface DisciplinePerk {
|
||||
id: string;
|
||||
type: PerkType;
|
||||
threshold: number;
|
||||
value: number;
|
||||
description: string;
|
||||
}
|
||||
|
||||
// ─── Discipline Definition ────────────────────────────────────────────────────
|
||||
export interface DisciplineDefinition {
|
||||
id: string;
|
||||
name: string;
|
||||
attunement: DisciplinesAttunementType;
|
||||
manaType: ManaType;
|
||||
baseCost: number;
|
||||
description: string;
|
||||
statBonus: { stat: string; baseValue: number };
|
||||
difficultyFactor: number;
|
||||
scalingFactor: number;
|
||||
drainBase: number;
|
||||
perks: DisciplinePerk[];
|
||||
requires?: string[];
|
||||
}
|
||||
|
||||
// ─── Discipline State ─────────────────────────────────────────────────────────
|
||||
export interface DisciplineState {
|
||||
id: string;
|
||||
xp: number;
|
||||
paused: boolean;
|
||||
}
|
||||
|
||||
// ─── Discipline Constants ─────────────────────────────────────────────────────
|
||||
export const MAX_CONCURRENT_DISCIPLINES = 1;
|
||||
export const BASE_CONCURRENT_DISCIPLINES = 1;
|
||||
@@ -0,0 +1,130 @@
|
||||
// ─── Discipline Math Utilities ────────────────────────────────────────────────
|
||||
// Continuous scaling formulas for Active Disciplines
|
||||
|
||||
import type { DisciplineState, DisciplineDefinition } from '../types/disciplines';
|
||||
|
||||
/**
|
||||
* Calculate continuous stat bonus from discipline XP
|
||||
* StatBonus = BaseValue * (XP / ScalingFactor)^0.65
|
||||
*/
|
||||
export function calculateStatBonus(
|
||||
baseValue: number,
|
||||
xp: number,
|
||||
scalingFactor: number
|
||||
): number {
|
||||
if (xp <= 0) return 0;
|
||||
const ratio = xp / scalingFactor;
|
||||
const power = Math.pow(ratio, 0.65);
|
||||
return baseValue * power;
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate dynamic mana drain per tick
|
||||
* ManaDrainPerTick = BaseDrain * (1 + (XP / DifficultyFactor)^0.4)
|
||||
*/
|
||||
export function calculateManaDrain(
|
||||
baseDrain: number,
|
||||
xp: number,
|
||||
difficultyFactor: number
|
||||
): number {
|
||||
if (xp <= 0) return baseDrain;
|
||||
const ratio = xp / difficultyFactor;
|
||||
const power = Math.pow(ratio, 0.4);
|
||||
return baseDrain * (1 + power);
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate infinite perk tier
|
||||
* PerkTier = Math.max(0, Math.floor((XP - Threshold) / Interval) + 1)
|
||||
*/
|
||||
export function calculatePerkTier(
|
||||
xp: number,
|
||||
threshold: number,
|
||||
interval: number
|
||||
): number {
|
||||
if (xp < threshold) return 0;
|
||||
const excess = xp - threshold;
|
||||
return Math.max(0, Math.floor(excess / interval) + 1);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if discipline can be activated (has required mana type)
|
||||
*/
|
||||
export function canActivateDiscipline(
|
||||
discipline: DisciplineDefinition,
|
||||
gameState: { elements?: Record<string, any> }
|
||||
): boolean {
|
||||
if (discipline.manaType === 'raw') return true;
|
||||
const element = gameState.elements?.[discipline.manaType];
|
||||
return element && element.unlocked;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if discipline can proceed (has sufficient mana for drain)
|
||||
*/
|
||||
export function canProceedDiscipline(
|
||||
discipline: DisciplineDefinition,
|
||||
disciplineState: DisciplineState,
|
||||
gameState: { elements?: Record<string, any>; rawMana?: number }
|
||||
): boolean {
|
||||
if (disciplineState.paused) return false;
|
||||
|
||||
const drain = calculateManaDrain(
|
||||
discipline.drainBase,
|
||||
disciplineState.xp,
|
||||
discipline.difficultyFactor
|
||||
);
|
||||
|
||||
if (discipline.manaType === 'raw') {
|
||||
return (gameState.rawMana || 0) >= drain;
|
||||
}
|
||||
|
||||
const element = gameState.elements?.[discipline.manaType];
|
||||
return element && element.current >= drain;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get unlocked perks for a discipline
|
||||
*/
|
||||
export function getUnlockedPerks(
|
||||
discipline: DisciplineDefinition,
|
||||
xp: number
|
||||
): DisciplinePerk[] {
|
||||
return discipline.perks.filter(perk => {
|
||||
if (perk.type === 'once') {
|
||||
return xp >= perk.threshold;
|
||||
} else if (perk.type === 'capped') {
|
||||
const tier = calculatePerkTier(xp, perk.threshold, perk.value);
|
||||
return tier > 0;
|
||||
} else if (perk.type === 'infinite') {
|
||||
return xp >= perk.threshold;
|
||||
}
|
||||
return false;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate total stats from all active disciplines
|
||||
*/
|
||||
export function calculateDisciplineStats(
|
||||
disciplines: DisciplineDefinition[],
|
||||
states: DisciplineState[]
|
||||
): Record<string, number> {
|
||||
const stats: Record<string, number> = {};
|
||||
|
||||
disciplines.forEach((discipline, index) => {
|
||||
const state = states[index];
|
||||
if (!state || state.paused) return;
|
||||
|
||||
const bonus = calculateStatBonus(
|
||||
discipline.statBonus.baseValue,
|
||||
state.xp,
|
||||
discipline.scalingFactor
|
||||
);
|
||||
|
||||
const statKey = discipline.statBonus.stat;
|
||||
stats[statKey] = (stats[statKey] || 0) + bonus;
|
||||
});
|
||||
|
||||
return stats;
|
||||
}
|
||||
Reference in New Issue
Block a user