mirror of
https://github.com/bitburner-official/bitburner-src.git
synced 2026-05-11 01:57:49 +02:00
422 lines
14 KiB
TypeScript
422 lines
14 KiB
TypeScript
import type { ActionIdentifier } from "../../Bladeburner/Types";
|
|
|
|
// Root React Component for the Corporation UI
|
|
import React, { useMemo, useState, useEffect, ReactNode } from "react";
|
|
import { Box, Button, IconButton, Table, TableBody, TableCell, TableRow, Tooltip, Typography } from "@mui/material";
|
|
import SaveIcon from "@mui/icons-material/Save";
|
|
import ClearAllIcon from "@mui/icons-material/ClearAll";
|
|
import { Theme, useTheme } from "@mui/material/styles";
|
|
import { makeStyles } from "tss-react/mui";
|
|
|
|
import { Player } from "@player";
|
|
import { formatHp, formatMoney, formatSkill } from "../formatNumber";
|
|
import { Reputation } from "./Reputation";
|
|
import { KillScriptsModal } from "./KillScriptsModal";
|
|
import { convertTimeMsToTimeElapsedString } from "../../utils/StringHelperFunctions";
|
|
|
|
import { Settings } from "../../Settings/Settings";
|
|
import { Router } from "../GameRoot";
|
|
import { Page } from "../Router";
|
|
import { StatsProgressBar } from "./StatsProgressBar";
|
|
|
|
import { isClassWork } from "../../Work/ClassWork";
|
|
import { CONSTANTS } from "../../Constants";
|
|
import { isCreateProgramWork } from "../../Work/CreateProgramWork";
|
|
import { isGraftingWork } from "../../Work/GraftingWork";
|
|
import { isFactionWork } from "../../Work/FactionWork";
|
|
import { ReputationRate } from "./ReputationRate";
|
|
import { isCompanyWork } from "../../Work/CompanyWork";
|
|
import { isCrimeWork } from "../../Work/CrimeWork";
|
|
import { EventEmitter } from "../../utils/EventEmitter";
|
|
import { useRerender } from "./hooks";
|
|
import { RemoteFileApiConnectionStatus } from "../../GameOptions/ui/RemoteFileApiConnectionStatus";
|
|
|
|
export const OverviewEventEmitter = new EventEmitter();
|
|
|
|
// These values aren't displayed, they're just used for comparison to check if state has changed
|
|
const valUpdaters = {
|
|
HP: () => Player.hp.current + "|" + Player.hp.max, // This isn't displayed, it's just compared for updates.
|
|
Money: () => Player.money,
|
|
Hack: () => Player.skills.hacking,
|
|
Str: () => Player.skills.strength,
|
|
Def: () => Player.skills.defense,
|
|
Dex: () => Player.skills.dexterity,
|
|
Agi: () => Player.skills.agility,
|
|
Cha: () => Player.skills.charisma,
|
|
Int: () => Player.skills.intelligence,
|
|
} as const;
|
|
|
|
//These formattedVals functions don't take in a value because of the weirdness around HP.
|
|
const formattedVals = {
|
|
HP: () => `${formatHp(Player.hp.current)} / ${formatHp(Player.hp.max)}`,
|
|
Money: () => formatMoney(Player.money),
|
|
Hack: () => formatSkill(Player.skills.hacking),
|
|
Str: () => formatSkill(Player.skills.strength),
|
|
Def: () => formatSkill(Player.skills.defense),
|
|
Dex: () => formatSkill(Player.skills.dexterity),
|
|
Agi: () => formatSkill(Player.skills.agility),
|
|
Cha: () => formatSkill(Player.skills.charisma),
|
|
Int: () => formatSkill(Player.skills.intelligence),
|
|
} as const;
|
|
|
|
interface ValProps {
|
|
name: keyof typeof valUpdaters;
|
|
color?: string;
|
|
}
|
|
export function Val({ name, color }: ValProps): React.ReactElement {
|
|
//val isn't actually used here, the update of val just forces a refresh of the formattedVal that gets shown
|
|
const [__, setVal] = useState(valUpdaters[name]());
|
|
useEffect(() => {
|
|
const clearSubscription = OverviewEventEmitter.subscribe(() => setVal(valUpdaters[name]()));
|
|
return clearSubscription;
|
|
}, [name]);
|
|
|
|
if (name === "Int" && Player.bitNodeOptions.intelligenceOverride !== undefined) {
|
|
return (
|
|
<Tooltip
|
|
title={`Persistent Intelligence: ${formatSkill(
|
|
Player.calculateSkill(Player.persistentIntelligenceData.exp, 1),
|
|
)}`}
|
|
>
|
|
<Typography color={color}>
|
|
{formatSkill(Player.skills.intelligence)}
|
|
<sup>*</sup>
|
|
</Typography>
|
|
</Tooltip>
|
|
);
|
|
}
|
|
return <Typography color={color}>{formattedVals[name]()}</Typography>;
|
|
}
|
|
|
|
interface DataRowProps {
|
|
name: keyof typeof formattedVals; //name for UI display
|
|
showBar: boolean;
|
|
color?: string;
|
|
cellType: "cellNone" | "cell";
|
|
}
|
|
export function DataRow({ name, showBar, color, cellType }: DataRowProps): React.ReactElement {
|
|
const { classes } = useStyles();
|
|
const skillBar = showBar && <StatsProgressBar name={name} color={color} />;
|
|
return (
|
|
<>
|
|
<TableRow>
|
|
<TableCell component="th" scope="row" classes={{ root: classes[cellType] }}>
|
|
<Typography color={color}>{name} </Typography>
|
|
</TableCell>
|
|
<TableCell align="right" classes={{ root: classes[cellType] }}>
|
|
<Val name={name} color={color} />
|
|
</TableCell>
|
|
<TableCell align="right" classes={{ root: classes[cellType] }}>
|
|
<Typography id={"overview-" + name.toLowerCase() + "-hook"} color={color}>
|
|
{}
|
|
</Typography>
|
|
</TableCell>
|
|
</TableRow>
|
|
{skillBar}
|
|
</>
|
|
);
|
|
}
|
|
|
|
interface OverviewProps {
|
|
parentOpen: boolean;
|
|
save: () => void;
|
|
killScripts: () => void;
|
|
}
|
|
|
|
export function CharacterOverview({ parentOpen, save, killScripts }: OverviewProps): React.ReactElement {
|
|
const [killOpen, setKillOpen] = useState(false);
|
|
const [hasIntelligence, setHasIntelligence] = useState(Player.skills.intelligence > 0);
|
|
const [showBars, setShowBars] = useState(!Settings.DisableOverviewProgressBars);
|
|
useEffect(() => {
|
|
if (!parentOpen) return; // No rerendering if overview is hidden, for performance
|
|
const interval = setInterval(() => {
|
|
setHasIntelligence(Player.skills.intelligence > 0);
|
|
setShowBars(!Settings.DisableOverviewProgressBars);
|
|
OverviewEventEmitter.emit(); // Tell every other updating component to update as well
|
|
}, 600);
|
|
return () => clearInterval(interval);
|
|
}, [parentOpen]);
|
|
const { classes } = useStyles();
|
|
const theme = useTheme();
|
|
return (
|
|
<>
|
|
<Table sx={{ display: "block", p: 1 }}>
|
|
<TableBody>
|
|
<DataRow name="HP" showBar={false} color={theme.colors.hp} cellType={"cellNone"} />
|
|
<DataRow name="Money" showBar={false} color={theme.colors.money} cellType={"cell"} />
|
|
<DataRow name="Hack" showBar={showBars} color={theme.colors.hack} cellType={"cell"} />
|
|
<DataRow name="Str" showBar={showBars} color={theme.colors.combat} cellType={"cellNone"} />
|
|
<DataRow name="Def" showBar={showBars} color={theme.colors.combat} cellType={"cellNone"} />
|
|
<DataRow name="Dex" showBar={showBars} color={theme.colors.combat} cellType={"cellNone"} />
|
|
<DataRow name="Agi" showBar={showBars} color={theme.colors.combat} cellType={"cell"} />
|
|
<DataRow name="Cha" showBar={showBars} color={theme.colors.cha} cellType={"cell"} />
|
|
{hasIntelligence ? (
|
|
<DataRow name="Int" showBar={showBars} color={theme.colors.int} cellType={"cell"} />
|
|
) : (
|
|
<></>
|
|
)}
|
|
<TableRow>
|
|
<TableCell component="th" scope="row" classes={{ root: classes.cell }}>
|
|
<Typography id="overview-extra-hook-0" color={theme.colors.hack}>
|
|
{}
|
|
</Typography>
|
|
</TableCell>
|
|
<TableCell component="th" scope="row" align="right" classes={{ root: classes.cell }}>
|
|
<Typography id="overview-extra-hook-1" color={theme.colors.hack}>
|
|
{}
|
|
</Typography>
|
|
</TableCell>
|
|
<TableCell component="th" scope="row" align="right" classes={{ root: classes.cell }}>
|
|
<Typography id="overview-extra-hook-2" color={theme.colors.hack}>
|
|
{}
|
|
</Typography>
|
|
</TableCell>
|
|
</TableRow>
|
|
<Work />
|
|
<BladeburnerText />
|
|
</TableBody>
|
|
</Table>
|
|
<Box sx={{ display: "flex", borderTop: `1px solid ${Settings.theme.welllight}` }}>
|
|
<Box sx={{ display: "flex", flex: 1, justifyContent: "flex-start", alignItems: "center" }}>
|
|
<IconButton aria-label="save game" onClick={save}>
|
|
<Tooltip title={Settings.AutosaveInterval !== 0 ? "Save game" : "Save game (auto-saves are disabled!)"}>
|
|
<SaveIcon color={Settings.AutosaveInterval !== 0 ? "primary" : "error"} />
|
|
</Tooltip>
|
|
</IconButton>
|
|
</Box>
|
|
<RemoteFileApiConnectionStatus showIcon={true} />
|
|
<Box sx={{ display: "flex", flex: 1, justifyContent: "flex-end", alignItems: "center" }}>
|
|
<IconButton aria-label="kill all scripts" onClick={() => setKillOpen(true)}>
|
|
<Tooltip title="Kill all running scripts">
|
|
<ClearAllIcon color="error" />
|
|
</Tooltip>
|
|
</IconButton>
|
|
</Box>
|
|
</Box>
|
|
<KillScriptsModal open={killOpen} onClose={() => setKillOpen(false)} killScripts={killScripts} />
|
|
</>
|
|
);
|
|
}
|
|
|
|
function ActionText({ action }: { action: ActionIdentifier }): React.ReactElement {
|
|
const bladeburner = Player.bladeburner;
|
|
if (!bladeburner) return <></>;
|
|
return (
|
|
<Typography>
|
|
{action.type}: {action.name}
|
|
</Typography>
|
|
);
|
|
}
|
|
|
|
function BladeburnerText(): React.ReactElement {
|
|
const { classes } = useStyles();
|
|
const rerender = useRerender();
|
|
useEffect(() => {
|
|
const clearSubscription = OverviewEventEmitter.subscribe(rerender);
|
|
return clearSubscription;
|
|
}, [rerender]);
|
|
|
|
const action = Player.bladeburner?.action;
|
|
return useMemo(
|
|
() =>
|
|
!action ? (
|
|
<></>
|
|
) : (
|
|
<>
|
|
<TableRow>
|
|
<TableCell component="th" scope="row" colSpan={2} classes={{ root: classes.cellNone }}>
|
|
<Typography>Bladeburner:</Typography>
|
|
</TableCell>
|
|
</TableRow>
|
|
<TableRow>
|
|
<TableCell component="th" scope="row" colSpan={2} classes={{ root: classes.cellNone }}>
|
|
<ActionText action={action} />
|
|
</TableCell>
|
|
</TableRow>
|
|
</>
|
|
),
|
|
[action, classes.cellNone],
|
|
);
|
|
}
|
|
|
|
interface WorkInProgressOverviewProps {
|
|
tooltip: React.ReactNode;
|
|
header: React.ReactNode;
|
|
children: React.ReactNode;
|
|
}
|
|
|
|
const onClickFocusWork = (): void => {
|
|
Player.startFocusing();
|
|
Router.toPage(Page.Work);
|
|
};
|
|
function WorkInProgressOverview({ tooltip, children, header }: WorkInProgressOverviewProps): React.ReactElement {
|
|
const { classes } = useStyles();
|
|
return (
|
|
<>
|
|
<TableRow>
|
|
<TableCell component="th" scope="row" colSpan={2} classes={{ root: classes.workCell }}>
|
|
<Tooltip title={<>{tooltip}</>}>
|
|
<Typography className={classes.workHeader} sx={{ pt: 1, pb: 0.5 }}>
|
|
{header}
|
|
</Typography>
|
|
</Tooltip>
|
|
</TableCell>
|
|
</TableRow>
|
|
<TableRow>
|
|
<TableCell component="th" scope="row" colSpan={2} classes={{ root: classes.workCell }}>
|
|
<Typography className={classes.workSubtitles}>{children}</Typography>
|
|
</TableCell>
|
|
</TableRow>
|
|
{useMemo(
|
|
() => (
|
|
<TableRow>
|
|
<TableCell component="th" scope="row" align="center" colSpan={2} classes={{ root: classes.cellNone }}>
|
|
<Button sx={{ mt: 1 }} onClick={onClickFocusWork}>
|
|
Focus
|
|
</Button>
|
|
</TableCell>
|
|
</TableRow>
|
|
),
|
|
[classes.cellNone],
|
|
)}
|
|
</>
|
|
);
|
|
}
|
|
|
|
function Work(): React.ReactElement {
|
|
const rerender = useRerender();
|
|
useEffect(() => {
|
|
const clearSubscription = OverviewEventEmitter.subscribe(rerender);
|
|
return clearSubscription;
|
|
}, [rerender]);
|
|
|
|
if (Player.currentWork === null || Player.focus) return <></>;
|
|
|
|
let details: ReactNode = "";
|
|
let header: ReactNode = "";
|
|
let innerText: ReactNode = "";
|
|
if (isCrimeWork(Player.currentWork)) {
|
|
const crime = Player.currentWork.getCrime();
|
|
const perc = (Player.currentWork.unitCompleted / crime.time) * 100;
|
|
|
|
details = <>{Player.currentWork.crimeType}</>;
|
|
header = <>You are attempting to {Player.currentWork.crimeType}</>;
|
|
innerText = <>{perc.toFixed(2)}%</>;
|
|
}
|
|
if (isClassWork(Player.currentWork)) {
|
|
details = <>{Player.currentWork.getClass().youAreCurrently}</>;
|
|
header = <>You are {Player.currentWork.getClass().youAreCurrently}</>;
|
|
innerText = <>{convertTimeMsToTimeElapsedString(Player.currentWork.cyclesWorked * CONSTANTS.MilliPerCycle)}</>;
|
|
}
|
|
if (isCreateProgramWork(Player.currentWork)) {
|
|
const create = Player.currentWork;
|
|
details = <>Coding {create.programName}</>;
|
|
header = <>Creating a program</>;
|
|
innerText = (
|
|
<>
|
|
{create.programName} {((create.unitCompleted / create.unitNeeded()) * 100).toFixed(2)}%
|
|
</>
|
|
);
|
|
}
|
|
if (isGraftingWork(Player.currentWork)) {
|
|
const graft = Player.currentWork;
|
|
details = <>Grafting {graft.augmentation}</>;
|
|
header = <>Grafting an Augmentation</>;
|
|
innerText = (
|
|
<>
|
|
<strong>{((graft.unitCompleted / graft.unitNeeded()) * 100).toFixed(2)}%</strong> done
|
|
</>
|
|
);
|
|
}
|
|
|
|
if (isFactionWork(Player.currentWork)) {
|
|
const factionWork = Player.currentWork;
|
|
details = <>Doing {factionWork.factionWorkType} work</>;
|
|
header = (
|
|
<>
|
|
Working for <strong>{factionWork.factionName}</strong>
|
|
</>
|
|
);
|
|
innerText = (
|
|
<>
|
|
<Reputation reputation={factionWork.getFaction().playerReputation} /> rep
|
|
<br />(
|
|
<ReputationRate reputation={factionWork.getReputationRate() * (1000 / CONSTANTS.MilliPerCycle)} />)
|
|
</>
|
|
);
|
|
}
|
|
if (isCompanyWork(Player.currentWork)) {
|
|
const companyWork = Player.currentWork;
|
|
const job = Player.jobs[companyWork.companyName];
|
|
if (!job) return <></>;
|
|
details = <>{job}</>;
|
|
|
|
header = (
|
|
<>
|
|
Working at <strong>{companyWork.companyName}</strong>
|
|
</>
|
|
);
|
|
innerText = (
|
|
<>
|
|
<Reputation reputation={companyWork.getCompany().playerReputation} /> rep
|
|
<br />(
|
|
<ReputationRate reputation={companyWork.getGainRates(job).reputation * (1000 / CONSTANTS.MilliPerCycle)} />)
|
|
</>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<WorkInProgressOverview tooltip={details} header={header}>
|
|
{innerText}
|
|
</WorkInProgressOverview>
|
|
);
|
|
}
|
|
|
|
const useStyles = makeStyles()((theme: Theme) => ({
|
|
workCell: {
|
|
textAlign: "center",
|
|
maxWidth: "200px",
|
|
borderBottom: "none",
|
|
padding: 0,
|
|
margin: 0,
|
|
},
|
|
|
|
workHeader: {
|
|
fontSize: "0.9rem",
|
|
},
|
|
|
|
workSubtitles: {
|
|
fontSize: "0.8rem",
|
|
},
|
|
|
|
cellNone: {
|
|
borderBottom: "none",
|
|
padding: 0,
|
|
margin: 0,
|
|
},
|
|
cell: {
|
|
padding: 0,
|
|
margin: 0,
|
|
},
|
|
hp: {
|
|
color: theme.colors.hp,
|
|
},
|
|
money: {
|
|
color: theme.colors.money,
|
|
},
|
|
hack: {
|
|
color: theme.colors.hack,
|
|
},
|
|
combat: {
|
|
color: theme.colors.combat,
|
|
},
|
|
cha: {
|
|
color: theme.colors.cha,
|
|
},
|
|
int: {
|
|
color: theme.colors.int,
|
|
},
|
|
}));
|
|
|
|
export { useStyles };
|