diff --git a/src/components/upgrades/GeneratorTooltipContent.tsx b/src/components/upgrades/GeneratorTooltipContent.tsx index 0174e7b..caac5c5 100644 --- a/src/components/upgrades/GeneratorTooltipContent.tsx +++ b/src/components/upgrades/GeneratorTooltipContent.tsx @@ -23,6 +23,8 @@ export function GeneratorTooltipContent({ percentOfTotal, nextMilestoneOwned, nextMilestoneMultiplier, + deltaTdPerSecond, + milestoneWillCross, } = computeGeneratorTooltipData(upgrade, owned, allOwned); const hasMultiplier = milestoneMultiplier > 1 || synergyMultiplier > 1; @@ -91,7 +93,34 @@ export function GeneratorTooltipContent({ )} - {nextMilestoneOwned !== null && ( + + + + + Next purchase adds + + + +{formatNumber(deltaTdPerSecond)} TD/s + + + + {milestoneWillCross && ( + + 🏆 Milestone! All units ×{nextMilestoneMultiplier} + + )} + + {!milestoneWillCross && nextMilestoneOwned !== null && ( <> diff --git a/src/components/upgrades/tooltipHelpers.test.ts b/src/components/upgrades/tooltipHelpers.test.ts index 0650557..fd5a1ba 100644 --- a/src/components/upgrades/tooltipHelpers.test.ts +++ b/src/components/upgrades/tooltipHelpers.test.ts @@ -87,7 +87,7 @@ describe("computeGeneratorTooltipData", () => { it("returns correct name and icon", () => { const data = computeGeneratorTooltipData(neuralNotepad, 0, {}); expect(data.name).toBe("Neural Notepad"); - expect(data.icon).toBe("\uD83D\uDCDD"); + expect(data.icon).toBe("📝"); }); it("effectiveTdPerUnit equals baseTdPerUnit when no multipliers apply", () => { @@ -104,4 +104,85 @@ describe("computeGeneratorTooltipData", () => { data.effectiveTdPerUnit * data.owned, ); }); + + // ── Delta TD/s tests ────────────────────────────────────────────────────── + + describe("deltaTdPerSecond", () => { + it("equals baseTdPerSecond for the first unit (0 → 1)", () => { + // 0 owned → 1 owned, no milestone yet: delta = 0.2 * 1 * 1 - 0 = 0.2 + const data = computeGeneratorTooltipData(neuralNotepad, 0, {}); + expect(data.deltaTdPerSecond).toBeCloseTo(0.2); + expect(data.milestoneWillCross).toBe(false); + }); + + it("equals baseTdPerSecond for a mid-range unit with no milestone (5 → 6)", () => { + // No milestone active (owned < 10): delta = base * 6 * 1 - base * 5 * 1 = base + const allOwned = { "neural-notepad": 5 }; + const data = computeGeneratorTooltipData(neuralNotepad, 5, allOwned); + expect(data.deltaTdPerSecond).toBeCloseTo(0.2); + expect(data.milestoneWillCross).toBe(false); + }); + + it("accounts for milestone crossing when buying the 10th unit (9 → 10)", () => { + // Buying the 10th unit crosses the x1.5 milestone. + // Current: 0.2 * 9 * 1 = 1.8 + // Future: 0.2 * 10 * 1.5 = 3.0 + // Delta: 3.0 - 1.8 = 1.2 + const allOwned = { "neural-notepad": 9 }; + const data = computeGeneratorTooltipData(neuralNotepad, 9, allOwned); + expect(data.deltaTdPerSecond).toBeCloseTo(1.2); + expect(data.milestoneWillCross).toBe(true); + }); + + it("accounts for milestone crossing when buying the 25th unit (24 → 25)", () => { + // Buying the 25th unit crosses the x2 milestone. + // Current: 0.2 * 24 * 1.5 = 7.2 + // Future: 0.2 * 25 * 2 = 10.0 + // Delta: 10.0 - 7.2 = 2.8 + const allOwned = { "neural-notepad": 24 }; + const data = computeGeneratorTooltipData(neuralNotepad, 24, allOwned); + expect(data.deltaTdPerSecond).toBeCloseTo(2.8); + expect(data.milestoneWillCross).toBe(true); + }); + + it("accounts for milestone crossing when buying the 50th unit (49 → 50)", () => { + // Buying the 50th unit crosses the x3 milestone. + // Current: 0.2 * 49 * 2 = 19.6 + // Future: 0.2 * 50 * 3 = 30.0 + // Delta: 30.0 - 19.6 = 10.4 + const allOwned = { "neural-notepad": 49 }; + const data = computeGeneratorTooltipData(neuralNotepad, 49, allOwned); + expect(data.deltaTdPerSecond).toBeCloseTo(10.4); + expect(data.milestoneWillCross).toBe(true); + }); + + it("applies normal delta after milestone (owned=10, 10 → 11)", () => { + // x1.5 milestone active, no crossing. + // Current: 0.2 * 10 * 1.5 = 3.0 + // Future: 0.2 * 11 * 1.5 = 3.3 + // Delta: 0.3 + const allOwned = { "neural-notepad": 10 }; + const data = computeGeneratorTooltipData(neuralNotepad, 10, allOwned); + expect(data.deltaTdPerSecond).toBeCloseTo(0.3); + expect(data.milestoneWillCross).toBe(false); + }); + + it("applies normal delta at max milestone (owned=100, 100 → 101)", () => { + // x6 milestone active, no further milestones. + // Current: 0.2 * 100 * 6 = 120.0 + // Future: 0.2 * 101 * 6 = 121.2 + // Delta: 1.2 + const allOwned = { "neural-notepad": 100 }; + const data = computeGeneratorTooltipData(neuralNotepad, 100, allOwned); + expect(data.deltaTdPerSecond).toBeCloseTo(1.2); + expect(data.milestoneWillCross).toBe(false); + }); + + it("milestoneWillCross is false well before a threshold", () => { + const data = computeGeneratorTooltipData(neuralNotepad, 7, { + "neural-notepad": 7, + }); + expect(data.milestoneWillCross).toBe(false); + }); + }); }); diff --git a/src/components/upgrades/tooltipHelpers.ts b/src/components/upgrades/tooltipHelpers.ts index 5d1f8cb..4034ee9 100644 --- a/src/components/upgrades/tooltipHelpers.ts +++ b/src/components/upgrades/tooltipHelpers.ts @@ -21,6 +21,10 @@ export interface GeneratorTooltipData { nextMilestoneOwned: number | null; nextMilestoneMultiplier: number | null; nextMilestoneLabel: string | null; + /** TD/s gained by purchasing one more unit (accounts for milestone crossing). */ + deltaTdPerSecond: number; + /** True when buying the next unit crosses a milestone threshold. */ + milestoneWillCross: boolean; } /** @@ -50,6 +54,18 @@ export function computeGeneratorTooltipData( const nextThreshold = MILESTONE_THRESHOLDS[milestoneLevel] ?? null; + // Delta: TD/s gained by purchasing one more unit. + // Milestone crossing is handled correctly: if owned+1 hits a threshold, + // ALL existing units benefit from the new multiplier too. + const newOwned = owned + 1; + const newAllOwned = { ...allOwned, [upgrade.id]: newOwned }; + const newMilestoneMultiplier = getMilestoneMultiplier(newOwned); + const newSynergyMultiplier = getSynergyMultiplier(upgrade.id, newAllOwned); + const futureTdForGenerator = + upgrade.baseTdPerSecond * newOwned * newMilestoneMultiplier * newSynergyMultiplier; + const deltaTdPerSecond = futureTdForGenerator - totalTdForGenerator; + const milestoneWillCross = getMilestoneLevel(newOwned) > milestoneLevel; + return { name: upgrade.name, icon: upgrade.icon, @@ -63,5 +79,7 @@ export function computeGeneratorTooltipData( nextMilestoneOwned: nextThreshold?.owned ?? null, nextMilestoneMultiplier: nextThreshold?.multiplier ?? null, nextMilestoneLabel: nextThreshold?.label ?? null, + deltaTdPerSecond, + milestoneWillCross, }; }