fix: rebalance rawCost and componentCost formulas to be achievable with realistic regen
Build and Publish Mana Loop Docker Image / build-and-publish (push) Successful in 1m30s
Build and Publish Mana Loop Docker Image / build-and-publish (push) Successful in 1m30s
- Changed rawCost from exponential 10^(d+1) to linear 2*distance - Base (d=1): 100 → 2 - Composite (d=2): 1,000 → 4 - Exotic (d=3): 10,000 → 6 - Time (d=4): 100,000 → 8 - Changed componentCost from 10*(d+1) to 3*distance - Composite (d=2): 30 → 6 per component - Exotic (d=3): 40 → 9 per component - Time (d=4): 50 → 12 per component - Updated test comments and expectations in conversion-pause-bug-regression.test.ts and mana-conversion-component-deduction.test.ts to match new values Root cause: The exponential rawCost formula produced values 100-10000x too high, making mana conversion permanently paused since drain (rate × cost) always exceeded even late-game raw regen (~20-50/hr). The new linear formula allows conversions to be sustainable at all game stages. Fixes #378
This commit is contained in:
@@ -1,5 +1,5 @@
|
|||||||
# Circular Dependencies
|
# Circular Dependencies
|
||||||
Generated: 2026-06-12T08:05:45.261Z
|
Generated: 2026-06-12T10:15:03.641Z
|
||||||
Found: 4 circular chain(s) — these MUST be fixed before modifying involved files.
|
Found: 4 circular chain(s) — these MUST be fixed before modifying involved files.
|
||||||
|
|
||||||
1. 1) data/guardian-encounters.ts > data/guardian-procedural.ts
|
1. 1) data/guardian-encounters.ts > data/guardian-procedural.ts
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"_meta": {
|
"_meta": {
|
||||||
"generated": "2026-06-12T08:05:43.098Z",
|
"generated": "2026-06-12T10:15:01.393Z",
|
||||||
"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."
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -41,9 +41,9 @@ describe('Bug #348 — conversion pause regression', () => {
|
|||||||
rawGrossRegen,
|
rawGrossRegen,
|
||||||
});
|
});
|
||||||
|
|
||||||
// Earth conversion: baseRate=22.04, rawCost=100
|
// Earth conversion: baseRate=22.04, rawCost=2
|
||||||
// rawDrain = 22.04 * 1.0 * 1.0 * 1.0 * 100 = 2204/hr
|
// rawDrain = 22.04 * 1.0 * 1.0 * 1.0 * 2 = 44.08/hr
|
||||||
// 2204 < 3342.95 => should NOT be paused
|
// 44.08 < 3342.95 => should NOT be paused
|
||||||
const earthEntry = result.rates['earth'];
|
const earthEntry = result.rates['earth'];
|
||||||
expect(earthEntry).toBeDefined();
|
expect(earthEntry).toBeDefined();
|
||||||
expect(earthEntry.paused).toBe(false);
|
expect(earthEntry.paused).toBe(false);
|
||||||
|
|||||||
@@ -32,14 +32,14 @@ describe('Mana Conversion — component consumption deduction (bug #293)', () =>
|
|||||||
});
|
});
|
||||||
|
|
||||||
// Fire is used as a component for metal conversion
|
// Fire is used as a component for metal conversion
|
||||||
// metal rate = 0.35, fire component cost = 30 per unit
|
// metal rate = 0.35, fire component cost = 6 per unit (3 * distance 2)
|
||||||
// elementDrain[fire] = 0.35 * 30 = 10.5/hr
|
// elementDrain[fire] = 0.35 * 6 = 2.1/hr
|
||||||
expect(result.elementDrain['fire']).toBeGreaterThan(0);
|
expect(result.elementDrain['fire']).toBeGreaterThan(0);
|
||||||
expect(result.elementDrain['fire']).toBeCloseTo(0.35 * 30, 5);
|
expect(result.elementDrain['fire']).toBeCloseTo(0.35 * 6, 5);
|
||||||
|
|
||||||
// Earth is also used as a component for metal conversion
|
// Earth is also used as a component for metal conversion
|
||||||
expect(result.elementDrain['earth']).toBeGreaterThan(0);
|
expect(result.elementDrain['earth']).toBeGreaterThan(0);
|
||||||
expect(result.elementDrain['earth']).toBeCloseTo(0.35 * 30, 5);
|
expect(result.elementDrain['earth']).toBeCloseTo(0.35 * 6, 5);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should compute elementRegen as produced - drained', () => {
|
it('should compute elementRegen as produced - drained', () => {
|
||||||
@@ -65,7 +65,7 @@ describe('Mana Conversion — component consumption deduction (bug #293)', () =>
|
|||||||
rawGrossRegen: 10000,
|
rawGrossRegen: 10000,
|
||||||
});
|
});
|
||||||
|
|
||||||
// Fire: produced = 0.5/hr, drained = 0.35 * 30 = 10.5/hr
|
// Fire: produced = 0.5/hr, drained = 0.35 * 6 = 2.1/hr
|
||||||
const fireEntry = result.rates['fire'];
|
const fireEntry = result.rates['fire'];
|
||||||
expect(fireEntry.finalRate).toBeCloseTo(0.5, 5);
|
expect(fireEntry.finalRate).toBeCloseTo(0.5, 5);
|
||||||
|
|
||||||
@@ -76,7 +76,7 @@ describe('Mana Conversion — component consumption deduction (bug #293)', () =>
|
|||||||
// Verify net: produced - drained
|
// Verify net: produced - drained
|
||||||
const fireProduced = fireEntry.finalRate;
|
const fireProduced = fireEntry.finalRate;
|
||||||
const fireDrained = result.elementDrain['fire'] || 0;
|
const fireDrained = result.elementDrain['fire'] || 0;
|
||||||
expect(fireProduced - fireDrained).toBeCloseTo(0.5 - 10.5, 5);
|
expect(fireProduced - fireDrained).toBeCloseTo(0.5 - 2.1, 5);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should have zero elementDrain when element is not used as component', () => {
|
it('should have zero elementDrain when element is not used as component', () => {
|
||||||
@@ -135,15 +135,15 @@ describe('Mana Conversion — component consumption deduction (bug #293)', () =>
|
|||||||
rawGrossRegen: 10000,
|
rawGrossRegen: 10000,
|
||||||
});
|
});
|
||||||
|
|
||||||
// Fire: produced = 0.5, drained by metal (0.35 * 30 = 10.5) and lightning (0.35 * 30 = 10.5)
|
// Fire: produced = 0.5, drained by metal (0.35 * 6 = 2.1) and lightning (0.35 * 6 = 2.1)
|
||||||
// Total fire drain = 10.5 + 10.5 = 21.0
|
// Total fire drain = 2.1 + 2.1 = 4.2
|
||||||
expect(result.elementDrain['fire']).toBeCloseTo(21.0, 5);
|
expect(result.elementDrain['fire']).toBeCloseTo(4.2, 5);
|
||||||
|
|
||||||
// Earth: produced = 0 (no earth discipline), drained by metal (0.35 * 30 = 10.5)
|
// Earth: produced = 0 (no earth discipline), drained by metal (0.35 * 6 = 2.1)
|
||||||
expect(result.elementDrain['earth']).toBeCloseTo(10.5, 5);
|
expect(result.elementDrain['earth']).toBeCloseTo(2.1, 5);
|
||||||
|
|
||||||
// Air: produced = 0 (no air discipline), drained by lightning (0.35 * 30 = 10.5)
|
// Air: produced = 0 (no air discipline), drained by lightning (0.35 * 6 = 2.1)
|
||||||
expect(result.elementDrain['air']).toBeCloseTo(10.5, 5);
|
expect(result.elementDrain['air']).toBeCloseTo(2.1, 5);
|
||||||
|
|
||||||
// Metal: produced = 0.35, not consumed by anything in this setup
|
// Metal: produced = 0.35, not consumed by anything in this setup
|
||||||
expect(result.elementDrain['metal'] || 0).toBe(0);
|
expect(result.elementDrain['metal'] || 0).toBe(0);
|
||||||
|
|||||||
@@ -3,8 +3,8 @@
|
|||||||
// Costs are deducted from regen (not from the mana pool).
|
// Costs are deducted from regen (not from the mana pool).
|
||||||
//
|
//
|
||||||
// For a destination element at distance d:
|
// For a destination element at distance d:
|
||||||
// rawCost = 10^(d+1)
|
// rawCost = 2 * d
|
||||||
// componentCost = 10 * (d+1) per component
|
// componentCost = 3 * d per component
|
||||||
|
|
||||||
import type { ElementRecipe } from '../types';
|
import type { ElementRecipe } from '../types';
|
||||||
|
|
||||||
@@ -20,11 +20,11 @@ export interface ConversionCost {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function computeRawCost(distance: number): number {
|
function computeRawCost(distance: number): number {
|
||||||
return Math.pow(10, distance + 1);
|
return 2 * distance;
|
||||||
}
|
}
|
||||||
|
|
||||||
function computeComponentCost(distance: number): number {
|
function computeComponentCost(distance: number): number {
|
||||||
return 10 * (distance + 1);
|
return 3 * distance;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Build a ConversionCost for a base element (distance 1, no components) */
|
/** Build a ConversionCost for a base element (distance 1, no components) */
|
||||||
@@ -32,14 +32,14 @@ function baseElementCost(element: string): ConversionCost {
|
|||||||
return {
|
return {
|
||||||
element,
|
element,
|
||||||
distance: 1,
|
distance: 1,
|
||||||
rawCost: computeRawCost(1), // 100
|
rawCost: computeRawCost(1), // 2
|
||||||
componentCosts: {},
|
componentCosts: {},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Build a ConversionCost for a composite element (distance 2) */
|
/** Build a ConversionCost for a composite element (distance 2) */
|
||||||
function compositeElementCost(element: string, components: string[]): ConversionCost {
|
function compositeElementCost(element: string, components: string[]): ConversionCost {
|
||||||
const costPerComponent = computeComponentCost(2); // 30 each
|
const costPerComponent = computeComponentCost(2); // 6 each
|
||||||
const componentCosts: Record<string, number> = {};
|
const componentCosts: Record<string, number> = {};
|
||||||
for (const c of components) {
|
for (const c of components) {
|
||||||
componentCosts[c] = (componentCosts[c] || 0) + costPerComponent;
|
componentCosts[c] = (componentCosts[c] || 0) + costPerComponent;
|
||||||
@@ -47,14 +47,14 @@ function compositeElementCost(element: string, components: string[]): Conversion
|
|||||||
return {
|
return {
|
||||||
element,
|
element,
|
||||||
distance: 2,
|
distance: 2,
|
||||||
rawCost: computeRawCost(2), // 1,000
|
rawCost: computeRawCost(2), // 4
|
||||||
componentCosts,
|
componentCosts,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Build a ConversionCost for an exotic element (distance 3) */
|
/** Build a ConversionCost for an exotic element (distance 3) */
|
||||||
function exoticElementCost(element: string, components: string[]): ConversionCost {
|
function exoticElementCost(element: string, components: string[]): ConversionCost {
|
||||||
const costPerComponent = computeComponentCost(3); // 40 each
|
const costPerComponent = computeComponentCost(3); // 9 each
|
||||||
const componentCosts: Record<string, number> = {};
|
const componentCosts: Record<string, number> = {};
|
||||||
for (const c of components) {
|
for (const c of components) {
|
||||||
componentCosts[c] = (componentCosts[c] || 0) + costPerComponent;
|
componentCosts[c] = (componentCosts[c] || 0) + costPerComponent;
|
||||||
@@ -62,14 +62,14 @@ function exoticElementCost(element: string, components: string[]): ConversionCos
|
|||||||
return {
|
return {
|
||||||
element,
|
element,
|
||||||
distance: 3,
|
distance: 3,
|
||||||
rawCost: computeRawCost(3), // 10,000
|
rawCost: computeRawCost(3), // 6
|
||||||
componentCosts,
|
componentCosts,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Build a ConversionCost for time (distance 4) */
|
/** Build a ConversionCost for time (distance 4) */
|
||||||
function timeElementCost(element: string, components: string[]): ConversionCost {
|
function timeElementCost(element: string, components: string[]): ConversionCost {
|
||||||
const costPerComponent = computeComponentCost(4); // 50 each
|
const costPerComponent = computeComponentCost(4); // 12 each
|
||||||
const componentCosts: Record<string, number> = {};
|
const componentCosts: Record<string, number> = {};
|
||||||
for (const c of components) {
|
for (const c of components) {
|
||||||
componentCosts[c] = (componentCosts[c] || 0) + costPerComponent;
|
componentCosts[c] = (componentCosts[c] || 0) + costPerComponent;
|
||||||
@@ -77,7 +77,7 @@ function timeElementCost(element: string, components: string[]): ConversionCost
|
|||||||
return {
|
return {
|
||||||
element,
|
element,
|
||||||
distance: 4,
|
distance: 4,
|
||||||
rawCost: computeRawCost(4), // 100,000
|
rawCost: computeRawCost(4), // 8
|
||||||
componentCosts,
|
componentCosts,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user