ab3afae2a6
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
119 lines
4.2 KiB
TypeScript
119 lines
4.2 KiB
TypeScript
// ─── 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)];
|
|
}
|