Merge pull request #3390 from nickofolas/improvement/stats-augs-ui

[Improvement] Overhaul Stats and Augmentations UI
This commit is contained in:
hydroflame 2022-04-12 13:43:09 -04:00 committed by GitHub
commit 243cdceb8c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 1058 additions and 817 deletions

@ -7,7 +7,7 @@ import React, { useState, useEffect } from "react";
import { InstalledAugmentations } from "./InstalledAugmentations"; import { InstalledAugmentations } from "./InstalledAugmentations";
import { PlayerMultipliers } from "./PlayerMultipliers"; import { PlayerMultipliers } from "./PlayerMultipliers";
import { PurchasedAugmentations } from "./PurchasedAugmentations"; import { PurchasedAugmentations } from "./PurchasedAugmentations";
import { SourceFiles } from "./SourceFiles"; import { SourceFilesElement } from "./SourceFiles";
import { canGetBonus } from "../../ExportBonus"; import { canGetBonus } from "../../ExportBonus";
import { use } from "../../ui/Context"; import { use } from "../../ui/Context";
@ -16,8 +16,55 @@ import Typography from "@mui/material/Typography";
import Button from "@mui/material/Button"; import Button from "@mui/material/Button";
import Tooltip from "@mui/material/Tooltip"; import Tooltip from "@mui/material/Tooltip";
import Box from "@mui/material/Box"; import Box from "@mui/material/Box";
import Paper from "@mui/material/Paper";
import Container from "@mui/material/Container";
import { Settings } from "../../Settings/Settings"; import { Settings } from "../../Settings/Settings";
import { ConfirmationModal } from "../../ui/React/ConfirmationModal"; import { ConfirmationModal } from "../../ui/React/ConfirmationModal";
import { IPlayer } from "../../PersonObjects/IPlayer";
import { AugmentationNames } from "../data/AugmentationNames";
import { Augmentations } from "../Augmentations";
import { CONSTANTS } from "../../Constants";
import { formatNumber } from "../../utils/StringHelperFunctions";
import { Info } from "@mui/icons-material";
interface NFGDisplayProps {
player: IPlayer;
}
const NeuroFluxDisplay = ({ player }: NFGDisplayProps): React.ReactElement => {
const level = player.augmentations.find((e) => e.name === AugmentationNames.NeuroFluxGovernor)?.level ?? 0;
return level > 0 ? (
<Paper sx={{ p: 1 }}>
<Typography variant="h5" color={Settings.theme.info}>
NeuroFlux Governor - Level {level}
</Typography>
<Typography color={Settings.theme.info}>{Augmentations[AugmentationNames.NeuroFluxGovernor].stats}</Typography>
</Paper>
) : (
<></>
);
};
interface EntropyDisplayProps {
player: IPlayer;
}
const EntropyDisplay = ({ player }: EntropyDisplayProps): React.ReactElement => {
return player.entropy > 0 ? (
<Paper sx={{ p: 1 }}>
<Typography variant="h5" color={Settings.theme.error}>
Entropy Virus - Level {player.entropy}
</Typography>
<Typography color={Settings.theme.error}>
<b>All multipliers decreased by:</b> {formatNumber((1 - CONSTANTS.EntropyEffect ** player.entropy) * 100, 3)}%
(multiplicative)
</Typography>
</Paper>
) : (
<></>
);
};
interface IProps { interface IProps {
exportGameFn: () => void; exportGameFn: () => void;
@ -55,14 +102,22 @@ export function AugmentationsRoot(props: IProps): React.ReactElement {
} }
return ( return (
<> <Container disableGutters maxWidth="lg" sx={{ mx: 0 }}>
<Typography variant="h4">Augmentations</Typography> <Typography variant="h4">Augmentations</Typography>
<Box mx={2}> <Box sx={{ mb: 1 }}>
<Paper sx={{ p: 1 }}>
<Typography variant="h5" color="primary" sx={{ display: "flex", alignItems: "center", flexWrap: "wrap" }}>
Purchased Augmentations
<Tooltip
title={
<>
<Typography> <Typography>
Below is a list of all Augmentations you have purchased but not yet installed. Click the button below to Below is a list of all Augmentations you have purchased but not yet installed. Click the button
install them. below to install them.
</Typography>
<Typography>
WARNING: Installing your Augmentations resets most of your progress, including:
</Typography> </Typography>
<Typography>WARNING: Installing your Augmentations resets most of your progress, including:</Typography>
<br /> <br />
<Typography>- Stats/Skill levels and Experience</Typography> <Typography>- Stats/Skill levels and Experience</Typography>
<Typography>- Money</Typography> <Typography>- Money</Typography>
@ -73,22 +128,16 @@ export function AugmentationsRoot(props: IProps): React.ReactElement {
<Typography>- Stocks</Typography> <Typography>- Stocks</Typography>
<br /> <br />
<Typography> <Typography>
Installing Augmentations lets you start over with the perks and benefits granted by all of the Augmentations Installing Augmentations lets you start over with the perks and benefits granted by all of the
you have ever installed. Also, you will keep any scripts and RAM/Core upgrades on your home computer (but you Augmentations you have ever installed. Also, you will keep any scripts and RAM/Core upgrades on your
will lose all programs besides NUKE.exe) home computer (but you will lose all programs besides NUKE.exe)
</Typography> </Typography>
</Box> </>
<Typography variant="h4" color="primary"> }
Purchased Augmentations >
</Typography> <Info sx={{ ml: 1, mb: 0.5 }} color="info" />
<Box mx={2}>
<Tooltip title={<Typography>'I never asked for this'</Typography>}>
<span>
<Button disabled={player.queuedAugmentations.length === 0} onClick={doInstall}>
Install Augmentations
</Button>
</span>
</Tooltip> </Tooltip>
</Typography>
<ConfirmationModal <ConfirmationModal
open={installOpen} open={installOpen}
onClose={() => setInstallOpen(false)} onClose={() => setInstallOpen(false)}
@ -109,27 +158,57 @@ export function AugmentationsRoot(props: IProps): React.ReactElement {
<br />- home ram and cores <br />- home ram and cores
<br /> <br />
<br /> <br />
It is recommended to install several Augmentations at once. Preferably everything from any faction of your It is recommended to install several Augmentations at once. Preferably everything from any faction of
choosing. your choosing.
</> </>
} }
/> />
<Box sx={{ display: "grid", width: "100%", gridTemplateColumns: "1fr 1fr" }}>
<Tooltip title={<Typography>'I never asked for this'</Typography>}>
<span>
<Button sx={{ width: "100%" }} disabled={player.queuedAugmentations.length === 0} onClick={doInstall}>
Install Augmentations
</Button>
</span>
</Tooltip>
<Tooltip title={<Typography>It's always a good idea to backup/export your save!</Typography>}> <Tooltip title={<Typography>It's always a good idea to backup/export your save!</Typography>}>
<Button sx={{ mx: 2 }} onClick={doExport} color="error"> <Button sx={{ width: "100%" }} onClick={doExport} color="error">
Backup Save {exportBonusStr()} Backup Save {exportBonusStr()}
</Button> </Button>
</Tooltip> </Tooltip>
<PurchasedAugmentations />
</Box> </Box>
<Typography variant="h4">Installed Augmentations</Typography> </Paper>
<Box mx={2}> {player.queuedAugmentations.length > 0 ? (
<Typography> <Box sx={{ display: "grid", gridTemplateColumns: "1fr 3fr" }}>
List of all Augmentations that have been installed. You have gained the effects of these. <PurchasedAugmentations />
</Typography> <PlayerMultipliers />
</Box>
) : (
<Paper sx={{ p: 1 }}>
<Typography>No Augmentations have been purchased yet</Typography>
</Paper>
)}
</Box>
<Box
sx={{
my: 1,
display: "grid",
gridTemplateColumns: `repeat(${
+!!((player.augmentations.find((e) => e.name === AugmentationNames.NeuroFluxGovernor)?.level ?? 0) > 0) +
+!!(player.entropy > 0)
}, 1fr)`,
gap: 1,
}}
>
<NeuroFluxDisplay player={player} />
<EntropyDisplay player={player} />
</Box>
<Box>
<InstalledAugmentations /> <InstalledAugmentations />
</Box> </Box>
<PlayerMultipliers /> <SourceFilesElement />
<SourceFiles /> </Container>
</>
); );
} }

