fix: Elemental Mana Capacity disciplines now increase element capacity
Build and Publish Mana Loop Docker Image / build-and-publish (push) Successful in 1m23s
Build and Publish Mana Loop Docker Image / build-and-publish (push) Successful in 1m23s
- Add optional baseMax field to ElementState to track prestige-derived max separately from bonuses - Add computeElementMaxWithBonuses action to manaStore that computes max = baseMax + per-element bonus - Apply per-element cap bonuses from disciplines and equipment in game tick (elementCap_* keys) - Fix resetMana to use correct prestige key (elementalAttune instead of nonexistent elemMax) - Add store migration (v1->v2) to populate baseMax for existing saved games - Extract pact ritual processing to pipelines/pact-ritual.ts - Extract element cap bonus utilities to utils/element-cap-bonus.ts - Fix inline element types in crafting-fabricator.ts - Update test fixtures to include baseMax in element literals Fixes #185
This commit is contained in:
@@ -1,4 +1,4 @@
|
|||||||
# Circular Dependencies
|
# Circular Dependencies
|
||||||
Generated: 2026-05-28T16:14:24.376Z
|
Generated: 2026-05-28T16:38:33.627Z
|
||||||
|
|
||||||
No circular dependencies found. ✅
|
No circular dependencies found. ✅
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"_meta": {
|
"_meta": {
|
||||||
"generated": "2026-05-28T16:14:22.630Z",
|
"generated": "2026-05-28T16:38:31.819Z",
|
||||||
"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."
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -333,6 +333,8 @@ Mana-Loop/
|
|||||||
│ │ ├── hooks/
|
│ │ ├── hooks/
|
||||||
│ │ │ └── useGameDerived.ts
|
│ │ │ └── useGameDerived.ts
|
||||||
│ │ ├── stores/
|
│ │ ├── stores/
|
||||||
|
│ │ │ ├── pipelines/
|
||||||
|
│ │ │ │ └── pact-ritual.ts
|
||||||
│ │ │ ├── attunementStore.ts
|
│ │ │ ├── attunementStore.ts
|
||||||
│ │ │ ├── combat-actions.ts
|
│ │ │ ├── combat-actions.ts
|
||||||
│ │ │ ├── combat-state.types.ts
|
│ │ │ ├── combat-state.types.ts
|
||||||
@@ -365,6 +367,7 @@ Mana-Loop/
|
|||||||
│ │ │ ├── activity-log.ts
|
│ │ │ ├── activity-log.ts
|
||||||
│ │ │ ├── combat-utils.ts
|
│ │ │ ├── combat-utils.ts
|
||||||
│ │ │ ├── discipline-math.ts
|
│ │ │ ├── discipline-math.ts
|
||||||
|
│ │ │ ├── element-cap-bonus.ts
|
||||||
│ │ │ ├── enemy-generator.ts
|
│ │ │ ├── enemy-generator.ts
|
||||||
│ │ │ ├── enemy-utils.ts
|
│ │ │ ├── enemy-utils.ts
|
||||||
│ │ │ ├── floor-utils.ts
|
│ │ │ ├── floor-utils.ts
|
||||||
|
|||||||
@@ -37,7 +37,7 @@ describe('DisciplineStore — reactivate after deactivate (bug #163)', () => {
|
|||||||
|
|
||||||
it('should reactivate a discipline with elements after deactivating it', () => {
|
it('should reactivate a discipline with elements after deactivating it', () => {
|
||||||
useDisciplineStore.getState().activate('attune-fire', {
|
useDisciplineStore.getState().activate('attune-fire', {
|
||||||
elements: { fire: { unlocked: true, current: 100, max: 100 } },
|
elements: { fire: { unlocked: true, current: 100, max: 100, baseMax: 100 } },
|
||||||
});
|
});
|
||||||
expect(useDisciplineStore.getState().activeIds).toContain('attune-fire');
|
expect(useDisciplineStore.getState().activeIds).toContain('attune-fire');
|
||||||
|
|
||||||
@@ -45,7 +45,7 @@ describe('DisciplineStore — reactivate after deactivate (bug #163)', () => {
|
|||||||
expect(useDisciplineStore.getState().activeIds).not.toContain('attune-fire');
|
expect(useDisciplineStore.getState().activeIds).not.toContain('attune-fire');
|
||||||
|
|
||||||
useDisciplineStore.getState().activate('attune-fire', {
|
useDisciplineStore.getState().activate('attune-fire', {
|
||||||
elements: { fire: { unlocked: true, current: 100, max: 100 } },
|
elements: { fire: { unlocked: true, current: 100, max: 100, baseMax: 100 } },
|
||||||
});
|
});
|
||||||
expect(useDisciplineStore.getState().activeIds).toContain('attune-fire');
|
expect(useDisciplineStore.getState().activeIds).toContain('attune-fire');
|
||||||
expect(useDisciplineStore.getState().disciplines['attune-fire'].paused).toBe(false);
|
expect(useDisciplineStore.getState().disciplines['attune-fire'].paused).toBe(false);
|
||||||
|
|||||||
@@ -35,14 +35,14 @@ describe('DisciplineStore', () => {
|
|||||||
it('should not activate capacity discipline when mana type is locked', () => {
|
it('should not activate capacity discipline when mana type is locked', () => {
|
||||||
// capacity disciplines now require the mana type to be unlocked
|
// capacity disciplines now require the mana type to be unlocked
|
||||||
useDisciplineStore.getState().activate('attune-fire', {
|
useDisciplineStore.getState().activate('attune-fire', {
|
||||||
elements: { fire: { unlocked: false, current: 100, max: 100 } },
|
elements: { fire: { unlocked: false, current: 100, max: 100, baseMax: 100 } },
|
||||||
});
|
});
|
||||||
expect(useDisciplineStore.getState().activeIds).not.toContain('attune-fire');
|
expect(useDisciplineStore.getState().activeIds).not.toContain('attune-fire');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should activate capacity discipline when mana type element is unlocked', () => {
|
it('should activate capacity discipline when mana type element is unlocked', () => {
|
||||||
useDisciplineStore.getState().activate('attune-fire', {
|
useDisciplineStore.getState().activate('attune-fire', {
|
||||||
elements: { fire: { unlocked: true, current: 100, max: 100 } },
|
elements: { fire: { unlocked: true, current: 100, max: 100, baseMax: 100 } },
|
||||||
});
|
});
|
||||||
expect(useDisciplineStore.getState().activeIds).toContain('attune-fire');
|
expect(useDisciplineStore.getState().activeIds).toContain('attune-fire');
|
||||||
});
|
});
|
||||||
@@ -57,7 +57,7 @@ describe('DisciplineStore', () => {
|
|||||||
|
|
||||||
it('should activate when required element is unlocked', () => {
|
it('should activate when required element is unlocked', () => {
|
||||||
useDisciplineStore.getState().activate('attune-fire', {
|
useDisciplineStore.getState().activate('attune-fire', {
|
||||||
elements: { fire: { unlocked: true, current: 100, max: 100 } },
|
elements: { fire: { unlocked: true, current: 100, max: 100, baseMax: 100 } },
|
||||||
});
|
});
|
||||||
expect(useDisciplineStore.getState().activeIds).toContain('attune-fire');
|
expect(useDisciplineStore.getState().activeIds).toContain('attune-fire');
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
// Separate file to avoid exceeding the 400-line limit in craftingStore.ts.
|
// Separate file to avoid exceeding the 400-line limit in craftingStore.ts.
|
||||||
|
|
||||||
import type { EquipmentCraftingProgress } from './types';
|
import type { EquipmentCraftingProgress } from './types';
|
||||||
|
import type { ElementState } from './types';
|
||||||
import type { FabricatorRecipe } from './data/fabricator-recipes';
|
import type { FabricatorRecipe } from './data/fabricator-recipes';
|
||||||
import { FABRICATOR_RECIPES } from './data/fabricator-recipes';
|
import { FABRICATOR_RECIPES } from './data/fabricator-recipes';
|
||||||
import { useManaStore } from './stores/manaStore';
|
import { useManaStore } from './stores/manaStore';
|
||||||
@@ -26,7 +27,7 @@ export function checkFabricatorCosts(
|
|||||||
recipe: FabricatorRecipe,
|
recipe: FabricatorRecipe,
|
||||||
materials: Record<string, number>,
|
materials: Record<string, number>,
|
||||||
rawMana: number,
|
rawMana: number,
|
||||||
elements: Record<string, { current: number; max: number; unlocked: boolean }>,
|
elements: Record<string, ElementState>,
|
||||||
): FabricatorCostCheck {
|
): FabricatorCostCheck {
|
||||||
const missingMaterials: Record<string, number> = {};
|
const missingMaterials: Record<string, number> = {};
|
||||||
let canCraft = true;
|
let canCraft = true;
|
||||||
@@ -52,13 +53,13 @@ export function checkFabricatorCosts(
|
|||||||
|
|
||||||
export interface ManaDeduction {
|
export interface ManaDeduction {
|
||||||
rawMana: number;
|
rawMana: number;
|
||||||
elements: Record<string, { current: number; max: number; unlocked: boolean }>;
|
elements: Record<string, ElementState>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function deductFabricatorMana(
|
export function deductFabricatorMana(
|
||||||
recipe: FabricatorRecipe,
|
recipe: FabricatorRecipe,
|
||||||
rawMana: number,
|
rawMana: number,
|
||||||
elements: Record<string, { current: number; max: number; unlocked: boolean }>,
|
elements: Record<string, ElementState>,
|
||||||
): ManaDeduction | null {
|
): ManaDeduction | null {
|
||||||
if (recipe.manaType === 'raw') {
|
if (recipe.manaType === 'raw') {
|
||||||
if (rawMana < recipe.manaCost) return null;
|
if (rawMana < recipe.manaCost) return null;
|
||||||
@@ -84,14 +85,14 @@ export function deductFabricatorMana(
|
|||||||
|
|
||||||
export interface ManaRefund {
|
export interface ManaRefund {
|
||||||
rawMana: number;
|
rawMana: number;
|
||||||
elements: Record<string, { current: number; max: number; unlocked: boolean }>;
|
elements: Record<string, ElementState>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function refundFabricatorMana(
|
export function refundFabricatorMana(
|
||||||
recipe: FabricatorRecipe,
|
recipe: FabricatorRecipe,
|
||||||
refundAmount: number,
|
refundAmount: number,
|
||||||
rawMana: number,
|
rawMana: number,
|
||||||
elements: Record<string, { current: number; max: number; unlocked: boolean }>,
|
elements: Record<string, ElementState>,
|
||||||
): ManaRefund {
|
): ManaRefund {
|
||||||
if (recipe.manaType === 'raw') {
|
if (recipe.manaType === 'raw') {
|
||||||
return { rawMana: rawMana + refundAmount, elements };
|
return { rawMana: rawMana + refundAmount, elements };
|
||||||
@@ -149,7 +150,7 @@ export interface CraftMaterialResult {
|
|||||||
success: boolean;
|
success: boolean;
|
||||||
newMaterials: Record<string, number>;
|
newMaterials: Record<string, number>;
|
||||||
newRawMana: number;
|
newRawMana: number;
|
||||||
newElements: Record<string, { current: number; max: number; unlocked: boolean }>;
|
newElements: Record<string, ElementState>;
|
||||||
logMessage: string;
|
logMessage: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -12,6 +12,8 @@ import type { ComputedEffects } from '../effects/upgrade-effects.types';
|
|||||||
|
|
||||||
import { computeDisciplineEffects } from '../effects/discipline-effects';
|
import { computeDisciplineEffects } from '../effects/discipline-effects';
|
||||||
import { computeMaxMana, computeRegen, getMeditationBonus, getIncursionStrength, calcInsight } from '../utils';
|
import { computeMaxMana, computeRegen, getMeditationBonus, getIncursionStrength, calcInsight } from '../utils';
|
||||||
|
import { mergePerElementCapBonuses } from '../utils/element-cap-bonus';
|
||||||
|
import { processPactRitual } from './pipelines/pact-ritual';
|
||||||
import { useUIStore } from './uiStore';
|
import { useUIStore } from './uiStore';
|
||||||
import { usePrestigeStore } from './prestigeStore';
|
import { usePrestigeStore } from './prestigeStore';
|
||||||
import { useManaStore } from './manaStore';
|
import { useManaStore } from './manaStore';
|
||||||
@@ -204,40 +206,18 @@ export const useGameStore = create<GameCoordinatorStore>()(
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Pact ritual
|
// Pact ritual
|
||||||
if (ctx.prestige.pactRitualFloor !== null) {
|
const pactResult = processPactRitual(
|
||||||
const guardian = getGuardianForFloor(ctx.prestige.pactRitualFloor);
|
ctx.prestige.pactRitualFloor,
|
||||||
if (guardian) {
|
ctx.prestige.pactRitualProgress,
|
||||||
const pactAffinity = Math.min(0.9,
|
ctx.prestige.signedPacts,
|
||||||
(ctx.prestige.prestigeUpgrades.pactAffinity || 0) * 0.1 + (disciplineEffects.bonuses.pactAffinityBonus || 0));
|
ctx.prestige.defeatedGuardians,
|
||||||
const requiredTime = guardian.pactTime * (1 - pactAffinity);
|
ctx.prestige.prestigeUpgrades.pactAffinity || 0,
|
||||||
|
disciplineEffects.bonuses.pactAffinityBonus || 0,
|
||||||
if (ctx.prestige.pactRitualProgress + HOURS_PER_TICK >= requiredTime) {
|
);
|
||||||
addLog(`📜 Pact signed with ${guardian.name}! You have gained their boons.`);
|
if (pactResult.writes) {
|
||||||
|
writes.prestige = { ...(writes.prestige || {}), ...pactResult.writes };
|
||||||
// Unlock mana types granted by this guardian
|
|
||||||
const manaStore = useManaStore.getState();
|
|
||||||
for (const manaType of guardian.unlocksMana || []) {
|
|
||||||
const result = manaStore.unlockElement(manaType, 0);
|
|
||||||
if (result.success) {
|
|
||||||
addLog(`✨ ${manaType.charAt(0).toUpperCase() + manaType.slice(1)} mana unlocked!`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
writes.prestige = {
|
|
||||||
...(writes.prestige || {}),
|
|
||||||
signedPacts: [...ctx.prestige.signedPacts, ctx.prestige.pactRitualFloor],
|
|
||||||
defeatedGuardians: ctx.prestige.defeatedGuardians.filter(f => f !== ctx.prestige.pactRitualFloor),
|
|
||||||
pactRitualFloor: null,
|
|
||||||
pactRitualProgress: 0,
|
|
||||||
};
|
|
||||||
} else {
|
|
||||||
writes.prestige = {
|
|
||||||
...(writes.prestige || {}),
|
|
||||||
pactRitualProgress: ctx.prestige.pactRitualProgress + HOURS_PER_TICK,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
pactResult.logs.forEach(l => addLog(l));
|
||||||
|
|
||||||
// Discipline tick
|
// Discipline tick
|
||||||
const disciplineResult = useDisciplineStore.getState().processTick({
|
const disciplineResult = useDisciplineStore.getState().processTick({
|
||||||
@@ -294,6 +274,21 @@ export const useGameStore = create<GameCoordinatorStore>()(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Apply per-element capacity bonuses from disciplines and equipment
|
||||||
|
const perElementCapBonuses = mergePerElementCapBonuses(
|
||||||
|
disciplineEffects.bonuses,
|
||||||
|
equipmentEffects.bonuses,
|
||||||
|
);
|
||||||
|
useManaStore.getState().computeElementMaxWithBonuses(perElementCapBonuses);
|
||||||
|
|
||||||
|
// Sync updated max/baseMax from mana store into tick elements snapshot
|
||||||
|
const manaStateAfter = useManaStore.getState();
|
||||||
|
for (const [ek, es] of Object.entries(manaStateAfter.elements)) {
|
||||||
|
if (elements[ek]) {
|
||||||
|
elements[ek] = { ...elements[ek], max: es.max, baseMax: es.baseMax };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Combat — delegate to combatStore
|
// Combat — delegate to combatStore
|
||||||
if (ctx.combat.currentAction === 'climb') {
|
if (ctx.combat.currentAction === 'climb') {
|
||||||
const combatResult = useCombatStore.getState().processCombatTick(
|
const combatResult = useCombatStore.getState().processCombatTick(
|
||||||
|
|||||||
@@ -39,6 +39,13 @@ export interface ManaActions {
|
|||||||
setElementMax: (max: number) => void;
|
setElementMax: (max: number) => void;
|
||||||
craftComposite: (target: string, recipe: string[]) => Result<void>;
|
craftComposite: (target: string, recipe: string[]) => Result<void>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Compute and apply per-element max from baseMax + bonuses.
|
||||||
|
* Caller provides the bonus map (elementCap_* from disciplines/equipment).
|
||||||
|
* This sets max = baseMax + bonus for each element, preventing double-counting.
|
||||||
|
*/
|
||||||
|
computeElementMaxWithBonuses: (perElementBonuses: Record<string, number>) => void;
|
||||||
|
|
||||||
// Helper for gameStore coordination
|
// Helper for gameStore coordination
|
||||||
processConvertAction: (rawMana: number) => { rawMana: number; elements: Record<string, ElementState> } | null;
|
processConvertAction: (rawMana: number) => { rawMana: number; elements: Record<string, ElementState> } | null;
|
||||||
|
|
||||||
@@ -64,6 +71,7 @@ export const useManaStore = create<ManaStore>()(
|
|||||||
{
|
{
|
||||||
current: 0,
|
current: 0,
|
||||||
max: 10,
|
max: 10,
|
||||||
|
baseMax: 10,
|
||||||
unlocked: BASE_UNLOCKED_ELEMENTS.includes(k),
|
unlocked: BASE_UNLOCKED_ELEMENTS.includes(k),
|
||||||
}
|
}
|
||||||
])
|
])
|
||||||
@@ -148,10 +156,27 @@ export const useManaStore = create<ManaStore>()(
|
|||||||
|
|
||||||
setElementMax: (max: number) => {
|
setElementMax: (max: number) => {
|
||||||
set((state) => ({
|
set((state) => ({
|
||||||
elements: Object.fromEntries(Object.entries(state.elements).map(([k, v]) => [k, { ...v, max }])) as Record<string, ElementState>,
|
elements: Object.fromEntries(Object.entries(state.elements).map(([k, v]) => [k, { ...v, max, baseMax: v.baseMax ?? max }])) as Record<string, ElementState>,
|
||||||
}));
|
}));
|
||||||
},
|
},
|
||||||
|
|
||||||
|
computeElementMaxWithBonuses: (perElementBonuses: Record<string, number>) => {
|
||||||
|
set((state) => {
|
||||||
|
const newElements = { ...state.elements };
|
||||||
|
let changed = false;
|
||||||
|
for (const [element, bonus] of Object.entries(perElementBonuses)) {
|
||||||
|
if (newElements[element] && bonus > 0) {
|
||||||
|
const newMax = (newElements[element].baseMax ?? newElements[element].max) + bonus;
|
||||||
|
if (newElements[element].max !== newMax) {
|
||||||
|
newElements[element] = { ...newElements[element], max: newMax };
|
||||||
|
changed = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return changed ? { elements: newElements } : state;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
craftComposite: (target: string, recipe: string[]) => {
|
craftComposite: (target: string, recipe: string[]) => {
|
||||||
const state = get();
|
const state = get();
|
||||||
const costs: Record<string, number> = {};
|
const costs: Record<string, number> = {};
|
||||||
@@ -162,12 +187,13 @@ export const useManaStore = create<ManaStore>()(
|
|||||||
}
|
}
|
||||||
|
|
||||||
const newElems = { ...state.elements };
|
const newElems = { ...state.elements };
|
||||||
|
const baseMax = state.elements[target]?.baseMax ?? 10;
|
||||||
for (const [r, amt] of Object.entries(costs)) {
|
for (const [r, amt] of Object.entries(costs)) {
|
||||||
newElems[r] = { ...newElems[r], current: newElems[r].current - amt };
|
newElems[r] = { ...newElems[r], current: newElems[r].current - amt };
|
||||||
}
|
}
|
||||||
|
|
||||||
const targetElem = newElems[target];
|
const targetElem = newElems[target];
|
||||||
newElems[target] = { ...(targetElem || { current: 0, max: 10, unlocked: false }), current: (targetElem?.current || 0) + 1, unlocked: true };
|
newElems[target] = { ...(targetElem || { current: 0, max: 10, baseMax: 10, unlocked: false }), current: (targetElem?.current || 0) + 1, unlocked: true, baseMax };
|
||||||
set({ elements: newElems });
|
set({ elements: newElems });
|
||||||
return okVoid();
|
return okVoid();
|
||||||
},
|
},
|
||||||
@@ -191,7 +217,7 @@ export const useManaStore = create<ManaStore>()(
|
|||||||
resetMana: (
|
resetMana: (
|
||||||
prestigeUpgrades: Record<string, number>,
|
prestigeUpgrades: Record<string, number>,
|
||||||
) => {
|
) => {
|
||||||
const elementMax = 10 + (prestigeUpgrades.elemMax || 0) * 5;
|
const elementMax = 10 + (prestigeUpgrades.elementalAttune || 0) * 25;
|
||||||
const startingMana = 10 + (prestigeUpgrades.manaStart || 0) * 10;
|
const startingMana = 10 + (prestigeUpgrades.manaStart || 0) * 10;
|
||||||
set({ rawMana: startingMana, meditateTicks: 0, totalManaGathered: 0, elements: makeInitialElements(elementMax, prestigeUpgrades) });
|
set({ rawMana: startingMana, meditateTicks: 0, totalManaGathered: 0, elements: makeInitialElements(elementMax, prestigeUpgrades) });
|
||||||
},
|
},
|
||||||
@@ -199,8 +225,19 @@ export const useManaStore = create<ManaStore>()(
|
|||||||
{
|
{
|
||||||
storage: createSafeStorage(),
|
storage: createSafeStorage(),
|
||||||
name: 'mana-loop-mana',
|
name: 'mana-loop-mana',
|
||||||
version: 1,
|
version: 2,
|
||||||
partialize: (state) => ({ rawMana: state.rawMana, meditateTicks: state.meditateTicks, totalManaGathered: state.totalManaGathered, elements: state.elements }),
|
partialize: (state) => ({ rawMana: state.rawMana, meditateTicks: state.meditateTicks, totalManaGathered: state.totalManaGathered, elements: state.elements }),
|
||||||
|
migrate: (persistedState: any, _version) => {
|
||||||
|
// Migration: add baseMax to elements that don't have it
|
||||||
|
if (persistedState && persistedState.elements) {
|
||||||
|
for (const k of Object.keys(persistedState.elements)) {
|
||||||
|
if (persistedState.elements[k].baseMax === undefined) {
|
||||||
|
persistedState.elements[k].baseMax = persistedState.elements[k].max ?? 10;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return persistedState;
|
||||||
|
},
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
@@ -214,7 +251,7 @@ export function makeInitialElements(
|
|||||||
const elements: Record<string, ElementState> = {};
|
const elements: Record<string, ElementState> = {};
|
||||||
for (const k of Object.keys(ELEMENTS)) {
|
for (const k of Object.keys(ELEMENTS)) {
|
||||||
const isUnlocked = BASE_UNLOCKED_ELEMENTS.includes(k);
|
const isUnlocked = BASE_UNLOCKED_ELEMENTS.includes(k);
|
||||||
elements[k] = { current: isUnlocked ? elemStart : 0, max: elementMax, unlocked: isUnlocked };
|
elements[k] = { current: isUnlocked ? elemStart : 0, max: elementMax, baseMax: elementMax, unlocked: isUnlocked };
|
||||||
}
|
}
|
||||||
return elements;
|
return elements;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,55 @@
|
|||||||
|
// ─── Pact Ritual Pipeline Phase ───────────────────────────────────────────────
|
||||||
|
// Processes pact ritual signing during the game tick.
|
||||||
|
|
||||||
|
import { useManaStore } from '../manaStore';
|
||||||
|
import { getGuardianForFloor } from '../../data/guardian-encounters';
|
||||||
|
import { HOURS_PER_TICK } from '../../constants';
|
||||||
|
|
||||||
|
export interface PactRitualResult {
|
||||||
|
writes: {
|
||||||
|
signedPacts?: number[];
|
||||||
|
deficientGuardians?: number[];
|
||||||
|
pactRitualFloor: number | null;
|
||||||
|
pactRitualProgress: number;
|
||||||
|
} | null;
|
||||||
|
logs: string[];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Process pact ritual progression. Advances progress and completes signing
|
||||||
|
* when enough enough hours have accumulated.
|
||||||
|
*/
|
||||||
|
export function processPactRitual(pactRitualFloor: number | null, pactRitualProgress: number, signedPacts: number[], defeatedGuardians: number[], pactAffinityUpgrade: number, pactAffinityBonus: number): PactRitualResult {
|
||||||
|
if (pactRitualFloor === null) return { writes: null, logs: [] };
|
||||||
|
const logs: string[] = [];
|
||||||
|
const guardian = getGuardianForFloor(pactRitualFloor);
|
||||||
|
if (!guardian) return { writes: null, logs: [] };
|
||||||
|
|
||||||
|
const pactAffinity = Math.min(0.9, pactAffinityUpgrade * 0.1 + pactAffinityBonus);
|
||||||
|
const requiredTime = guardian.pactTime * (1 - pactAffinity);
|
||||||
|
|
||||||
|
if (pactRitualProgress + HOURS_PER_TICK >= requiredTime) {
|
||||||
|
logs.push(`📜 Pact signed with ${guardian.name}! You have gained their boons.`);
|
||||||
|
const manaStore = useManaStore.getState();
|
||||||
|
for (const manaType of guardian.unlocksMana || []) {
|
||||||
|
const result = manaStore.unlockElement(manaType, 0);
|
||||||
|
if (result.success) {
|
||||||
|
logs.push(`✨ ${manaType.charAt(0).toUpperCase() + manaType.slice(1)} mana unlocked!`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
writes: {
|
||||||
|
signedPacts: [...signedPacts, pactRitualFloor],
|
||||||
|
defeatedGuardians: defeatedGuardians.filter(f => f !== pactRitualFloor),
|
||||||
|
pactRitualFloor: null,
|
||||||
|
pactRitualProgress: 0,
|
||||||
|
},
|
||||||
|
logs,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
writes: { pactRitualFloor, pactRitualProgress: pactRitualProgress + HOURS_PER_TICK, signedPacts, defeatedGuardians },
|
||||||
|
logs,
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -16,5 +16,8 @@ export interface ElementDef {
|
|||||||
export interface ElementState {
|
export interface ElementState {
|
||||||
current: number;
|
current: number;
|
||||||
max: number;
|
max: number;
|
||||||
|
/** Base max from prestige upgrades (elementalAttune), without discipline/equipment bonuses.
|
||||||
|
* If not set, falls back to max (legacy behavior for backwards compat). */
|
||||||
|
baseMax?: number;
|
||||||
unlocked: boolean;
|
unlocked: boolean;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,38 @@
|
|||||||
|
// ─── Element Capacity Bonus Utilities ─────────────────────────────────────────
|
||||||
|
// Extracts and merges per-element capacity bonuses from discipline and equipment
|
||||||
|
// effects for application during the game tick.
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Extract elementCap_* bonuses from a bonus record.
|
||||||
|
* Returns a map of element name → bonus amount.
|
||||||
|
*/
|
||||||
|
export function extractElementCapBonuses(
|
||||||
|
bonuses: Record<string, number>
|
||||||
|
): Record<string, number> {
|
||||||
|
const result: Record<string, number> = {};
|
||||||
|
for (const [key, value] of Object.entries(bonuses)) {
|
||||||
|
if (key.startsWith('elementCap_') && value > 0) {
|
||||||
|
const element = key.replace('elementCap_', '');
|
||||||
|
result[element] = (result[element] || 0) + value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Merge elementCap_* bonuses from discipline and equipment effects
|
||||||
|
* into a single per-element bonus map.
|
||||||
|
*/
|
||||||
|
export function mergePerElementCapBonuses(
|
||||||
|
disciplineBonuses: Record<string, number>,
|
||||||
|
equipmentBonuses: Record<string, number>,
|
||||||
|
): Record<string, number> {
|
||||||
|
const merged: Record<string, number> = {};
|
||||||
|
for (const [element, value] of Object.entries(extractElementCapBonuses(disciplineBonuses))) {
|
||||||
|
merged[element] = (merged[element] || 0) + value;
|
||||||
|
}
|
||||||
|
for (const [element, value] of Object.entries(extractElementCapBonuses(equipmentBonuses))) {
|
||||||
|
merged[element] = (merged[element] || 0) + value;
|
||||||
|
}
|
||||||
|
return merged;
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user