From e462bfcc1317595bc769c34761c5bb064d22f001 Mon Sep 17 00:00:00 2001 From: n8n-gitea Date: Sat, 16 May 2026 19:17:12 +0200 Subject: [PATCH] feat: implement Active Disciplines system --- docs/circular-deps.txt | 2 +- docs/dependency-graph.json | 2 +- docs/project-structure.txt | 16 ++ src/components/game/tabs/DisciplinesTab.tsx | 251 ++++++++++++++++++ .../game/data/disciplines/base-disciplines.ts | 34 +++ src/lib/game/data/disciplines/base.ts | 56 ++++ .../data/disciplines/enchanter-disciplines.ts | 34 +++ src/lib/game/data/disciplines/enchanter.ts | 56 ++++ .../disciplines/fabricator-disciplines.ts | 21 ++ src/lib/game/data/disciplines/fabricator.ts | 56 ++++ .../data/disciplines/invoker-disciplines.ts | 21 ++ src/lib/game/data/disciplines/invoker.ts | 56 ++++ src/lib/game/effects.ts | 133 +++------- src/lib/game/effects/discipline-effects.ts | 49 ++++ src/lib/game/stores/discipline-slice.ts | 126 +++++++++ src/lib/game/types/disciplines.ts | 49 ++++ src/lib/game/utils/discipline-math.ts | 130 +++++++++ 17 files changed, 992 insertions(+), 100 deletions(-) create mode 100644 src/components/game/tabs/DisciplinesTab.tsx create mode 100644 src/lib/game/data/disciplines/base-disciplines.ts create mode 100644 src/lib/game/data/disciplines/base.ts create mode 100644 src/lib/game/data/disciplines/enchanter-disciplines.ts create mode 100644 src/lib/game/data/disciplines/enchanter.ts create mode 100644 src/lib/game/data/disciplines/fabricator-disciplines.ts create mode 100644 src/lib/game/data/disciplines/fabricator.ts create mode 100644 src/lib/game/data/disciplines/invoker-disciplines.ts create mode 100644 src/lib/game/data/disciplines/invoker.ts create mode 100644 src/lib/game/effects/discipline-effects.ts create mode 100644 src/lib/game/stores/discipline-slice.ts create mode 100644 src/lib/game/types/disciplines.ts create mode 100644 src/lib/game/utils/discipline-math.ts diff --git a/docs/circular-deps.txt b/docs/circular-deps.txt index 9976576..f0bf371 100644 --- a/docs/circular-deps.txt +++ b/docs/circular-deps.txt @@ -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) diff --git a/docs/dependency-graph.json b/docs/dependency-graph.json index 1b40652..ff39ff8 100644 --- a/docs/dependency-graph.json +++ b/docs/dependency-graph.json @@ -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." }, diff --git a/docs/project-structure.txt b/docs/project-structure.txt index 22f5795..9587c5a 100644 --- a/docs/project-structure.txt +++ b/docs/project-structure.txt @@ -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 diff --git a/src/components/game/tabs/DisciplinesTab.tsx b/src/components/game/tabs/DisciplinesTab.tsx new file mode 100644 index 0000000..b4e5b94 --- /dev/null +++ b/src/components/game/tabs/DisciplinesTab.tsx @@ -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((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 ( +
+

{name}

+

{description}

