Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
138 changes: 92 additions & 46 deletions src/components/upgrades/BoosterCard.tsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,20 @@
import { Badge, Button, Card, Group, Text } from "@mantine/core";
import {
ActionIcon,
Badge,
Button,
Card,
Group,
Popover,
Text,
} from "@mantine/core";
import { notifications } from "@mantine/notifications";
import type { DecimalSource } from "break_infinity.js";
import { useCallback, useRef, useState } from "react";
import type { Booster } from "../../data/boosters";
import { useReducedMotion } from "../../hooks/useReducedMotion";
import { D } from "../../utils/decimal";
import { formatNumber } from "../../utils/formatNumber";
import { BoosterTooltipContent } from "./BoosterTooltipContent";

interface BoosterCardProps {
booster: Booster;
Expand All @@ -25,6 +34,8 @@ export function BoosterCard({
const canAfford = !purchased && D(trainingData).gte(booster.cost);
const locked = evolutionStage < booster.unlockStage;
const [isGlowing, setIsGlowing] = useState(false);
const [tooltipOpen, setTooltipOpen] = useState(false);
const isHoverDevice = useRef(false);
const glowTimerRef = useRef<ReturnType<typeof setTimeout>>(undefined);
const prefersReduced = useReducedMotion();

Expand All @@ -46,53 +57,88 @@ export function BoosterCard({
if (locked) return null;

return (
<Card
className={isGlowing ? "glow-pulse" : undefined}
padding="sm"
radius="sm"
withBorder
style={{
borderColor: purchased
? "var(--mantine-color-violet-8)"
: canAfford
? "var(--mantine-color-green-8)"
: "var(--mantine-color-dark-4)",
opacity: purchased ? 0.7 : canAfford ? 1 : 0.5,
animation: isGlowing ? "glow-pulse 0.6s ease-in-out" : undefined,
}}
<Popover
opened={tooltipOpen}
onChange={setTooltipOpen}
position="right"
withArrow
shadow="md"
withinPortal
>
<Group justify="space-between" mb={4}>
<Text size="sm" fw={700} ff="monospace">
{booster.icon} {booster.name}
</Text>
{purchased && (
<Badge size="sm" variant="light" color="violet">
ACTIVE
</Badge>
)}
</Group>
<Popover.Target>
<Card
onMouseEnter={() => {
isHoverDevice.current = true;
setTooltipOpen(true);
}}
onMouseLeave={() => setTooltipOpen(false)}
className={isGlowing ? "glow-pulse" : undefined}
padding="sm"
radius="sm"
withBorder
style={{
borderColor: purchased
? "var(--mantine-color-violet-8)"
: canAfford
? "var(--mantine-color-green-8)"
: "var(--mantine-color-dark-4)",
opacity: purchased ? 0.7 : canAfford ? 1 : 0.5,
animation: isGlowing ? "glow-pulse 0.6s ease-in-out" : undefined,
}}
>
<Group justify="space-between" mb={4} wrap="nowrap">
<Text size="sm" fw={700} ff="monospace">
{booster.icon} {booster.name}
</Text>
<Group gap={4} wrap="nowrap">
{purchased && (
<Badge size="sm" variant="light" color="violet">
ACTIVE
</Badge>
)}
<ActionIcon
size="xs"
variant="subtle"
color="gray"
aria-label={`Show details for ${booster.name}`}
onClick={(e) => {
e.stopPropagation();
if (!isHoverDevice.current) {
setTooltipOpen((o) => !o);
}
}}
>
</ActionIcon>
</Group>
</Group>

<Text size="xs" c="dimmed" ff="monospace" mb="xs">
{booster.description}
</Text>
<Text size="xs" c="dimmed" ff="monospace" mb="xs">
{booster.description}
</Text>

<Group justify="space-between" align="center">
<Text size="xs" ff="monospace" c="violet">
×{booster.multiplier} all auto-gen
</Text>
{!purchased && (
<Button
size="compact-xs"
variant={canAfford ? "filled" : "default"}
color="violet"
disabled={!canAfford}
onClick={handlePurchase}
ff="monospace"
>
{formatNumber(booster.cost)} TD
</Button>
)}
</Group>
</Card>
<Group justify="space-between" align="center">
<Text size="xs" ff="monospace" c="violet">
×{booster.multiplier} all auto-gen
</Text>
{!purchased && (
<Button
size="compact-xs"
variant={canAfford ? "filled" : "default"}
color="violet"
disabled={!canAfford}
onClick={handlePurchase}
ff="monospace"
>
{formatNumber(booster.cost)} TD
</Button>
)}
</Group>
</Card>
</Popover.Target>
<Popover.Dropdown>
<BoosterTooltipContent booster={booster} purchased={purchased} />
</Popover.Dropdown>
</Popover>
);
}
100 changes: 100 additions & 0 deletions src/components/upgrades/BoosterTooltipContent.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
import { Divider, Group, Stack, Text } from "@mantine/core";
import type { Booster } from "../../data/boosters";
import { BOOSTERS } from "../../data/boosters";
import { getIdleBoostMultiplier } from "../../data/prestigeShop";
import { getSpeciesBonus } from "../../data/species";
import { UPGRADES } from "../../data/upgrades";
import {
computeBoosterMultiplier,
getTotalTdPerSecond,
} from "../../engine/upgradeEngine";
import { useGameStore } from "../../store";
import { formatNumber } from "../../utils/formatNumber";
import { computeGlobalMultiplierTooltipData } from "./tooltipHelpers";

interface BoosterTooltipContentProps {
booster: Booster;
purchased: boolean;
}

export function BoosterTooltipContent({
booster,
purchased,
}: BoosterTooltipContentProps) {
const upgradeOwned = useGameStore((s) => s.upgradeOwned);
const boostersPurchased = useGameStore((s) => s.boostersPurchased);
const prestigeUpgrades = useGameStore((s) => s.prestigeUpgrades);
const currentSpecies = useGameStore((s) => s.currentSpecies);
const activeChallengeId = useGameStore((s) => s.activeChallengeId);

// Mirror the no-prestige challenge logic from the game engine
const effectivePrestige =
activeChallengeId === "no-prestige" ? {} : prestigeUpgrades;
const idleBoost = getIdleBoostMultiplier(
(effectivePrestige as Record<string, number>)["idle-boost"] ?? 0,
);
const speciesBonus = getSpeciesBonus(currentSpecies);
const boosterMult = computeBoosterMultiplier(BOOSTERS, boostersPurchased);
const currentTdPerSecond = getTotalTdPerSecond(
UPGRADES,
upgradeOwned,
idleBoost * speciesBonus.autoGen,
boosterMult,
);

const { currentTdPerSecond: current, newTdPerSecond } =
computeGlobalMultiplierTooltipData(booster, currentTdPerSecond);

return (
<Stack gap="xs" w={220}>
<Text size="sm" fw={700} ff="monospace">
{booster.icon} {booster.name}
</Text>
<Divider />

<Text size="xs" c="dimmed" ff="monospace">
{booster.description}
</Text>

<Group justify="space-between">
<Text size="xs" c="dimmed" ff="monospace">
Multiplier
</Text>
<Text size="xs" c="violet" fw={700} ff="monospace">
×{booster.multiplier} all TD/s
</Text>
</Group>

{!purchased && (
<>
<Divider />
<Group justify="space-between">
<Text size="xs" c="dimmed" ff="monospace">
Current TD/s
</Text>
<Text size="xs" ff="monospace">
{formatNumber(current)}
</Text>
</Group>

<Group justify="space-between">
<Text size="xs" c="dimmed" ff="monospace">
After purchase
</Text>
<Text
size="xs"
c="violet"
fw={700}
ff="monospace"
style={{
textShadow: "0 0 6px var(--mantine-color-violet-5)",
}}
>
{formatNumber(newTdPerSecond)} TD/s
</Text>
</Group>
</>
)}
</Stack>
);
}
85 changes: 76 additions & 9 deletions src/components/upgrades/ClickUpgradeTooltipContent.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,20 @@
import { Badge, Divider, Group, Stack, Text } from "@mantine/core";
import { BOOSTERS } from "../../data/boosters";
import type { ClickUpgrade } from "../../data/clickUpgrades";
import { CLICK_UPGRADES } from "../../data/clickUpgrades";
import {
getClickMasteryBonus,
getIdleBoostMultiplier,
} from "../../data/prestigeShop";
import { getSpeciesBonus } from "../../data/species";
import { UPGRADES } from "../../data/upgrades";
import {
computeBoosterMultiplier,
getTotalTdPerSecond,
} from "../../engine/upgradeEngine";
import { useGameStore } from "../../store";
import { formatNumber } from "../../utils/formatNumber";
import { computeClickBonusTooltipData } from "./tooltipHelpers";

interface ClickUpgradeTooltipContentProps {
upgrade: ClickUpgrade;
Expand All @@ -11,8 +25,42 @@ export function ClickUpgradeTooltipContent({
upgrade,
purchased,
}: ClickUpgradeTooltipContentProps) {
const clickUpgradesPurchased = useGameStore((s) => s.clickUpgradesPurchased);
const upgradeOwned = useGameStore((s) => s.upgradeOwned);
const boostersPurchased = useGameStore((s) => s.boostersPurchased);
const prestigeUpgrades = useGameStore((s) => s.prestigeUpgrades);
const currentSpecies = useGameStore((s) => s.currentSpecies);
const activeChallengeId = useGameStore((s) => s.activeChallengeId);

// Mirror the no-prestige challenge logic from the game engine
const effectivePrestige =
activeChallengeId === "no-prestige" ? {} : prestigeUpgrades;
const idleBoost = getIdleBoostMultiplier(
(effectivePrestige as Record<string, number>)["idle-boost"] ?? 0,
);
const speciesBonus = getSpeciesBonus(currentSpecies);
const boosterMult = computeBoosterMultiplier(BOOSTERS, boostersPurchased);
const tdPerSecond = getTotalTdPerSecond(
UPGRADES,
upgradeOwned,
idleBoost * speciesBonus.autoGen,
boosterMult,
);
const clickMasteryBonus = getClickMasteryBonus(
(effectivePrestige as Record<string, number>)["click-mastery"] ?? 0,
);

const { deltaClickPower } = computeClickBonusTooltipData(
upgrade,
clickUpgradesPurchased,
CLICK_UPGRADES,
tdPerSecond,
clickMasteryBonus,
speciesBonus.clickPower,
);

return (
<Stack gap="xs" w={210}>
<Stack gap="xs" w={220}>
<Group justify="space-between" wrap="nowrap">
<Text size="sm" fw={700} ff="monospace">
{upgrade.icon} {upgrade.name}
Expand All @@ -39,14 +87,33 @@ export function ClickUpgradeTooltipContent({
</Group>

{!purchased && (
<Group justify="space-between">
<Text size="xs" c="dimmed" ff="monospace">
Cost
</Text>
<Text size="xs" ff="monospace">
{formatNumber(upgrade.cost)} TD
</Text>
</Group>
<>
<Group justify="space-between">
<Text size="xs" c="dimmed" ff="monospace">
Click bonus
</Text>
<Text
size="xs"
c="yellow"
fw={700}
ff="monospace"
style={{
textShadow: "0 0 6px var(--mantine-color-yellow-5)",
}}
>
+{formatNumber(deltaClickPower)} TD/click
</Text>
</Group>

<Group justify="space-between">
<Text size="xs" c="dimmed" ff="monospace">
Cost
</Text>
<Text size="xs" ff="monospace">
{formatNumber(upgrade.cost)} TD
</Text>
</Group>
</>
)}
</Stack>
);
Expand Down
Loading
Loading