Merge pull request #3414 from danielyxie/dev

clean up root files.
This commit is contained in:
hydroflame 2022-04-12 17:38:31 -04:00 committed by GitHub
commit 997f20aa68
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
56 changed files with 1797 additions and 1487 deletions

@ -1,3 +1,3 @@
last 4 versions last 8 versions
not dead not dead
not ie <= 11 not ie <= 11

@ -353,6 +353,7 @@ module.exports = {
"no-useless-constructor": [ "no-useless-constructor": [
"off", // Valid for typescript due to property ctor shorthand "off", // Valid for typescript due to property ctor shorthand
], ],
"@typescript-eslint/no-unused-vars": ["error", { argsIgnorePattern: "^_" }],
"@typescript-eslint/ban-ts-comment": "off", "@typescript-eslint/ban-ts-comment": "off",
"@typescript-eslint/ban-ts-ignore": "off", "@typescript-eslint/ban-ts-ignore": "off",
"@typescript-eslint/camelcase": "off", "@typescript-eslint/camelcase": "off",

11
dist/bitburner.d.ts vendored

@ -2101,7 +2101,7 @@ export declare interface Hacknet {
* // NS1: * // NS1:
* var upgradeName = "Sell for Corporation Funds"; * var upgradeName = "Sell for Corporation Funds";
* if (hacknet.numHashes() > hacknet.hashCost(upgradeName)) { * if (hacknet.numHashes() > hacknet.hashCost(upgradeName)) {
* hacknet.spendHashes(upgName); * hacknet.spendHashes(upgradeName);
* } * }
* ``` * ```
* @example * @example
@ -2109,7 +2109,7 @@ export declare interface Hacknet {
* // NS2: * // NS2:
* const upgradeName = "Sell for Corporation Funds"; * const upgradeName = "Sell for Corporation Funds";
* if (ns.hacknet.numHashes() > ns.hacknet.hashCost(upgradeName)) { * if (ns.hacknet.numHashes() > ns.hacknet.hashCost(upgradeName)) {
* ns.hacknet.spendHashes(upgName); * ns.hacknet.spendHashes(upgradeName);
* } * }
* ``` * ```
* @param upgName - Name of the upgrade of Hacknet Node. * @param upgName - Name of the upgrade of Hacknet Node.
@ -2825,9 +2825,10 @@ export declare interface NS extends Singularity {
* Returns the security increase that would occur if a hack with this many threads happened. * Returns the security increase that would occur if a hack with this many threads happened.
* *
* @param threads - Amount of threads that will be used. * @param threads - Amount of threads that will be used.
* @param hostname - Hostname of the target server. The number of threads is limited to the number needed to hack the servers maximum amount of money.
* @returns The security increase. * @returns The security increase.
*/ */
hackAnalyzeSecurity(threads: number): number; hackAnalyzeSecurity(threads: number, hostname?: string): number;
/** /**
* Get the chance of successfully hacking a server. * Get the chance of successfully hacking a server.
@ -4671,8 +4672,10 @@ export declare interface Office {
maxMor: number; maxMor: number;
/** Name of all the employees */ /** Name of all the employees */
employees: string[]; employees: string[];
/** Positions of the employees */ /** Production of the employees */
employeeProd: EmployeeJobs; employeeProd: EmployeeJobs;
/** Positions of the employees */
employeeJobs: EmployeeJobs;
} }
/** /**

4
dist/main.bundle.js vendored

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

42
dist/vendor.bundle.js vendored

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

@ -73,5 +73,5 @@
<link rel="shortcut icon" href="favicon.ico"></head> <link rel="shortcut icon" href="favicon.ico"></head>
<body> <body>
<div id="root"/> <div id="root"/>
<script type="text/javascript" src="dist/vendor.bundle.js"></script><script type="text/javascript" src="main.bundle.js"></script></body> <script type="text/javascript" src="dist/vendor.bundle.js"></script><script type="text/javascript" src="dist/main.bundle.js"></script></body>
</html> </html>

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

26
package-lock.json generated

@ -5965,13 +5965,19 @@
} }
}, },
"node_modules/caniuse-lite": { "node_modules/caniuse-lite": {
"version": "1.0.30001265", "version": "1.0.30001328",
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001265.tgz", "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001328.tgz",
"integrity": "sha512-YzBnspggWV5hep1m9Z6sZVLOt7vrju8xWooFAgN6BA5qvy98qPAPb7vNUzypFaoh2pb3vlfzbDO8tB57UPGbtw==", "integrity": "sha512-Ue55jHkR/s4r00FLNiX+hGMMuwml/QGqqzVeMQ5thUewznU2EdULFvI3JR7JJid6OrjJNfFvHY2G2dIjmRaDDQ==",
"funding": { "funding": [
"type": "opencollective", {
"url": "https://opencollective.com/browserslist" "type": "opencollective",
} "url": "https://opencollective.com/browserslist"
},
{
"type": "tidelift",
"url": "https://tidelift.com/funding/github/npm/caniuse-lite"
}
]
}, },
"node_modules/caseless": { "node_modules/caseless": {
"version": "0.12.0", "version": "0.12.0",
@ -26994,9 +27000,9 @@
"dev": true "dev": true
}, },
"caniuse-lite": { "caniuse-lite": {
"version": "1.0.30001265", "version": "1.0.30001328",
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001265.tgz", "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001328.tgz",
"integrity": "sha512-YzBnspggWV5hep1m9Z6sZVLOt7vrju8xWooFAgN6BA5qvy98qPAPb7vNUzypFaoh2pb3vlfzbDO8tB57UPGbtw==" "integrity": "sha512-Ue55jHkR/s4r00FLNiX+hGMMuwml/QGqqzVeMQ5thUewznU2EdULFvI3JR7JJid6OrjJNfFvHY2G2dIjmRaDDQ=="
}, },
"caseless": { "caseless": {
"version": "0.12.0", "version": "0.12.0",

@ -115,15 +115,15 @@
"start:container": "webpack-dev-server --progress --env.devServer --mode development --env.runInContainer", "start:container": "webpack-dev-server --progress --env.devServer --mode development --env.runInContainer",
"build": "webpack --mode production", "build": "webpack --mode production",
"build:dev": "webpack --mode development", "build:dev": "webpack --mode development",
"lint": "eslint --fix . --ext js,jsx,ts,tsx", "lint": "eslint --fix --ext js,jsx,ts,tsx --max-warnings 0 .",
"lint:report": "eslint --ext js,jsx,ts,tsx .", "lint:report": "eslint --ext js,jsx,ts,tsx --max-warnings 0 .",
"preinstall": "node ./tools/engines-check/engines-check.js", "preinstall": "node ./tools/engines-check/engines-check.js",
"postinstall": "cd electron && npm install", "postinstall": "cd electron && npm install",
"test": "jest", "test": "jest",
"test:watch": "jest --watch", "test:watch": "jest --watch",
"watch": "webpack --watch --mode production", "watch": "webpack --watch --mode production",
"watch:dev": "webpack --watch --mode development", "watch:dev": "webpack --watch --mode development",
"electron": "sh ./package.sh", "electron": "sh ./tools/package-electron.sh",
"electron:packager": "electron-packager .package bitburner --all --out .build --overwrite --icon .package/icon.png --no-prune", "electron:packager": "electron-packager .package bitburner --all --out .build --overwrite --icon .package/icon.png --no-prune",
"electron:packager-all": "electron-packager .package bitburner --all --out .build --overwrite --icon .package/icon.png", "electron:packager-all": "electron-packager .package bitburner --all --out .build --overwrite --icon .package/icon.png",
"electron:packager-win": "electron-packager .package bitburner --platform win32 --arch x64 --out .build --overwrite --icon .package/icon.png", "electron:packager-win": "electron-packager .package bitburner --platform win32 --arch x64 --out .build --overwrite --icon .package/icon.png",

@ -1,31 +0,0 @@
#!/bin/sh
# Clear out any files remaining from old builds
rm -rf .package
mkdir -p .package/dist/src/ThirdParty || true
mkdir -p .package/src/ThirdParty || true
mkdir -p .package/node_modules || true
cp index.html .package
cp -r electron/* .package
cp -r dist/ext .package/dist
cp -r dist/icons .package/dist
cp -r dist/images .package/dist
# The js files.
cp dist/vendor.bundle.js .package/dist/vendor.bundle.js
cp main.bundle.js .package/main.bundle.js
# Source maps
cp dist/vendor.bundle.js.map .package/dist/vendor.bundle.js.map
cp main.bundle.js.map .package/main.bundle.js.map
# Install electron sub-dependencies
cd electron
npm install
cd ..
BUILD_PLATFORM="${1:-"all"}"
# And finally build the app.
npm run electron:packager-$BUILD_PLATFORM

@ -21,6 +21,7 @@ import {
initNeuroFluxGovernor, initNeuroFluxGovernor,
initUnstableCircadianModulator, initUnstableCircadianModulator,
} from "./AugmentationCreator"; } from "./AugmentationCreator";
import { Router } from "../ui/GameRoot";
export function AddToAugmentations(aug: Augmentation): void { export function AddToAugmentations(aug: Augmentation): void {
const name = aug.name; const name = aug.name;
@ -194,6 +195,7 @@ function installAugmentations(force?: boolean): boolean {
); );
} }
prestigeAugmentation(); prestigeAugmentation();
Router.toTerminal();
return true; return true;
} }

@ -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>
); );
} }

@ -29,6 +29,16 @@ export class OfficeSpace {
[EmployeePositions.RandD]: 0, [EmployeePositions.RandD]: 0,
total: 0, total: 0,
}; };
employeeJobs: { [key: string]: number } = {
[EmployeePositions.Operations]: 0,
[EmployeePositions.Engineer]: 0,
[EmployeePositions.Business]: 0,
[EmployeePositions.Management]: 0,
[EmployeePositions.RandD]: 0,
[EmployeePositions.Training]: 0,
[EmployeePositions.Unassigned]: 0,
total: 0,
};
constructor(params: IParams = {}) { constructor(params: IParams = {}) {
this.loc = params.loc ? params.loc : ""; this.loc = params.loc ? params.loc : "";
@ -48,6 +58,8 @@ export class OfficeSpace {
} }
} }
this.calculateTotalEmployees();
// Process Office properties // Process Office properties
this.maxEne = 100; this.maxEne = 100;
this.maxHap = 100; this.maxHap = 100;
@ -101,6 +113,19 @@ export class OfficeSpace {
return salaryPaid; return salaryPaid;
} }
calculateTotalEmployees(): void {
//Reset
for (const name of Object.keys(this.employeeJobs)) {
this.employeeJobs[name] = 0;
}
for (let i = 0; i < this.employees.length; ++i) {
const employee = this.employees[i];
this.employeeJobs[employee.pos]++;
}
this.employeeJobs.total = this.employees.length;
}
calculateEmployeeProductivity(corporation: ICorporation, industry: IIndustry): void { calculateEmployeeProductivity(corporation: ICorporation, industry: IIndustry): void {
//Reset //Reset
for (const name of Object.keys(this.employeeProd)) { for (const name of Object.keys(this.employeeProd)) {

@ -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>
); );

@ -81,7 +81,6 @@ export function hasAugmentationPrereqs(aug: Augmentation): boolean {
} }
export function purchaseAugmentation(aug: Augmentation, fac: Faction, sing = false): string { export function purchaseAugmentation(aug: Augmentation, fac: Faction, sing = false): string {
const factionInfo = fac.getInfo();
const hasPrereqs = hasAugmentationPrereqs(aug); const hasPrereqs = hasAugmentationPrereqs(aug);
if (!hasPrereqs) { if (!hasPrereqs) {
const txt = `You must first purchase or install ${aug.prereqs.join(",")} before you can purchase this one.`; const txt = `You must first purchase or install ${aug.prereqs.join(",")} before you can purchase this one.`;
@ -90,7 +89,7 @@ export function purchaseAugmentation(aug: Augmentation, fac: Faction, sing = fal
} else { } else {
dialogBoxCreate(txt); dialogBoxCreate(txt);
} }
} else if (aug.baseCost !== 0 && Player.money < aug.baseCost * factionInfo.augmentationPriceMult) { } else if (aug.baseCost !== 0 && Player.money < aug.baseCost) {
const txt = "You don't have enough money to purchase " + aug.name; const txt = "You don't have enough money to purchase " + aug.name;
if (sing) { if (sing) {
return txt; return txt;
@ -102,14 +101,14 @@ export function purchaseAugmentation(aug: Augmentation, fac: Faction, sing = fal
return txt; return txt;
} }
dialogBoxCreate(txt); dialogBoxCreate(txt);
} else if (aug.baseCost === 0 || Player.money >= aug.baseCost * factionInfo.augmentationPriceMult) { } else if (aug.baseCost === 0 || Player.money >= aug.baseCost) {
const queuedAugmentation = new PlayerOwnedAugmentation(aug.name); const queuedAugmentation = new PlayerOwnedAugmentation(aug.name);
if (aug.name == AugmentationNames.NeuroFluxGovernor) { if (aug.name == AugmentationNames.NeuroFluxGovernor) {
queuedAugmentation.level = getNextNeurofluxLevel(); queuedAugmentation.level = getNextNeurofluxLevel();
} }
Player.queuedAugmentations.push(queuedAugmentation); Player.queuedAugmentations.push(queuedAugmentation);
Player.loseMoney(aug.baseCost * factionInfo.augmentationPriceMult, "augmentations"); Player.loseMoney(aug.baseCost, "augmentations");
// If you just purchased Neuroflux Governor, recalculate the cost // If you just purchased Neuroflux Governor, recalculate the cost
if (aug.name == AugmentationNames.NeuroFluxGovernor) { if (aug.name == AugmentationNames.NeuroFluxGovernor) {

@ -2,20 +2,20 @@ import React from "react";
import { IMap } from "../types"; import { IMap } from "../types";
import { FactionNames } from "./data/FactionNames"; import { FactionNames } from "./data/FactionNames";
interface FactionInfoParams {
infoText?: JSX.Element;
enemies?: string[];
offerHackingWork?: boolean;
offerFieldWork?: boolean;
offerSecurityWork?: boolean;
special?: boolean;
keepOnInstall?: boolean;
}
/** /**
* Contains the "information" property for all the Factions, which is just a description of each faction * Contains the "information" property for all the Factions, which is just a description of each faction
*/ */
export class FactionInfo { export class FactionInfo {
/**
* The multiplier to apply to augmentation base purchase price.
*/
augmentationPriceMult: number;
/**
* The multiplier to apply to augmentation reputation base requirement.
*/
augmentationRepRequirementMult: number;
/** /**
* The names of all other factions considered to be enemies to this faction. * The names of all other factions considered to be enemies to this faction.
*/ */
@ -31,11 +31,6 @@ export class FactionInfo {
*/ */
offerFieldWork: boolean; offerFieldWork: boolean;
/**
* A flag indicating if the faction supports hacking missions to earn reputation.
*/
offerHackingMission: boolean;
/** /**
* A flag indicating if the faction supports hacking work to earn reputation. * A flag indicating if the faction supports hacking work to earn reputation.
*/ */
@ -56,32 +51,19 @@ export class FactionInfo {
*/ */
special: boolean; special: boolean;
constructor( constructor(params: FactionInfoParams) {
infoText: JSX.Element, this.infoText = params.infoText ?? <></>;
enemies: string[], this.enemies = params.enemies ?? [];
offerHackingMission: boolean, this.offerHackingWork = params.offerHackingWork ?? false;
offerHackingWork: boolean, this.offerFieldWork = params.offerFieldWork ?? false;
offerFieldWork: boolean, this.offerSecurityWork = params.offerSecurityWork ?? false;
offerSecurityWork: boolean,
special: boolean,
keep: boolean,
) {
this.infoText = infoText;
this.enemies = enemies;
this.offerHackingMission = offerHackingMission;
this.offerHackingWork = offerHackingWork;
this.offerFieldWork = offerFieldWork;
this.offerSecurityWork = offerSecurityWork;
// These are always all 1 for now. this.keep = params.keepOnInstall ?? false;
this.augmentationPriceMult = 1; this.special = params.special ?? false;
this.augmentationRepRequirementMult = 1;
this.keep = keep;
this.special = special;
} }
offersWork(): boolean { offersWork(): boolean {
return this.offerFieldWork || this.offerHackingMission || this.offerHackingWork || this.offerSecurityWork; return this.offerFieldWork || this.offerHackingWork || this.offerSecurityWork;
} }
} }
@ -91,35 +73,25 @@ export class FactionInfo {
// tslint:disable-next-line:variable-name // tslint:disable-next-line:variable-name
export const FactionInfos: IMap<FactionInfo> = { export const FactionInfos: IMap<FactionInfo> = {
// Endgame // Endgame
[FactionNames.Illuminati]: new FactionInfo( [FactionNames.Illuminati]: new FactionInfo({
( infoText: (
<> <>
Humanity never changes. No matter how civilized society becomes, it will eventually fall back into chaos. And Humanity never changes. No matter how civilized society becomes, it will eventually fall back into chaos. And
from this chaos, we are the invisible hand that guides them to order.{" "} from this chaos, we are the invisible hand that guides them to order.{" "}
</> </>
), ),
[], offerHackingWork: true,
true, offerFieldWork: true,
true, }),
true,
false,
false,
false,
),
[FactionNames.Daedalus]: new FactionInfo( [FactionNames.Daedalus]: new FactionInfo({
<>Yesterday we obeyed kings and bent our necks to emperors. Today we kneel only to truth.</>, infoText: <>Yesterday we obeyed kings and bent our necks to emperors. Today we kneel only to truth.</>,
[], offerHackingWork: true,
true, offerFieldWork: true,
true, }),
true,
false,
false,
false,
),
[FactionNames.TheCovenant]: new FactionInfo( [FactionNames.TheCovenant]: new FactionInfo({
( infoText: (
<> <>
Surrender yourself. Give up your empty individuality to become part of something great, something eternal. Surrender yourself. Give up your empty individuality to become part of something great, something eternal.
Become a slave. Submit your mind, body, and soul. Only then can you set yourself free. Become a slave. Submit your mind, body, and soul. Only then can you set yourself free.
@ -128,35 +100,27 @@ export const FactionInfos: IMap<FactionInfo> = {
Only then can you discover immortality. Only then can you discover immortality.
</> </>
), ),
[], offerHackingWork: true,
true, offerFieldWork: true,
true, }),
true,
false,
false,
false,
),
// Megacorporations, each forms its own faction // Megacorporations, each forms its own faction
[FactionNames.ECorp]: new FactionInfo( [FactionNames.ECorp]: new FactionInfo({
( infoText: (
<> <>
{FactionNames.ECorp}'s mission is simple: to connect the world of today with the technology of tomorrow. With {FactionNames.ECorp}'s mission is simple: to connect the world of today with the technology of tomorrow. With
our wide range of Internet-related software and commercial hardware, {FactionNames.ECorp} makes the world's our wide range of Internet-related software and commercial hardware, {FactionNames.ECorp} makes the world's
information universally accessible. information universally accessible.
</> </>
), ),
[], offerHackingWork: true,
true, offerFieldWork: true,
true, offerSecurityWork: true,
true, keepOnInstall: true,
true, }),
false,
true,
),
[FactionNames.MegaCorp]: new FactionInfo( [FactionNames.MegaCorp]: new FactionInfo({
( infoText: (
<> <>
{FactionNames.MegaCorp} does what no other dares to do. We imagine. We create. We invent. We create what others {FactionNames.MegaCorp} does what no other dares to do. We imagine. We create. We invent. We create what others
have never even dreamed of. Our work fills the world's needs for food, water, power, and transportation on an have never even dreamed of. Our work fills the world's needs for food, water, power, and transportation on an
@ -167,17 +131,14 @@ export const FactionInfos: IMap<FactionInfo> = {
the world. the world.
</> </>
), ),
[], offerHackingWork: true,
true, offerFieldWork: true,
true, offerSecurityWork: true,
true, keepOnInstall: true,
true, }),
false,
true,
),
[FactionNames.BachmanAssociates]: new FactionInfo( [FactionNames.BachmanAssociates]: new FactionInfo({
( infoText: (
<> <>
Where Law and Business meet - thats where we are. Where Law and Business meet - thats where we are.
<br /> <br />
@ -185,112 +146,87 @@ export const FactionInfos: IMap<FactionInfo> = {
Legal Insight - Business Instinct - Innovative Experience. Legal Insight - Business Instinct - Innovative Experience.
</> </>
), ),
[], offerHackingWork: true,
true, offerFieldWork: true,
true, offerSecurityWork: true,
true, keepOnInstall: true,
true, }),
false,
true,
),
[FactionNames.BladeIndustries]: new FactionInfo( [FactionNames.BladeIndustries]: new FactionInfo({
<>Augmentation is Salvation.</>, infoText: <>Augmentation is Salvation.</>,
[], offerHackingWork: true,
true, offerFieldWork: true,
true, offerSecurityWork: true,
true, keepOnInstall: true,
true, }),
false,
true,
),
[FactionNames.NWO]: new FactionInfo( [FactionNames.NWO]: new FactionInfo({
( infoText: (
<> <>
Humans don't truly desire freedom. They want to be observed, understood, and judged. They want to be given Humans don't truly desire freedom. They want to be observed, understood, and judged. They want to be given
purpose and direction in life. That is why they created God. And that is why they created civilization - not purpose and direction in life. That is why they created God. And that is why they created civilization - not
because of willingness, but because of a need to be incorporated into higher orders of structure and meaning. because of willingness, but because of a need to be incorporated into higher orders of structure and meaning.
</> </>
), ),
[], offerHackingWork: true,
true, offerFieldWork: true,
true, offerSecurityWork: true,
true, keepOnInstall: true,
true, }),
false,
true,
),
[FactionNames.ClarkeIncorporated]: new FactionInfo( [FactionNames.ClarkeIncorporated]: new FactionInfo({
<>The Power of the Genome - Unlocked.</>, infoText: <>The Power of the Genome - Unlocked.</>,
[], offerHackingWork: true,
true, offerFieldWork: true,
true, offerSecurityWork: true,
true, keepOnInstall: true,
true, }),
false,
true,
),
[FactionNames.OmniTekIncorporated]: new FactionInfo( [FactionNames.OmniTekIncorporated]: new FactionInfo({
<>Simply put, our mission is to design and build robots that make a difference.</>, infoText: <>Simply put, our mission is to design and build robots that make a difference.</>,
[], offerHackingWork: true,
true, offerFieldWork: true,
true, offerSecurityWork: true,
true, keepOnInstall: true,
true, }),
false,
true,
),
[FactionNames.FourSigma]: new FactionInfo( [FactionNames.FourSigma]: new FactionInfo({
( infoText: (
<> <>
The scientific method is the best way to approach investing. Big strategies backed up with big data. Driven by The scientific method is the best way to approach investing. Big strategies backed up with big data. Driven by
deep learning and innovative ideas. And improved by iteration. That's {FactionNames.FourSigma}. deep learning and innovative ideas. And improved by iteration. That's {FactionNames.FourSigma}.
</> </>
), ),
[], offerHackingWork: true,
true, offerFieldWork: true,
true, offerSecurityWork: true,
true, keepOnInstall: true,
true, }),
false,
true,
),
[FactionNames.KuaiGongInternational]: new FactionInfo( [FactionNames.KuaiGongInternational]: new FactionInfo({
<>Dream big. Work hard. Make history.</>, infoText: <>Dream big. Work hard. Make history.</>,
[], offerHackingWork: true,
true, offerFieldWork: true,
true, offerSecurityWork: true,
true, keepOnInstall: true,
true, }),
false,
true,
),
// Other Corporations // Other Corporations
[FactionNames.FulcrumSecretTechnologies]: new FactionInfo( [FactionNames.FulcrumSecretTechnologies]: new FactionInfo({
( infoText: (
<> <>
The human organism has an innate desire to worship. That is why they created gods. If there were no gods, it The human organism has an innate desire to worship. That is why they created gods. If there were no gods, it
would be necessary to create them. And now we can. would be necessary to create them. And now we can.
</> </>
), ),
[], offerHackingWork: true,
true, offerSecurityWork: true,
true, keepOnInstall: true,
false, }),
true,
false,
true,
),
// Hacker groups // Hacker groups
[FactionNames.BitRunners]: new FactionInfo( [FactionNames.BitRunners]: new FactionInfo({
( infoText: (
<> <>
Our entire lives are controlled by bits. All of our actions, our thoughts, our personal information. It's all Our entire lives are controlled by bits. All of our actions, our thoughts, our personal information. It's all
transformed into bits, stored in bits, communicated through bits. Its impossible for any person to move, to transformed into bits, stored in bits, communicated through bits. Its impossible for any person to move, to
@ -302,17 +238,11 @@ export const FactionInfos: IMap<FactionInfo> = {
Those who run the bits, run the world. Those who run the bits, run the world.
</> </>
), ),
[], offerHackingWork: true,
true, }),
true,
false,
false,
false,
false,
),
[FactionNames.TheBlackHand]: new FactionInfo( [FactionNames.TheBlackHand]: new FactionInfo({
( infoText: (
<> <>
The world, so afraid of strong government, now has no government. Only power - Digital power. Financial power. The world, so afraid of strong government, now has no government. Only power - Digital power. Financial power.
Technological power. And those at the top rule with an invisible hand. They built a society where the rich get Technological power. And those at the top rule with an invisible hand. They built a society where the rich get
@ -322,17 +252,13 @@ export const FactionInfos: IMap<FactionInfo> = {
So much pain. So many lives. Their darkness must end. So much pain. So many lives. Their darkness must end.
</> </>
), ),
[], offerHackingWork: true,
true, offerFieldWork: true,
true, }),
true,
false,
false,
false,
),
// prettier-ignore // prettier-ignore
[FactionNames.NiteSec]: new FactionInfo(<> [FactionNames.NiteSec]: new FactionInfo({
infoText:(<>
{" __..__ "}<br /> {" __..__ "}<br />
{" _.nITESECNIt. "}<br /> {" _.nITESECNIt. "}<br />
{" .-'NITESECNITESEc. "}<br /> {" .-'NITESECNITESEc. "}<br />
@ -367,105 +293,87 @@ export const FactionInfos: IMap<FactionInfo> = {
{" / .d/$/$; , ; "}<br /> {" / .d/$/$; , ; "}<br />
{" d .dNITESEC $ | "}<br /> {" d .dNITESEC $ | "}<br />
{" :bp.__.gNITESEC/$ :$ ; "}<br /> {" :bp.__.gNITESEC/$ :$ ; "}<br />
{" NITESECNITESECNIT /$b : "}<br /></>, {" NITESECNITESECNIT /$b : "}<br /></>),
[], offerHackingWork: true,
true, offerFieldWork: false,
true, offerSecurityWork: false,
false, special: false,
false, keepOnInstall: false,
false, }),
false,
),
// City factions, essentially governments // City factions, essentially governments
[FactionNames.Aevum]: new FactionInfo( [FactionNames.Aevum]: new FactionInfo({
<>The Silicon City.</>, infoText: <>The Silicon City.</>,
[FactionNames.Chongqing, FactionNames.NewTokyo, FactionNames.Ishima, FactionNames.Volhaven], enemies: [FactionNames.Chongqing, FactionNames.NewTokyo, FactionNames.Ishima, FactionNames.Volhaven],
true, offerHackingWork: true,
true, offerFieldWork: true,
true, offerSecurityWork: true,
true, }),
false, [FactionNames.Chongqing]: new FactionInfo({
false, infoText: <>Serve the People.</>,
), enemies: [FactionNames.Sector12, FactionNames.Aevum, FactionNames.Volhaven],
[FactionNames.Chongqing]: new FactionInfo( offerHackingWork: true,
<>Serve the People.</>, offerFieldWork: true,
[FactionNames.Sector12, FactionNames.Aevum, FactionNames.Volhaven], offerSecurityWork: true,
true, }),
true, [FactionNames.Ishima]: new FactionInfo({
true, infoText: <>The East Asian Order of the Future.</>,
true, enemies: [FactionNames.Sector12, FactionNames.Aevum, FactionNames.Volhaven],
false, offerHackingWork: true,
false, offerFieldWork: true,
), offerSecurityWork: true,
[FactionNames.Ishima]: new FactionInfo( }),
<>The East Asian Order of the Future.</>, [FactionNames.NewTokyo]: new FactionInfo({
[FactionNames.Sector12, FactionNames.Aevum, FactionNames.Volhaven], infoText: <>Asia's World City.</>,
true, enemies: [FactionNames.Sector12, FactionNames.Aevum, FactionNames.Volhaven],
true, offerHackingWork: true,
true, offerFieldWork: true,
true, offerSecurityWork: true,
false, }),
false, [FactionNames.Sector12]: new FactionInfo({
), infoText: <>The City of the Future.</>,
[FactionNames.NewTokyo]: new FactionInfo( enemies: [FactionNames.Chongqing, FactionNames.NewTokyo, FactionNames.Ishima, FactionNames.Volhaven],
<>Asia's World City.</>, offerHackingWork: true,
[FactionNames.Sector12, FactionNames.Aevum, FactionNames.Volhaven], offerFieldWork: true,
true, offerSecurityWork: true,
true, }),
true, [FactionNames.Volhaven]: new FactionInfo({
true, infoText: <>Benefit, Honor, and Glory.</>,
false, enemies: [
false, FactionNames.Chongqing,
), FactionNames.Sector12,
[FactionNames.Sector12]: new FactionInfo( FactionNames.NewTokyo,
<>The City of the Future.</>, FactionNames.Aevum,
[FactionNames.Chongqing, FactionNames.NewTokyo, FactionNames.Ishima, FactionNames.Volhaven], FactionNames.Ishima,
true, ],
true, offerHackingWork: true,
true, offerFieldWork: true,
true, offerSecurityWork: true,
false, }),
false,
),
[FactionNames.Volhaven]: new FactionInfo(
<>Benefit, Honor, and Glory.</>,
[FactionNames.Chongqing, FactionNames.Sector12, FactionNames.NewTokyo, FactionNames.Aevum, FactionNames.Ishima],
true,
true,
true,
true,
false,
false,
),
// Criminal Organizations/Gangs // Criminal Organizations/Gangs
[FactionNames.SpeakersForTheDead]: new FactionInfo( [FactionNames.SpeakersForTheDead]: new FactionInfo({
<>It is better to reign in Hell than to serve in Heaven.</>, infoText: <>It is better to reign in Hell than to serve in Heaven.</>,
[], offerHackingWork: true,
true, offerFieldWork: true,
true, offerSecurityWork: true,
true, }),
true,
false,
false,
),
[FactionNames.TheDarkArmy]: new FactionInfo( [FactionNames.TheDarkArmy]: new FactionInfo({
<>The World doesn't care about right or wrong. It only cares about power.</>, infoText: <>The World doesn't care about right or wrong. It only cares about power.</>,
[], offerHackingWork: true,
true, offerFieldWork: true,
true, }),
true,
false,
false,
false,
),
[FactionNames.TheSyndicate]: new FactionInfo(<>Honor holds you back.</>, [], true, true, true, true, false, false), [FactionNames.TheSyndicate]: new FactionInfo({
infoText: <>Honor holds you back.</>,
offerHackingWork: true,
offerFieldWork: true,
offerSecurityWork: true,
}),
[FactionNames.Silhouette]: new FactionInfo( [FactionNames.Silhouette]: new FactionInfo({
( infoText: (
<> <>
Corporations have filled the void of power left behind by the collapse of Western government. The issue is Corporations have filled the void of power left behind by the collapse of Western government. The issue is
they've become so big that you don't know who they're working for. And if you're employed at one of these they've become so big that you don't know who they're working for. And if you're employed at one of these
@ -475,100 +383,66 @@ export const FactionInfos: IMap<FactionInfo> = {
That's terror. Terror, fear, and corruption. All born into the system, all propagated by the system. That's terror. Terror, fear, and corruption. All born into the system, all propagated by the system.
</> </>
), ),
[], offerHackingWork: true,
true, offerFieldWork: true,
true, }),
true,
false,
false,
false,
),
[FactionNames.Tetrads]: new FactionInfo( [FactionNames.Tetrads]: new FactionInfo({
<>Following the mandate of Heaven and carrying out the way.</>, infoText: <>Following the mandate of Heaven and carrying out the way.</>,
[],
false,
false,
true,
true,
false,
false,
),
[FactionNames.SlumSnakes]: new FactionInfo( offerFieldWork: true,
<>{FactionNames.SlumSnakes} rule!</>, offerSecurityWork: true,
[], }),
false,
false, [FactionNames.SlumSnakes]: new FactionInfo({
true, infoText: <>{FactionNames.SlumSnakes} rule!</>,
true,
false, offerFieldWork: true,
false, offerSecurityWork: true,
), }),
// Earlygame factions - factions the player will prestige with early on that don't belong in other categories. // Earlygame factions - factions the player will prestige with early on that don't belong in other categories.
[FactionNames.Netburners]: new FactionInfo( [FactionNames.Netburners]: new FactionInfo({
<>{"~~//*>H4CK||3T 8URN3R5**>?>\\~~"}</>, infoText: <>{"~~//*>H4CK||3T 8URN3R5**>?>\\~~"}</>,
[], offerHackingWork: true,
true, }),
true,
false,
false,
false,
false,
),
[FactionNames.TianDiHui]: new FactionInfo( [FactionNames.TianDiHui]: new FactionInfo({
<>Obey Heaven and work righteously.</>, infoText: <>Obey Heaven and work righteously.</>,
[], offerHackingWork: true,
true,
true,
false,
true,
false,
false,
),
[FactionNames.CyberSec]: new FactionInfo( offerSecurityWork: true,
( }),
[FactionNames.CyberSec]: new FactionInfo({
infoText: (
<> <>
The Internet is the first thing that was built that we don't fully understand, the largest experiment in anarchy The Internet is the first thing that was built that we don't fully understand, the largest experiment in anarchy
that we have ever had. And as the world becomes increasingly dominated by it, society approaches the brink of that we have ever had. And as the world becomes increasingly dominated by it, society approaches the brink of
total chaos. We serve only to protect society, to protect humanity, to protect the world from imminent collapse. total chaos. We serve only to protect society, to protect humanity, to protect the world from imminent collapse.
</> </>
), ),
[], offerHackingWork: true,
true, }),
true,
false,
false,
false,
false,
),
// Special Factions // Special Factions
[FactionNames.Bladeburners]: new FactionInfo( [FactionNames.Bladeburners]: new FactionInfo({
( infoText: (
<> <>
It's too bad they won't live. But then again, who does? It's too bad they won't live. But then again, who does?
<br /> <br />
<br /> <br />
Note that for this faction, reputation can only be gained through {FactionNames.Bladeburners} actions. Note that for this faction, reputation can only be gained through {FactionNames.Bladeburners} actions.{" "}
Completing {FactionNames.Bladeburners} Completing {FactionNames.Bladeburners} contracts/operations will increase your reputation.
contracts/operations will increase your reputation.
</> </>
), ),
[],
false, special: true,
false, }),
false,
false,
true,
false,
),
// prettier-ignore // prettier-ignore
[FactionNames.ChurchOfTheMachineGod]: new FactionInfo(<> [FactionNames.ChurchOfTheMachineGod]: new FactionInfo({
infoText:(<>
{" `` "}<br /> {" `` "}<br />
{" -odmmNmds: "}<br /> {" -odmmNmds: "}<br />
{" `hNmo:..-omNh. "}<br /> {" `hNmo:..-omNh. "}<br />
@ -600,13 +474,11 @@ export const FactionInfos: IMap<FactionInfo> = {
Many cultures predict an end to humanity in the near future, a final Many cultures predict an end to humanity in the near future, a final
Armageddon that will end the world; but we disagree. Armageddon that will end the world; but we disagree.
<br /><br />Note that for this faction, reputation can <br /><br />Note that for this faction, reputation can
only be gained by charging Stanek's gift.</>, only be gained by charging Stanek's gift.</>),
[], offerHackingWork: false,
false, offerFieldWork: false,
false, offerSecurityWork: false,
false, special: true,
false, keepOnInstall: true,
true, }),
true,
),
}; };

@ -78,10 +78,10 @@ export function AugmentationsPage(props: IProps): React.ReactElement {
const augs = getAugs(); const augs = getAugs();
function canBuy(augName: string): boolean { function canBuy(augName: string): boolean {
const aug = Augmentations[augName]; const aug = Augmentations[augName];
const repCost = aug.baseRepRequirement * props.faction.getInfo().augmentationRepRequirementMult; const repCost = aug.baseRepRequirement;
const hasReq = props.faction.playerReputation >= repCost; const hasReq = props.faction.playerReputation >= repCost;
const hasRep = hasAugmentationPrereqs(aug); const hasRep = hasAugmentationPrereqs(aug);
const hasCost = aug.baseCost !== 0 && player.money > aug.baseCost * props.faction.getInfo().augmentationPriceMult; const hasCost = aug.baseCost !== 0 && player.money > aug.baseCost;
return hasCost && hasReq && hasRep; return hasCost && hasReq && hasRep;
} }
const buy = augs.filter(canBuy).sort((augName1, augName2) => { const buy = augs.filter(canBuy).sort((augName1, augName2) => {

@ -20,7 +20,6 @@ interface IProps {
export function PurchaseAugmentationModal(props: IProps): React.ReactElement { export function PurchaseAugmentationModal(props: IProps): React.ReactElement {
const player = use.Player(); const player = use.Player();
const factionInfo = props.faction.getInfo();
function buy(): void { function buy(): void {
if (!isRepeatableAug(props.aug) && player.hasAugmentation(props.aug)) { if (!isRepeatableAug(props.aug) && player.hasAugmentation(props.aug)) {
@ -43,7 +42,7 @@ export function PurchaseAugmentationModal(props: IProps): React.ReactElement {
<br /> <br />
<br /> <br />
Would you like to purchase the {props.aug.name} Augmentation for&nbsp; Would you like to purchase the {props.aug.name} Augmentation for&nbsp;
<Money money={props.aug.baseCost * factionInfo.augmentationPriceMult} />? <Money money={props.aug.baseCost} />?
<br /> <br />
<br /> <br />
</Typography> </Typography>

@ -84,11 +84,11 @@ export function PurchaseableAugmentation(props: IProps): React.ReactElement {
return <></>; return <></>;
} }
const moneyCost = aug.baseCost * props.faction.getInfo().augmentationPriceMult; const moneyCost = aug.baseCost;
const repCost = aug.baseRepRequirement * props.faction.getInfo().augmentationRepRequirementMult; const repCost = aug.baseRepRequirement;
const hasReq = hasAugmentationPrereqs(aug); const hasReq = hasAugmentationPrereqs(aug);
const hasRep = props.faction.playerReputation >= repCost; const hasRep = props.faction.playerReputation >= repCost;
const hasCost = aug.baseCost === 0 || props.p.money > aug.baseCost * props.faction.getInfo().augmentationPriceMult; const hasCost = aug.baseCost === 0 || props.p.money > aug.baseCost;
// Determine UI properties // Determine UI properties
const color: "error" | "primary" = !hasReq || !hasRep || !hasCost ? "error" : "primary"; const color: "error" | "primary" = !hasReq || !hasRep || !hasCost ? "error" : "primary";

@ -174,10 +174,11 @@ export function SpecialLocation(props: IProps): React.ReactElement {
applyAugmentation({ name: AugmentationNames.StaneksGift1, level: 1 }); applyAugmentation({ name: AugmentationNames.StaneksGift1, level: 1 });
} }
router.toFaction(faction); router.toStaneksGift();
} }
function renderCotMG(): React.ReactElement { function renderCotMG(): React.ReactElement {
const toStanek = <Button onClick={() => router.toStaneksGift()}>Open Stanek's Gift</Button>;
// prettier-ignore // prettier-ignore
const symbol = <Typography sx={{ lineHeight: '1em', whiteSpace: 'pre' }}> const symbol = <Typography sx={{ lineHeight: '1em', whiteSpace: 'pre' }}>
{" `` "}<br /> {" `` "}<br />
@ -218,6 +219,9 @@ export function SpecialLocation(props: IProps): React.ReactElement {
seems. Curious, Just how much of a machine's soul do you house in that body? seems. Curious, Just how much of a machine's soul do you house in that body?
</i> </i>
</Typography> </Typography>
<br />
{toStanek}
<br />
{symbol} {symbol}
</> </>
); );
@ -232,6 +236,9 @@ export function SpecialLocation(props: IProps): React.ReactElement {
mastery of the gift clearly demonstrates that. My hopes are climbing by the day for you. mastery of the gift clearly demonstrates that. My hopes are climbing by the day for you.
</i> </i>
</Typography> </Typography>
<br />
{toStanek}
<br />
{symbol} {symbol}
</> </>
); );
@ -242,6 +249,9 @@ export function SpecialLocation(props: IProps): React.ReactElement {
<Typography> <Typography>
<i>Allison "Mother" Stanek: Welcome back my child!</i> <i>Allison "Mother" Stanek: Welcome back my child!</i>
</Typography> </Typography>
<br />
{toStanek}
<br />
{symbol} {symbol}
</> </>
); );

@ -15,6 +15,7 @@ export const RamCostConstants: IMap<number> = {
ScriptWeakenRamCost: 0.15, ScriptWeakenRamCost: 0.15,
ScriptWeakenAnalyzeRamCost: 1, ScriptWeakenAnalyzeRamCost: 1,
ScriptScanRamCost: 0.2, ScriptScanRamCost: 0.2,
ScriptRecentScriptsRamCost: 0.2,
ScriptPortProgramRamCost: 0.05, ScriptPortProgramRamCost: 0.05,
ScriptRunRamCost: 1.0, ScriptRunRamCost: 1.0,
ScriptExecRamCost: 1.3, ScriptExecRamCost: 1.3,
@ -140,6 +141,7 @@ export const RamCosts: IMap<any> = {
scp: RamCostConstants.ScriptScpRamCost, scp: RamCostConstants.ScriptScpRamCost,
ls: RamCostConstants.ScriptScanRamCost, ls: RamCostConstants.ScriptScanRamCost,
ps: RamCostConstants.ScriptScanRamCost, ps: RamCostConstants.ScriptScanRamCost,
getRecentScripts: RamCostConstants.ScriptRecentScriptsRamCost,
hasRootAccess: RamCostConstants.ScriptHasRootAccessRamCost, hasRootAccess: RamCostConstants.ScriptHasRootAccessRamCost,
getIp: RamCostConstants.ScriptGetHostnameRamCost, getIp: RamCostConstants.ScriptGetHostnameRamCost,
getHostname: RamCostConstants.ScriptGetHostnameRamCost, getHostname: RamCostConstants.ScriptGetHostnameRamCost,

@ -1,27 +1,24 @@
import { RunningScript } from "src/Script/RunningScript"; import { RunningScript } from "src/Script/RunningScript";
import { Settings } from "../Settings/Settings";
import { WorkerScript } from "./WorkerScript"; import { WorkerScript } from "./WorkerScript";
export const recentScripts: RecentScript[] = []; export const recentScripts: RecentScript[] = [];
export function AddRecentScript(workerScript: WorkerScript): void { export function AddRecentScript(workerScript: WorkerScript): void {
if (recentScripts.find((r) => r.pid === workerScript.pid)) return; if (recentScripts.find((r) => r.runningScript.pid === workerScript.pid)) return;
recentScripts.unshift({
filename: workerScript.name,
args: workerScript.args,
pid: workerScript.pid,
timestamp: new Date(),
const killedTime = new Date();
recentScripts.unshift({
timeOfDeath: killedTime,
runningScript: workerScript.scriptRef, runningScript: workerScript.scriptRef,
}); });
while (recentScripts.length > 50) {
while (recentScripts.length > Settings.MaxRecentScriptsCapacity) {
recentScripts.pop(); recentScripts.pop();
} }
} }
export interface RecentScript { export interface RecentScript {
filename: string; timeOfDeath: Date;
args: string[];
pid: number;
timestamp: Date;
runningScript: RunningScript; runningScript: RunningScript;
} }

@ -0,0 +1,37 @@
import { WorkerScript } from "./WorkerScript";
/**
* Script death marker.
*
* IMPORTANT: the game engine should not base any of it's decisions on the data
* carried in a ScriptDeath instance.
*
* This is because ScriptDeath instances are thrown through player code when a
* script is killed. Which grants the player access to the class and the ability
* to construct new instances with arbitrary data.
*/
export class ScriptDeath {
/** Process ID number. */
pid: number;
/** Filename of the script. */
name: string;
/** IP Address on which the script was running */
hostname: string;
/** Status message in case of script error. */
errorMessage = "";
constructor(ws: WorkerScript) {
this.pid = ws.pid;
this.name = ws.name;
this.hostname = ws.hostname;
this.errorMessage = ws.errorMessage;
Object.freeze(this);
}
}
Object.freeze(ScriptDeath);
Object.freeze(ScriptDeath.prototype);

@ -60,7 +60,7 @@ export class WorkerScript {
env: Environment; env: Environment;
/** /**
* Status message in case of script error. Currently unused I think * Status message in case of script error.
*/ */
errorMessage = ""; errorMessage = "";

@ -2,6 +2,7 @@
* Stops an actively-running script (represented by a WorkerScript object) * Stops an actively-running script (represented by a WorkerScript object)
* and removes it from the global pool of active scripts. * and removes it from the global pool of active scripts.
*/ */
import { ScriptDeath } from "./ScriptDeath";
import { WorkerScript } from "./WorkerScript"; import { WorkerScript } from "./WorkerScript";
import { workerScripts } from "./WorkerScripts"; import { workerScripts } from "./WorkerScripts";
import { WorkerScriptStartStopEventEmitter } from "./WorkerScriptStartStopEventEmitter"; import { WorkerScriptStartStopEventEmitter } from "./WorkerScriptStartStopEventEmitter";
@ -139,7 +140,7 @@ function killNetscriptDelay(workerScript: WorkerScript): void {
if (workerScript.delay) { if (workerScript.delay) {
clearTimeout(workerScript.delay); clearTimeout(workerScript.delay);
if (workerScript.delayReject) { if (workerScript.delayReject) {
workerScript.delayReject(workerScript); workerScript.delayReject(new ScriptDeath(workerScript));
} }
} }
} }

@ -1,15 +1,20 @@
import { isString } from "./utils/helpers/isString"; import { isString } from "./utils/helpers/isString";
import { GetServer } from "./Server/AllServers"; import { GetServer } from "./Server/AllServers";
import { ScriptDeath } from "./Netscript/ScriptDeath";
import { WorkerScript } from "./Netscript/WorkerScript"; import { WorkerScript } from "./Netscript/WorkerScript";
export function netscriptDelay(time: number, workerScript: WorkerScript): Promise<void> { export function netscriptDelay(time: number, workerScript: WorkerScript): Promise<void> {
// Cancel any pre-existing netscriptDelay'ed function call
// TODO: the rejection almost certainly ends up in the uncaught rejection handler.
// Maybe reject with a stack-trace'd error message?
if (workerScript.delayReject) workerScript.delayReject(); if (workerScript.delayReject) workerScript.delayReject();
return new Promise(function (resolve, reject) { return new Promise(function (resolve, reject) {
workerScript.delay = window.setTimeout(() => { workerScript.delay = window.setTimeout(() => {
workerScript.delay = null; workerScript.delay = null;
workerScript.delayReject = undefined; workerScript.delayReject = undefined;
if (workerScript.env.stopFlag) reject(workerScript); if (workerScript.env.stopFlag) reject(new ScriptDeath(workerScript));
else resolve(); else resolve();
}, time); }, time);
workerScript.delayReject = reject; workerScript.delayReject = reject;

@ -79,6 +79,8 @@ import {
Gang as IGang, Gang as IGang,
Bladeburner as IBladeburner, Bladeburner as IBladeburner,
Stanek as IStanek, Stanek as IStanek,
RunningScript as IRunningScript,
RecentScript as IRecentScript,
SourceFileLvl, SourceFileLvl,
BasicHGWOptions, BasicHGWOptions,
ProcessInfo, ProcessInfo,
@ -98,6 +100,7 @@ import { SnackbarEvents } from "./ui/React/Snackbar";
import { Flags } from "./NetscriptFunctions/Flags"; import { Flags } from "./NetscriptFunctions/Flags";
import { calculateIntelligenceBonus } from "./PersonObjects/formulas/intelligence"; import { calculateIntelligenceBonus } from "./PersonObjects/formulas/intelligence";
import { CalculateShareMult, StartSharing } from "./NetworkShare/Share"; import { CalculateShareMult, StartSharing } from "./NetworkShare/Share";
import { recentScripts } from "./Netscript/RecentScripts";
import { CityName } from "./Locations/data/CityNames"; import { CityName } from "./Locations/data/CityNames";
import { wrapAPI } from "./Netscript/APIWrapper"; import { wrapAPI } from "./Netscript/APIWrapper";
@ -213,6 +216,32 @@ export function NetscriptFunctions(workerScript: WorkerScript): NS {
return null; return null;
}; };
/**
* Sanitizes a `RunningScript` to remove sensitive information, making it suitable for
* return through an NS function.
* @see NS.getRecentScripts
* @see NS.getRunningScript
* @param runningScript Existing, internal RunningScript
* @returns A sanitized, NS-facing copy of the RunningScript
*/
const createPublicRunningScript = function (runningScript: RunningScript): IRunningScript {
return {
args: runningScript.args.slice(),
filename: runningScript.filename,
logs: runningScript.logs.slice(),
offlineExpGained: runningScript.offlineExpGained,
offlineMoneyMade: runningScript.offlineMoneyMade,
offlineRunningTime: runningScript.offlineRunningTime,
onlineExpGained: runningScript.onlineExpGained,
onlineMoneyMade: runningScript.onlineMoneyMade,
onlineRunningTime: runningScript.onlineRunningTime,
pid: runningScript.pid,
ramUsage: runningScript.ramUsage,
server: runningScript.server,
threads: runningScript.threads,
};
};
/** /**
* Helper function for getting the error log message when the user specifies * Helper function for getting the error log message when the user specifies
* a nonexistent running script * a nonexistent running script
@ -585,9 +614,25 @@ export function NetscriptFunctions(workerScript: WorkerScript): NS {
return calculatePercentMoneyHacked(server, Player); return calculatePercentMoneyHacked(server, Player);
}, },
hackAnalyzeSecurity: function (_threads: unknown): number { hackAnalyzeSecurity: function (_threads: unknown, _hostname?: unknown): number {
updateDynamicRam("hackAnalyzeSecurity", getRamCost(Player, "hackAnalyzeSecurity")); updateDynamicRam("hackAnalyzeSecurity", getRamCost(Player, "hackAnalyzeSecurity"));
const threads = helper.number("hackAnalyzeSecurity", "threads", _threads); let threads = helper.number("hackAnalyzeSecurity", "threads", _threads);
if (_hostname) {
const hostname = helper.string("hackAnalyzeSecurity", "hostname", _hostname);
const server = safeGetServer(hostname, "hackAnalyze");
if (!(server instanceof Server)) {
workerScript.log("hackAnalyzeSecurity", () => "Cannot be executed on this server.");
return 0;
}
const percentHacked = calculatePercentMoneyHacked(server, Player);
if (percentHacked > 0) {
// thread count is limited to the maximum number of threads needed
threads = Math.ceil(1 / percentHacked);
}
}
return CONSTANTS.ServerFortifyAmount * threads; return CONSTANTS.ServerFortifyAmount * threads;
}, },
hackAnalyzeChance: function (_hostname: unknown): number { hackAnalyzeChance: function (_hostname: unknown): number {
@ -1415,6 +1460,13 @@ export function NetscriptFunctions(workerScript: WorkerScript): NS {
allFiles.sort(); allFiles.sort();
return allFiles; return allFiles;
}, },
getRecentScripts: function (): IRecentScript[] {
updateDynamicRam("getRecentScripts", getRamCost(Player, "getRecentScripts"));
return recentScripts.map((rs) => ({
timeOfDeath: rs.timeOfDeath,
...createPublicRunningScript(rs.runningScript),
}));
},
ps: function (_hostname: unknown = workerScript.hostname): ProcessInfo[] { ps: function (_hostname: unknown = workerScript.hostname): ProcessInfo[] {
updateDynamicRam("ps", getRamCost(Player, "ps")); updateDynamicRam("ps", getRamCost(Player, "ps"));
const hostname = helper.string("ps", "hostname", _hostname); const hostname = helper.string("ps", "hostname", _hostname);
@ -2130,21 +2182,7 @@ export function NetscriptFunctions(workerScript: WorkerScript): NS {
runningScript = getRunningScript(fn, hostname, "getRunningScript", args); runningScript = getRunningScript(fn, hostname, "getRunningScript", args);
} }
if (runningScript === null) return null; if (runningScript === null) return null;
return { return createPublicRunningScript(runningScript);
args: runningScript.args.slice(),
filename: runningScript.filename,
logs: runningScript.logs.slice(),
offlineExpGained: runningScript.offlineExpGained,
offlineMoneyMade: runningScript.offlineMoneyMade,
offlineRunningTime: runningScript.offlineRunningTime,
onlineExpGained: runningScript.onlineExpGained,
onlineMoneyMade: runningScript.onlineMoneyMade,
onlineRunningTime: runningScript.onlineRunningTime,
pid: runningScript.pid,
ramUsage: runningScript.ramUsage,
server: runningScript.server,
threads: runningScript.threads,
};
}, },
getHackTime: function (_hostname: unknown = workerScript.hostname): number { getHackTime: function (_hostname: unknown = workerScript.hostname): number {
updateDynamicRam("getHackTime", getRamCost(Player, "getHackTime")); updateDynamicRam("getHackTime", getRamCost(Player, "getHackTime"));

@ -637,9 +637,6 @@ export function NetscriptCorporation(
const office = getOffice(divisionName, cityName); const office = getOffice(divisionName, cityName);
if (!Object.values(EmployeePositions).includes(job)) throw new Error(`'${job}' is not a valid job.`); if (!Object.values(EmployeePositions).includes(job)) throw new Error(`'${job}' is not a valid job.`);
return netscriptDelay(1000, workerScript).then(function () { return netscriptDelay(1000, workerScript).then(function () {
if (workerScript.env.stopFlag) {
return Promise.reject(workerScript);
}
return Promise.resolve(office.setEmployeeToJob(job, amount)); return Promise.resolve(office.setEmployeeToJob(job, amount));
}); });
}, },
@ -753,6 +750,15 @@ export function NetscriptCorporation(
"Research & Development": office.employeeProd[EmployeePositions.RandD], "Research & Development": office.employeeProd[EmployeePositions.RandD],
Training: office.employeeProd[EmployeePositions.Training], Training: office.employeeProd[EmployeePositions.Training],
}, },
employeeJobs: {
Operations: office.employeeJobs[EmployeePositions.Operations],
Engineer: office.employeeJobs[EmployeePositions.Engineer],
Business: office.employeeJobs[EmployeePositions.Business],
Management: office.employeeJobs[EmployeePositions.Management],
"Research & Development": office.employeeJobs[EmployeePositions.RandD],
Training: office.employeeJobs[EmployeePositions.Training],
Unassigned: office.employeeJobs[EmployeePositions.Unassigned],
},
}; };
}, },
getEmployee: function (_divisionName: unknown, _cityName: unknown, _employeeName: unknown): NSEmployee { getEmployee: function (_divisionName: unknown, _cityName: unknown, _employeeName: unknown): NSEmployee {

@ -219,7 +219,7 @@ export function NetscriptSingularity(
workerScript.running = false; workerScript.running = false;
killWorkerScript(workerScript); killWorkerScript(workerScript);
}, },
installAugmentations: function (_cbScript: unknown): boolean { installAugmentations: function (_cbScript: unknown = ""): boolean {
updateRam("installAugmentations"); updateRam("installAugmentations");
const cbScript = helper.string("installAugmentations", "cbScript", _cbScript); const cbScript = helper.string("installAugmentations", "cbScript", _cbScript);
helper.checkSingularityAccess("installAugmentations"); helper.checkSingularityAccess("installAugmentations");

@ -12,7 +12,7 @@ import {
Stanek as IStanek, Stanek as IStanek,
} from "../ScriptEditor/NetscriptDefinitions"; } from "../ScriptEditor/NetscriptDefinitions";
import { AugmentationNames } from "../Augmentation/data/AugmentationNames"; import { AugmentationNames } from "../Augmentation/data/AugmentationNames";
import { NetscriptContext, InternalAPI } from "src/Netscript/APIWrapper"; import { NetscriptContext, InternalAPI } from "../Netscript/APIWrapper";
export function NetscriptStanek( export function NetscriptStanek(
player: IPlayer, player: IPlayer,
@ -26,12 +26,12 @@ export function NetscriptStanek(
} }
return { return {
giftWidth: (_ctx: NetscriptContext) => giftWidth: () =>
function (): number { function (): number {
checkStanekAPIAccess("giftWidth"); checkStanekAPIAccess("giftWidth");
return staneksGift.width(); return staneksGift.width();
}, },
giftHeight: (_ctx: NetscriptContext) => giftHeight: () =>
function (): number { function (): number {
checkStanekAPIAccess("giftHeight"); checkStanekAPIAccess("giftHeight");
return staneksGift.height(); return staneksGift.height();

@ -202,7 +202,7 @@ function _getScriptUrls(script: Script, scripts: Script[], seen: Script[]): Scri
// We automatically define a print function() in the NetscriptJS module so that // We automatically define a print function() in the NetscriptJS module so that
// accidental calls to window.print() do not bring up the "print screen" dialog // accidental calls to window.print() do not bring up the "print screen" dialog
transformedCode += `\n\nfunction print() {throw new Error("Invalid call to window.print(). Did you mean to use Netscript's print()?");}`; transformedCode += `\n\nfunction print() {throw new Error("Invalid call to window.print(). Did you mean to use Netscript's print()?");}\n//# sourceURL=${script.server}/${script.filename}`;
const blob = URL.createObjectURL(makeScriptBlob(transformedCode)); const blob = URL.createObjectURL(makeScriptBlob(transformedCode));
// Push the blob URL onto the top of the stack. // Push the blob URL onto the top of the stack.

@ -3,6 +3,7 @@
* that allows for scripts to run * that allows for scripts to run
*/ */
import { killWorkerScript } from "./Netscript/killWorkerScript"; import { killWorkerScript } from "./Netscript/killWorkerScript";
import { ScriptDeath } from "./Netscript/ScriptDeath";
import { WorkerScript } from "./Netscript/WorkerScript"; import { WorkerScript } from "./Netscript/WorkerScript";
import { workerScripts } from "./Netscript/WorkerScripts"; import { workerScripts } from "./Netscript/WorkerScripts";
import { WorkerScriptStartStopEventEmitter } from "./Netscript/WorkerScriptStartStopEventEmitter"; import { WorkerScriptStartStopEventEmitter } from "./Netscript/WorkerScriptStartStopEventEmitter";
@ -59,7 +60,7 @@ export function prestigeWorkerScripts(): void {
// JS script promises need a little massaging to have the same guarantees as netscript // JS script promises need a little massaging to have the same guarantees as netscript
// promises. This does said massaging and kicks the script off. It returns a promise // promises. This does said massaging and kicks the script off. It returns a promise
// that resolves or rejects when the corresponding worker script is done. // that resolves or rejects when the corresponding worker script is done.
function startNetscript2Script(player: IPlayer, workerScript: WorkerScript): Promise<WorkerScript> { function startNetscript2Script(player: IPlayer, workerScript: WorkerScript): Promise<void> {
workerScript.running = true; workerScript.running = true;
// The name of the currently running netscript function, to prevent concurrent // The name of the currently running netscript function, to prevent concurrent
@ -79,7 +80,7 @@ function startNetscript2Script(player: IPlayer, workerScript: WorkerScript): Pro
// This is not a problem for legacy Netscript because it also checks the // This is not a problem for legacy Netscript because it also checks the
// stop flag in the evaluator. // stop flag in the evaluator.
if (workerScript.env.stopFlag) { if (workerScript.env.stopFlag) {
throw workerScript; throw new ScriptDeath(workerScript);
} }
if (propName === "asleep") return f(...args); // OK for multiple simultaneous calls to sleep. if (propName === "asleep") return f(...args); // OK for multiple simultaneous calls to sleep.
@ -90,7 +91,7 @@ function startNetscript2Script(player: IPlayer, workerScript: WorkerScript): Pro
"promise-returning function? (Currently running: %s tried to run: %s)"; "promise-returning function? (Currently running: %s tried to run: %s)";
if (runningFn) { if (runningFn) {
workerScript.errorMessage = makeRuntimeRejectMsg(workerScript, sprintf(msg, runningFn, propName)); workerScript.errorMessage = makeRuntimeRejectMsg(workerScript, sprintf(msg, runningFn, propName));
throw workerScript; throw new ScriptDeath(workerScript);
} }
runningFn = propName; runningFn = propName;
@ -116,18 +117,29 @@ function startNetscript2Script(player: IPlayer, workerScript: WorkerScript): Pro
}; };
} }
for (const prop of Object.keys(workerScript.env.vars)) { function wrapObject(vars: any, ...tree: string[]): void {
if (typeof workerScript.env.vars[prop] !== "function") continue; for (const prop of Object.keys(vars)) {
workerScript.env.vars[prop] = wrap(prop, workerScript.env.vars[prop]); switch (typeof vars[prop]) {
case "function": {
vars[prop] = wrap([...tree, prop].join("."), vars[prop]);
break;
}
case "object": {
if (Array.isArray(vars[prop])) continue;
wrapObject(vars[prop], ...tree, prop);
break;
}
}
}
} }
workerScript.env.vars.stanek.charge = wrap("stanek.charge", workerScript.env.vars.stanek.charge); wrapObject(workerScript.env.vars);
// Note: the environment that we pass to the JS script only needs to contain the functions visible // Note: the environment that we pass to the JS script only needs to contain the functions visible
// to that script, which env.vars does at this point. // to that script, which env.vars does at this point.
return new Promise<WorkerScript>((resolve, reject) => { return new Promise<void>((resolve, reject) => {
executeJSScript(player, workerScript.getServer().scripts, workerScript) executeJSScript(player, workerScript.getServer().scripts, workerScript)
.then(() => { .then(() => {
resolve(workerScript); resolve();
}) })
.catch((e) => reject(e)); .catch((e) => reject(e));
}).catch((e) => { }).catch((e) => {
@ -140,20 +152,21 @@ function startNetscript2Script(player: IPlayer, workerScript: WorkerScript): Pro
e.message + ((e.stack && "\nstack:\n" + e.stack.toString()) || ""), e.message + ((e.stack && "\nstack:\n" + e.stack.toString()) || ""),
); );
} }
throw workerScript; throw new ScriptDeath(workerScript);
} else if (isScriptErrorMessage(e)) { } else if (isScriptErrorMessage(e)) {
workerScript.errorMessage = e; workerScript.errorMessage = e;
throw workerScript; throw new ScriptDeath(workerScript);
} else if (e instanceof WorkerScript) { } else if (e instanceof ScriptDeath) {
throw e; throw e;
} }
workerScript.errorMessage = makeRuntimeRejectMsg(workerScript, e); // Don't know what to do with it, let's try making an error message out of it
throw workerScript; // Don't know what to do with it, let's rethrow. workerScript.errorMessage = makeRuntimeRejectMsg(workerScript, "" + e);
throw new ScriptDeath(workerScript);
}); });
} }
function startNetscript1Script(workerScript: WorkerScript): Promise<WorkerScript> { function startNetscript1Script(workerScript: WorkerScript): Promise<void> {
const code = workerScript.code; const code = workerScript.code;
workerScript.running = true; workerScript.running = true;
@ -168,7 +181,7 @@ function startNetscript1Script(workerScript: WorkerScript): Promise<WorkerScript
workerScript.env.stopFlag = true; workerScript.env.stopFlag = true;
workerScript.running = false; workerScript.running = false;
killWorkerScript(workerScript); killWorkerScript(workerScript);
return Promise.resolve(workerScript); return Promise.resolve();
} }
const interpreterInitialization = function (int: any, scope: any): void { const interpreterInitialization = function (int: any, scope: any): void {
@ -201,7 +214,7 @@ function startNetscript1Script(workerScript: WorkerScript): Promise<WorkerScript
}) })
.catch(function (err: any) { .catch(function (err: any) {
// workerscript is when you cancel a delay // workerscript is when you cancel a delay
if (!(err instanceof WorkerScript)) { if (!(err instanceof ScriptDeath)) {
console.error(err); console.error(err);
const errorTextArray = err.split("|DELIMITER|"); const errorTextArray = err.split("|DELIMITER|");
const hostname = errorTextArray[1]; const hostname = errorTextArray[1];
@ -214,7 +227,7 @@ function startNetscript1Script(workerScript: WorkerScript): Promise<WorkerScript
workerScript.env.stopFlag = true; workerScript.env.stopFlag = true;
workerScript.running = false; workerScript.running = false;
killWorkerScript(workerScript); killWorkerScript(workerScript);
return Promise.resolve(workerScript); return Promise.resolve();
} }
}); });
}; };
@ -277,14 +290,14 @@ function startNetscript1Script(workerScript: WorkerScript): Promise<WorkerScript
workerScript.env.stopFlag = true; workerScript.env.stopFlag = true;
workerScript.running = false; workerScript.running = false;
killWorkerScript(workerScript); killWorkerScript(workerScript);
return Promise.resolve(workerScript); return Promise.resolve();
} }
return new Promise(function (resolve, reject) { return new Promise(function (resolve, reject) {
function runInterpreter(): void { function runInterpreter(): void {
try { try {
if (workerScript.env.stopFlag) { if (workerScript.env.stopFlag) {
return reject(workerScript); return reject(new ScriptDeath(workerScript));
} }
let more = true; let more = true;
@ -297,7 +310,7 @@ function startNetscript1Script(workerScript: WorkerScript): Promise<WorkerScript
if (more) { if (more) {
setTimeout(runInterpreter, Settings.CodeInstructionRunTime); setTimeout(runInterpreter, Settings.CodeInstructionRunTime);
} else { } else {
resolve(workerScript); resolve();
} }
} catch (e: any) { } catch (e: any) {
e = e.toString(); e = e.toString();
@ -305,7 +318,7 @@ function startNetscript1Script(workerScript: WorkerScript): Promise<WorkerScript
e = makeRuntimeRejectMsg(workerScript, e); e = makeRuntimeRejectMsg(workerScript, e);
} }
workerScript.errorMessage = e; workerScript.errorMessage = e;
return reject(workerScript); return reject(new ScriptDeath(workerScript));
} }
} }
@ -314,11 +327,12 @@ function startNetscript1Script(workerScript: WorkerScript): Promise<WorkerScript
} catch (e: any) { } catch (e: any) {
if (isString(e)) { if (isString(e)) {
workerScript.errorMessage = e; workerScript.errorMessage = e;
return reject(workerScript); return reject(new ScriptDeath(workerScript));
} else if (e instanceof WorkerScript) { } else if (e instanceof ScriptDeath) {
return reject(e); return reject(e);
} else { } else {
return reject(workerScript); console.error(e);
return reject(new ScriptDeath(workerScript));
} }
} }
}); });
@ -541,83 +555,85 @@ function createAndAddWorkerScript(
// Create the WorkerScript. NOTE: WorkerScript ctor will set the underlying // Create the WorkerScript. NOTE: WorkerScript ctor will set the underlying
// RunningScript's PID as well // RunningScript's PID as well
const s = new WorkerScript(runningScriptObj, pid, NetscriptFunctions); const workerScript = new WorkerScript(runningScriptObj, pid, NetscriptFunctions);
s.ramUsage = oneRamUsage; workerScript.ramUsage = oneRamUsage;
// Add the WorkerScript to the global pool // Add the WorkerScript to the global pool
workerScripts.set(pid, s); workerScripts.set(pid, workerScript);
WorkerScriptStartStopEventEmitter.emit(); WorkerScriptStartStopEventEmitter.emit();
// Start the script's execution // Start the script's execution
let p: Promise<WorkerScript> | null = null; // Script's resulting promise let scriptExecution: Promise<void> | null = null; // Script's resulting promise
if (s.name.endsWith(".js") || s.name.endsWith(".ns")) { if (workerScript.name.endsWith(".js") || workerScript.name.endsWith(".ns")) {
p = startNetscript2Script(player, s); scriptExecution = startNetscript2Script(player, workerScript);
} else { } else {
p = startNetscript1Script(s); scriptExecution = startNetscript1Script(workerScript);
if (!(p instanceof Promise)) { if (!(scriptExecution instanceof Promise)) {
return false; return false;
} }
} }
// Once the code finishes (either resolved or rejected, doesnt matter), set its // Once the code finishes (either resolved or rejected, doesnt matter), set its
// running status to false // running status to false
p.then(function (w: WorkerScript) { scriptExecution
w.running = false; .then(function () {
w.env.stopFlag = true; workerScript.running = false;
// On natural death, the earnings are transfered to the parent if it still exists. workerScript.env.stopFlag = true;
if (parent !== undefined) { // On natural death, the earnings are transfered to the parent if it still exists.
if (parent.running) { if (parent !== undefined) {
parent.scriptRef.onlineExpGained += runningScriptObj.onlineExpGained; if (parent.running) {
parent.scriptRef.onlineMoneyMade += runningScriptObj.onlineMoneyMade; parent.scriptRef.onlineExpGained += runningScriptObj.onlineExpGained;
parent.scriptRef.onlineMoneyMade += runningScriptObj.onlineMoneyMade;
}
} }
}
killWorkerScript(s); killWorkerScript(workerScript);
w.log("", () => "Script finished running"); workerScript.log("", () => "Script finished running");
}).catch(function (w) { })
if (w instanceof Error) { .catch(function (e) {
dialogBoxCreate("Script runtime unknown error. This is a bug please contact game developer"); if (e instanceof Error) {
console.error("Evaluating workerscript returns an Error. THIS SHOULDN'T HAPPEN: " + w.toString()); dialogBoxCreate("Script runtime unknown error. This is a bug please contact game developer");
return; console.error("Evaluating workerscript returns an Error. THIS SHOULDN'T HAPPEN: " + e.toString());
} else if (w instanceof WorkerScript) { return;
if (isScriptErrorMessage(w.errorMessage)) { } else if (e instanceof ScriptDeath) {
const errorTextArray = w.errorMessage.split("|DELIMITER|"); if (isScriptErrorMessage(workerScript.errorMessage)) {
if (errorTextArray.length != 4) { const errorTextArray = workerScript.errorMessage.split("|DELIMITER|");
console.error("ERROR: Something wrong with Error text in evaluator..."); if (errorTextArray.length != 4) {
console.error("Error text: " + w.errorMessage); console.error("ERROR: Something wrong with Error text in evaluator...");
return; console.error("Error text: " + workerScript.errorMessage);
return;
}
const hostname = errorTextArray[1];
const scriptName = errorTextArray[2];
const errorMsg = errorTextArray[3];
let msg = `RUNTIME ERROR<br>${scriptName}@${hostname} (PID - ${workerScript.pid})<br>`;
if (workerScript.args.length > 0) {
msg += `Args: ${arrayToString(workerScript.args)}<br>`;
}
msg += "<br>";
msg += errorMsg;
dialogBoxCreate(msg);
workerScript.log("", () => "Script crashed with runtime error");
} else {
workerScript.log("", () => "Script killed");
return; // Already killed, so stop here
} }
const hostname = errorTextArray[1]; } else if (isScriptErrorMessage(e)) {
const scriptName = errorTextArray[2]; dialogBoxCreate("Script runtime unknown error. This is a bug please contact game developer");
const errorMsg = errorTextArray[3]; console.error(
"ERROR: Evaluating workerscript returns only error message rather than WorkerScript object. THIS SHOULDN'T HAPPEN: " +
let msg = `RUNTIME ERROR<br>${scriptName}@${hostname}<br>`; e.toString(),
if (w.args.length > 0) { );
msg += `Args: ${arrayToString(w.args)}<br>`; return;
}
msg += "<br>";
msg += errorMsg;
dialogBoxCreate(msg);
w.log("", () => "Script crashed with runtime error");
} else { } else {
w.log("", () => "Script killed"); dialogBoxCreate("An unknown script died for an unknown reason. This is a bug please contact game dev");
return; // Already killed, so stop here console.error(e);
} }
} else if (isScriptErrorMessage(w)) {
dialogBoxCreate("Script runtime unknown error. This is a bug please contact game developer");
console.error(
"ERROR: Evaluating workerscript returns only error message rather than WorkerScript object. THIS SHOULDN'T HAPPEN: " +
w.toString(),
);
return;
} else {
dialogBoxCreate("An unknown script died for an unknown reason. This is a bug please contact game dev");
console.error(w);
}
killWorkerScript(s); killWorkerScript(workerScript);
}); });
return true; return true;
} }

@ -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>

@ -101,24 +101,46 @@ interface Player {
/** /**
* @public * @public
*/ */
interface RunningScript { export interface RunningScript {
/** Arguments the script was called with */
args: string[]; args: string[];
/** Filename of the script */
filename: string; filename: string;
/**
* Script logs as an array. The newest log entries are at the bottom.
* Timestamps, if enabled, are placed inside `[brackets]` at the start of each line.
**/
logs: string[]; logs: string[];
/** Total amount of hacking experience earned from this script when offline */
offlineExpGained: number; offlineExpGained: number;
/** Total amount of money made by this script when offline */
offlineMoneyMade: number; offlineMoneyMade: number;
/** Offline running time of the script, in seconds **/ /** Number of seconds that the script has been running offline */
offlineRunningTime: number; offlineRunningTime: number;
/** Total amount of hacking experience earned from this script when online */
onlineExpGained: number; onlineExpGained: number;
/** Total amount of money made by this script when online */
onlineMoneyMade: number; onlineMoneyMade: number;
/** Online running time of the script, in seconds **/ /** Number of seconds that this script has been running online */
onlineRunningTime: number; onlineRunningTime: number;
/** Process ID. Must be an integer */
pid: number; pid: number;
/** How much RAM this script uses for ONE thread */
ramUsage: number; ramUsage: number;
/** Hostname of the server on which this script runs */
server: string; server: string;
/** Number of threads that this script runs with */
threads: number; threads: number;
} }
/**
* @public
*/
export interface RecentScript extends RunningScript {
/** Timestamp of when the script was killed */
timeOfDeath: Date;
}
/** /**
* Data representing the internal values of a crime. * Data representing the internal values of a crime.
* @public * @public
@ -2618,7 +2640,7 @@ export interface Hacknet {
* // NS1: * // NS1:
* var upgradeName = "Sell for Corporation Funds"; * var upgradeName = "Sell for Corporation Funds";
* if (hacknet.numHashes() > hacknet.hashCost(upgradeName)) { * if (hacknet.numHashes() > hacknet.hashCost(upgradeName)) {
* hacknet.spendHashes(upgName); * hacknet.spendHashes(upgradeName);
* } * }
* ``` * ```
* @example * @example
@ -2626,7 +2648,7 @@ export interface Hacknet {
* // NS2: * // NS2:
* const upgradeName = "Sell for Corporation Funds"; * const upgradeName = "Sell for Corporation Funds";
* if (ns.hacknet.numHashes() > ns.hacknet.hashCost(upgradeName)) { * if (ns.hacknet.numHashes() > ns.hacknet.hashCost(upgradeName)) {
* ns.hacknet.spendHashes(upgName); * ns.hacknet.spendHashes(upgradeName);
* } * }
* ``` * ```
* @param upgName - Name of the upgrade of Hacknet Node. * @param upgName - Name of the upgrade of Hacknet Node.
@ -4555,9 +4577,10 @@ export interface NS extends Singularity {
* Returns the security increase that would occur if a hack with this many threads happened. * Returns the security increase that would occur if a hack with this many threads happened.
* *
* @param threads - Amount of threads that will be used. * @param threads - Amount of threads that will be used.
* @param hostname - Hostname of the target server. The number of threads is limited to the number needed to hack the servers maximum amount of money.
* @returns The security increase. * @returns The security increase.
*/ */
hackAnalyzeSecurity(threads: number): number; hackAnalyzeSecurity(threads: number, hostname?: string): number;
/** /**
* Get the chance of successfully hacking a server. * Get the chance of successfully hacking a server.
@ -4781,6 +4804,27 @@ export interface NS extends Singularity {
*/ */
getScriptLogs(fn?: string, host?: string, ...args: any[]): string[]; getScriptLogs(fn?: string, host?: string, ...args: any[]): string[];
/**
* Get an array of recently killed scripts across all servers.
* @remarks
* RAM cost: 0.2 GB
*
* The most recently killed script is the first element in the array.
* Note that there is a maximum number of recently killed scripts which are tracked.
* This is configurable in the game's options as `Recently killed scripts size`.
*
* @usage below:
* ```ts
* let recentScripts = ns.getRecentScripts();
* let mostRecent = recentScripts.shift()
* if (mostRecent)
* ns.tprint(mostRecent.logs.join('\n'))
* ```
*
* @returns Array with information about previously killed scripts.
*/
getRecentScripts(): RecentScript[];
/** /**
* Open the tail window of a script. * Open the tail window of a script.
* @remarks * @remarks
@ -6965,8 +7009,10 @@ interface Office {
maxMor: number; maxMor: number;
/** Name of all the employees */ /** Name of all the employees */
employees: string[]; employees: string[];
/** Positions of the employees */ /** Production of the employees */
employeeProd: EmployeeJobs; employeeProd: EmployeeJobs;
/** Positions of the employees */
employeeJobs: EmployeeJobs;
} }
/** /**

@ -981,11 +981,11 @@ export function Root(props: IProps): React.ReactElement {
</Button> </Button>
<Typography> <Typography>
{" "} {" "}
Documentation:{" "} <strong>Documentation:</strong>{" "}
<Link target="_blank" href="https://bitburner.readthedocs.io/en/latest/index.html"> <Link target="_blank" href="https://bitburner.readthedocs.io/en/latest/index.html">
Basic Basic
</Link>{" "} </Link>
| {" | "}
<Link target="_blank" href="https://github.com/danielyxie/bitburner/blob/dev/markdown/bitburner.ns.md"> <Link target="_blank" href="https://github.com/danielyxie/bitburner/blob/dev/markdown/bitburner.ns.md">
Full Full
</Link> </Link>

@ -63,6 +63,11 @@ interface IDefaultSettings {
*/ */
Locale: string; Locale: string;
/**
* Limit the number of recently killed script entries being tracked.
*/
MaxRecentScriptsCapacity: number;
/** /**
* Limit the number of log entries for each script being executed on each server. * Limit the number of log entries for each script being executed on each server.
*/ */
@ -191,6 +196,7 @@ export const defaultSettings: IDefaultSettings = {
EnableBashHotkeys: false, EnableBashHotkeys: false,
TimestampsFormat: "", TimestampsFormat: "",
Locale: "en", Locale: "en",
MaxRecentScriptsCapacity: 50,
MaxLogCapacity: 50, MaxLogCapacity: 50,
MaxPortCapacity: 50, MaxPortCapacity: 50,
MaxTerminalCapacity: 500, MaxTerminalCapacity: 500,
@ -228,6 +234,7 @@ export const Settings: ISettings & ISelfInitializer & ISelfLoading = {
EnableBashHotkeys: defaultSettings.EnableBashHotkeys, EnableBashHotkeys: defaultSettings.EnableBashHotkeys,
TimestampsFormat: defaultSettings.TimestampsFormat, TimestampsFormat: defaultSettings.TimestampsFormat,
Locale: "en", Locale: "en",
MaxRecentScriptsCapacity: defaultSettings.MaxRecentScriptsCapacity,
MaxLogCapacity: defaultSettings.MaxLogCapacity, MaxLogCapacity: defaultSettings.MaxLogCapacity,
MaxPortCapacity: defaultSettings.MaxPortCapacity, MaxPortCapacity: defaultSettings.MaxPortCapacity,
MaxTerminalCapacity: defaultSettings.MaxTerminalCapacity, MaxTerminalCapacity: defaultSettings.MaxTerminalCapacity,

@ -191,8 +191,8 @@ export const HelpTexts: IMap<string[]> = {
"Usage: connect [hostname]", "Usage: connect [hostname]",
" ", " ",
"Connect to a remote server. The hostname of the remote server must be given as the argument ", "Connect to a remote server. The hostname of the remote server must be given as the argument ",
"to this command. Note that only servers that are immediately adjacent to the current server in the network can be connected to. To ", "to this command. Note that only servers that are immediately adjacent to the current server in the network and the ones that have",
"see which servers can be connected to, use the 'scan' command.", "a backdoor installed can be connected to. To see which servers can be connected to, use the 'scan' command.",
" ", " ",
], ],
cp: ["Usage: cp [src] [dst]", " ", "Copy a file on this server. To copy a file to another server use scp.", " "], cp: ["Usage: cp [src] [dst]", " ", "Copy a file on this server. To copy a file to another server use scp.", " "],

@ -4,7 +4,8 @@ import { getSubdirectories } from "./DirectoryServerHelpers";
import { Aliases, GlobalAliases, substituteAliases } from "../Alias"; import { Aliases, GlobalAliases, substituteAliases } from "../Alias";
import { DarkWebItems } from "../DarkWeb/DarkWebItems"; import { DarkWebItems } from "../DarkWeb/DarkWebItems";
import { IPlayer } from "../PersonObjects/IPlayer"; import { IPlayer } from "../PersonObjects/IPlayer";
import { GetServer, GetAllServers } from "../Server/AllServers"; import { GetAllServers } from "../Server/AllServers";
import { Server } from "../Server/Server";
import { ParseCommand, ParseCommands } from "./Parser"; import { ParseCommand, ParseCommands } from "./Parser";
import { HelpTexts } from "./HelpText"; import { HelpTexts } from "./HelpText";
import { isScriptFilename } from "../Script/isScriptFilename"; import { isScriptFilename } from "../Script/isScriptFilename";
@ -238,16 +239,14 @@ export async function determineAllPossibilitiesForTabCompletion(
} }
if (isCommand("connect")) { if (isCommand("connect")) {
// All network connections // All directly connected and backdoored servers are reachable
for (let i = 0; i < currServ.serversOnNetwork.length; ++i) { console.log(GetAllServers());
const serv = GetServer(currServ.serversOnNetwork[i]); return GetAllServers()
if (serv == null) { .filter(
continue; (server) =>
} currServ.serversOnNetwork.includes(server.hostname) || (server instanceof Server && server.backdoorInstalled),
allPos.push(serv.hostname); )
} .map((server) => server.hostname);
return allPos;
} }
if (isCommand("nano") || isCommand("vim")) { if (isCommand("nano") || isCommand("vim")) {

@ -1,4 +1,4 @@
import { WorkerScript } from "./Netscript/WorkerScript"; import { ScriptDeath } from "./Netscript/ScriptDeath";
import { isScriptErrorMessage } from "./NetscriptEvaluator"; import { isScriptErrorMessage } from "./NetscriptEvaluator";
import { dialogBoxCreate } from "./ui/React/DialogBox"; import { dialogBoxCreate } from "./ui/React/DialogBox";
@ -14,9 +14,9 @@ export function setupUncaughtPromiseHandler(): void {
msg += "<br>"; msg += "<br>";
msg += errorMsg; msg += errorMsg;
dialogBoxCreate(msg); dialogBoxCreate(msg);
} else if (e.reason instanceof WorkerScript) { } else if (e.reason instanceof ScriptDeath) {
const msg = const msg =
`UNCAUGHT PROMISE ERROR<br>You forgot to await a promise<br>${e.reason.name}@${e.reason.hostname}<br>` + `UNCAUGHT PROMISE ERROR<br>You forgot to await a promise<br>${e.reason.name}@${e.reason.hostname} (PID - ${e.reason.pid})<br>` +
`Maybe hack / grow / weaken ?`; `Maybe hack / grow / weaken ?`;
dialogBoxCreate(msg); dialogBoxCreate(msg);
} }

@ -1256,7 +1256,7 @@ export const codingContractTypesMetadata: ICodingContractTypeMetadata[] = [
"Convert it into a binary string and encode it as a 'Hamming-Code'. eg:\n ", "Convert it into a binary string and encode it as a 'Hamming-Code'. eg:\n ",
"Value 8 will result into binary '1000', which will be encoded", "Value 8 will result into binary '1000', which will be encoded",
"with the pattern 'pppdpddd', where p is a paritybit and d a databit,\n", "with the pattern 'pppdpddd', where p is a paritybit and d a databit,\n",
"or '10101' (Value 21) will result into (pppdpdddpd) '1111101011'.\n\n", "or '10101' (Value 21) will result into (pppdpdddpd) '1001101011'.\n\n",
"NOTE: You need an parity Bit on Index 0 as an 'overall'-paritybit. \n", "NOTE: You need an parity Bit on Index 0 as an 'overall'-paritybit. \n",
"NOTE 2: You should watch the HammingCode-video from 3Blue1Brown, which explains the 'rule' of encoding,", "NOTE 2: You should watch the HammingCode-video from 3Blue1Brown, which explains the 'rule' of encoding,",
"including the first Index parity-bit mentioned on the first note.\n\n", "including the first Index parity-bit mentioned on the first note.\n\n",

@ -57,8 +57,8 @@ export function RecentScriptAccordion(props: IProps): React.ReactElement {
<ListItemText <ListItemText
primary={ primary={
<Typography> <Typography>
{recentScript.filename} (died{" "} {recentScript.runningScript.filename} (died{" "}
{convertTimeMsToTimeElapsedString(new Date().getTime() - recentScript.timestamp.getTime())} ago) {convertTimeMsToTimeElapsedString(new Date().getTime() - recentScript.timeOfDeath.getTime())} ago)
</Typography> </Typography>
} }
/> />
@ -78,7 +78,7 @@ export function RecentScriptAccordion(props: IProps): React.ReactElement {
</TableRow> </TableRow>
<TableRow> <TableRow>
<TableCell className={classes.noborder} colSpan={2}> <TableCell className={classes.noborder} colSpan={2}>
<Typography> Args: {arrayToString(recentScript.args)}</Typography> <Typography> Args: {arrayToString(recentScript.runningScript.args)}</Typography>
</TableCell> </TableCell>
</TableRow> </TableRow>
<TableRow> <TableRow>

@ -13,7 +13,7 @@ export function RecentScriptsPage(): React.ReactElement {
<> <>
<Typography>List of all recently killed scripts.</Typography> <Typography>List of all recently killed scripts.</Typography>
{recentScripts.map((r) => ( {recentScripts.map((r) => (
<RecentScriptAccordion key={r.pid} recentScript={r} /> <RecentScriptAccordion key={r.runningScript.pid} recentScript={r} />
))} ))}
</> </>
); );

@ -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>
); );
} }

@ -491,7 +491,6 @@ export function GameRoot({ player, engine, terminal }: IProps): React.ReactEleme
}} }}
installAugmentationsFn={() => { installAugmentationsFn={() => {
installAugmentations(); installAugmentations();
Router.toTerminal();
}} }}
/> />
); );

@ -64,6 +64,7 @@ export function GameOptionsRoot(props: IProps): React.ReactElement {
const importInput = useRef<HTMLInputElement>(null); const importInput = useRef<HTMLInputElement>(null);
const [execTime, setExecTime] = useState(Settings.CodeInstructionRunTime); const [execTime, setExecTime] = useState(Settings.CodeInstructionRunTime);
const [recentScriptsSize, setRecentScriptsSize] = useState(Settings.MaxRecentScriptsCapacity);
const [logSize, setLogSize] = useState(Settings.MaxLogCapacity); const [logSize, setLogSize] = useState(Settings.MaxLogCapacity);
const [portSize, setPortSize] = useState(Settings.MaxPortCapacity); const [portSize, setPortSize] = useState(Settings.MaxPortCapacity);
const [terminalSize, setTerminalSize] = useState(Settings.MaxTerminalCapacity); const [terminalSize, setTerminalSize] = useState(Settings.MaxTerminalCapacity);
@ -79,6 +80,11 @@ export function GameOptionsRoot(props: IProps): React.ReactElement {
Settings.CodeInstructionRunTime = newValue as number; Settings.CodeInstructionRunTime = newValue as number;
} }
function handleRecentScriptsSizeChange(event: any, newValue: number | number[]): void {
setRecentScriptsSize(newValue as number);
Settings.MaxRecentScriptsCapacity = newValue as number;
}
function handleLogSizeChange(event: any, newValue: number | number[]): void { function handleLogSizeChange(event: any, newValue: number | number[]): void {
setLogSize(newValue as number); setLogSize(newValue as number);
Settings.MaxLogCapacity = newValue as number; Settings.MaxLogCapacity = newValue as number;
@ -176,6 +182,24 @@ export function GameOptionsRoot(props: IProps): React.ReactElement {
max={100} max={100}
valueLabelDisplay="auto" valueLabelDisplay="auto"
/> />
<Tooltip
title={
<Typography>
The maximum number of recently killed script entries being tracked. Setting this too high can
cause the game to use a lot of memory.
</Typography>
}
>
<Typography>Recently killed scripts size</Typography>
</Tooltip>
<Slider
value={recentScriptsSize}
onChange={handleRecentScriptsSizeChange}
step={25}
min={0}
max={500}
valueLabelDisplay="auto"
/>
<Tooltip <Tooltip
title={ title={
<Typography> <Typography>

@ -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>
); );

@ -42,10 +42,10 @@ describe("determineAllPossibilitiesForTabCompletion", function () {
requiredHackingSkill: 1, requiredHackingSkill: 1,
serverGrowth: 3000, serverGrowth: 3000,
}); });
Player.getHomeComputer().serversOnNetwork.push(closeServer.ip); Player.getHomeComputer().serversOnNetwork.push(closeServer.hostname);
closeServer.serversOnNetwork.push(Player.getHomeComputer().ip); closeServer.serversOnNetwork.push(Player.getHomeComputer().hostname);
closeServer.serversOnNetwork.push(farServer.ip); closeServer.serversOnNetwork.push(farServer.hostname);
farServer.serversOnNetwork.push(closeServer.ip); farServer.serversOnNetwork.push(closeServer.hostname);
AddToAllServers(closeServer); AddToAllServers(closeServer);
AddToAllServers(farServer); AddToAllServers(farServer);
}); });

