fix: make discipline perk numerical bonuses functional via structured BonusSpec
Build and Publish Mana Loop Docker Image / build-and-publish (push) Successful in 1m23s

- Add PerkBonus type and optional bonus field to DisciplinePerk
- Populate bonus data on 39 perks across base, elemental, elemental-regen,
  elemental-regen-advanced, and invoker discipline files
- Rewrite computeDisciplineEffects() to apply once/infinite/capped perk bonuses
  through known stat keys (maxManaBonus, baseDamageBonus, regen_*, elementCap_*)
- Add per-element cap bonus routing in effects.ts computeAllEffects()
- Remove dead enchantPower bonus (no consumer in effects pipeline)
This commit is contained in:
2026-05-26 18:00:29 +02:00
parent ae30c4770c
commit da4f9eccb3
10 changed files with 88 additions and 15 deletions
+2 -2
View File
@@ -1,8 +1,8 @@
# Circular Dependencies # Circular Dependencies
Generated: 2026-05-26T09:20:42.073Z Generated: 2026-05-26T15:02:41.168Z
Found: 6 circular chain(s) — these MUST be fixed before modifying involved files. Found: 6 circular chain(s) — these MUST be fixed before modifying involved files.
1. Processed 135 files (1.7s) (2 warnings) 1. Processed 135 files (1.5s) (2 warnings)
2. 1) utils/floor-utils.ts > utils/room-utils.ts > utils/enemy-utils.ts 2. 1) utils/floor-utils.ts > utils/room-utils.ts > utils/enemy-utils.ts
3. 2) utils/floor-utils.ts > utils/room-utils.ts 3. 2) utils/floor-utils.ts > utils/room-utils.ts
4. 3) stores/gameStore.ts > stores/gameActions.ts 4. 3) stores/gameStore.ts > stores/gameActions.ts
+2 -1
View File
@@ -1,6 +1,6 @@
{ {
"_meta": { "_meta": {
"generated": "2026-05-26T09:20:40.084Z", "generated": "2026-05-26T15:02:39.505Z",
"description": "Import dependency graph for src/lib/game. Keys are files, values are arrays of files they import.", "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." "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."
}, },
@@ -567,6 +567,7 @@
"stores/prestigeStore.ts": [ "stores/prestigeStore.ts": [
"constants.ts", "constants.ts",
"data/guardian-encounters.ts", "data/guardian-encounters.ts",
"stores/manaStore.ts",
"utils/result.ts", "utils/result.ts",
"utils/safe-persist.ts" "utils/safe-persist.ts"
], ],
+2
View File
@@ -23,6 +23,7 @@ export const baseDisciplines: DisciplineDefinition[] = [
threshold: 100, threshold: 100,
value: 0, value: 0,
description: '+50 Max Mana', description: '+50 Max Mana',
bonus: { stat: 'maxManaBonus', amount: 50 },
}, },
{ {
id: 'raw-mastery-2', id: 'raw-mastery-2',
@@ -30,6 +31,7 @@ export const baseDisciplines: DisciplineDefinition[] = [
threshold: 500, threshold: 500,
value: 100, value: 100,
description: 'Every 100 XP: +25 Max Mana', description: 'Every 100 XP: +25 Max Mana',
bonus: { stat: 'maxManaBonus', amount: 25 },
}, },
], ],
}, },
@@ -26,6 +26,7 @@ export const elementalRegenAdvancedDisciplines: DisciplineDefinition[] = [
threshold: 150, threshold: 150,
value: 0, value: 0,
description: '+0.35 Metal Regen/tick', description: '+0.35 Metal Regen/tick',
bonus: { stat: 'regen_metal', amount: 0.35 },
}, },
{ {
id: 'regen-metal-inf', id: 'regen-metal-inf',
@@ -33,6 +34,7 @@ export const elementalRegenAdvancedDisciplines: DisciplineDefinition[] = [
threshold: 400, threshold: 400,
value: 100, value: 100,
description: 'Every 100 XP: +0.15 Metal Regen/tick', description: 'Every 100 XP: +0.15 Metal Regen/tick',
bonus: { stat: 'regen_metal', amount: 0.15 },
}, },
], ],
}, },
@@ -55,6 +57,7 @@ export const elementalRegenAdvancedDisciplines: DisciplineDefinition[] = [
threshold: 150, threshold: 150,
value: 0, value: 0,
description: '+0.35 Sand Regen/tick', description: '+0.35 Sand Regen/tick',
bonus: { stat: 'regen_sand', amount: 0.35 },
}, },
{ {
id: 'regen-sand-inf', id: 'regen-sand-inf',
@@ -62,6 +65,7 @@ export const elementalRegenAdvancedDisciplines: DisciplineDefinition[] = [
threshold: 400, threshold: 400,
value: 100, value: 100,
description: 'Every 100 XP: +0.15 Sand Regen/tick', description: 'Every 100 XP: +0.15 Sand Regen/tick',
bonus: { stat: 'regen_sand', amount: 0.15 },
}, },
], ],
}, },
@@ -84,6 +88,7 @@ export const elementalRegenAdvancedDisciplines: DisciplineDefinition[] = [
threshold: 150, threshold: 150,
value: 0, value: 0,
description: '+0.35 Lightning Regen/tick', description: '+0.35 Lightning Regen/tick',
bonus: { stat: 'regen_lightning', amount: 0.35 },
}, },
{ {
id: 'regen-lightning-inf', id: 'regen-lightning-inf',
@@ -91,6 +96,7 @@ export const elementalRegenAdvancedDisciplines: DisciplineDefinition[] = [
threshold: 400, threshold: 400,
value: 100, value: 100,
description: 'Every 100 XP: +0.15 Lightning Regen/tick', description: 'Every 100 XP: +0.15 Lightning Regen/tick',
bonus: { stat: 'regen_lightning', amount: 0.15 },
}, },
], ],
}, },
@@ -114,6 +120,7 @@ export const elementalRegenAdvancedDisciplines: DisciplineDefinition[] = [
threshold: 200, threshold: 200,
value: 0, value: 0,
description: '+0.25 Crystal Regen/tick', description: '+0.25 Crystal Regen/tick',
bonus: { stat: 'regen_crystal', amount: 0.25 },
}, },
{ {
id: 'regen-crystal-inf', id: 'regen-crystal-inf',
@@ -121,6 +128,7 @@ export const elementalRegenAdvancedDisciplines: DisciplineDefinition[] = [
threshold: 500, threshold: 500,
value: 100, value: 100,
description: 'Every 100 XP: +0.1 Crystal Regen/tick', description: 'Every 100 XP: +0.1 Crystal Regen/tick',
bonus: { stat: 'regen_crystal', amount: 0.1 },
}, },
], ],
}, },
@@ -143,6 +151,7 @@ export const elementalRegenAdvancedDisciplines: DisciplineDefinition[] = [
threshold: 200, threshold: 200,
value: 0, value: 0,
description: '+0.25 Stellar Regen/tick', description: '+0.25 Stellar Regen/tick',
bonus: { stat: 'regen_stellar', amount: 0.25 },
}, },
{ {
id: 'regen-stellar-inf', id: 'regen-stellar-inf',
@@ -150,6 +159,7 @@ export const elementalRegenAdvancedDisciplines: DisciplineDefinition[] = [
threshold: 500, threshold: 500,
value: 100, value: 100,
description: 'Every 100 XP: +0.1 Stellar Regen/tick', description: 'Every 100 XP: +0.1 Stellar Regen/tick',
bonus: { stat: 'regen_stellar', amount: 0.1 },
}, },
], ],
}, },
@@ -172,6 +182,7 @@ export const elementalRegenAdvancedDisciplines: DisciplineDefinition[] = [
threshold: 200, threshold: 200,
value: 0, value: 0,
description: '+0.25 Void Regen/tick', description: '+0.25 Void Regen/tick',
bonus: { stat: 'regen_void', amount: 0.25 },
}, },
{ {
id: 'regen-void-inf', id: 'regen-void-inf',
@@ -179,6 +190,7 @@ export const elementalRegenAdvancedDisciplines: DisciplineDefinition[] = [
threshold: 500, threshold: 500,
value: 100, value: 100,
description: 'Every 100 XP: +0.1 Void Regen/tick', description: 'Every 100 XP: +0.1 Void Regen/tick',
bonus: { stat: 'regen_void', amount: 0.1 },
}, },
], ],
}, },
@@ -31,6 +31,7 @@ function makeBaseRegen(id: string, name: string, manaType: string, cost: number)
threshold: 100, threshold: 100,
value: 0, value: 0,
description: `+${BASE_REGEN} ${name} Regen/tick`, description: `+${BASE_REGEN} ${name} Regen/tick`,
bonus: { stat: `regen_${shortId}`, amount: BASE_REGEN },
}, },
{ {
id: `${id}-inf`, id: `${id}-inf`,
@@ -38,6 +39,7 @@ function makeBaseRegen(id: string, name: string, manaType: string, cost: number)
threshold: 300, threshold: 300,
value: 100, value: 100,
description: `Every 100 XP: +0.25 ${name} Regen/tick`, description: `Every 100 XP: +0.25 ${name} Regen/tick`,
bonus: { stat: `regen_${shortId}`, amount: 0.25 },
}, },
], ],
}; };
@@ -73,6 +75,7 @@ export const elementalRegenDisciplines: DisciplineDefinition[] = [
threshold: 100, threshold: 100,
value: 0, value: 0,
description: '+0.4 Transference Regen/tick', description: '+0.4 Transference Regen/tick',
bonus: { stat: 'regen_transference', amount: 0.4 },
}, },
{ {
id: 'regen-transference-inf', id: 'regen-transference-inf',
@@ -80,6 +83,7 @@ export const elementalRegenDisciplines: DisciplineDefinition[] = [
threshold: 300, threshold: 300,
value: 100, value: 100,
description: 'Every 100 XP: +0.2 Transference Regen/tick', description: 'Every 100 XP: +0.2 Transference Regen/tick',
bonus: { stat: 'regen_transference', amount: 0.2 },
}, },
], ],
}, },
@@ -45,6 +45,7 @@ function makeElementalAttunement(cfg: ElementalAttunementConfig): DisciplineDefi
threshold: 200, threshold: 200,
value: 0, value: 0,
description: `+10 ${cfg.name} Capacity`, description: `+10 ${cfg.name} Capacity`,
bonus: { stat: `elementCap_${cfg.manaType}`, amount: 10 },
}, },
], ],
}; };
+2
View File
@@ -23,6 +23,7 @@ export const invokerDisciplines: DisciplineDefinition[] = [
threshold: 200, threshold: 200,
value: 0, value: 0,
description: '+10 Base Damage', description: '+10 Base Damage',
bonus: { stat: 'baseDamageBonus', amount: 10 },
}, },
{ {
id: 'spell-2', id: 'spell-2',
@@ -30,6 +31,7 @@ export const invokerDisciplines: DisciplineDefinition[] = [
threshold: 400, threshold: 400,
value: 30, value: 30,
description: 'Every 300 XP: +5 Base Damage', description: 'Every 300 XP: +5 Base Damage',
bonus: { stat: 'baseDamageBonus', amount: 5 },
}, },
], ],
}, },
+8
View File
@@ -102,6 +102,14 @@ export function computeAllEffects(
} }
} }
// Merge per-element cap bonuses from discipline effects (elementCap_{element})
for (const [key, value] of Object.entries(disciplineEffects.bonuses)) {
if (key.startsWith('elementCap_')) {
const element = key.replace('elementCap_', '');
perElementCapBonus[element] = (perElementCapBonus[element] || 0) + value;
}
}
const merged: UnifiedEffects = { const merged: UnifiedEffects = {
...upgradeEffects, ...upgradeEffects,
maxManaBonus: upgradeEffects.maxManaBonus + (equipmentEffects.bonuses.maxMana || 0) + (disciplineEffects.bonuses.maxManaBonus || 0), maxManaBonus: upgradeEffects.maxManaBonus + (equipmentEffects.bonuses.maxMana || 0) + (disciplineEffects.bonuses.maxManaBonus || 0),
+47 -10
View File
@@ -5,7 +5,23 @@ import type { DisciplineStoreState } from '../stores/discipline-slice';
import type { DisciplineState } from '../types/disciplines'; import type { DisciplineState } from '../types/disciplines';
import { useDisciplineStore } from '../stores/discipline-slice'; import { useDisciplineStore } from '../stores/discipline-slice';
import { ALL_DISCIPLINES } from '../data/disciplines'; import { ALL_DISCIPLINES } from '../data/disciplines';
import { calculateStatBonus, getUnlockedPerks } from '../utils/discipline-math'; import {
calculateStatBonus,
calculatePerkTier,
getUnlockedPerks,
} from '../utils/discipline-math';
/**
* Known stat keys consumed by computeAllEffects() in effects.ts.
* Perk bonuses are routed to these keys so they flow into the unified system.
*/
const KNOWN_BONUS_STATS = new Set([
'maxManaBonus',
'regenBonus',
'clickManaBonus',
'baseDamageBonus',
'elementCapBonus',
]);
export function computeDisciplineEffects(_state?: DisciplineStoreState): { export function computeDisciplineEffects(_state?: DisciplineStoreState): {
bonuses: Record<string, number>; bonuses: Record<string, number>;
@@ -22,26 +38,47 @@ export function computeDisciplineEffects(_state?: DisciplineStoreState): {
const multipliers: Record<string, number> = {}; const multipliers: Record<string, number> = {};
const specials = new Set<string>(); const specials = new Set<string>();
function addBonus(stat: string, amount: number) {
bonuses[stat] = (bonuses[stat] || 0) + amount;
}
for (const { disc, def } of activeDiscs) { for (const { disc, def } of activeDiscs) {
// Continuous stat bonus // Continuous stat bonus
const statBonus = calculateStatBonus(def.statBonus.baseValue, disc.xp, def.scalingFactor); const statBonus = calculateStatBonus(def.statBonus.baseValue, disc.xp, def.scalingFactor);
if (def.statBonus.stat) { if (def.statBonus.stat) {
bonuses[def.statBonus.stat] = (bonuses[def.statBonus.stat] || 0) + statBonus; addBonus(def.statBonus.stat, statBonus);
} }
// Perk unlocks // Perk unlocks
const perks = getUnlockedPerks(def, disc.xp); const perks = getUnlockedPerks(def, disc.xp);
for (const perk of perks) { for (const perk of perks) {
if (perk.type === 'once' || perk.type === 'infinite') { if (perk.type === 'once') {
// Once/infinite perks can be treated as additive bonuses or special flags if (perk.bonus) {
// For simplicity, we add them as a special flag; actual effect depends on perk.id addBonus(perk.bonus.stat, perk.bonus.amount);
} else if (!perk.unlocksEffects) {
// Fallback: qualitative perk with no structured bonus — add as special flag
specials.add(perk.id); specials.add(perk.id);
}
// Perks with unlocksEffects are handled by discipline-slice.ts processTick()
} else if (perk.type === 'infinite') {
if (perk.bonus) {
const interval = perk.value;
const tier = calculatePerkTier(disc.xp, perk.threshold, interval);
if (tier > 0) {
addBonus(perk.bonus.stat, tier * perk.bonus.amount);
}
} else if (!perk.unlocksEffects) {
specials.add(perk.id);
}
} else if (perk.type === 'capped') { } else if (perk.type === 'capped') {
// Capped perks act as multipliers after certain thresholds if (perk.bonus) {
// For now, we treat them as additive to a multiplier stat (example) const tier = calculatePerkTier(disc.xp, perk.threshold, perk.value);
// In a real implementation, each perk would have a specific effect. if (tier > 0) {
// Here we just add a generic perk multiplier placeholder. addBonus(perk.bonus.stat, tier * perk.bonus.amount);
multipliers[`perk_${perk.id}`] = (multipliers[`perk_${perk.id}`] || 1) + perk.value / 100; }
} else if (!perk.unlocksEffects) {
specials.add(perk.id);
}
} }
} }
} }
+6
View File
@@ -13,6 +13,11 @@ export enum DisciplinesAttunementType {
// ─── Perk Types ─────────────────────────────────────────────────────────────── // ─── Perk Types ───────────────────────────────────────────────────────────────
export type PerkType = 'capped' | 'once' | 'infinite'; export type PerkType = 'capped' | 'once' | 'infinite';
export interface PerkBonus {
stat: string;
amount: number;
}
export interface DisciplinePerk { export interface DisciplinePerk {
id: string; id: string;
type: PerkType; type: PerkType;
@@ -20,6 +25,7 @@ export interface DisciplinePerk {
value: number; value: number;
description: string; description: string;
unlocksEffects?: string[]; unlocksEffects?: string[];
bonus?: PerkBonus;
} }
// ─── Discipline Definition ──────────────────────────────────────────────────── // ─── Discipline Definition ────────────────────────────────────────────────────