feat: overhaul mana conversion system to unified regen-deduction model
Build and Publish Mana Loop Docker Image / build-and-publish (push) Failing after 1m2s

- New files: element-distance.ts, conversion-costs.ts, conversion-rates.ts
- All conversion types (discipline, attunement, pact) use unified formula
- Conversion costs scale exponentially by element tier (10^(d+1) raw, 10*(d+1) per component)
- Costs deducted from regen, not from mana pool
- Auto-pause on insufficient regen with UI warning
- Meditation boosts conversion rates (reduced by distance)
- Attunement levels provide +50% multiplicative bonus per level
- Guardian pacts provide +0.15/hr base rate + invoker level bonus
- Removed convertMana, processConvertAction, craftComposite from manaStore
- Stats tab shows per-element conversion breakdown with formulas
- ManaDisplay shows per-element net regen rates
- All 916 tests pass, all files under 400 lines
This commit is contained in:
2026-06-04 18:12:41 +02:00
parent 94a2b671b9
commit ab3afae2a6
19 changed files with 742 additions and 572 deletions
+118
View File
@@ -0,0 +1,118 @@
// ─── Conversion Cost Ratios ───────────────────────────────────────────────────
// All conversions produce 1 unit of destination mana.
// Costs are deducted from regen (not from the mana pool).
//
// For a destination element at distance d:
// rawCost = 10^(d+1)
// componentCost = 10 * (d+1) per component
import type { ElementRecipe } from '../types';
export interface ConversionCost {
/** Destination element ID */
element: string;
/** Distance from raw mana */
distance: number;
/** Raw mana cost per 1 unit of destination */
rawCost: number;
/** Component costs: element ID → amount per 1 unit of destination */
componentCosts: Record<string, number>;
}
function computeRawCost(distance: number): number {
return Math.pow(10, distance + 1);
}
function computeComponentCost(distance: number): number {
return 10 * (distance + 1);
}
/** Build a ConversionCost for a base element (distance 1, no components) */
function baseElementCost(element: string): ConversionCost {
return {
element,
distance: 1,
rawCost: computeRawCost(1), // 100
componentCosts: {},
};
}
/** Build a ConversionCost for a composite element (distance 2) */
function compositeElementCost(element: string, components: string[]): ConversionCost {
return {
element,
distance: 2,
rawCost: computeRawCost(2), // 1,000
componentCosts: Object.fromEntries(
components.map(c => [c, computeComponentCost(2)]), // 30 each
),
};
}
/** Build a ConversionCost for an exotic element (distance 3) */
function exoticElementCost(element: string, components: string[]): ConversionCost {
return {
element,
distance: 3,
rawCost: computeRawCost(3), // 10,000
componentCosts: Object.fromEntries(
components.map(c => [c, computeComponentCost(3)]), // 40 each
),
};
}
/** Build a ConversionCost for time (distance 4) */
function timeElementCost(element: string, components: string[]): ConversionCost {
return {
element,
distance: 4,
rawCost: computeRawCost(4), // 100,000
componentCosts: Object.fromEntries(
components.map(c => [c, computeComponentCost(4)]), // 50 each
),
};
}
// ─── Full Cost Table ──────────────────────────────────────────────────────────
export const CONVERSION_COSTS: Record<string, ConversionCost> = {
// Base (distance 1)
fire: baseElementCost('fire'),
water: baseElementCost('water'),
air: baseElementCost('air'),
earth: baseElementCost('earth'),
light: baseElementCost('light'),
dark: baseElementCost('dark'),
death: baseElementCost('death'),
// Utility (distance 1)
transference: baseElementCost('transference'),
// Composite (distance 2)
metal: compositeElementCost('metal', ['fire', 'earth']),
sand: compositeElementCost('sand', ['earth', 'water']),
lightning: compositeElementCost('lightning', ['fire', 'air']),
frost: compositeElementCost('frost', ['air', 'water']),
blackflame: compositeElementCost('blackflame', ['dark', 'fire']),
radiantflames: compositeElementCost('radiantflames', ['light', 'fire']),
miasma: compositeElementCost('miasma', ['air', 'death']),
shadowglass: compositeElementCost('shadowglass', ['earth', 'dark']),
// Exotic (distance 3)
crystal: exoticElementCost('crystal', ['sand', 'light']),
stellar: exoticElementCost('stellar', ['plasma', 'light']),
void: exoticElementCost('void', ['dark', 'death']),
soul: exoticElementCost('soul', ['light', 'dark', 'transference']),
plasma: exoticElementCost('plasma', ['lightning', 'fire', 'transference']),
// Time (distance 4)
time: timeElementCost('time', ['soul', 'sand', 'transference']),
};
/** Get the conversion cost for an element. Returns null if not found. */
export function getConversionCost(element: string): ConversionCost | null {
return CONVERSION_COSTS[element] ?? null;
}
/** Get all source types (raw + components) for a conversion */
export function getConversionSources(element: string): string[] {
const cost = CONVERSION_COSTS[element];
if (!cost) return [];
return ['raw', ...Object.keys(cost.componentCosts)];
}