feat: add enchanter disciplines to unlock enchantment effects via perk progression
Build and Publish Mana Loop Docker Image / build-and-publish (push) Successful in 1m19s

- Add unlocksEffects field to DisciplinePerk type
- Add unlockEffects action to crafting store (deduplicating merge)
- Modify discipline processTick to detect perk thresholds and return unlocked effect IDs
- Wire gameStore tick to pass unlocked effects to crafting store
- Create 8 new enchanter disciplines with tiered effect unlocks:
  Basic/Advanced Weapon, Utility, Mana, Basic/Intermediate/Advanced Spell, Special
- Higher-tier disciplines require prerequisite disciplines
- Add processedPerks tracking to prevent duplicate unlocks
- Split enchanter disciplines into modular files (enchanter, enchanter-utility, enchanter-spells, enchanter-special)
- All tests pass (784/784), no new TS errors, all files under 400 lines
This commit is contained in:
2026-05-23 19:29:45 +02:00
parent 868dfb6225
commit 14f25fffda
11 changed files with 576 additions and 4 deletions
+3
View File
@@ -246,6 +246,9 @@ Mana-Loop/
│ │ ├── data/
│ │ │ ├── disciplines/
│ │ │ │ ├── base.ts
│ │ │ │ ├── enchanter-special.ts
│ │ │ │ ├── enchanter-spells.ts
│ │ │ │ ├── enchanter-utility.ts
│ │ │ │ ├── enchanter.ts
│ │ │ │ ├── fabricator.ts
│ │ │ │ ├── index.ts
@@ -0,0 +1,70 @@
// ─── Enchanter Special Disciplines ─────────────────────────────────────────────
// Disciplines for unlocking unique and powerful special enchantment effects
import type { DisciplineDefinition } from '../../types/disciplines';
export const enchanterSpecialDisciplines: DisciplineDefinition[] = [
{
id: 'study-special-enchantments',
name: 'Study Special Enchantments',
attunement: 'enchanter',
manaType: 'death',
baseCost: 22,
description: 'Learn to enchant equipment with unique and powerful effects.',
statBonus: { stat: 'enchantPower', baseValue: 5 },
difficultyFactor: 220,
scalingFactor: 130,
drainBase: 4,
requires: ['study-advanced-weapon-enchantments'],
perks: [
{
id: 'special-spell-echo',
type: 'once',
threshold: 100,
value: 0,
description: 'Unlock Echo Chamber enchantment',
unlocksEffects: ['spell_echo_10'],
},
{
id: 'special-guardian-dmg',
type: 'once',
threshold: 80,
value: 0,
description: 'Unlock Bane enchantment',
unlocksEffects: ['guardian_dmg_10'],
},
{
id: 'special-overpower',
type: 'once',
threshold: 150,
value: 0,
description: 'Unlock Overpower enchantment',
unlocksEffects: ['overpower_80'],
},
{
id: 'special-first-strike',
type: 'once',
threshold: 120,
value: 0,
description: 'Unlock First Strike enchantment',
unlocksEffects: ['first_strike'],
},
{
id: 'special-combo-master',
type: 'once',
threshold: 200,
value: 0,
description: 'Unlock Combo Master enchantment',
unlocksEffects: ['combo_master'],
},
{
id: 'special-adrenaline-rush',
type: 'once',
threshold: 180,
value: 0,
description: 'Unlock Adrenaline Rush enchantment',
unlocksEffects: ['adrenaline_rush'],
},
],
},
];
@@ -0,0 +1,243 @@
// ─── Enchanter Spell Disciplines ───────────────────────────────────────────────
// Disciplines for unlocking spell enchantment effects on casters
import type { DisciplineDefinition } from '../../types/disciplines';
export const enchanterSpellDisciplines: DisciplineDefinition[] = [
{
id: 'study-basic-spell-enchantments',
name: 'Study Basic Spell Enchantments',
attunement: 'enchanter',
manaType: 'air',
baseCost: 18,
description: 'Learn to enchant casters with basic spell effects.',
statBonus: { stat: 'enchantPower', baseValue: 4 },
difficultyFactor: 160,
scalingFactor: 100,
drainBase: 3,
perks: [
{
id: 'spell-mana-bolt',
type: 'once',
threshold: 50,
value: 0,
description: 'Unlock Mana Bolt spell enchant',
unlocksEffects: ['spell_manaBolt'],
},
{
id: 'spell-fireball',
type: 'once',
threshold: 100,
value: 0,
description: 'Unlock Fireball spell enchant',
unlocksEffects: ['spell_fireball'],
},
{
id: 'spell-water-jet',
type: 'once',
threshold: 100,
value: 0,
description: 'Unlock Water Jet spell enchant',
unlocksEffects: ['spell_waterJet'],
},
{
id: 'spell-gust',
type: 'once',
threshold: 100,
value: 0,
description: 'Unlock Gust spell enchant',
unlocksEffects: ['spell_gust'],
},
{
id: 'spell-stone-bullet',
type: 'once',
threshold: 100,
value: 0,
description: 'Unlock Stone Bullet spell enchant',
unlocksEffects: ['spell_stoneBullet'],
},
{
id: 'spell-light-lance',
type: 'once',
threshold: 150,
value: 0,
description: 'Unlock Light Lance spell enchant',
unlocksEffects: ['spell_lightLance'],
},
{
id: 'spell-shadow-bolt',
type: 'once',
threshold: 150,
value: 0,
description: 'Unlock Shadow Bolt spell enchant',
unlocksEffects: ['spell_shadowBolt'],
},
{
id: 'spell-drain',
type: 'once',
threshold: 150,
value: 0,
description: 'Unlock Drain spell enchant',
unlocksEffects: ['spell_drain'],
},
],
},
{
id: 'study-intermediate-spell-enchantments',
name: 'Study Intermediate Spell Enchantments',
attunement: 'enchanter',
manaType: 'earth',
baseCost: 25,
description: 'Learn to enchant casters with intermediate and compound spell effects.',
statBonus: { stat: 'enchantPower', baseValue: 6 },
difficultyFactor: 250,
scalingFactor: 150,
drainBase: 5,
requires: ['study-basic-spell-enchantments'],
perks: [
{
id: 'spell-inferno',
type: 'once',
threshold: 100,
value: 0,
description: 'Unlock Inferno spell enchant',
unlocksEffects: ['spell_inferno'],
},
{
id: 'spell-tidal-wave',
type: 'once',
threshold: 100,
value: 0,
description: 'Unlock Tidal Wave spell enchant',
unlocksEffects: ['spell_tidalWave'],
},
{
id: 'spell-earthquake',
type: 'once',
threshold: 120,
value: 0,
description: 'Unlock Earthquake spell enchant',
unlocksEffects: ['spell_earthquake'],
},
{
id: 'spell-chain-lightning',
type: 'once',
threshold: 100,
value: 0,
description: 'Unlock Chain Lightning spell enchant',
unlocksEffects: ['spell_chainLightning'],
},
{
id: 'spell-metal-shard',
type: 'once',
threshold: 80,
value: 0,
description: 'Unlock Metal Shard spell enchant',
unlocksEffects: ['spell_metalShard'],
},
{
id: 'spell-sand-blast',
type: 'once',
threshold: 80,
value: 0,
description: 'Unlock Sand Blast spell enchant',
unlocksEffects: ['spell_sandBlast'],
},
],
},
{
id: 'study-advanced-spell-enchantments',
name: 'Study Advanced Spell Enchantments',
attunement: 'enchanter',
manaType: 'dark',
baseCost: 35,
description: 'Learn to enchant casters with master and exotic spell effects.',
statBonus: { stat: 'enchantPower', baseValue: 10 },
difficultyFactor: 350,
scalingFactor: 200,
drainBase: 7,
requires: ['study-intermediate-spell-enchantments'],
perks: [
{
id: 'spell-pyroclasm',
type: 'once',
threshold: 100,
value: 0,
description: 'Unlock Pyroclasm spell enchant',
unlocksEffects: ['spell_pyroclasm'],
},
{
id: 'spell-tsunami',
type: 'once',
threshold: 100,
value: 0,
description: 'Unlock Tsunami spell enchant',
unlocksEffects: ['spell_tsunami'],
},
{
id: 'spell-meteor-strike',
type: 'once',
threshold: 120,
value: 0,
description: 'Unlock Meteor Strike spell enchant',
unlocksEffects: ['spell_meteorStrike'],
},
{
id: 'spell-heaven-light',
type: 'once',
threshold: 100,
value: 0,
description: "Unlock Heaven's Light spell enchant",
unlocksEffects: ['spell_heavenLight'],
},
{
id: 'spell-oblivion',
type: 'once',
threshold: 100,
value: 0,
description: 'Unlock Oblivion spell enchant',
unlocksEffects: ['spell_oblivion'],
},
{
id: 'spell-furnace-blast',
type: 'once',
threshold: 100,
value: 0,
description: 'Unlock Furnace Blast spell enchant',
unlocksEffects: ['spell_furnaceBlast'],
},
{
id: 'spell-dune-collapse',
type: 'once',
threshold: 100,
value: 0,
description: 'Unlock Dune Collapse spell enchant',
unlocksEffects: ['spell_duneCollapse'],
},
{
id: 'spell-stellar-nova',
type: 'once',
threshold: 200,
value: 0,
description: 'Unlock Stellar Nova spell enchant',
unlocksEffects: ['spell_stellarNova'],
},
{
id: 'spell-void-collapse',
type: 'once',
threshold: 180,
value: 0,
description: 'Unlock Void Collapse spell enchant',
unlocksEffects: ['spell_voidCollapse'],
},
{
id: 'spell-crystal-shatter',
type: 'once',
threshold: 160,
value: 0,
description: 'Unlock Crystal Shatter spell enchant',
unlocksEffects: ['spell_crystalShatter'],
},
],
},
];
@@ -0,0 +1,107 @@
// ─── Enchanter Utility & Mana Disciplines ──────────────────────────────────────
// Disciplines for unlocking utility and mana enchantment effects
import type { DisciplineDefinition } from '../../types/disciplines';
export const enchanterUtilityDisciplines: DisciplineDefinition[] = [
{
id: 'study-utility-enchantments',
name: 'Study Utility Enchantments',
attunement: 'enchanter',
manaType: 'light',
baseCost: 8,
description: 'Learn to enchant equipment with utility effects.',
statBonus: { stat: 'studySpeed', baseValue: 0.05 },
difficultyFactor: 80,
scalingFactor: 60,
drainBase: 2,
perks: [
{
id: 'utility-meditate',
type: 'once',
threshold: 50,
value: 0,
description: 'Unlock Meditative Focus enchantment',
unlocksEffects: ['meditate_10'],
},
{
id: 'utility-study',
type: 'once',
threshold: 100,
value: 0,
description: 'Unlock Quick Study enchantment',
unlocksEffects: ['study_10'],
},
{
id: 'utility-insight',
type: 'once',
threshold: 150,
value: 0,
description: 'Unlock Insightful enchantment',
unlocksEffects: ['insight_5'],
},
],
},
{
id: 'study-mana-enchantments',
name: 'Study Mana Enchantments',
attunement: 'enchanter',
manaType: 'water',
baseCost: 15,
description: 'Learn to enchant equipment with mana-boosting effects.',
statBonus: { stat: 'maxMana', baseValue: 10 },
difficultyFactor: 150,
scalingFactor: 100,
drainBase: 3,
perks: [
{
id: 'mana-cap-50',
type: 'once',
threshold: 75,
value: 0,
description: 'Unlock Mana Reserve enchantment',
unlocksEffects: ['mana_cap_50'],
},
{
id: 'mana-cap-100',
type: 'once',
threshold: 150,
value: 0,
description: 'Unlock Mana Reservoir enchantment',
unlocksEffects: ['mana_cap_100'],
},
{
id: 'mana-regen-1',
type: 'once',
threshold: 100,
value: 0,
description: 'Unlock Trickle enchantment',
unlocksEffects: ['mana_regen_1'],
},
{
id: 'mana-regen-2',
type: 'once',
threshold: 200,
value: 0,
description: 'Unlock Stream enchantment',
unlocksEffects: ['mana_regen_2'],
},
{
id: 'click-mana-1',
type: 'once',
threshold: 125,
value: 0,
description: 'Unlock Mana Tap enchantment',
unlocksEffects: ['click_mana_1'],
},
{
id: 'click-mana-3',
type: 'once',
threshold: 225,
value: 0,
description: 'Unlock Mana Surge enchantment',
unlocksEffects: ['click_mana_3'],
},
],
},
];
+87 -2
View File
@@ -1,5 +1,5 @@
// ─── Enchanter Discipline Files ──────────────────────────────────────────────
// Attunement-focused disciplines for Enchanter role
// ─── Enchanter Disciplines ────────────────────────────────────────────────────
// Core enchanter disciplines and weapon enchantment studies
import type { DisciplineDefinition } from '../../types/disciplines';
@@ -53,4 +53,89 @@ export const enchanterDisciplines: DisciplineDefinition[] = [
},
],
},
{
id: 'study-basic-weapon-enchantments',
name: 'Study Basic Weapon Enchantments',
attunement: 'enchanter',
manaType: 'fire',
baseCost: 10,
description: 'Learn to enchant weapons with basic elemental effects.',
statBonus: { stat: 'enchantPower', baseValue: 3 },
difficultyFactor: 100,
scalingFactor: 80,
drainBase: 2,
perks: [
{
id: 'basic-weapon-fire',
type: 'once',
threshold: 50,
value: 0,
description: 'Unlock Fire Enchant for weapons',
unlocksEffects: ['sword_fire'],
},
{
id: 'basic-weapon-frost',
type: 'once',
threshold: 100,
value: 0,
description: 'Unlock Frost Enchant for weapons',
unlocksEffects: ['sword_frost'],
},
{
id: 'basic-weapon-lightning',
type: 'once',
threshold: 150,
value: 0,
description: 'Unlock Lightning Enchant for weapons',
unlocksEffects: ['sword_lightning'],
},
],
},
{
id: 'study-advanced-weapon-enchantments',
name: 'Study Advanced Weapon Enchantments',
attunement: 'enchanter',
manaType: 'dark',
baseCost: 20,
description: 'Learn to enchant weapons with exotic and combat effects.',
statBonus: { stat: 'enchantPower', baseValue: 5 },
difficultyFactor: 200,
scalingFactor: 120,
drainBase: 4,
requires: ['study-basic-weapon-enchantments'],
perks: [
{
id: 'advanced-weapon-void',
type: 'once',
threshold: 100,
value: 0,
description: 'Unlock Void Enchant for weapons',
unlocksEffects: ['sword_void'],
},
{
id: 'advanced-weapon-damage-5',
type: 'once',
threshold: 150,
value: 0,
description: 'Unlock Minor Power enchantment',
unlocksEffects: ['damage_5'],
},
{
id: 'advanced-weapon-crit',
type: 'once',
threshold: 200,
value: 0,
description: 'Unlock Sharp Edge enchantment',
unlocksEffects: ['crit_5'],
},
{
id: 'advanced-weapon-attack-speed',
type: 'once',
threshold: 250,
value: 0,
description: 'Unlock Swift Casting enchantment',
unlocksEffects: ['attack_speed_10'],
},
],
},
];
+9
View File
@@ -3,6 +3,9 @@
import { baseDisciplines } from './base';
import { enchanterDisciplines } from './enchanter';
import { enchanterUtilityDisciplines } from './enchanter-utility';
import { enchanterSpellDisciplines } from './enchanter-spells';
import { enchanterSpecialDisciplines } from './enchanter-special';
import { fabricatorDisciplines } from './fabricator';
import { invokerDisciplines } from './invoker';
import type { DisciplineDefinition } from '../../types/disciplines';
@@ -10,11 +13,17 @@ import type { DisciplineDefinition } from '../../types/disciplines';
export const ALL_DISCIPLINES: DisciplineDefinition[] = [
...baseDisciplines,
...enchanterDisciplines,
...enchanterUtilityDisciplines,
...enchanterSpellDisciplines,
...enchanterSpecialDisciplines,
...fabricatorDisciplines,
...invokerDisciplines,
];
export { baseDisciplines } from './base';
export { enchanterDisciplines } from './enchanter';
export { enchanterUtilityDisciplines } from './enchanter-utility';
export { enchanterSpellDisciplines } from './enchanter-spells';
export { enchanterSpecialDisciplines } from './enchanter-special';
export { fabricatorDisciplines } from './fabricator';
export { invokerDisciplines } from './invoker';
+15
View File
@@ -335,6 +335,21 @@ export const useCraftingStore = create<CraftingStore>()(
unequipItem: (slot: EquipmentSlot) => {
unequipItemAction(slot, set);
},
unlockEffects: (effectIds: string[]) => {
set((state) => {
const existing = new Set(state.unlockedEffects);
let changed = false;
for (const id of effectIds) {
if (!existing.has(id)) {
existing.add(id);
changed = true;
}
}
if (!changed) return state;
return { unlockedEffects: Array.from(existing) };
});
},
};
},
{
@@ -69,6 +69,7 @@ export interface CraftingActions {
setSelectedEquipmentInstance: (id: string | null) => void;
resetEnchantmentSelection: () => void;
clearLastError: () => void;
unlockEffects: (effectIds: string[]) => void;
}
export type CraftingStore = CraftingState & CraftingActions;
+31 -1
View File
@@ -7,9 +7,13 @@ import {
calculateManaDrain,
calculateStatBonus,
canProceedDiscipline,
getUnlockedPerks,
} from '../utils/discipline-math';
import { baseDisciplines } from '../data/disciplines/base';
import { enchanterDisciplines } from '../data/disciplines/enchanter';
import { enchanterUtilityDisciplines } from '../data/disciplines/enchanter-utility';
import { enchanterSpellDisciplines } from '../data/disciplines/enchanter-spells';
import { enchanterSpecialDisciplines } from '../data/disciplines/enchanter-special';
import { fabricatorDisciplines } from '../data/disciplines/fabricator';
import { invokerDisciplines } from '../data/disciplines/invoker';
import { MAX_CONCURRENT_DISCIPLINES } from '../types/disciplines';
@@ -17,6 +21,9 @@ import { MAX_CONCURRENT_DISCIPLINES } from '../types/disciplines';
const ALL_DISCIPLINES = [
...baseDisciplines,
...enchanterDisciplines,
...enchanterUtilityDisciplines,
...enchanterSpellDisciplines,
...enchanterSpecialDisciplines,
...fabricatorDisciplines,
...invokerDisciplines,
];
@@ -27,6 +34,7 @@ export interface DisciplineStoreState {
activeIds: string[];
concurrentLimit: number;
totalXP: number;
processedPerks: string[];
}
export interface DisciplineStoreActions {
@@ -35,6 +43,7 @@ export interface DisciplineStoreActions {
processTick: (mana: { rawMana: number; elements: Record<string, { current: number }> }) => {
rawMana: number;
elements: Record<string, { current: number }>;
unlockedEffects: string[];
};
}
@@ -47,6 +56,7 @@ export const useDisciplineStore = create<DisciplineStore>()(
activeIds: [],
concurrentLimit: MAX_CONCURRENT_DISCIPLINES,
totalXP: 0,
processedPerks: [],
activate(id, gameState) {
set((s) => {
@@ -95,6 +105,8 @@ export const useDisciplineStore = create<DisciplineStore>()(
const elements = { ...mana.elements };
let newXP = s.totalXP;
const newDisciplines = { ...s.disciplines };
const newUnlockedEffects: string[] = [];
const newProcessedPerks = [...s.processedPerks];
for (const id of s.activeIds) {
const disc = newDisciplines[id];
@@ -122,8 +134,25 @@ export const useDisciplineStore = create<DisciplineStore>()(
};
}
const oldXP = disc.xp;
newDisciplines[id] = { ...disc, xp: disc.xp + 1 };
newXP += 1;
// Check for newly unlocked perks that unlock effects
if (def.perks.length > 0) {
const oldPerks = getUnlockedPerks(def, oldXP);
const newPerks = getUnlockedPerks(def, disc.xp + 1);
const oldPerkIds = new Set(oldPerks.map(p => p.id));
for (const perk of newPerks) {
if (!oldPerkIds.has(perk.id) && perk.unlocksEffects) {
const perkKey = `${id}:${perk.id}`;
if (!newProcessedPerks.includes(perkKey)) {
newUnlockedEffects.push(...perk.unlocksEffects);
newProcessedPerks.push(perkKey);
}
}
}
}
}
const newLimit = Math.min(
@@ -135,9 +164,10 @@ export const useDisciplineStore = create<DisciplineStore>()(
disciplines: newDisciplines,
totalXP: newXP,
concurrentLimit: Math.max(s.concurrentLimit, newLimit),
processedPerks: newProcessedPerks,
});
return { rawMana, elements };
return { rawMana, elements, unlockedEffects: newUnlockedEffects };
},
}),
{ storage: createSafeStorage(), name: 'mana-loop-discipline-store' }
+8
View File
@@ -267,6 +267,14 @@ export const useGameStore = create<GameCoordinatorStore>()(
rawMana = disciplineResult.rawMana;
elements = disciplineResult.elements;
// Unlock enchantment effects from newly unlocked discipline perks
if (disciplineResult.unlockedEffects.length > 0) {
useCraftingStore.getState().unlockEffects(disciplineResult.unlockedEffects);
for (const effectId of disciplineResult.unlockedEffects) {
addLog(`✨ Discipline insight unlocked: ${effectId}`);
}
}
// Combat — delegate to combatStore
if (ctx.combat.currentAction === 'climb') {
const combatResult = useCombatStore.getState().processCombatTick(
+1
View File
@@ -19,6 +19,7 @@ export interface DisciplinePerk {
threshold: number;
value: number;
description: string;
unlocksEffects?: string[];
}
// ─── Discipline Definition ────────────────────────────────────────────────────