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
2 changes: 2 additions & 0 deletions src/components/GameLayout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import { computeOfflineProgress } from "../engine/offlineEngine";
import { useDailyObjectiveTracking } from "../hooks/useDailyObjectiveTracking";
import { useGameLoop } from "../hooks/useGameLoop";
import { useKonamiCode } from "../hooks/useKonamiCode";
import { useOfflineNotification } from "../hooks/useOfflineNotification";
import { useReducedMotion } from "../hooks/useReducedMotion";
import { useSound } from "../hooks/useSound";
import { useGameStore } from "../store";
Expand All @@ -30,6 +31,7 @@ import { UpgradesSidebar } from "./UpgradesSidebar";
export function GameLayout() {
useGameLoop();
useDailyObjectiveTracking();
useOfflineNotification();

const [offlineResult, setOfflineResult] =
useState<OfflineProgressResult | null>(null);
Expand Down
30 changes: 30 additions & 0 deletions src/components/SettingsPanel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import {
Switch,
Text,
Textarea,
Tooltip,
} from "@mantine/core";
import { notifications } from "@mantine/notifications";
import { useState } from "react";
Expand Down Expand Up @@ -38,6 +39,16 @@ export function SettingsPanel({
const setNumberFormat = useSettingsStore((s) => s.setNumberFormat);
const soundEnabled = useSettingsStore((s) => s.soundEnabled);
const setSoundEnabled = useSettingsStore((s) => s.setSoundEnabled);
const notificationsEnabled = useSettingsStore((s) => s.notificationsEnabled);
const setNotificationsEnabled = useSettingsStore(
(s) => s.setNotificationsEnabled,
);

// The toggle is disabled (and shows a tooltip) when the OS/browser has blocked
// the Notification API. We read the permission eagerly — the drawer is opened on
// demand so we always see the current state.
const notificationsBlocked =
typeof Notification !== "undefined" && Notification.permission === "denied";

const [resetStage, setResetStage] = useState(0);
const [importError, setImportError] = useState<string | null>(null);
Expand Down Expand Up @@ -197,6 +208,25 @@ export function SettingsPanel({
onChange={(e) => setSoundEnabled(e.currentTarget.checked)}
styles={{ label: { fontFamily: "monospace" } }}
/>
<Tooltip
label="Notifications are blocked in your browser settings."
disabled={!notificationsBlocked}
multiline
maw={240}
>
<div>
<Switch
label="Browser Notifications"
description="Get a reminder when GLORP's 8-hour offline cap is reached"
checked={notificationsEnabled && !notificationsBlocked}
onChange={(e) =>
setNotificationsEnabled(e.currentTarget.checked)
}
disabled={notificationsBlocked}
styles={{ label: { fontFamily: "monospace" } }}
/>
</div>
</Tooltip>

<Divider />

Expand Down
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>
);
}
Loading
Loading