+ +
+ {Math.round(progressPercent)}% +
+
0 ? 'bg-green-500' : 'bg-red-500' + }`} + style={{ width: `${progressPercent}%` }} + /> +
+
+ +
+ Drain: {estimatedDrain.toFixed(1)} ✦{' '} + XP: {displayXp} +
+ + {/* Bonus display */} +
+ Stat Bonus: {activeStatBonus.toFixed(2)} on {statBonus} +
+ + {/* Perks */} +
+ Perks: +
    + {unlockedPerks?.map((p) => ( +
  • {p.replace(/-([0-9]+)$/, ' $1')}
  • + )) : ( +
  • —locked—
  • + )} +
+
+ + {/* Action button */} +
+ +
+
+ ); + }; + + if (!mounted) { + return ( +
+ Loading disciplines… +
+ ); + } + + return ( +
+ {/* Tab bar */} +
+ {attunementTabs.map((tab) => { + const isActiveTab = store + .getState() + .activeAttunement === tab.label.toLowerCase(); + return ( + + ); + })} +
+ + {/* Discipline cards */} +
+ {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 && ( + 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() + } +
+ + {/* Summary info */} +
+
Active Disciple{activeIds.length}{activeIds.length === 1 ? '' : 's'} / {concurrentLimit}
+
Concurrent Limit: {concurrentLimit}
+
+
+ ); +}; \ No newline at end of file diff --git a/src/lib/game/data/disciplines/base-disciplines.ts b/src/lib/game/data/disciplines/base-disciplines.ts new file mode 100644 index 0000000..4e87c8f --- /dev/null +++ b/src/lib/game/data/disciplines/base-disciplines.ts @@ -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: [] +} +]; \ No newline at end of file diff --git a/src/lib/game/data/disciplines/base.ts b/src/lib/game/data/disciplines/base.ts new file mode 100644 index 0000000..5f4a310 --- /dev/null +++ b/src/lib/game/data/disciplines/base.ts @@ -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', + }, + ], + }, +]; \ No newline at end of file diff --git a/src/lib/game/data/disciplines/enchanter-disciplines.ts b/src/lib/game/data/disciplines/enchanter-disciplines.ts new file mode 100644 index 0000000..032bed0 --- /dev/null +++ b/src/lib/game/data/disciplines/enchanter-disciplines.ts @@ -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"}] +} +]; \ No newline at end of file diff --git a/src/lib/game/data/disciplines/enchanter.ts b/src/lib/game/data/disciplines/enchanter.ts new file mode 100644 index 0000000..8148572 --- /dev/null +++ b/src/lib/game/data/disciplines/enchanter.ts @@ -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', + }, + ], + }, +]; \ No newline at end of file diff --git a/src/lib/game/data/disciplines/fabricator-disciplines.ts b/src/lib/game/data/disciplines/fabricator-disciplines.ts new file mode 100644 index 0000000..8418354 --- /dev/null +++ b/src/lib/game/data/disciplines/fabricator-disciplines.ts @@ -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; \ No newline at end of file diff --git a/src/lib/game/data/disciplines/fabricator.ts b/src/lib/game/data/disciplines/fabricator.ts new file mode 100644 index 0000000..0ae0b43 --- /dev/null +++ b/src/lib/game/data/disciplines/fabricator.ts @@ -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', + }, + ], + }, +]; \ No newline at end of file diff --git a/src/lib/game/data/disciplines/invoker-disciplines.ts b/src/lib/game/data/disciplines/invoker-disciplines.ts new file mode 100644 index 0000000..2fc37ee --- /dev/null +++ b/src/lib/game/data/disciplines/invoker-disciplines.ts @@ -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; diff --git a/src/lib/game/data/disciplines/invoker.ts b/src/lib/game/data/disciplines/invoker.ts new file mode 100644 index 0000000..fdf90e6 --- /dev/null +++ b/src/lib/game/data/disciplines/invoker.ts @@ -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', + }, + ], + }, +]; \ No newline at end of file diff --git a/src/lib/game/effects.ts b/src/lib/game/effects.ts index 3bb1a03..b93c849 100755 --- a/src/lib/game/effects.ts +++ b/src/lib/game/effects.ts @@ -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, equippedInstances: Record, @@ -34,24 +31,18 @@ export function computeEquipmentEffects( const multipliers: Record = {}; const specials = new Set(); - // 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; equipmentMultipliers: Record; equipmentSpecials: Set; + disciplineBonuses: Record; + disciplineMultipliers: Record; + disciplineSpecials: Set; } -/** - * Compute all effects from all sources: skill upgrades + equipment enchantments - */ export function computeAllEffects( skillUpgrades: Record, skillTiers: Record, equipmentInstances: Record, - equippedInstances: Record + equippedInstances: Record, + 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 = { ...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): UnifiedEffects { +export function getUnifiedEffects(state: Pick): 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, 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, 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, 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); -} +} \ No newline at end of file diff --git a/src/lib/game/effects/discipline-effects.ts b/src/lib/game/effects/discipline-effects.ts new file mode 100644 index 0000000..12bc72a --- /dev/null +++ b/src/lib/game/effects/discipline-effects.ts @@ -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; + multipliers: Record; + specials: Set; +} { + 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 } => !!entry.def); + + const bonuses: Record = {}; + const multipliers: Record = {}; + const specials = new Set(); + + 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 }; +} \ No newline at end of file diff --git a/src/lib/game/stores/discipline-slice.ts b/src/lib/game/stores/discipline-slice.ts new file mode 100644 index 0000000..1d84f10 --- /dev/null +++ b/src/lib/game/stores/discipline-slice.ts @@ -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; + activeIds: string[]; + concurrentLimit: number; + totalXP: number; +} + +export interface DisciplineStoreActions { + activate: (id: string, gameState?: { elements?: Record }) => void; + deactivate: (id: string) => void; + processTick: (mana: { rawMana: number; elements: Record }) => { + rawMana: number; + elements: Record; + }; +} + +export type DisciplineStore = DisciplineStoreState & DisciplineStoreActions; + +export const useDisciplineStore = create()( + 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' } + ) +); \ No newline at end of file diff --git a/src/lib/game/types/disciplines.ts b/src/lib/game/types/disciplines.ts new file mode 100644 index 0000000..0610e42 --- /dev/null +++ b/src/lib/game/types/disciplines.ts @@ -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; \ No newline at end of file diff --git a/src/lib/game/utils/discipline-math.ts b/src/lib/game/utils/discipline-math.ts new file mode 100644 index 0000000..5c7f961 --- /dev/null +++ b/src/lib/game/utils/discipline-math.ts @@ -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 } +): 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; 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 { + const stats: Record = {}; + + 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; +} \ No newline at end of file