@ -5,28 +5,23 @@
* It also contains 'configuration' buttons that allow you to change how the * It also contains 'configuration' buttons that allow you to change how the
* Augs/SF's are displayed * Augs/SF's are displayed
*/ */
import { Box, ListItemButton, Paper, Typography } from "@mui/material";
import Button from "@mui/material/Button";
import List from "@mui/material/List";
import Tooltip from "@mui/material/Tooltip";
import React, { useState } from "react"; import React, { useState } from "react";
import { OwnedAugmentationsOrderSetting } from "../../Settings/SettingEnums";
import { AugmentationAccordion } from "../../ui/React/AugmentationAccordion";
import { Augmentations } from "../Augmentations";
import { AugmentationNames } from "../data/AugmentationNames";
import { Settings } from "../../Settings/Settings"; import { Settings } from "../../Settings/Settings";
import { use } from "../../ui/Context"; import { use } from "../../ui/Context";
import { OwnedAugmentationsOrderSetting } from "../../Settings/SettingEnums"; import { Augmentations } from "../Augmentations";
import Button from "@mui/material/Button"; import { AugmentationNames } from "../data/AugmentationNames";
import Tooltip from "@mui/material/Tooltip";
import List from "@mui/material/List";
import { ExpandLess, ExpandMore } from "@mui/icons-material";
import { Box, Paper, ListItemButton, ListItemText, Typography, Collapse } from "@mui/material";
import { CONSTANTS } from "../../Constants";
import { formatNumber } from "../../utils/StringHelperFunctions";
export function InstalledAugmentations(): React.ReactElement { export function InstalledAugmentations(): React.ReactElement {
const setRerender = useState(true)[1]; const setRerender = useState(true)[1];
const player = use.Player(); const player = use.Player();
const sourceAugs = player.augmentations.slice().filter((aug) => aug.name !== AugmentationNames.NeuroFluxGovernor);
const sourceAugs = player.augmentations.slice(); const [selectedAug, setSelectedAug] = useState(sourceAugs[0]);
if (Settings.OwnedAugmentationsOrder === OwnedAugmentationsOrderSetting.Alphabetically) { if (Settings.OwnedAugmentationsOrder === OwnedAugmentationsOrderSetting.Alphabetically) {
sourceAugs.sort((aug1, aug2) => { sourceAugs.sort((aug1, aug2) => {
@ -49,59 +44,60 @@ export function InstalledAugmentations(): React.ReactElement {
} }
return ( return (
<> <Box sx={{ width: "100%" }}>
<Paper sx={{ p: 1 }}>
<Typography variant="h5">Installed Augmentations</Typography>
<Box sx={{ display: "grid", gridTemplateColumns: "1fr 1fr" }}>
<Tooltip title={"Sorts the Augmentations alphabetically in numeral order"}> <Tooltip title={"Sorts the Augmentations alphabetically in numeral order"}>
<Button onClick={sortInOrder}>Sort in Order</Button> <Button sx={{ width: "100%" }} onClick={sortInOrder}>
</Tooltip> Sort in Order
<Tooltip title={"Sorts the Augmentations based on when you acquired them (same as default)"}>
<Button sx={{ mx: 2 }} onClick={sortByAcquirementTime}>
Sort by Acquirement Time
</Button> </Button>
</Tooltip> </Tooltip>
<List dense> <Tooltip title={"Sorts the Augmentations based on when you acquired them (same as default)"}>
{player.entropy > 0 && <Button sx={{ width: "100%" }} onClick={sortByAcquirementTime}>
(() => { Sort by Time of Acquirement
const [open, setOpen] = useState(false); </Button>
</Tooltip>
return ( </Box>
<Box component={Paper}> </Paper>
<ListItemButton onClick={() => setOpen((old) => !old)}> {sourceAugs.length > 0 ? (
<ListItemText <Paper sx={{ display: "grid", gridTemplateColumns: "1fr 3fr" }}>
primary={ <Box>
<Typography color={Settings.theme.hp} style={{ whiteSpace: "pre-wrap" }}> <List sx={{ height: 400, overflowY: "scroll", borderRight: `1px solid ${Settings.theme.welllight}` }}>
Entropy Virus - Level {player.entropy} {sourceAugs.map((k, i) => (
</Typography> <ListItemButton key={i + 1} onClick={() => setSelectedAug(k)} selected={selectedAug === k}>
} <Typography>{k.name}</Typography>
/>
{open ? (
<ExpandLess sx={{ color: Settings.theme.hp }} />
) : (
<ExpandMore sx={{ color: Settings.theme.hp }} />
)}
</ListItemButton> </ListItemButton>
<Collapse in={open} unmountOnExit> ))}
<Box m={4}>
<Typography color={Settings.theme.hp}>
<b>All multipliers decreased by:</b>{" "}
{formatNumber((1 - CONSTANTS.EntropyEffect ** player.entropy) * 100, 3)}% (multiplicative)
</Typography>
</Box>
</Collapse>
</Box>
);
})()}
{sourceAugs.map((e) => {
const aug = Augmentations[e.name];
let level = null;
if (e.name === AugmentationNames.NeuroFluxGovernor) {
level = e.level;
}
return <AugmentationAccordion key={aug.name} aug={aug} level={level} />;
})}
</List> </List>
</Box>
<Box sx={{ m: 1 }}>
<Typography variant="h6" sx={{ display: "flex", alignItems: "center", flexWrap: "wrap" }}>
{selectedAug.name}
</Typography>
<Typography sx={{ maxHeight: 350, overflowY: "scroll" }}>
{(() => {
const aug = Augmentations[selectedAug.name];
const info = typeof aug.info === "string" ? <span>{aug.info}</span> : aug.info;
const tooltip = (
<>
{info}
<br />
<br />
{aug.stats}
</> </>
); );
return tooltip;
})()}
</Typography>
</Box>
</Paper>
) : (
<Paper sx={{ p: 1 }}>
<Typography>No Augmentations have been installed yet</Typography>
</Paper>
)}
</Box>
);
} }

@ -1,20 +1,22 @@
/** /**
* React component for displaying the player's multipliers on the Augmentation UI page * React component for displaying the player's multipliers on the Augmentation UI page
*/ */
import { DoubleArrow } from "@mui/icons-material";
import { List, ListItem, ListItemText, Paper, Typography } from "@mui/material";
import * as React from "react"; import * as React from "react";
import { BitNodeMultipliers } from "../../BitNode/BitNodeMultipliers";
import { Player } from "../../Player"; import { Player } from "../../Player";
import { Settings } from "../../Settings/Settings";
import { SourceFileFlags } from "../../SourceFile/SourceFileFlags";
import { numeralWrapper } from "../../ui/numeralFormat"; import { numeralWrapper } from "../../ui/numeralFormat";
import { Augmentations } from "../Augmentations"; import { Augmentations } from "../Augmentations";
import { Table, TableCell } from "../../ui/React/Table";
import TableBody from "@mui/material/TableBody";
import TableRow from "@mui/material/TableRow";
import Typography from "@mui/material/Typography";
import Box from "@mui/material/Box";
import { BitNodeMultipliers } from "../../BitNode/BitNodeMultipliers";
function calculateAugmentedStats(): any { interface IAugmentedStats {
const augP: any = {}; [index: string]: number;
}
function calculateAugmentedStats(): IAugmentedStats {
const augP: IAugmentedStats = {};
for (const aug of Player.queuedAugmentations) { for (const aug of Player.queuedAugmentations) {
const augObj = Augmentations[aug.name]; const augObj = Augmentations[aug.name];
for (const mult of Object.keys(augObj.mults)) { for (const mult of Object.keys(augObj.mults)) {
@ -25,112 +27,82 @@ function calculateAugmentedStats(): any {
return augP; return augP;
} }
function Improvements({ r, m }: { r: number; m: number }): React.ReactElement { interface IBitNodeModifiedStatsProps {
if (r) {
return (
<>
<TableCell key="2">
<Typography>&nbsp;{"=>"}&nbsp;</Typography>
</TableCell>
<TableCell key="3">
<Typography>
{numeralWrapper.formatPercentage(r)} <BN5Stat base={r} mult={m} />
</Typography>
</TableCell>
</>
);
}
return <></>;
}
interface IBN5StatsProps {
base: number; base: number;
mult: number; mult: number;
color: string;
} }
function BN5Stat(props: IBN5StatsProps): React.ReactElement { function BitNodeModifiedStats(props: IBitNodeModifiedStatsProps): React.ReactElement {
if (props.mult === 1) return <></>; // If player doesn't have SF5 or if the property isn't affected by BitNode mults
return <>({numeralWrapper.formatPercentage(props.base * props.mult)})</>; if (props.mult === 1 || SourceFileFlags[5] === 0)
} return <Typography color={props.color}>{numeralWrapper.formatPercentage(props.base)}</Typography>;
function MultiplierTable({ rows }: { rows: [string, number, number, number][] }): React.ReactElement {
return ( return (
<Table size="small" padding="none"> <Typography color={props.color}>
<TableBody> <span style={{ opacity: 0.5 }}>{numeralWrapper.formatPercentage(props.base)}</span>{" "}
{rows.map((r: any) => ( {numeralWrapper.formatPercentage(props.base * props.mult)}
<TableRow key={r[0]}>
<TableCell key="0">
<Typography noWrap>{r[0]} multiplier:&nbsp;</Typography>
</TableCell>
<TableCell key="1" style={{ textAlign: "right" }}>
<Typography noWrap>
{numeralWrapper.formatPercentage(r[1])} <BN5Stat base={r[1]} mult={r[3]} />
</Typography> </Typography>
</TableCell>
<Improvements r={r[2]} m={r[3]} />
</TableRow>
))}
</TableBody>
</Table>
); );
} }
type MultiplierListItemData = [
multiplier: string,
currentValue: number,
augmentedValue: number,
bitNodeMultiplier: number,
color: string,
];
interface IMultiplierListProps {
rows: MultiplierListItemData[];
}
function MultiplierList(props: IMultiplierListProps): React.ReactElement {
const listItems = props.rows
.map((data) => {
const [multiplier, currentValue, augmentedValue, bitNodeMultiplier, color] = data;
if (!isNaN(augmentedValue)) {
return (
<ListItem key={multiplier} disableGutters sx={{ py: 0 }}>
<ListItemText
sx={{ my: 0.1 }}
primary={
<Typography color={color}>
<b>{multiplier}</b>
</Typography>
}
secondary={
<span style={{ display: "flex", alignItems: "center", flexWrap: "wrap" }}>
<BitNodeModifiedStats base={currentValue} mult={bitNodeMultiplier} color={color} />
<DoubleArrow fontSize="small" color="success" sx={{ mb: 0.5, mx: 1 }} />
<BitNodeModifiedStats base={augmentedValue} mult={bitNodeMultiplier} color={Settings.theme.success} />
</span>
}
disableTypography
/>
</ListItem>
);
}
return;
})
.filter((i) => i !== undefined);
return listItems.length > 0 ? <List disablePadding>{listItems}</List> : <></>;
}
export function PlayerMultipliers(): React.ReactElement { export function PlayerMultipliers(): React.ReactElement {
const mults = calculateAugmentedStats(); const mults = calculateAugmentedStats();
function BladeburnerMults(): React.ReactElement { // Column data is a bit janky, so it's set up here to allow for
if (!Player.canAccessBladeburner()) return <></>; // easier logic in setting up the layout
return ( const leftColData: MultiplierListItemData[] = [
<> ...[
<MultiplierTable
rows={[
[
"Bladeburner Success Chance",
Player.bladeburner_success_chance_mult,
Player.bladeburner_success_chance_mult * mults.bladeburner_success_chance_mult,
1,
],
[
"Bladeburner Max Stamina",
Player.bladeburner_max_stamina_mult,
Player.bladeburner_max_stamina_mult * mults.bladeburner_max_stamina_mult,
1,
],
[
"Bladeburner Stamina Gain",
Player.bladeburner_stamina_gain_mult,
Player.bladeburner_stamina_gain_mult * mults.bladeburner_stamina_gain_mult,
1,
],
[
"Bladeburner Field Analysis",
Player.bladeburner_analysis_mult,
Player.bladeburner_analysis_mult * mults.bladeburner_analysis_mult,
1,
],
]}
/>
<br />
</>
);
}
return (
<>
<Typography variant="h4">Multipliers</Typography>
<Box mx={2}>
<MultiplierTable
rows={[
["Hacking Chance ", Player.hacking_chance_mult, Player.hacking_chance_mult * mults.hacking_chance_mult, 1], ["Hacking Chance ", Player.hacking_chance_mult, Player.hacking_chance_mult * mults.hacking_chance_mult, 1],
["Hacking Speed ", Player.hacking_speed_mult, Player.hacking_speed_mult * mults.hacking_speed_mult, 1], ["Hacking Speed ", Player.hacking_speed_mult, Player.hacking_speed_mult * mults.hacking_speed_mult, 1],
["Hacking Money ", Player.hacking_money_mult, Player.hacking_money_mult * mults.hacking_money_mult, 1], ["Hacking Money ", Player.hacking_money_mult, Player.hacking_money_mult * mults.hacking_money_mult, 1],
["Hacking Growth ", Player.hacking_grow_mult, Player.hacking_grow_mult * mults.hacking_grow_mult, 1], ["Hacking Growth ", Player.hacking_grow_mult, Player.hacking_grow_mult * mults.hacking_grow_mult, 1],
]}
/>
<br />
<MultiplierTable
rows={[
[ [
"Hacking Level ", "Hacking Level ",
Player.hacking_mult, Player.hacking_mult,
@ -143,12 +115,8 @@ export function PlayerMultipliers(): React.ReactElement {
Player.hacking_exp_mult * mults.hacking_exp_mult, Player.hacking_exp_mult * mults.hacking_exp_mult,
BitNodeMultipliers.HackExpGain, BitNodeMultipliers.HackExpGain,
], ],
]} ].map((data): MultiplierListItemData => (data as any).concat([Settings.theme.hack])),
/> ...[
<br />
<MultiplierTable
rows={[
[ [
"Strength Level ", "Strength Level ",
Player.strength_mult, Player.strength_mult,
@ -156,12 +124,6 @@ export function PlayerMultipliers(): React.ReactElement {
BitNodeMultipliers.StrengthLevelMultiplier, BitNodeMultipliers.StrengthLevelMultiplier,
], ],
["Strength Experience ", Player.strength_exp_mult, Player.strength_exp_mult * mults.strength_exp_mult, 1], ["Strength Experience ", Player.strength_exp_mult, Player.strength_exp_mult * mults.strength_exp_mult, 1],
]}
/>
<br />
<MultiplierTable
rows={[
[ [
"Defense Level ", "Defense Level ",
Player.defense_mult, Player.defense_mult,
@ -169,30 +131,13 @@ export function PlayerMultipliers(): React.ReactElement {
BitNodeMultipliers.DefenseLevelMultiplier, BitNodeMultipliers.DefenseLevelMultiplier,
], ],
["Defense Experience ", Player.defense_exp_mult, Player.defense_exp_mult * mults.defense_exp_mult, 1], ["Defense Experience ", Player.defense_exp_mult, Player.defense_exp_mult * mults.defense_exp_mult, 1],
]}
/>
<br />
<MultiplierTable
rows={[
[ [
"Dexterity Level ", "Dexterity Level ",
Player.dexterity_mult, Player.dexterity_mult,
Player.dexterity_mult * mults.dexterity_mult, Player.dexterity_mult * mults.dexterity_mult,
BitNodeMultipliers.DexterityLevelMultiplier, BitNodeMultipliers.DexterityLevelMultiplier,
], ],
[ ["Dexterity Experience ", Player.dexterity_exp_mult, Player.dexterity_exp_mult * mults.dexterity_exp_mult, 1],
"Dexterity Experience ",
Player.dexterity_exp_mult,
Player.dexterity_exp_mult * mults.dexterity_exp_mult,
1,
],
]}
/>
<br />
<MultiplierTable
rows={[
[ [
"Agility Level ", "Agility Level ",
Player.agility_mult, Player.agility_mult,
@ -200,25 +145,24 @@ export function PlayerMultipliers(): React.ReactElement {
BitNodeMultipliers.AgilityLevelMultiplier, BitNodeMultipliers.AgilityLevelMultiplier,
], ],
["Agility Experience ", Player.agility_exp_mult, Player.agility_exp_mult * mults.agility_exp_mult, 1], ["Agility Experience ", Player.agility_exp_mult, Player.agility_exp_mult * mults.agility_exp_mult, 1],
]} ].map((data): MultiplierListItemData => (data as any).concat([Settings.theme.combat])),
/>
<br />
<MultiplierTable
rows={[
[ [
"Charisma Level ", "Charisma Level ",
Player.charisma_mult, Player.charisma_mult,
Player.charisma_mult * mults.charisma_mult, Player.charisma_mult * mults.charisma_mult,
BitNodeMultipliers.CharismaLevelMultiplier, BitNodeMultipliers.CharismaLevelMultiplier,
Settings.theme.cha,
], ],
["Charisma Experience ", Player.charisma_exp_mult, Player.charisma_exp_mult * mults.charisma_exp_mult, 1], [
]} "Charisma Experience ",
/> Player.charisma_exp_mult,
<br /> Player.charisma_exp_mult * mults.charisma_exp_mult,
1,
<MultiplierTable Settings.theme.cha,
rows={[ ],
];
const rightColData: MultiplierListItemData[] = [
...[
[ [
"Hacknet Node production ", "Hacknet Node production ",
Player.hacknet_node_money_mult, Player.hacknet_node_money_mult,
@ -249,12 +193,6 @@ export function PlayerMultipliers(): React.ReactElement {
Player.hacknet_node_level_cost_mult * mults.hacknet_node_level_cost_mult, Player.hacknet_node_level_cost_mult * mults.hacknet_node_level_cost_mult,
1, 1,
], ],
]}
/>
<br />
<MultiplierTable
rows={[
["Company reputation gain ", Player.company_rep_mult, Player.company_rep_mult * mults.company_rep_mult, 1], ["Company reputation gain ", Player.company_rep_mult, Player.company_rep_mult * mults.company_rep_mult, 1],
[ [
"Faction reputation gain ", "Faction reputation gain ",
@ -262,31 +200,76 @@ export function PlayerMultipliers(): React.ReactElement {
Player.faction_rep_mult * mults.faction_rep_mult, Player.faction_rep_mult * mults.faction_rep_mult,
BitNodeMultipliers.FactionWorkRepGain, BitNodeMultipliers.FactionWorkRepGain,
], ],
].map((data): MultiplierListItemData => (data as any).concat([Settings.theme.primary])),
[ [
"Salary ", "Salary ",
Player.work_money_mult, Player.work_money_mult,
Player.work_money_mult * mults.work_money_mult, Player.work_money_mult * mults.work_money_mult,
BitNodeMultipliers.CompanyWorkMoney, BitNodeMultipliers.CompanyWorkMoney,
Settings.theme.money,
],
[
"Crime success ",
Player.crime_success_mult,
Player.crime_success_mult * mults.crime_success_mult,
1,
Settings.theme.combat,
], ],
]}
/>
<br />
<MultiplierTable
rows={[
["Crime success ", Player.crime_success_mult, Player.crime_success_mult * mults.crime_success_mult, 1],
[ [
"Crime money ", "Crime money ",
Player.crime_money_mult, Player.crime_money_mult,
Player.crime_money_mult * mults.crime_money_mult, Player.crime_money_mult * mults.crime_money_mult,
BitNodeMultipliers.CrimeMoney, BitNodeMultipliers.CrimeMoney,
Settings.theme.money,
], ],
]} ];
/>
<br />
<BladeburnerMults /> if (Player.canAccessBladeburner()) {
</Box> rightColData.push(
</> ...[
[
"Bladeburner Success Chance",
Player.bladeburner_success_chance_mult,
Player.bladeburner_success_chance_mult * mults.bladeburner_success_chance_mult,
1,
],
[
"Bladeburner Max Stamina",
Player.bladeburner_max_stamina_mult,
Player.bladeburner_max_stamina_mult * mults.bladeburner_max_stamina_mult,
1,
],
[
"Bladeburner Stamina Gain",
Player.bladeburner_stamina_gain_mult,
Player.bladeburner_stamina_gain_mult * mults.bladeburner_stamina_gain_mult,
1,
],
[
"Bladeburner Field Analysis",
Player.bladeburner_analysis_mult,
Player.bladeburner_analysis_mult * mults.bladeburner_analysis_mult,
1,
],
].map((data): MultiplierListItemData => (data as any).concat([Settings.theme.primary])),
);
}
const hasLeftImprovements = +!!(leftColData.filter((item) => item[2] !== 0).length > 0),
hasRightImprovements = +!!(rightColData.filter((item) => item[2] !== 0).length > 0);
return (
<Paper
sx={{
p: 1,
maxHeight: 400,
overflowY: "scroll",
display: "grid",
gridTemplateColumns: `repeat(${hasLeftImprovements + hasRightImprovements}, 1fr)`,
}}
>
<MultiplierList rows={leftColData} />
<MultiplierList rows={rightColData} />
</Paper>
); );
} }

@ -2,14 +2,11 @@
* React component for displaying all of the player's purchased (but not installed) * React component for displaying all of the player's purchased (but not installed)
* Augmentations on the Augmentations UI. * Augmentations on the Augmentations UI.
*/ */
import { List, ListItemText, Paper, Tooltip, Typography } from "@mui/material";
import * as React from "react"; import * as React from "react";
import { Player } from "../../Player";
import { Augmentations } from "../Augmentations"; import { Augmentations } from "../Augmentations";
import { AugmentationNames } from "../data/AugmentationNames"; import { AugmentationNames } from "../data/AugmentationNames";
import { Player } from "../../Player";
import { AugmentationAccordion } from "../../ui/React/AugmentationAccordion";
import List from "@mui/material/List";
export function PurchasedAugmentations(): React.ReactElement { export function PurchasedAugmentations(): React.ReactElement {
const augs: React.ReactElement[] = []; const augs: React.ReactElement[] = [];
@ -23,14 +20,48 @@ export function PurchasedAugmentations(): React.ReactElement {
} }
for (let i = 0; i < Player.queuedAugmentations.length; i++) { for (let i = 0; i < Player.queuedAugmentations.length; i++) {
const ownedAug = Player.queuedAugmentations[i]; const ownedAug = Player.queuedAugmentations[i];
let displayName = ownedAug.name;
if (ownedAug.name === AugmentationNames.NeuroFluxGovernor && i !== nfgIndex) continue; if (ownedAug.name === AugmentationNames.NeuroFluxGovernor && i !== nfgIndex) continue;
const aug = Augmentations[ownedAug.name]; const aug = Augmentations[ownedAug.name];
let level = null; let level = null;
if (ownedAug.name === AugmentationNames.NeuroFluxGovernor) { if (ownedAug.name === AugmentationNames.NeuroFluxGovernor) {
level = ownedAug.level; level = ownedAug.level;
} displayName += ` - Level ${level}`;
augs.push(<AugmentationAccordion key={aug.name} aug={aug} level={level} />);
} }
return <List dense>{augs}</List>; augs.push(
<Tooltip
title={
<Typography>
{(() => {
const info = typeof aug.info === "string" ? <span>{aug.info}</span> : aug.info;
const tooltip = (
<>
{info}
<br />
<br />
{aug.stats}
</>
);
return tooltip;
})()}
</Typography>
}
enterNextDelay={500}
key={displayName}
>
<ListItemText sx={{ px: 2, py: 1 }} primary={displayName} />
</Tooltip>,
);
}
return (
<Paper sx={{ py: 1, maxHeight: 400, overflowY: "scroll" }}>
<List sx={{ height: 400, overflowY: "scroll" }} disablePadding>
{augs}
</List>
</Paper>
);
} }

@ -1,21 +1,159 @@
import React from "react"; import { ListItemButton, ListItemText, Paper } from "@mui/material";
import { SourceFileMinus1 } from "./SourceFileMinus1";
import { OwnedSourceFiles } from "./OwnedSourceFiles";
import List from "@mui/material/List";
import Typography from "@mui/material/Typography";
import Box from "@mui/material/Box"; import Box from "@mui/material/Box";
import List from "@mui/material/List";
import Typography from "@mui/material/Typography";
import React, { useState } from "react";
import { Exploit, ExploitName } from "../../Exploits/Exploit";
import { Player } from "../../Player";
import { OwnedAugmentationsOrderSetting } from "../../Settings/SettingEnums";
import { Settings } from "../../Settings/Settings";
import { SourceFile } from "../../SourceFile/SourceFile";
import { SourceFiles } from "../../SourceFile/SourceFiles";
export function SourceFiles(): React.ReactElement { interface SfMinus1 {
return ( info: React.ReactElement;
n: number;
name: string;
lvl: number;
}
const safeGetSf = (sfNum: number): SourceFile | SfMinus1 | null => {
if (sfNum === -1) {
const sfMinus1: SfMinus1 = {
info: (
<> <>
<Typography variant="h4">Source Files</Typography> This Source-File can only be acquired with obscure knowledge of the game, javascript, and the web ecosystem.
<Box mx={2}> <br />
<List dense> <br />
<SourceFileMinus1 /> It increases all of the player's multipliers by 0.1%
<OwnedSourceFiles /> <br />
<br />
You have found the following exploits:
<br />
<br />
{Player.exploits.map((c: Exploit) => (
<React.Fragment key={c}>
* {ExploitName(c)}
<br />
</React.Fragment>
))}
</>
),
lvl: Player.exploits.length,
n: -1,
name: "Source-File -1: Exploits in the BitNodes",
};
return sfMinus1;
}
const srcFileKey = "SourceFile" + sfNum;
const sfObj = SourceFiles[srcFileKey];
if (sfObj == null) {
console.error(`Invalid source file number: ${sfNum}`);
return null;
}
return sfObj;
};
const getMaxLevel = (sfObj: SourceFile | SfMinus1): string | number => {
let maxLevel;
switch (sfObj.n) {
case 12:
maxLevel = "∞";
break;
case -1:
maxLevel = Object.keys(Exploit).length;
break;
default:
maxLevel = "3";
}
return maxLevel;
};
export function SourceFilesElement(): React.ReactElement {
const sourceSfs = Player.sourceFiles.slice();
const exploits = Player.exploits;
// Create a fake SF for -1, if "owned"
if (exploits.length > 0) {
sourceSfs.unshift({
n: -1,
lvl: exploits.length,
});
}
if (Settings.OwnedAugmentationsOrder === OwnedAugmentationsOrderSetting.Alphabetically) {
sourceSfs.sort((sf1, sf2) => {
return sf1.n - sf2.n;
});
}
if (sourceSfs.length === 0) {
return <></>;
}
const [selectedSf, setSelectedSf] = useState(sourceSfs[0]);
return (
<Box sx={{ width: "100%", mt: 1 }}>
<Paper sx={{ p: 1 }}>
<Typography variant="h5">Source Files</Typography>
</Paper>
<Paper sx={{ display: "grid", gridTemplateColumns: "1fr 3fr" }}>
<Box>
<List
sx={{ height: 400, overflowY: "scroll", borderRight: `1px solid ${Settings.theme.welllight}` }}
disablePadding
>
{sourceSfs.map((e, i) => {
const sfObj = safeGetSf(e.n);
if (!sfObj) return;
const maxLevel = getMaxLevel(sfObj);
return (
<ListItemButton
key={i + 1}
onClick={() => setSelectedSf(e)}
selected={selectedSf.n === e.n}
sx={{ py: 0 }}
>
<ListItemText
disableTypography
primary={<Typography>{sfObj.name}</Typography>}
secondary={
<Typography>
Level {e.lvl} / {maxLevel}
</Typography>
}
/>
</ListItemButton>
);
})}
</List> </List>
</Box> </Box>
<Box sx={{ m: 1 }}>
<Typography variant="h6" sx={{ display: "flex", alignItems: "center", flexWrap: "wrap" }}>
{safeGetSf(selectedSf.n)?.name}
</Typography>
<Typography sx={{ maxHeight: 350, overflowY: "scroll" }}>
{(() => {
const sfObj = safeGetSf(selectedSf.n);
if (!sfObj) return;
const maxLevel = getMaxLevel(sfObj);
return (
<>
Level {selectedSf.lvl} / {maxLevel}
<br />
<br />
{sfObj.info}
</> </>
); );
})()}
</Typography>
</Box>
</Paper>
</Box>
);
} }

@ -1,19 +1,18 @@
import { Clear, ExpandMore, Reply, ReplyAll } from "@mui/icons-material";
import {
Accordion,
AccordionDetails,
AccordionSummary,
Button,
IconButton,
MenuItem,
Select,
SelectChangeEvent,
Typography,
} from "@mui/material";
import React, { useState } from "react"; import React, { useState } from "react";
import Accordion from "@mui/material/Accordion";
import AccordionSummary from "@mui/material/AccordionSummary";
import AccordionDetails from "@mui/material/AccordionDetails";
import ExpandMoreIcon from "@mui/icons-material/ExpandMore";
import Select, { SelectChangeEvent } from "@mui/material/Select";
import { IPlayer } from "../../PersonObjects/IPlayer";
import { AugmentationNames } from "../../Augmentation/data/AugmentationNames"; import { AugmentationNames } from "../../Augmentation/data/AugmentationNames";
import Typography from "@mui/material/Typography"; import { IPlayer } from "../../PersonObjects/IPlayer";
import MenuItem from "@mui/material/MenuItem";
import IconButton from "@mui/material/IconButton";
import ReplyAllIcon from "@mui/icons-material/ReplyAll";
import ReplyIcon from "@mui/icons-material/Reply";
import ClearIcon from "@mui/icons-material/Clear";
interface IProps { interface IProps {
player: IPlayer; player: IPlayer;
@ -39,36 +38,33 @@ export function Augmentations(props: IProps): React.ReactElement {
props.player.augmentations = []; props.player.augmentations = [];
} }
function clearQueuedAugs(): void {
props.player.queuedAugmentations = [];
}
return ( return (
<Accordion TransitionProps={{ unmountOnExit: true }}> <Accordion TransitionProps={{ unmountOnExit: true }}>
<AccordionSummary expandIcon={<ExpandMoreIcon />}> <AccordionSummary expandIcon={<ExpandMore />}>
<Typography>Augmentations</Typography> <Typography>Augmentations</Typography>
</AccordionSummary> </AccordionSummary>
<AccordionDetails> <AccordionDetails>
<table>
<tbody>
<tr>
<td>
<Typography>Aug:</Typography>
</td>
<td>
<Select <Select
onChange={setAugmentationDropdown} onChange={setAugmentationDropdown}
value={augmentation} value={augmentation}
startAdornment={ startAdornment={
<> <>
<IconButton onClick={queueAllAugs} size="large"> <IconButton onClick={queueAllAugs} size="large">
<ReplyAllIcon /> <ReplyAll />
</IconButton> </IconButton>
<IconButton onClick={queueAug} size="large"> <IconButton onClick={queueAug} size="large">
<ReplyIcon /> <Reply />
</IconButton> </IconButton>
</> </>
} }
endAdornment={ endAdornment={
<> <>
<IconButton onClick={clearAugs} size="large"> <IconButton onClick={clearAugs} size="large">
<ClearIcon /> <Clear />
</IconButton> </IconButton>
</> </>
} }
@ -79,10 +75,9 @@ export function Augmentations(props: IProps): React.ReactElement {
</MenuItem> </MenuItem>
))} ))}
</Select> </Select>
</td> <Button sx={{ display: "block" }} onClick={clearQueuedAugs}>
</tr> Clear Queued Augmentations
</tbody> </Button>
</table>
</AccordionDetails> </AccordionDetails>
</Accordion> </Accordion>
); );

@ -94,7 +94,7 @@ export const GraftingRoot = (): React.ReactElement => {
<Typography variant="h5">Graft Augmentations</Typography> <Typography variant="h5">Graft Augmentations</Typography>
{getAvailableAugs(player).length > 0 ? ( {getAvailableAugs(player).length > 0 ? (
<Paper sx={{ my: 1, width: "fit-content", display: "grid", gridTemplateColumns: "1fr 3fr" }}> <Paper sx={{ my: 1, width: "fit-content", display: "grid", gridTemplateColumns: "1fr 3fr" }}>
<List sx={{ maxHeight: 400, overflowY: "scroll", borderRight: `1px solid ${Settings.theme.welllight}` }}> <List sx={{ height: 400, overflowY: "scroll", borderRight: `1px solid ${Settings.theme.welllight}` }}>
{getAvailableAugs(player).map((k, i) => ( {getAvailableAugs(player).map((k, i) => (
<ListItemButton key={i + 1} onClick={() => setSelectedAug(k)} selected={selectedAug === k}> <ListItemButton key={i + 1} onClick={() => setSelectedAug(k)} selected={selectedAug === k}>
<Typography>{k}</Typography> <Typography>{k}</Typography>

@ -1,144 +1,80 @@
import React, { useState, useEffect } from "react"; import { Paper, Table, TableBody, Box, IconButton, Typography, Container, Tooltip } from "@mui/material";
import { MoreHoriz, Info } from "@mui/icons-material";
import { numeralWrapper } from "./numeralFormat"; import React, { useEffect, useState } from "react";
import { convertTimeMsToTimeElapsedString } from "../utils/StringHelperFunctions";
import { BitNodeMultipliers } from "../BitNode/BitNodeMultipliers";
import { SourceFileFlags } from "../SourceFile/SourceFileFlags";
import { getPurchaseServerLimit } from "../Server/ServerPurchases";
import { HacknetServerConstants } from "../Hacknet/data/Constants";
import { StatsTable } from "./React/StatsTable";
import { Money } from "./React/Money";
import { use } from "./Context";
import { MoneySourceTracker } from "../utils/MoneySourceTracker";
import { BitNodes } from "../BitNode/BitNode"; import { BitNodes } from "../BitNode/BitNode";
import { BitNodeMultipliers } from "../BitNode/BitNodeMultipliers";
import Typography from "@mui/material/Typography"; import { HacknetServerConstants } from "../Hacknet/data/Constants";
import Box from "@mui/material/Box"; import { getPurchaseServerLimit } from "../Server/ServerPurchases";
import IconButton from "@mui/material/IconButton"; import { Settings } from "../Settings/Settings";
import MoreHorizIcon from "@mui/icons-material/MoreHoriz"; import { SourceFileFlags } from "../SourceFile/SourceFileFlags";
import { MoneySourceTracker } from "../utils/MoneySourceTracker";
import { convertTimeMsToTimeElapsedString } from "../utils/StringHelperFunctions";
import { use } from "./Context";
import { numeralWrapper } from "./numeralFormat";
import { Modal } from "./React/Modal"; import { Modal } from "./React/Modal";
import { Money } from "./React/Money";
import { StatsRow } from "./React/StatsRow";
import { StatsTable } from "./React/StatsTable";
import TableBody from "@mui/material/TableBody"; interface EmployersModalProps {
import { Table, TableCell } from "./React/Table"; open: boolean;
import TableRow from "@mui/material/TableRow"; onClose: () => void;
function LastEmployer(): React.ReactElement {
const player = use.Player();
if (player.companyName) {
return <Typography>Employer at which you last worked: {player.companyName}</Typography>;
}
return <></>;
} }
function LastJob(): React.ReactElement { const EmployersModal = ({ open, onClose }: EmployersModalProps): React.ReactElement => {
const player = use.Player(); const player = use.Player();
if (player.companyName !== "") {
return <Typography>Job you last worked: {player.jobs[player.companyName]}</Typography>;
}
return <></>;
}
function Employers(): React.ReactElement {
const player = use.Player();
if (player.jobs && Object.keys(player.jobs).length !== 0)
return ( return (
<Modal open={open} onClose={onClose}>
<> <>
<Typography>All Employers:</Typography> <Typography variant="h5">All Employers</Typography>
<ul> <ul>
{Object.keys(player.jobs).map((j) => ( {Object.keys(player.jobs).map((j) => (
<Typography key={j}> * {j}</Typography> <Typography key={j}>* {j}</Typography>
))} ))}
</ul> </ul>
</> </>
</Modal>
); );
return <></>; };
interface MultTableProps {
rows: (string | number)[][];
color: string;
noMargin?: boolean;
} }
function Hacknet(): React.ReactElement { function MultiplierTable(props: MultTableProps): React.ReactElement {
const player = use.Player();
// Can't import HacknetHelpers for some reason.
if (!(player.bitNodeN === 9 || SourceFileFlags[9] > 0)) {
return ( return (
<> <Table sx={{ display: "table", width: "100%", mb: (props.noMargin ?? false) === true ? 0 : 2 }}>
<Typography>{`Hacknet Nodes owned: ${player.hacknetNodes.length}`}</Typography>
<br />
</>
);
} else {
return (
<>
<Typography>{`Hacknet Servers owned: ${player.hacknetNodes.length} / ${HacknetServerConstants.MaxServers}`}</Typography>
<br />
</>
);
}
}
function Intelligence(): React.ReactElement {
const player = use.Player();
if (player.intelligence > 0 && (player.bitNodeN === 5 || SourceFileFlags[5] > 0)) {
return (
<TableRow>
<TableCell>
<Typography>Intelligence:&nbsp;</Typography>
</TableCell>
<TableCell align="right">
<Typography>{numeralWrapper.formatSkill(player.intelligence)}&nbsp;</Typography>
</TableCell>
<TableCell align="right">
<Typography noWrap>({numeralWrapper.formatExp(player.intelligence_exp)} exp)</Typography>
</TableCell>
</TableRow>
);
}
return <></>;
}
function MultiplierTable(props: any): React.ReactElement {
function bn5Stat(r: any): JSX.Element {
if (SourceFileFlags[5] > 0 && r.length > 2 && r[1] != r[2]) {
return (
<TableCell key="2" align="right">
<Typography noWrap>({numeralWrapper.formatPercentage(r[2])})</Typography>
</TableCell>
);
}
return <></>;
}
return (
<>
<Table size="small" padding="none">
<TableBody> <TableBody>
{props.rows.map((r: any) => ( {props.rows.map((data) => {
<TableRow key={r[0]}> const mult = data[0] as string,
<TableCell key="0"> value = data[1] as number,
<Typography noWrap>{`${r[0]} multiplier:`}&nbsp;</Typography> modded = data[2] as number | null;
</TableCell>
<TableCell key="1" align="right"> if (modded && modded !== value && SourceFileFlags[5] > 0) {
<Typography noWrap>{numeralWrapper.formatPercentage(r[1])}</Typography> return (
</TableCell> <StatsRow key={mult} name={mult} color={props.color} data={{}}>
{bn5Stat(r)} <>
</TableRow> <Typography color={props.color}>
))} <span style={{ opacity: 0.5 }}>{numeralWrapper.formatPercentage(value)}</span>{" "}
{numeralWrapper.formatPercentage(modded)}
</Typography>
</>
</StatsRow>
);
}
return (
<StatsRow
key={mult}
name={mult}
color={props.color}
data={{ content: numeralWrapper.formatPercentage(value) }}
/>
);
})}
</TableBody> </TableBody>
</Table> </Table>
</>
);
}
function BladeburnerMults(): React.ReactElement {
const player = use.Player();
if (!player.canAccessBladeburner()) return <></>;
return (
<MultiplierTable
rows={[
["Bladeburner Success Chance", player.bladeburner_success_chance_mult],
["Bladeburner Max Stamina", player.bladeburner_max_stamina_mult],
["Bladeburner Stamina Gain", player.bladeburner_stamina_gain_mult],
["Bladeburner Field Analysis", player.bladeburner_analysis_mult],
]}
/>
); );
} }
@ -146,15 +82,17 @@ function CurrentBitNode(): React.ReactElement {
const player = use.Player(); const player = use.Player();
if (player.sourceFiles.length > 0) { if (player.sourceFiles.length > 0) {
const index = "BitNode" + player.bitNodeN; const index = "BitNode" + player.bitNodeN;
const currentSourceFile = player.sourceFiles.find((sourceFile) => sourceFile.n == player.bitNodeN);
const lvl = currentSourceFile ? currentSourceFile.lvl : 0;
return ( return (
<> <Box>
<Typography variant="h4"> <Paper sx={{ p: 1 }}>
BitNode {player.bitNodeN}: {BitNodes[index].name} <Typography variant="h5">
BitNode {player.bitNodeN}: {BitNodes[index].name} (Level {lvl})
</Typography> </Typography>
<Typography sx={{ mx: 2 }} style={{ whiteSpace: "pre-wrap", overflowWrap: "break-word" }}> <Typography sx={{ whiteSpace: "pre-wrap", overflowWrap: "break-word" }}>{BitNodes[index].info}</Typography>
{BitNodes[index].info} </Paper>
</Typography> </Box>
</>
); );
} }
@ -262,6 +200,7 @@ function MoneyModal({ open, onClose }: IMoneyModalProps): React.ReactElement {
export function CharacterStats(): React.ReactElement { export function CharacterStats(): React.ReactElement {
const player = use.Player(); const player = use.Player();
const [moneyOpen, setMoneyOpen] = useState(false); const [moneyOpen, setMoneyOpen] = useState(false);
const [employersOpen, setEmployersOpen] = useState(false);
const setRerender = useState(false)[1]; const setRerender = useState(false)[1];
function rerender(): void { function rerender(): void {
setRerender((old) => !old); setRerender((old) => !old);
@ -273,111 +212,151 @@ export function CharacterStats(): React.ReactElement {
}, []); }, []);
const timeRows = [ const timeRows = [
["Time played since last Augmentation:", convertTimeMsToTimeElapsedString(player.playtimeSinceLastAug)], ["Since last Augmentation installation", convertTimeMsToTimeElapsedString(player.playtimeSinceLastAug)],
]; ];
if (player.sourceFiles.length > 0) { if (player.sourceFiles.length > 0) {
timeRows.push([ timeRows.push(["Since last Bitnode destroyed", convertTimeMsToTimeElapsedString(player.playtimeSinceLastBitnode)]);
"Time played since last Bitnode destroyed:",
convertTimeMsToTimeElapsedString(player.playtimeSinceLastBitnode),
]);
} }
timeRows.push(["Total Time played:", convertTimeMsToTimeElapsedString(player.totalPlaytime)]); timeRows.push(["Total", convertTimeMsToTimeElapsedString(player.totalPlaytime)]);
return ( return (
<> <Container maxWidth="lg" disableGutters sx={{ mx: 0 }}>
<Typography variant="h4">General</Typography>
<Box sx={{ mx: 2 }}>
<Typography>Current City: {player.city}</Typography>
<LastEmployer />
<LastJob />
<Employers />
<Typography>
Money: <Money money={player.money} />
<IconButton onClick={() => setMoneyOpen(true)}>
<MoreHorizIcon color="info" />
</IconButton>
</Typography>
</Box>
<br />
<Typography variant="h4">Stats</Typography> <Typography variant="h4">Stats</Typography>
<Box sx={{ mx: 2 }}> <Box sx={{ display: "grid", gridTemplateColumns: "1fr 1fr", minWidth: "fit-content", mb: 1, gap: 1 }}>
<Table size="small" padding="none"> <Paper sx={{ p: 1 }}>
<Typography variant="h5">General</Typography>
<Table>
<TableBody> <TableBody>
<TableRow> <StatsRow name="Current City" color={Settings.theme.primary} data={{ content: player.city }} />
<TableCell> <StatsRow name="Money" color={Settings.theme.money} data={{}}>
<Typography noWrap>Hacking:&nbsp;</Typography> <>
</TableCell> <Money money={player.money} />
<TableCell align="right"> <IconButton onClick={() => setMoneyOpen(true)} sx={{ p: 0 }}>
<Typography noWrap>{numeralWrapper.formatSkill(player.hacking)}&nbsp;</Typography> <MoreHoriz color="info" />
</TableCell> </IconButton>
<TableCell align="right"> </>
<Typography noWrap>({numeralWrapper.formatExp(player.hacking_exp)} exp)</Typography> </StatsRow>
</TableCell> {player.companyName ? (
</TableRow> <>
<TableRow> <StatsRow
<TableCell> name="Last Employer"
<Typography noWrap>Strength:&nbsp;</Typography> color={Settings.theme.primary}
</TableCell> data={{ content: player.companyName }}
<TableCell align="right"> />
<Typography noWrap>{numeralWrapper.formatSkill(player.strength)}&nbsp;</Typography> <StatsRow
</TableCell> name="Last Job"
<TableCell align="right"> color={Settings.theme.primary}
<Typography noWrap>({numeralWrapper.formatExp(player.strength_exp)} exp)</Typography> data={{ content: player.jobs[player.companyName] }}
</TableCell> />
</TableRow> </>
<TableRow> ) : (
<TableCell> <></>
<Typography noWrap>Defense:&nbsp;</Typography> )}
</TableCell> {player.jobs && Object.keys(player.jobs).length !== 0 ? (
<TableCell align="right"> <StatsRow name="All Employers" color={Settings.theme.primary} data={{}}>
<Typography noWrap>{numeralWrapper.formatSkill(player.defense)}&nbsp;</Typography> <>
</TableCell> <span style={{ color: Settings.theme.primary }}>{Object.keys(player.jobs).length} total</span>
<TableCell align="right"> <IconButton onClick={() => setEmployersOpen(true)} sx={{ p: 0 }}>
<Typography noWrap>({numeralWrapper.formatExp(player.defense_exp)} exp)</Typography> <MoreHoriz color="info" />
</TableCell> </IconButton>
</TableRow> </>
<TableRow> </StatsRow>
<TableCell> ) : (
<Typography noWrap>Dexterity:&nbsp;</Typography> <></>
</TableCell> )}
<TableCell align="right"> <StatsRow
<Typography noWrap>{numeralWrapper.formatSkill(player.dexterity)}&nbsp;</Typography> name="Servers Owned"
</TableCell> color={Settings.theme.primary}
<TableCell align="right"> data={{ content: `${player.purchasedServers.length} / ${getPurchaseServerLimit()}` }}
<Typography noWrap>({numeralWrapper.formatExp(player.dexterity_exp)} exp)</Typography> />
</TableCell> <StatsRow
</TableRow> name={`Hacknet ${player.bitNodeN === 9 || SourceFileFlags[9] > 0 ? "Servers" : "Nodes"} owned`}
<TableRow> color={Settings.theme.primary}
<TableCell> data={{
<Typography noWrap>Agility:&nbsp;</Typography> content: `${player.hacknetNodes.length}${
</TableCell> player.bitNodeN === 9 || SourceFileFlags[9] > 0 ? ` / ${HacknetServerConstants.MaxServers}` : ""
<TableCell align="right"> }`,
<Typography noWrap>{numeralWrapper.formatSkill(player.agility)}&nbsp;</Typography> }}
</TableCell> />
<TableCell align="right"> <StatsRow
<Typography noWrap>({numeralWrapper.formatExp(player.agility_exp)} exp)</Typography> name="Augmentations Installed"
</TableCell> color={Settings.theme.primary}
</TableRow> data={{ content: String(player.augmentations.length) }}
<TableRow> />
<TableCell>
<Typography noWrap>Charisma:&nbsp;</Typography>
</TableCell>
<TableCell align="right">
<Typography noWrap>{numeralWrapper.formatSkill(player.charisma)}&nbsp;</Typography>
</TableCell>
<TableCell align="right">
<Typography noWrap>({numeralWrapper.formatExp(player.charisma_exp)} exp)</Typography>
</TableCell>
</TableRow>
<Intelligence />
</TableBody> </TableBody>
</Table> </Table>
<br /> </Paper>
<Paper sx={{ p: 1 }}>
<Typography variant="h5">Skills</Typography>
<Table>
<TableBody>
<StatsRow
name="Hacking"
color={Settings.theme.hack}
data={{ level: player.hacking, exp: player.hacking_exp }}
/>
<StatsRow
name="Strength"
color={Settings.theme.combat}
data={{ level: player.strength, exp: player.strength_exp }}
/>
<StatsRow
name="Defense"
color={Settings.theme.combat}
data={{ level: player.defense, exp: player.defense_exp }}
/>
<StatsRow
name="Dexterity"
color={Settings.theme.combat}
data={{ level: player.dexterity, exp: player.dexterity_exp }}
/>
<StatsRow
name="Agility"
color={Settings.theme.combat}
data={{ level: player.agility, exp: player.agility_exp }}
/>
<StatsRow
name="Charisma"
color={Settings.theme.cha}
data={{ level: player.charisma, exp: player.charisma_exp }}
/>
{player.intelligence > 0 && (player.bitNodeN === 5 || SourceFileFlags[5] > 0) && (
<StatsRow
name="Intelligence"
color={Settings.theme.int}
data={{ level: player.intelligence, exp: player.intelligence_exp }}
/>
)}
</TableBody>
</Table>
</Paper>
</Box> </Box>
<Box sx={{ mb: 1 }}>
<Paper sx={{ p: 1 }}>
<Typography variant="h5" color="primary" sx={{ display: "flex", alignItems: "center", flexWrap: "wrap" }}>
Multipliers
{SourceFileFlags[5] > 0 && (
<Tooltip
title={
<Typography>
Displays your current multipliers.
<br /> <br />
<Typography variant="h4">Multipliers</Typography> <br />
<Box sx={{ mx: 2 }}> When there is a dim number next to a multiplier, that means that the multiplier in question is being
affected by BitNode multipliers.
<br />
<br />
The dim number is the raw multiplier, and the undimmed number is the effective multiplier, as
dictated by the BitNode.
</Typography>
}
>
<Info sx={{ ml: 1, mb: 0.5 }} color="info" />
</Tooltip>
)}
</Typography>
<Box sx={{ display: "grid", gridTemplateColumns: "1fr 1fr", gap: 1 }}>
<Box>
<MultiplierTable <MultiplierTable
rows={[ rows={[
["Hacking Chance", player.hacking_chance_mult], ["Hacking Chance", player.hacking_chance_mult],
@ -393,32 +372,45 @@ export function CharacterStats(): React.ReactElement {
player.hacking_grow_mult * BitNodeMultipliers.ServerGrowthRate, player.hacking_grow_mult * BitNodeMultipliers.ServerGrowthRate,
], ],
]} ]}
color={Settings.theme.hack}
/> />
<br />
<MultiplierTable <MultiplierTable
rows={[ rows={[
["Hacking Level", player.hacking_mult, player.hacking_mult * BitNodeMultipliers.HackingLevelMultiplier], [
["Hacking Experience", player.hacking_exp_mult, player.hacking_exp_mult * BitNodeMultipliers.HackExpGain], "Hacking Level",
player.hacking_mult,
player.hacking_mult * BitNodeMultipliers.HackingLevelMultiplier,
],
[
"Hacking Experience",
player.hacking_exp_mult,
player.hacking_exp_mult * BitNodeMultipliers.HackExpGain,
],
]} ]}
color={Settings.theme.hack}
/> />
<br />
<MultiplierTable <MultiplierTable
rows={[ rows={[
["Strength Level", player.strength_mult, player.strength_mult * BitNodeMultipliers.StrengthLevelMultiplier], [
"Strength Level",
player.strength_mult,
player.strength_mult * BitNodeMultipliers.StrengthLevelMultiplier,
],
["Strength Experience", player.strength_exp_mult], ["Strength Experience", player.strength_exp_mult],
]} ]}
color={Settings.theme.combat}
/> />
<br />
<MultiplierTable <MultiplierTable
rows={[ rows={[
["Defense Level", player.defense_mult, player.defense_mult * BitNodeMultipliers.DefenseLevelMultiplier], [
"Defense Level",
player.defense_mult,
player.defense_mult * BitNodeMultipliers.DefenseLevelMultiplier,
],
["Defense Experience", player.defense_exp_mult], ["Defense Experience", player.defense_exp_mult],
]} ]}
color={Settings.theme.combat}
/> />
<br />
<MultiplierTable <MultiplierTable
rows={[ rows={[
[ [
@ -428,25 +420,34 @@ export function CharacterStats(): React.ReactElement {
], ],
["Dexterity Experience", player.dexterity_exp_mult], ["Dexterity Experience", player.dexterity_exp_mult],
]} ]}
color={Settings.theme.combat}
/> />
<br />
<MultiplierTable <MultiplierTable
rows={[ rows={[
["Agility Level", player.agility_mult, player.agility_mult * BitNodeMultipliers.AgilityLevelMultiplier], [
"Agility Level",
player.agility_mult,
player.agility_mult * BitNodeMultipliers.AgilityLevelMultiplier,
],
["Agility Experience", player.agility_exp_mult], ["Agility Experience", player.agility_exp_mult],
]} ]}
color={Settings.theme.combat}
/> />
<br />
<MultiplierTable <MultiplierTable
rows={[ rows={[
["Charisma Level", player.charisma_mult, player.charisma_mult * BitNodeMultipliers.CharismaLevelMultiplier], [
"Charisma Level",
player.charisma_mult,
player.charisma_mult * BitNodeMultipliers.CharismaLevelMultiplier,
],
["Charisma Experience", player.charisma_exp_mult], ["Charisma Experience", player.charisma_exp_mult],
]} ]}
color={Settings.theme.cha}
noMargin
/> />
<br /> </Box>
<Box>
<MultiplierTable <MultiplierTable
rows={[ rows={[
[ [
@ -459,9 +460,8 @@ export function CharacterStats(): React.ReactElement {
["Hacknet Node Core purchase cost", player.hacknet_node_core_cost_mult], ["Hacknet Node Core purchase cost", player.hacknet_node_core_cost_mult],
["Hacknet Node level upgrade cost", player.hacknet_node_level_cost_mult], ["Hacknet Node level upgrade cost", player.hacknet_node_level_cost_mult],
]} ]}
color={Settings.theme.primary}
/> />
<br />
<MultiplierTable <MultiplierTable
rows={[ rows={[
["Company reputation gain", player.company_rep_mult], ["Company reputation gain", player.company_rep_mult],
@ -472,30 +472,47 @@ export function CharacterStats(): React.ReactElement {
], ],
["Salary", player.work_money_mult, player.work_money_mult * BitNodeMultipliers.CompanyWorkMoney], ["Salary", player.work_money_mult, player.work_money_mult * BitNodeMultipliers.CompanyWorkMoney],
]} ]}
color={Settings.theme.money}
/> />
<br />
<MultiplierTable <MultiplierTable
rows={[ rows={[
["Crime success", player.crime_success_mult], ["Crime success", player.crime_success_mult],
["Crime money", player.crime_money_mult, player.crime_money_mult * BitNodeMultipliers.CrimeMoney], ["Crime money", player.crime_money_mult, player.crime_money_mult * BitNodeMultipliers.CrimeMoney],
]} ]}
color={Settings.theme.combat}
/> />
<br /> {player.canAccessBladeburner() && (
<BladeburnerMults /> <MultiplierTable
rows={[
["Bladeburner Success Chance", player.bladeburner_success_chance_mult],
["Bladeburner Max Stamina", player.bladeburner_max_stamina_mult],
["Bladeburner Stamina Gain", player.bladeburner_stamina_gain_mult],
["Bladeburner Field Analysis", player.bladeburner_analysis_mult],
]}
color={Settings.theme.primary}
noMargin
/>
)}
</Box>
</Box>
</Paper>
</Box> </Box>
<br />
<Typography variant="h4">Misc</Typography> <Box sx={{ mb: 1 }}>
<Box sx={{ mx: 2 }}> <Paper sx={{ p: 1 }}>
<Typography>{`Servers owned: ${player.purchasedServers.length} / ${getPurchaseServerLimit()}`}</Typography> <Typography variant="h5">Time Played</Typography>
<Hacknet /> <Table>
<Typography>{`Augmentations installed: ${player.augmentations.length}`}</Typography> <TableBody>
<StatsTable rows={timeRows} /> {timeRows.map(([name, content]) => (
<StatsRow key={name} name={name} color={Settings.theme.primary} data={{ content: content }} />
))}
</TableBody>
</Table>
</Paper>
</Box> </Box>
<br />
<CurrentBitNode /> <CurrentBitNode />
<MoneyModal open={moneyOpen} onClose={() => setMoneyOpen(false)} /> <MoneyModal open={moneyOpen} onClose={() => setMoneyOpen(false)} />
</> <EmployersModal open={employersOpen} onClose={() => setEmployersOpen(false)} />
</Container>
); );
} }

@ -17,9 +17,10 @@ interface IProps {
color: string; color: string;
classes?: any; classes?: any;
data: ITableRowData; data: ITableRowData;
children?: React.ReactElement;
} }
export const StatsRow = ({ name, color, classes = useStyles(), data }: IProps): React.ReactElement => { export const StatsRow = ({ name, color, classes = useStyles(), children, data }: IProps): React.ReactElement => {
let content; let content;
if (data.content !== undefined) { if (data.content !== undefined) {
@ -36,7 +37,8 @@ export const StatsRow = ({ name, color, classes = useStyles(), data }: IProps):
<Typography style={{ color: color }}>{name}</Typography> <Typography style={{ color: color }}>{name}</Typography>
</TableCell> </TableCell>
<TableCell align="right" classes={{ root: classes.cellNone }}> <TableCell align="right" classes={{ root: classes.cellNone }}>
<Typography style={{ color: color }}>{content}</Typography> {content ? <Typography style={{ color: color }}>{content}</Typography> : <></>}
{children}
</TableCell> </TableCell>
</TableRow> </TableRow>
); );