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,81 +102,113 @@ 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 }}>
<Typography> <Paper sx={{ p: 1 }}>
Below is a list of all Augmentations you have purchased but not yet installed. Click the button below to <Typography variant="h5" color="primary" sx={{ display: "flex", alignItems: "center", flexWrap: "wrap" }}>
install them. Purchased Augmentations
</Typography> <Tooltip
<Typography>WARNING: Installing your Augmentations resets most of your progress, including:</Typography> title={
<br /> <>
<Typography>- Stats/Skill levels and Experience</Typography> <Typography>
<Typography>- Money</Typography> Below is a list of all Augmentations you have purchased but not yet installed. Click the button
<Typography>- Scripts on every computer but your home computer</Typography> below to install them.
<Typography>- Purchased servers</Typography> </Typography>
<Typography>- Hacknet Nodes</Typography> <Typography>
<Typography>- Faction/Company reputation</Typography> WARNING: Installing your Augmentations resets most of your progress, including:
<Typography>- Stocks</Typography> </Typography>
<br /> <br />
<Typography> <Typography>- Stats/Skill levels and Experience</Typography>
Installing Augmentations lets you start over with the perks and benefits granted by all of the Augmentations <Typography>- Money</Typography>
you have ever installed. Also, you will keep any scripts and RAM/Core upgrades on your home computer (but you <Typography>- Scripts on every computer but your home computer</Typography>
will lose all programs besides NUKE.exe) <Typography>- Purchased servers</Typography>
</Typography> <Typography>- Hacknet Nodes</Typography>
<Typography>- Faction/Company reputation</Typography>
<Typography>- Stocks</Typography>
<br />
<Typography>
Installing Augmentations lets you start over with the perks and benefits granted by all of the
Augmentations you have ever installed. Also, you will keep any scripts and RAM/Core upgrades on your
home computer (but you will lose all programs besides NUKE.exe)
</Typography>
</>
}
>
<Info sx={{ ml: 1, mb: 0.5 }} color="info" />
</Tooltip>
</Typography>
<ConfirmationModal
open={installOpen}
onClose={() => setInstallOpen(false)}
onConfirm={props.installAugmentationsFn}
confirmationText={
<>
Installing will reset
<br />
<br />- money
<br />- skill / experience
<br />- every server except home
<br />- factions and reputation
<br />
<br />
You will keep:
<br />
<br />- All scripts on home
<br />- home ram and cores
<br />
<br />
It is recommended to install several Augmentations at once. Preferably everything from any faction of
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>}>
<Button sx={{ width: "100%" }} onClick={doExport} color="error">
Backup Save {exportBonusStr()}
</Button>
</Tooltip>
</Box>
</Paper>
{player.queuedAugmentations.length > 0 ? (
<Box sx={{ display: "grid", gridTemplateColumns: "1fr 3fr" }}>
<PurchasedAugmentations />
<PlayerMultipliers />
</Box>
) : (
<Paper sx={{ p: 1 }}>
<Typography>No Augmentations have been purchased yet</Typography>
</Paper>
)}
</Box> </Box>
<Typography variant="h4" color="primary">
Purchased Augmentations <Box
</Typography> sx={{
<Box mx={2}> my: 1,
<Tooltip title={<Typography>'I never asked for this'</Typography>}> display: "grid",
<span> gridTemplateColumns: `repeat(${
<Button disabled={player.queuedAugmentations.length === 0} onClick={doInstall}> +!!((player.augmentations.find((e) => e.name === AugmentationNames.NeuroFluxGovernor)?.level ?? 0) > 0) +
Install Augmentations +!!(player.entropy > 0)
</Button> }, 1fr)`,
</span> gap: 1,
</Tooltip> }}
<ConfirmationModal >
open={installOpen} <NeuroFluxDisplay player={player} />
onClose={() => setInstallOpen(false)} <EntropyDisplay player={player} />
onConfirm={props.installAugmentationsFn}
confirmationText={
<>
Installing will reset
<br />
<br />- money
<br />- skill / experience
<br />- every server except home
<br />- factions and reputation
<br />
<br />
You will keep:
<br />
<br />- All scripts on home
<br />- home ram and cores
<br />
<br />
It is recommended to install several Augmentations at once. Preferably everything from any faction of your
choosing.
</>
}
/>
<Tooltip title={<Typography>It's always a good idea to backup/export your save!</Typography>}>
<Button sx={{ mx: 2 }} onClick={doExport} color="error">
Backup Save {exportBonusStr()}
</Button>
</Tooltip>
<PurchasedAugmentations />
</Box> </Box>
<Typography variant="h4">Installed Augmentations</Typography>
<Box mx={2}> <Box>
<Typography>
List of all Augmentations that have been installed. You have gained the effects of these.
</Typography>
<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%" }}>
<Tooltip title={"Sorts the Augmentations alphabetically in numeral order"}> <Paper sx={{ p: 1 }}>
<Button onClick={sortInOrder}>Sort in Order</Button> <Typography variant="h5">Installed Augmentations</Typography>
</Tooltip> <Box sx={{ display: "grid", gridTemplateColumns: "1fr 1fr" }}>
<Tooltip title={"Sorts the Augmentations based on when you acquired them (same as default)"}> <Tooltip title={"Sorts the Augmentations alphabetically in numeral order"}>
<Button sx={{ mx: 2 }} onClick={sortByAcquirementTime}> <Button sx={{ width: "100%" }} onClick={sortInOrder}>
Sort by Acquirement Time Sort in Order
</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}> </List>
<Typography color={Settings.theme.hp}> </Box>
<b>All multipliers decreased by:</b>{" "} <Box sx={{ m: 1 }}>
{formatNumber((1 - CONSTANTS.EntropyEffect ** player.entropy) * 100, 3)}% (multiplicative) <Typography variant="h6" sx={{ display: "flex", alignItems: "center", flexWrap: "wrap" }}>
</Typography> {selectedAug.name}
</Box> </Typography>
</Collapse> <Typography sx={{ maxHeight: 350, overflowY: "scroll" }}>
</Box> {(() => {
); const aug = Augmentations[selectedAug.name];
})()}
{sourceAugs.map((e) => { const info = typeof aug.info === "string" ? <span>{aug.info}</span> : aug.info;
const aug = Augmentations[e.name]; const tooltip = (
<>
let level = null; {info}
if (e.name === AugmentationNames.NeuroFluxGovernor) { <br />
level = e.level; <br />
} {aug.stats}
</>
return <AugmentationAccordion key={aug.name} aug={aug} level={level} />; );
})} return tooltip;
</List> })()}
</> </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,268 +27,249 @@ 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]}> </Typography>
<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>
</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 ["Hacking Chance ", Player.hacking_chance_mult, Player.hacking_chance_mult * mults.hacking_chance_mult, 1],
rows={[ ["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],
"Bladeburner Success Chance", ["Hacking Growth ", Player.hacking_grow_mult, Player.hacking_grow_mult * mults.hacking_grow_mult, 1],
Player.bladeburner_success_chance_mult, [
Player.bladeburner_success_chance_mult * mults.bladeburner_success_chance_mult, "Hacking Level ",
1, Player.hacking_mult,
], Player.hacking_mult * mults.hacking_mult,
[ BitNodeMultipliers.HackingLevelMultiplier,
"Bladeburner Max Stamina", ],
Player.bladeburner_max_stamina_mult, [
Player.bladeburner_max_stamina_mult * mults.bladeburner_max_stamina_mult, "Hacking Experience ",
1, Player.hacking_exp_mult,
], Player.hacking_exp_mult * mults.hacking_exp_mult,
[ BitNodeMultipliers.HackExpGain,
"Bladeburner Stamina Gain", ],
Player.bladeburner_stamina_gain_mult, ].map((data): MultiplierListItemData => (data as any).concat([Settings.theme.hack])),
Player.bladeburner_stamina_gain_mult * mults.bladeburner_stamina_gain_mult, ...[
1, [
], "Strength Level ",
[ Player.strength_mult,
"Bladeburner Field Analysis", Player.strength_mult * mults.strength_mult,
Player.bladeburner_analysis_mult, BitNodeMultipliers.StrengthLevelMultiplier,
Player.bladeburner_analysis_mult * mults.bladeburner_analysis_mult, ],
1, ["Strength Experience ", Player.strength_exp_mult, Player.strength_exp_mult * mults.strength_exp_mult, 1],
], [
]} "Defense Level ",
/> Player.defense_mult,
<br /> Player.defense_mult * mults.defense_mult,
</> BitNodeMultipliers.DefenseLevelMultiplier,
],
["Defense Experience ", Player.defense_exp_mult, Player.defense_exp_mult * mults.defense_exp_mult, 1],
[
"Dexterity Level ",
Player.dexterity_mult,
Player.dexterity_mult * mults.dexterity_mult,
BitNodeMultipliers.DexterityLevelMultiplier,
],
["Dexterity Experience ", Player.dexterity_exp_mult, Player.dexterity_exp_mult * mults.dexterity_exp_mult, 1],
[
"Agility Level ",
Player.agility_mult,
Player.agility_mult * mults.agility_mult,
BitNodeMultipliers.AgilityLevelMultiplier,
],
["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])),
[
"Charisma Level ",
Player.charisma_mult,
Player.charisma_mult * mults.charisma_mult,
BitNodeMultipliers.CharismaLevelMultiplier,
Settings.theme.cha,
],
[
"Charisma Experience ",
Player.charisma_exp_mult,
Player.charisma_exp_mult * mults.charisma_exp_mult,
1,
Settings.theme.cha,
],
];
const rightColData: MultiplierListItemData[] = [
...[
[
"Hacknet Node production ",
Player.hacknet_node_money_mult,
Player.hacknet_node_money_mult * mults.hacknet_node_money_mult,
BitNodeMultipliers.HacknetNodeMoney,
],
[
"Hacknet Node purchase cost ",
Player.hacknet_node_purchase_cost_mult,
Player.hacknet_node_purchase_cost_mult * mults.hacknet_node_purchase_cost_mult,
1,
],
[
"Hacknet Node RAM upgrade cost ",
Player.hacknet_node_ram_cost_mult,
Player.hacknet_node_ram_cost_mult * mults.hacknet_node_ram_cost_mult,
1,
],
[
"Hacknet Node Core purchase cost ",
Player.hacknet_node_core_cost_mult,
Player.hacknet_node_core_cost_mult * mults.hacknet_node_core_cost_mult,
1,
],
[
"Hacknet Node level upgrade cost ",
Player.hacknet_node_level_cost_mult,
Player.hacknet_node_level_cost_mult * mults.hacknet_node_level_cost_mult,
1,
],
["Company reputation gain ", Player.company_rep_mult, Player.company_rep_mult * mults.company_rep_mult, 1],
[
"Faction reputation gain ",
Player.faction_rep_mult,
Player.faction_rep_mult * mults.faction_rep_mult,
BitNodeMultipliers.FactionWorkRepGain,
],
].map((data): MultiplierListItemData => (data as any).concat([Settings.theme.primary])),
[
"Salary ",
Player.work_money_mult,
Player.work_money_mult * mults.work_money_mult,
BitNodeMultipliers.CompanyWorkMoney,
Settings.theme.money,
],
[
"Crime success ",
Player.crime_success_mult,
Player.crime_success_mult * mults.crime_success_mult,
1,
Settings.theme.combat,
],
[
"Crime money ",
Player.crime_money_mult,
Player.crime_money_mult * mults.crime_money_mult,
BitNodeMultipliers.CrimeMoney,
Settings.theme.money,
],
];
if (Player.canAccessBladeburner()) {
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 ( return (
<> <Paper
<Typography variant="h4">Multipliers</Typography> sx={{
<Box mx={2}> p: 1,
<MultiplierTable maxHeight: 400,
rows={[ overflowY: "scroll",
["Hacking Chance ", Player.hacking_chance_mult, Player.hacking_chance_mult * mults.hacking_chance_mult, 1], display: "grid",
["Hacking Speed ", Player.hacking_speed_mult, Player.hacking_speed_mult * mults.hacking_speed_mult, 1], gridTemplateColumns: `repeat(${hasLeftImprovements + hasRightImprovements}, 1fr)`,
["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], >
]} <MultiplierList rows={leftColData} />
/> <MultiplierList rows={rightColData} />
<br /> </Paper>
<MultiplierTable
rows={[
[
"Hacking Level ",
Player.hacking_mult,
Player.hacking_mult * mults.hacking_mult,
BitNodeMultipliers.HackingLevelMultiplier,
],
[
"Hacking Experience ",
Player.hacking_exp_mult,
Player.hacking_exp_mult * mults.hacking_exp_mult,
BitNodeMultipliers.HackExpGain,
],
]}
/>
<br />
<MultiplierTable
rows={[
[
"Strength Level ",
Player.strength_mult,
Player.strength_mult * mults.strength_mult,
BitNodeMultipliers.StrengthLevelMultiplier,
],
["Strength Experience ", Player.strength_exp_mult, Player.strength_exp_mult * mults.strength_exp_mult, 1],
]}
/>
<br />
<MultiplierTable
rows={[
[
"Defense Level ",
Player.defense_mult,
Player.defense_mult * mults.defense_mult,
BitNodeMultipliers.DefenseLevelMultiplier,
],
["Defense Experience ", Player.defense_exp_mult, Player.defense_exp_mult * mults.defense_exp_mult, 1],
]}
/>
<br />
<MultiplierTable
rows={[
[
"Dexterity Level ",
Player.dexterity_mult,
Player.dexterity_mult * mults.dexterity_mult,
BitNodeMultipliers.DexterityLevelMultiplier,
],
[
"Dexterity Experience ",
Player.dexterity_exp_mult,
Player.dexterity_exp_mult * mults.dexterity_exp_mult,
1,
],
]}
/>
<br />
<MultiplierTable
rows={[
[
"Agility Level ",
Player.agility_mult,
Player.agility_mult * mults.agility_mult,
BitNodeMultipliers.AgilityLevelMultiplier,
],
["Agility Experience ", Player.agility_exp_mult, Player.agility_exp_mult * mults.agility_exp_mult, 1],
]}
/>
<br />
<MultiplierTable
rows={[
[
"Charisma Level ",
Player.charisma_mult,
Player.charisma_mult * mults.charisma_mult,
BitNodeMultipliers.CharismaLevelMultiplier,
],
["Charisma Experience ", Player.charisma_exp_mult, Player.charisma_exp_mult * mults.charisma_exp_mult, 1],
]}
/>
<br />
<MultiplierTable
rows={[
[
"Hacknet Node production ",
Player.hacknet_node_money_mult,
Player.hacknet_node_money_mult * mults.hacknet_node_money_mult,
BitNodeMultipliers.HacknetNodeMoney,
],
[
"Hacknet Node purchase cost ",
Player.hacknet_node_purchase_cost_mult,
Player.hacknet_node_purchase_cost_mult * mults.hacknet_node_purchase_cost_mult,
1,
],
[
"Hacknet Node RAM upgrade cost ",
Player.hacknet_node_ram_cost_mult,
Player.hacknet_node_ram_cost_mult * mults.hacknet_node_ram_cost_mult,
1,
],
[
"Hacknet Node Core purchase cost ",
Player.hacknet_node_core_cost_mult,
Player.hacknet_node_core_cost_mult * mults.hacknet_node_core_cost_mult,
1,
],
[
"Hacknet Node level upgrade cost ",
Player.hacknet_node_level_cost_mult,
Player.hacknet_node_level_cost_mult * mults.hacknet_node_level_cost_mult,
1,
],
]}
/>
<br />
<MultiplierTable
rows={[
["Company reputation gain ", Player.company_rep_mult, Player.company_rep_mult * mults.company_rep_mult, 1],
[
"Faction reputation gain ",
Player.faction_rep_mult,
Player.faction_rep_mult * mults.faction_rep_mult,
BitNodeMultipliers.FactionWorkRepGain,
],
[
"Salary ",
Player.work_money_mult,
Player.work_money_mult * mults.work_money_mult,
BitNodeMultipliers.CompanyWorkMoney,
],
]}
/>
<br />
<MultiplierTable
rows={[
["Crime success ", Player.crime_success_mult, Player.crime_success_mult * mults.crime_success_mult, 1],
[
"Crime money ",
Player.crime_money_mult,
Player.crime_money_mult * mults.crime_money_mult,
BitNodeMultipliers.CrimeMoney,
],
]}
/>
<br />
<BladeburnerMults />
</Box>
</>
); );
} }

@ -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} />);
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 <List dense>{augs}</List>; 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";
interface SfMinus1 {
info: React.ReactElement;
n: number;
name: string;
lvl: number;
}
const safeGetSf = (sfNum: number): SourceFile | SfMinus1 | null => {
if (sfNum === -1) {
const sfMinus1: SfMinus1 = {
info: (
<>
This Source-File can only be acquired with obscure knowledge of the game, javascript, and the web ecosystem.
<br />
<br />
It increases all of the player's multipliers by 0.1%
<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]);
export function SourceFiles(): React.ReactElement {
return ( return (
<> <Box sx={{ width: "100%", mt: 1 }}>
<Typography variant="h4">Source Files</Typography> <Paper sx={{ p: 1 }}>
<Box mx={2}> <Typography variant="h5">Source Files</Typography>
<List dense> </Paper>
<SourceFileMinus1 /> <Paper sx={{ display: "grid", gridTemplateColumns: "1fr 3fr" }}>
<OwnedSourceFiles /> <Box>
</List> <List
</Box> 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>
</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,50 +38,46 @@ 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> <Select
<tbody> onChange={setAugmentationDropdown}
<tr> value={augmentation}
<td> startAdornment={
<Typography>Aug:</Typography> <>
</td> <IconButton onClick={queueAllAugs} size="large">
<td> <ReplyAll />
<Select </IconButton>
onChange={setAugmentationDropdown} <IconButton onClick={queueAug} size="large">
value={augmentation} <Reply />
startAdornment={ </IconButton>
<> </>
<IconButton onClick={queueAllAugs} size="large"> }
<ReplyAllIcon /> endAdornment={
</IconButton> <>
<IconButton onClick={queueAug} size="large"> <IconButton onClick={clearAugs} size="large">
<ReplyIcon /> <Clear />
</IconButton> </IconButton>
</> </>
} }
endAdornment={ >
<> {Object.values(AugmentationNames).map((aug) => (
<IconButton onClick={clearAugs} size="large"> <MenuItem key={aug} value={aug}>
<ClearIcon /> {aug}
</IconButton> </MenuItem>
</> ))}
} </Select>
> <Button sx={{ display: "block" }} onClick={clearQueuedAugs}>
{Object.values(AugmentationNames).map((aug) => ( Clear Queued Augmentations
<MenuItem key={aug} value={aug}> </Button>
{aug}
</MenuItem>
))}
</Select>
</td>
</tr>
</tbody>
</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 (
return <Typography>Job you last worked: {player.jobs[player.companyName]}</Typography>; <Modal open={open} onClose={onClose}>
}
return <></>;
}
function Employers(): React.ReactElement {
const player = use.Player();
if (player.jobs && Object.keys(player.jobs).length !== 0)
return (
<> <>
<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 <></>;
}
function Hacknet(): React.ReactElement {
const player = use.Player();
// Can't import HacknetHelpers for some reason.
if (!(player.bitNodeN === 9 || SourceFileFlags[9] > 0)) {
return (
<>
<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>
{props.rows.map((r: any) => (
<TableRow key={r[0]}>
<TableCell key="0">
<Typography noWrap>{`${r[0]} multiplier:`}&nbsp;</Typography>
</TableCell>
<TableCell key="1" align="right">
<Typography noWrap>{numeralWrapper.formatPercentage(r[1])}</Typography>
</TableCell>
{bn5Stat(r)}
</TableRow>
))}
</TableBody>
</Table>
</>
); );
};
interface MultTableProps {
rows: (string | number)[][];
color: string;
noMargin?: boolean;
} }
function BladeburnerMults(): React.ReactElement { function MultiplierTable(props: MultTableProps): React.ReactElement {
const player = use.Player();
if (!player.canAccessBladeburner()) return <></>;
return ( return (
<MultiplierTable <Table sx={{ display: "table", width: "100%", mb: (props.noMargin ?? false) === true ? 0 : 2 }}>
rows={[ <TableBody>
["Bladeburner Success Chance", player.bladeburner_success_chance_mult], {props.rows.map((data) => {
["Bladeburner Max Stamina", player.bladeburner_max_stamina_mult], const mult = data[0] as string,
["Bladeburner Stamina Gain", player.bladeburner_stamina_gain_mult], value = data[1] as number,
["Bladeburner Field Analysis", player.bladeburner_analysis_mult], modded = data[2] as number | null;
]}
/> if (modded && modded !== value && SourceFileFlags[5] > 0) {
return (
<StatsRow key={mult} name={mult} color={props.color} data={{}}>
<>
<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>
</Table>
); );
} }
@ -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">
</Typography> BitNode {player.bitNodeN}: {BitNodes[index].name} (Level {lvl})
<Typography sx={{ mx: 2 }} style={{ whiteSpace: "pre-wrap", overflowWrap: "break-word" }}> </Typography>
{BitNodes[index].info} <Typography sx={{ whiteSpace: "pre-wrap", overflowWrap: "break-word" }}>{BitNodes[index].info}</Typography>
</Typography> </Paper>
</> </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,229 +212,307 @@ 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 }}>
<TableBody> <Typography variant="h5">General</Typography>
<TableRow> <Table>
<TableCell> <TableBody>
<Typography noWrap>Hacking:&nbsp;</Typography> <StatsRow name="Current City" color={Settings.theme.primary} data={{ content: player.city }} />
</TableCell> <StatsRow name="Money" color={Settings.theme.money} data={{}}>
<TableCell align="right"> <>
<Typography noWrap>{numeralWrapper.formatSkill(player.hacking)}&nbsp;</Typography> <Money money={player.money} />
</TableCell> <IconButton onClick={() => setMoneyOpen(true)} sx={{ p: 0 }}>
<TableCell align="right"> <MoreHoriz color="info" />
<Typography noWrap>({numeralWrapper.formatExp(player.hacking_exp)} exp)</Typography> </IconButton>
</TableCell> </>
</TableRow> </StatsRow>
<TableRow> {player.companyName ? (
<TableCell> <>
<Typography noWrap>Strength:&nbsp;</Typography> <StatsRow
</TableCell> name="Last Employer"
<TableCell align="right"> color={Settings.theme.primary}
<Typography noWrap>{numeralWrapper.formatSkill(player.strength)}&nbsp;</Typography> data={{ content: player.companyName }}
</TableCell> />
<TableCell align="right"> <StatsRow
<Typography noWrap>({numeralWrapper.formatExp(player.strength_exp)} exp)</Typography> name="Last Job"
</TableCell> color={Settings.theme.primary}
</TableRow> data={{ content: player.jobs[player.companyName] }}
<TableRow> />
<TableCell> </>
<Typography noWrap>Defense:&nbsp;</Typography> ) : (
</TableCell> <></>
<TableCell align="right"> )}
<Typography noWrap>{numeralWrapper.formatSkill(player.defense)}&nbsp;</Typography> {player.jobs && Object.keys(player.jobs).length !== 0 ? (
</TableCell> <StatsRow name="All Employers" color={Settings.theme.primary} data={{}}>
<TableCell align="right"> <>
<Typography noWrap>({numeralWrapper.formatExp(player.defense_exp)} exp)</Typography> <span style={{ color: Settings.theme.primary }}>{Object.keys(player.jobs).length} total</span>
</TableCell> <IconButton onClick={() => setEmployersOpen(true)} sx={{ p: 0 }}>
</TableRow> <MoreHoriz color="info" />
<TableRow> </IconButton>
<TableCell> </>
<Typography noWrap>Dexterity:&nbsp;</Typography> </StatsRow>
</TableCell> ) : (
<TableCell align="right"> <></>
<Typography noWrap>{numeralWrapper.formatSkill(player.dexterity)}&nbsp;</Typography> )}
</TableCell> <StatsRow
<TableCell align="right"> name="Servers Owned"
<Typography noWrap>({numeralWrapper.formatExp(player.dexterity_exp)} exp)</Typography> color={Settings.theme.primary}
</TableCell> data={{ content: `${player.purchasedServers.length} / ${getPurchaseServerLimit()}` }}
</TableRow> />
<TableRow> <StatsRow
<TableCell> name={`Hacknet ${player.bitNodeN === 9 || SourceFileFlags[9] > 0 ? "Servers" : "Nodes"} owned`}
<Typography noWrap>Agility:&nbsp;</Typography> color={Settings.theme.primary}
</TableCell> data={{
<TableCell align="right"> content: `${player.hacknetNodes.length}${
<Typography noWrap>{numeralWrapper.formatSkill(player.agility)}&nbsp;</Typography> player.bitNodeN === 9 || SourceFileFlags[9] > 0 ? ` / ${HacknetServerConstants.MaxServers}` : ""
</TableCell> }`,
<TableCell align="right"> }}
<Typography noWrap>({numeralWrapper.formatExp(player.agility_exp)} exp)</Typography> />
</TableCell> <StatsRow
</TableRow> name="Augmentations Installed"
<TableRow> color={Settings.theme.primary}
<TableCell> data={{ content: String(player.augmentations.length) }}
<Typography noWrap>Charisma:&nbsp;</Typography> />
</TableCell> </TableBody>
<TableCell align="right"> </Table>
<Typography noWrap>{numeralWrapper.formatSkill(player.charisma)}&nbsp;</Typography> </Paper>
</TableCell> <Paper sx={{ p: 1 }}>
<TableCell align="right"> <Typography variant="h5">Skills</Typography>
<Typography noWrap>({numeralWrapper.formatExp(player.charisma_exp)} exp)</Typography> <Table>
</TableCell> <TableBody>
</TableRow> <StatsRow
<Intelligence /> name="Hacking"
</TableBody> color={Settings.theme.hack}
</Table> data={{ level: player.hacking, exp: player.hacking_exp }}
<br /> />
<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>
<br /> <Box sx={{ mb: 1 }}>
<Typography variant="h4">Multipliers</Typography> <Paper sx={{ p: 1 }}>
<Box sx={{ mx: 2 }}> <Typography variant="h5" color="primary" sx={{ display: "flex", alignItems: "center", flexWrap: "wrap" }}>
<MultiplierTable Multipliers
rows={[ {SourceFileFlags[5] > 0 && (
["Hacking Chance", player.hacking_chance_mult], <Tooltip
["Hacking Speed", player.hacking_speed_mult], title={
[ <Typography>
"Hacking Money", Displays your current multipliers.
player.hacking_money_mult, <br />
player.hacking_money_mult * BitNodeMultipliers.ScriptHackMoney, <br />
], When there is a dim number next to a multiplier, that means that the multiplier in question is being
[ affected by BitNode multipliers.
"Hacking Growth", <br />
player.hacking_grow_mult, <br />
player.hacking_grow_mult * BitNodeMultipliers.ServerGrowthRate, The dim number is the raw multiplier, and the undimmed number is the effective multiplier, as
], dictated by the BitNode.
]} </Typography>
/> }
<br /> >
<MultiplierTable <Info sx={{ ml: 1, mb: 0.5 }} color="info" />
rows={[ </Tooltip>
["Hacking Level", player.hacking_mult, player.hacking_mult * BitNodeMultipliers.HackingLevelMultiplier], )}
["Hacking Experience", player.hacking_exp_mult, player.hacking_exp_mult * BitNodeMultipliers.HackExpGain], </Typography>
]} <Box sx={{ display: "grid", gridTemplateColumns: "1fr 1fr", gap: 1 }}>
/> <Box>
<br /> <MultiplierTable
rows={[
["Hacking Chance", player.hacking_chance_mult],
["Hacking Speed", player.hacking_speed_mult],
[
"Hacking Money",
player.hacking_money_mult,
player.hacking_money_mult * BitNodeMultipliers.ScriptHackMoney,
],
[
"Hacking Growth",
player.hacking_grow_mult,
player.hacking_grow_mult * BitNodeMultipliers.ServerGrowthRate,
],
]}
color={Settings.theme.hack}
/>
<MultiplierTable
rows={[
[
"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}
/>
<MultiplierTable
rows={[
[
"Strength Level",
player.strength_mult,
player.strength_mult * BitNodeMultipliers.StrengthLevelMultiplier,
],
["Strength Experience", player.strength_exp_mult],
]}
color={Settings.theme.combat}
/>
<MultiplierTable
rows={[
[
"Defense Level",
player.defense_mult,
player.defense_mult * BitNodeMultipliers.DefenseLevelMultiplier,
],
["Defense Experience", player.defense_exp_mult],
]}
color={Settings.theme.combat}
/>
<MultiplierTable
rows={[
[
"Dexterity Level",
player.dexterity_mult,
player.dexterity_mult * BitNodeMultipliers.DexterityLevelMultiplier,
],
["Dexterity Experience", player.dexterity_exp_mult],
]}
color={Settings.theme.combat}
/>
<MultiplierTable
rows={[
[
"Agility Level",
player.agility_mult,
player.agility_mult * BitNodeMultipliers.AgilityLevelMultiplier,
],
["Agility Experience", player.agility_exp_mult],
]}
color={Settings.theme.combat}
/>
<MultiplierTable
rows={[
[
"Charisma Level",
player.charisma_mult,
player.charisma_mult * BitNodeMultipliers.CharismaLevelMultiplier,
],
["Charisma Experience", player.charisma_exp_mult],
]}
color={Settings.theme.cha}
noMargin
/>
</Box>
<MultiplierTable <Box>
rows={[ <MultiplierTable
["Strength Level", player.strength_mult, player.strength_mult * BitNodeMultipliers.StrengthLevelMultiplier], rows={[
["Strength Experience", player.strength_exp_mult], [
]} "Hacknet Node production",
/> player.hacknet_node_money_mult,
<br /> player.hacknet_node_money_mult * BitNodeMultipliers.HacknetNodeMoney,
],
<MultiplierTable ["Hacknet Node purchase cost", player.hacknet_node_purchase_cost_mult],
rows={[ ["Hacknet Node RAM upgrade cost", player.hacknet_node_ram_cost_mult],
["Defense Level", player.defense_mult, player.defense_mult * BitNodeMultipliers.DefenseLevelMultiplier], ["Hacknet Node Core purchase cost", player.hacknet_node_core_cost_mult],
["Defense Experience", player.defense_exp_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],
[ [
"Dexterity Level", "Faction reputation gain",
player.dexterity_mult, player.faction_rep_mult,
player.dexterity_mult * BitNodeMultipliers.DexterityLevelMultiplier, player.faction_rep_mult * BitNodeMultipliers.FactionWorkRepGain,
], ],
["Dexterity Experience", player.dexterity_exp_mult], ["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],
["Agility Level", player.agility_mult, player.agility_mult * BitNodeMultipliers.AgilityLevelMultiplier], ["Crime money", player.crime_money_mult, player.crime_money_mult * BitNodeMultipliers.CrimeMoney],
["Agility Experience", player.agility_exp_mult], ]}
]} color={Settings.theme.combat}
/> />
<br /> {player.canAccessBladeburner() && (
<MultiplierTable
<MultiplierTable rows={[
rows={[ ["Bladeburner Success Chance", player.bladeburner_success_chance_mult],
["Charisma Level", player.charisma_mult, player.charisma_mult * BitNodeMultipliers.CharismaLevelMultiplier], ["Bladeburner Max Stamina", player.bladeburner_max_stamina_mult],
["Charisma Experience", player.charisma_exp_mult], ["Bladeburner Stamina Gain", player.bladeburner_stamina_gain_mult],
]} ["Bladeburner Field Analysis", player.bladeburner_analysis_mult],
/> ]}
<br /> color={Settings.theme.primary}
noMargin
<MultiplierTable />
rows={[ )}
[ </Box>
"Hacknet Node production", </Box>
player.hacknet_node_money_mult, </Paper>
player.hacknet_node_money_mult * BitNodeMultipliers.HacknetNodeMoney,
],
["Hacknet Node purchase cost", player.hacknet_node_purchase_cost_mult],
["Hacknet Node RAM upgrade cost", player.hacknet_node_ram_cost_mult],
["Hacknet Node Core purchase cost", player.hacknet_node_core_cost_mult],
["Hacknet Node level upgrade cost", player.hacknet_node_level_cost_mult],
]}
/>
<br />
<MultiplierTable
rows={[
["Company reputation gain", player.company_rep_mult],
[
"Faction reputation gain",
player.faction_rep_mult,
player.faction_rep_mult * BitNodeMultipliers.FactionWorkRepGain,
],
["Salary", player.work_money_mult, player.work_money_mult * BitNodeMultipliers.CompanyWorkMoney],
]}
/>
<br />
<MultiplierTable
rows={[
["Crime success", player.crime_success_mult],
["Crime money", player.crime_money_mult, player.crime_money_mult * BitNodeMultipliers.CrimeMoney],
]}
/>
<br />
<BladeburnerMults />
</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>
); );