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:
@@ -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',
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
+35
-98
@@ -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