bitburner-src/src/Sidebar/ui/SidebarRoot.tsx

407 lines
15 KiB
TypeScript
Raw Normal View History

import React, { useMemo, useCallback, useState, useEffect } from "react";
import { KEYCODE } from "../../utils/helpers/keyCodes";
2021-09-18 01:43:08 +02:00
import { styled, Theme, CSSObject } from "@mui/material/styles";
2021-09-17 01:23:03 +02:00
import createStyles from "@mui/styles/createStyles";
import makeStyles from "@mui/styles/makeStyles";
2021-09-17 01:42:55 +02:00
import MuiDrawer from "@mui/material/Drawer";
2021-09-17 01:23:03 +02:00
import List from "@mui/material/List";
import Divider from "@mui/material/Divider";
import Tooltip from "@mui/material/Tooltip";
2021-09-17 01:23:03 +02:00
import ChevronLeftIcon from "@mui/icons-material/ChevronLeft";
import ChevronRightIcon from "@mui/icons-material/ChevronRight";
import ListItem from "@mui/material/ListItem";
import ListItemIcon from "@mui/material/ListItemIcon";
import ListItemText from "@mui/material/ListItemText";
import Typography from "@mui/material/Typography";
2021-09-14 21:23:16 +02:00
2021-09-17 01:23:03 +02:00
import ComputerIcon from "@mui/icons-material/Computer";
import LastPageIcon from "@mui/icons-material/LastPage"; // Terminal
import CreateIcon from "@mui/icons-material/Create"; // Create Script
import StorageIcon from "@mui/icons-material/Storage"; // Active Scripts
import BugReportIcon from "@mui/icons-material/BugReport"; // Create Program
import EqualizerIcon from "@mui/icons-material/Equalizer"; // Stats
import ContactsIcon from "@mui/icons-material/Contacts"; // Factions
import DoubleArrowIcon from "@mui/icons-material/DoubleArrow"; // Augmentations
import AccountTreeIcon from "@mui/icons-material/AccountTree"; // Hacknet
import PeopleAltIcon from "@mui/icons-material/PeopleAlt"; // Sleeves
import LocationCityIcon from "@mui/icons-material/LocationCity"; // City
import AirplanemodeActiveIcon from "@mui/icons-material/AirplanemodeActive"; // Travel
import WorkIcon from "@mui/icons-material/Work"; // Job
import TrendingUpIcon from "@mui/icons-material/TrendingUp"; // Stock Market
import FormatBoldIcon from "@mui/icons-material/FormatBold"; // Bladeburner
import BusinessIcon from "@mui/icons-material/Business"; // Corp
import SportsMmaIcon from "@mui/icons-material/SportsMma"; // Gang
import CheckIcon from "@mui/icons-material/Check"; // Milestones
import HelpIcon from "@mui/icons-material/Help"; // Tutorial
import SettingsIcon from "@mui/icons-material/Settings"; // options
import DeveloperBoardIcon from "@mui/icons-material/DeveloperBoard"; // Dev
import EmojiEventsIcon from "@mui/icons-material/EmojiEvents"; // Achievements
2021-09-17 01:23:03 +02:00
import AccountBoxIcon from "@mui/icons-material/AccountBox";
import PublicIcon from "@mui/icons-material/Public";
import LiveHelpIcon from "@mui/icons-material/LiveHelp";
2021-09-14 21:23:16 +02:00
2022-09-06 15:07:12 +02:00
import { Router } from "../../ui/GameRoot";
2022-12-04 09:14:06 +01:00
import { Page, SimplePage } from "../../ui/Router";
import { SidebarAccordion } from "./SidebarAccordion";
import { Player } from "@player";
2021-09-14 21:23:16 +02:00
import { CONSTANTS } from "../../Constants";
2021-09-11 07:54:19 +02:00
import { iTutorialSteps, iTutorialNextStep, ITutorial } from "../../InteractiveTutorial";
import { getAvailableCreatePrograms } from "../../Programs/ProgramHelpers";
2021-09-13 00:03:07 +02:00
import { Settings } from "../../Settings/Settings";
2021-09-25 23:21:50 +02:00
import { AugmentationNames } from "../../Augmentation/data/AugmentationNames";
2021-09-13 00:03:07 +02:00
2021-10-23 21:22:58 +02:00
import { ProgramsSeen } from "../../Programs/ui/ProgramsRoot";
import { InvitationsSeen } from "../../Faction/ui/FactionsRoot";
2021-12-17 02:07:34 +01:00
import { hash } from "../../hash/hash";
2022-07-19 20:21:12 +02:00
import { Locations } from "../../Locations/Locations";
2021-09-11 07:54:19 +02:00
// All icon instances need to be constant, so they have stable object identity.
// Otherwise, the memoization of all the higher-level components doesn't work.
const computerIcon = <ComputerIcon />;
const lastPageIcon = <LastPageIcon />;
const createIcon = <CreateIcon />;
const storageIcon = <StorageIcon />;
const bugReportIcon = <BugReportIcon />;
const equalizerIcon = <EqualizerIcon />;
const contactsIcon = <ContactsIcon />;
const doubleArrowIcon = <DoubleArrowIcon style={{ transform: "rotate(-90deg)" }} />;
const accountTreeIcon = <AccountTreeIcon />;
const peopleAltIcon = <PeopleAltIcon />;
const locationCityIcon = <LocationCityIcon />;
const airplanemodeActiveIcon = <AirplanemodeActiveIcon />;
const workIcon = <WorkIcon />;
const trendingUpIcon = <TrendingUpIcon />;
const formatBoldIcon = <FormatBoldIcon />;
const businessIcon = <BusinessIcon />;
const sportsMmaIcon = <SportsMmaIcon />;
const checkIcon = <CheckIcon />;
const helpIcon = <HelpIcon />;
const settingsIcon = <SettingsIcon />;
const developerBoardIcon = <DeveloperBoardIcon />;
const emojiEventsIcon = <EmojiEventsIcon />;
const accountBoxIcon = <AccountBoxIcon />;
const publicIcon = <PublicIcon />;
const liveHelpIcon = <LiveHelpIcon />;
const chevronLeftIcon = <ChevronLeftIcon color="primary" />;
const chevronRightIcon = <ChevronRightIcon color="primary" />;
// Use constant Dividers just for performance
const divider = <Divider />;
2021-09-17 01:42:55 +02:00
const openedMixin = (theme: Theme): CSSObject => ({
2021-09-17 02:51:25 +02:00
width: theme.spacing(31),
2021-09-17 01:42:55 +02:00
transition: theme.transitions.create("width", {
easing: theme.transitions.easing.sharp,
duration: theme.transitions.duration.enteringScreen,
}),
overflowX: "hidden",
});
const closedMixin = (theme: Theme): CSSObject => ({
transition: theme.transitions.create("width", {
easing: theme.transitions.easing.sharp,
duration: theme.transitions.duration.leavingScreen,
}),
overflowX: "hidden",
2021-09-17 02:51:25 +02:00
width: `calc(${theme.spacing(2)} + 1px)`,
2021-09-17 01:42:55 +02:00
[theme.breakpoints.up("sm")]: {
2021-09-17 02:51:25 +02:00
width: `calc(${theme.spacing(7)} + 1px)`,
2021-09-17 01:42:55 +02:00
},
});
const Drawer = styled(MuiDrawer, { shouldForwardProp: (prop) => prop !== "open" })(({ theme, open }) => ({
2021-09-17 02:51:25 +02:00
width: theme.spacing(31),
2021-09-17 01:42:55 +02:00
whiteSpace: "nowrap",
boxSizing: "border-box",
...(open && {
...openedMixin(theme),
"& .MuiDrawer-paper": openedMixin(theme),
}),
...(!open && {
...closedMixin(theme),
"& .MuiDrawer-paper": closedMixin(theme),
}),
}));
2021-09-14 21:23:16 +02:00
const useStyles = makeStyles((theme: Theme) =>
createStyles({
active: {
2021-09-18 01:43:08 +02:00
borderLeft: "3px solid " + theme.palette.primary.main,
2021-09-14 21:23:16 +02:00
},
listitem: {},
2021-09-14 21:23:16 +02:00
}),
);
2021-09-11 07:54:19 +02:00
interface IProps {
2021-09-17 08:04:44 +02:00
page: Page;
2021-09-11 07:54:19 +02:00
}
export function SidebarRoot(props: IProps): React.ReactElement {
const setRerender = useState(false)[1];
function rerender(): void {
setRerender((old) => !old);
}
useEffect(() => {
2021-09-14 21:23:16 +02:00
const id = setInterval(rerender, 200);
2021-09-11 07:54:19 +02:00
return () => clearInterval(id);
}, []);
2022-12-04 09:14:06 +01:00
let flash: Page | null = null;
switch (ITutorial.currStep) {
case iTutorialSteps.CharacterGoToTerminalPage:
case iTutorialSteps.ActiveScriptsPage:
flash = Page.Terminal;
break;
case iTutorialSteps.GoToCharacterPage:
flash = Page.Stats;
break;
case iTutorialSteps.TerminalGoToActiveScriptsPage:
flash = Page.ActiveScripts;
break;
case iTutorialSteps.GoToHacknetNodesPage:
flash = Page.Hacknet;
break;
case iTutorialSteps.HacknetNodesGoToWorldPage:
flash = Page.City;
break;
case iTutorialSteps.WorldDescription:
flash = Page.Tutorial;
break;
}
2021-09-11 07:54:19 +02:00
2022-09-06 15:07:12 +02:00
const augmentationCount = Player.queuedAugmentations.length;
const invitationsCount = Player.factionInvitations.filter((f) => !InvitationsSeen.includes(f)).length;
const programCount = getAvailableCreatePrograms().length - ProgramsSeen.length;
2021-09-11 07:54:19 +02:00
const canOpenFactions =
2022-09-06 15:07:12 +02:00
Player.factionInvitations.length > 0 ||
Player.factions.length > 0 ||
Player.augmentations.length > 0 ||
Player.queuedAugmentations.length > 0 ||
Player.sourceFiles.length > 0;
2021-09-11 07:54:19 +02:00
const canOpenAugmentations =
2022-09-13 18:37:24 +02:00
Player.augmentations.length > 0 || Player.queuedAugmentations.length > 0 || Player.sourceFiles.length > 0;
2021-09-11 07:54:19 +02:00
2022-09-06 15:07:12 +02:00
const canOpenSleeves = Player.sleeves.length > 0;
2021-09-11 07:54:19 +02:00
2022-09-06 15:07:12 +02:00
const canCorporation = !!Player.corporation;
const canGang = !!Player.gang;
const canJob = Object.values(Player.jobs).length > 0;
const canStockMarket = Player.hasWseAccount;
const canBladeburner = !!Player.bladeburner;
const canStaneksGift = Player.augmentations.some((aug) => aug.name === AugmentationNames.StaneksGift1);
2021-09-11 07:54:19 +02:00
2021-09-13 00:03:07 +02:00
useEffect(() => {
// Shortcuts to navigate through the game
// Alt-t - Terminal
// Alt-c - Character
// Alt-e - Script editor
// Alt-s - Active scripts
// Alt-h - Hacknet Nodes
// Alt-w - City
// Alt-j - Job
// Alt-r - Travel Agency of current city
// Alt-p - Create program
// Alt-f - Factions
// Alt-a - Augmentations
// Alt-u - Tutorial
// Alt-o - Options
// Alt-b - Bladeburner
// Alt-g - Gang
2022-07-15 01:02:59 +02:00
function handleShortcuts(this: Document, event: KeyboardEvent): void {
2021-09-13 00:03:07 +02:00
if (Settings.DisableHotkeys) return;
2022-09-06 15:07:12 +02:00
if ((Player.currentWork && Player.focus) || Router.page() === Page.BitVerse) return;
if (event.code === KEYCODE.T && event.altKey) {
2021-09-13 00:03:07 +02:00
event.preventDefault();
2022-12-04 09:14:06 +01:00
clickPage(Page.Terminal);
} else if (event.code === KEYCODE.C && event.altKey) {
2021-09-13 00:03:07 +02:00
event.preventDefault();
2022-12-04 09:14:06 +01:00
clickPage(Page.Stats);
} else if (event.code === KEYCODE.E && event.altKey) {
2021-09-13 00:03:07 +02:00
event.preventDefault();
2022-12-04 09:14:06 +01:00
clickPage(Page.ScriptEditor);
} else if (event.code === KEYCODE.S && event.altKey) {
2021-09-13 00:03:07 +02:00
event.preventDefault();
2022-12-04 09:14:06 +01:00
clickPage(Page.ActiveScripts);
} else if (event.code === KEYCODE.H && event.altKey) {
2021-09-13 00:03:07 +02:00
event.preventDefault();
2022-12-04 09:14:06 +01:00
clickPage(Page.Hacknet);
} else if (event.code === KEYCODE.W && event.altKey) {
2021-09-13 00:03:07 +02:00
event.preventDefault();
2022-12-04 09:14:06 +01:00
clickPage(Page.City);
} else if (event.code === KEYCODE.J && event.altKey && !event.ctrlKey && !event.metaKey && canJob) {
// ctrl/cmd + alt + j is shortcut to open Chrome dev tools
2021-09-13 00:03:07 +02:00
event.preventDefault();
2022-12-04 09:14:06 +01:00
clickPage(Page.Job);
} else if (event.code === KEYCODE.R && event.altKey) {
2021-09-13 00:03:07 +02:00
event.preventDefault();
2022-12-04 09:14:06 +01:00
clickPage(Page.Travel);
} else if (event.code === KEYCODE.P && event.altKey) {
2021-09-13 00:03:07 +02:00
event.preventDefault();
2022-12-04 09:14:06 +01:00
clickPage(Page.CreateProgram);
} else if (event.code === KEYCODE.F && event.altKey) {
2021-09-22 07:36:17 +02:00
if (props.page == Page.Terminal && Settings.EnableBashHotkeys) {
2021-09-13 00:03:07 +02:00
return;
}
event.preventDefault();
2022-12-04 09:14:06 +01:00
clickPage(Page.Factions);
} else if (event.code === KEYCODE.A && event.altKey) {
2021-09-13 00:03:07 +02:00
event.preventDefault();
2022-12-04 09:14:06 +01:00
clickPage(Page.Augmentations);
} else if (event.code === KEYCODE.U && event.altKey) {
2021-09-13 00:03:07 +02:00
event.preventDefault();
2022-12-04 09:14:06 +01:00
clickPage(Page.Tutorial);
} else if (event.code === KEYCODE.O && event.altKey) {
event.preventDefault();
2022-12-04 09:14:06 +01:00
clickPage(Page.Options);
} else if (event.code === KEYCODE.B && event.altKey && Player.bladeburner) {
2021-09-13 00:03:07 +02:00
event.preventDefault();
2022-12-04 09:14:06 +01:00
clickPage(Page.Bladeburner);
} else if (event.code === KEYCODE.G && event.altKey && Player.gang) {
2021-09-13 00:03:07 +02:00
event.preventDefault();
2022-12-04 09:14:06 +01:00
clickPage(Page.Gang);
2021-09-13 00:03:07 +02:00
}
}
2021-12-12 20:02:45 +01:00
document.addEventListener("keydown", handleShortcuts);
return () => document.removeEventListener("keydown", handleShortcuts);
2021-09-13 00:03:07 +02:00
}, []);
2022-12-04 09:14:06 +01:00
const clickPage = useCallback(
(page: Page) => {
if (page === Page.Job) {
Router.toJob(Locations[Object.keys(Player.jobs)[0]]);
} else if (page == Page.ScriptEditor) {
Router.toScriptEditor();
} else if ((Object.values(SimplePage) as Page[]).includes(page)) {
Router.toPage(page as SimplePage);
} else {
throw new Error("Can't handle click on Page " + page);
}
if (flash === page) {
iTutorialNextStep();
}
},
[flash],
);
2022-12-04 09:14:06 +01:00
const classes = useStyles();
const [open, setOpen] = useState(Settings.IsSidebarOpened);
const toggleDrawer = (): void =>
setOpen((old) => {
Settings.IsSidebarOpened = !old;
return !old;
});
const li_classes = useMemo(() => ({ root: classes.listitem }), [classes.listitem]);
// Explicitily useMemo() to save rerendering deep chunks of this tree.
// memo() can't be (easily) used on components like <List>, because the
// props.children array will be a different object every time.
2021-09-11 07:54:19 +02:00
return (
2021-09-17 08:04:44 +02:00
<Drawer open={open} anchor="left" variant="permanent">
{useMemo(
() => (
<ListItem classes={li_classes} button onClick={toggleDrawer}>
<ListItemIcon>{!open ? chevronRightIcon : chevronLeftIcon}</ListItemIcon>
<ListItemText
primary={
<Tooltip title={hash()}>
<Typography>Bitburner v{CONSTANTS.VersionString}</Typography>
</Tooltip>
}
/>
2021-09-14 21:23:16 +02:00
</ListItem>
),
[li_classes, open],
)}
{divider}
<List>
<SidebarAccordion
key_="Hacking"
page={props.page}
clickPage={clickPage}
flash={flash}
icon={computerIcon}
sidebarOpen={open}
classes={classes}
items={[
{ key_: Page.Terminal, icon: lastPageIcon },
{ key_: Page.ScriptEditor, icon: createIcon },
{ key_: Page.ActiveScripts, icon: storageIcon },
{ key_: Page.CreateProgram, icon: bugReportIcon, count: programCount },
canStaneksGift && { key_: Page.StaneksGift, icon: developerBoardIcon },
]}
/>
{divider}
<SidebarAccordion
key_="Character"
page={props.page}
clickPage={clickPage}
flash={flash}
icon={accountBoxIcon}
sidebarOpen={open}
classes={classes}
items={[
{ key_: Page.Stats, icon: equalizerIcon },
canOpenFactions && {
key_: Page.Factions,
icon: contactsIcon,
active: [Page.Factions as Page, Page.Faction].includes(props.page),
count: invitationsCount,
},
canOpenAugmentations && {
key_: Page.Augmentations,
icon: doubleArrowIcon,
count: augmentationCount,
},
{ key_: Page.Hacknet, icon: accountTreeIcon },
canOpenSleeves && { key_: Page.Sleeves, icon: peopleAltIcon },
]}
/>
{divider}
<SidebarAccordion
key_="World"
page={props.page}
clickPage={clickPage}
flash={flash}
icon={publicIcon}
sidebarOpen={open}
classes={classes}
items={[
{
key_: Page.City,
icon: locationCityIcon,
active: [Page.City as Page, Page.Grafting, Page.Location].includes(props.page),
},
{ key_: Page.Travel, icon: airplanemodeActiveIcon },
canJob && { key_: Page.Job, icon: workIcon },
canStockMarket && { key_: Page.StockMarket, icon: trendingUpIcon },
canBladeburner && { key_: Page.Bladeburner, icon: formatBoldIcon },
canCorporation && { key_: Page.Corporation, icon: businessIcon },
canGang && { key_: Page.Gang, icon: sportsMmaIcon },
]}
/>
{divider}
<SidebarAccordion
key_="Help"
page={props.page}
clickPage={clickPage}
flash={flash}
icon={liveHelpIcon}
sidebarOpen={open}
classes={classes}
items={[
{ key_: Page.Milestones, icon: checkIcon },
{ key_: Page.Tutorial, icon: helpIcon },
{ key_: Page.Achievements, icon: emojiEventsIcon },
{ key_: Page.Options, icon: settingsIcon },
process.env.NODE_ENV === "development" && { key_: Page.DevMenu, icon: developerBoardIcon },
]}
/>
2021-09-17 08:04:44 +02:00
</List>
</Drawer>
2021-09-11 07:54:19 +02:00
);
}