feat: overhaul mana conversion system to unified regen-deduction model
Build and Publish Mana Loop Docker Image / build-and-publish (push) Failing after 1m2s
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:
@@ -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)];
|
||||
}
|
||||
Reference in New Issue
Block a user