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,
};
}