22
tools/package-electron.sh Normal file

@ -0,0 +1,22 @@
#!/bin/sh
set -euxo pipefail
# Clear out any files remaining from old builds
rm -rf .package
mkdir -p .package/dist/ || true
cp index.html .package
cp favicon.ico .package
cp -r electron/* .package
cp -r dist .package
# Install electron sub-dependencies
cd electron
npm install
cd ..
BUILD_PLATFORM="${1:-"all"}"
# And finally build the app.
npm run electron:packager-$BUILD_PLATFORM

@ -11,7 +11,7 @@ module.exports = (env, argv) => {
const runInContainer = (env || {}).runInContainer === true; const runInContainer = (env || {}).runInContainer === true;
const isDevelopment = argv.mode === "development"; const isDevelopment = argv.mode === "development";
const isFastRefresh = argv.fast === "true"; const isFastRefresh = argv.fast === "true";
const outputDirectory = isDevServer ? "dist-dev" : "dist"; const outputDirectory = "dist";
const entry = "./src/index.tsx"; const entry = "./src/index.tsx";
const statsConfig = { const statsConfig = {
@ -46,6 +46,48 @@ module.exports = (env, argv) => {
// https://stackoverflow.com/a/38401256 // https://stackoverflow.com/a/38401256
const commitHash = require("child_process").execSync("git rev-parse --short HEAD").toString().trim(); const commitHash = require("child_process").execSync("git rev-parse --short HEAD").toString().trim();
const htmlConfig = {
title: "Bitburner",
template: "src/index.html",
favicon: "favicon.ico",
googleAnalytics: {
trackingId: "UA-100157497-1",
},
meta: {},
minify: isDevelopment
? false
: {
collapseBooleanAttributes: true,
collapseInlineTagWhitespace: false,
collapseWhitespace: false,
conservativeCollapse: false,
html5: true,
includeAutoGeneratedTags: false,
keepClosingSlash: true,
minifyCSS: false,
minifyJS: false,
minifyURLs: false,
preserveLineBreaks: false,
preventAttributesEscaping: false,
processConditionalComments: false,
quoteCharacter: '"',
removeAttributeQuotes: false,
removeComments: false,
removeEmptyAttributes: false,
removeEmptyElements: false,
removeOptionalTags: false,
removeScriptTypeAttributes: false,
removeStyleLinkTypeAttributes: false,
removeTagWhitespace: false,
sortAttributes: false,
sortClassName: false,
useShortDoctype: false,
},
};
if (!isDevelopment) {
htmlConfig.filename = "../index.html";
}
return { return {
plugins: [ plugins: [
new webpack.DefinePlugin({ new webpack.DefinePlugin({
@ -60,44 +102,7 @@ module.exports = (env, argv) => {
jQuery: "jquery", jQuery: "jquery",
$: "jquery", $: "jquery",
}), }),
new HtmlWebpackPlugin({ new HtmlWebpackPlugin(htmlConfig),
title: "Bitburner",
template: "src/index.html",
favicon: "favicon.ico",
googleAnalytics: {
trackingId: "UA-100157497-1",
},
meta: {},
minify: isDevelopment
? false
: {
collapseBooleanAttributes: true,
collapseInlineTagWhitespace: false,
collapseWhitespace: false,
conservativeCollapse: false,
html5: true,
includeAutoGeneratedTags: false,
keepClosingSlash: true,
minifyCSS: false,
minifyJS: false,
minifyURLs: false,
preserveLineBreaks: false,
preventAttributesEscaping: false,
processConditionalComments: false,
quoteCharacter: '"',
removeAttributeQuotes: false,
removeComments: false,
removeEmptyAttributes: false,
removeEmptyElements: false,
removeOptionalTags: false,
removeScriptTypeAttributes: false,
removeStyleLinkTypeAttributes: false,
removeTagWhitespace: false,
sortAttributes: false,
sortClassName: false,
useShortDoctype: false,
},
}),
new MiniCssExtractPlugin({ new MiniCssExtractPlugin({
filename: "[name].css", filename: "[name].css",
}), }),
@ -136,7 +141,7 @@ module.exports = (env, argv) => {
// }, // },
entry: entry, entry: entry,
output: { output: {
path: path.resolve(__dirname, "./"), path: path.resolve(__dirname, outputDirectory),
filename: "[name].bundle.js", filename: "[name].bundle.js",
}, },
module: { module: {
@ -161,7 +166,7 @@ module.exports = (env, argv) => {
loader: "file-loader", loader: "file-loader",
options: { options: {
name: "[contenthash].[ext]", name: "[contenthash].[ext]",
outputPath: "dist/images", outputPath: "images",
}, },
}, },
], ],
@ -184,7 +189,7 @@ module.exports = (env, argv) => {
cacheGroups: { cacheGroups: {
vendor: { vendor: {
test: /[\\/]node_modules[\\/]/, test: /[\\/]node_modules[\\/]/,
name: `${outputDirectory}/vendor`, name: `vendor`,
chunks: "all", chunks: "all",
}, },
}, },