Merge branch 'dev' of github.com:danielyxie/bitburner into feature/grafting

This commit is contained in:
nickofolas 2022-03-21 11:08:48 -05:00
commit dfca624e35
59 changed files with 2910 additions and 2520 deletions

@ -3,9 +3,9 @@ name: CI
on: on:
# Triggers the workflow on push or pull request events but only for the dev branch # Triggers the workflow on push or pull request events but only for the dev branch
push: push:
branches: [ dev ] branches: [dev]
pull_request: pull_request:
branches: [ dev ] branches: [dev]
# Allows you to run this workflow manually from the Actions tab # Allows you to run this workflow manually from the Actions tab
workflow_dispatch: workflow_dispatch:
@ -20,7 +20,7 @@ jobs:
uses: actions/setup-node@v2 uses: actions/setup-node@v2
with: with:
node-version: 16.13.1 node-version: 16.13.1
cache: 'npm' cache: "npm"
- name: Install npm dependencies - name: Install npm dependencies
run: npm ci run: npm ci
- name: Build the production app - name: Build the production app
@ -34,11 +34,25 @@ jobs:
uses: actions/setup-node@v2 uses: actions/setup-node@v2
with: with:
node-version: 16.13.1 node-version: 16.13.1
cache: 'npm' cache: "npm"
- name: Install npm dependencies - name: Install npm dependencies
run: npm ci run: npm ci
- name: Run linter - name: Run linter
run: npm run lint:report run: npm run lint:report
prettier:
name: Prettier
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: Use Node.js 16.13.1
uses: actions/setup-node@v2
with:
node-version: 16.13.1
cache: "npm"
- name: Install npm dependencies
run: npm ci
- name: Run prettier check
run: npm run format:report
test: test:
name: Test name: Test
runs-on: ubuntu-latest runs-on: ubuntu-latest
@ -48,7 +62,7 @@ jobs:
uses: actions/setup-node@v2 uses: actions/setup-node@v2
with: with:
node-version: 16.13.1 node-version: 16.13.1
cache: 'npm' cache: "npm"
- name: Install npm dependencies - name: Install npm dependencies
run: npm ci run: npm ci
- name: Run tests - name: Run tests

34
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

@ -347,7 +347,7 @@ Kills all scripts on the current server.
ls ls
^^ ^^
$ ls [dir] [| grep pattern] $ ls [dir] [--grep pattern]
Prints files and directories on the current server to the Terminal screen. Prints files and directories on the current server to the Terminal screen.
@ -358,19 +358,21 @@ followed by the files (also in alphabetical order).
The :code:`dir` optional parameter allows you to specify the directory for which to display The :code:`dir` optional parameter allows you to specify the directory for which to display
files. files.
The :code:`| grep pattern` optional parameter allows you to only display files and directories The :code:`--grep pattern` optional parameter allows you to only display files and directories
with a certain pattern in their names. with a certain pattern in their names.
The :code:`-l` optional parameter allows you to force each item onto a single line.
Examples:: Examples::
// List files/directories with the '.script' extension in the current directory // List files/directories with the '.script' extension in the current directory
$ ls | grep .script $ ls -l --grep .script
// List files/directories with the '.js' extension in the root directory // List files/directories with the '.js' extension in the root directory
$ ls / | grep .js $ ls / -l --grep .js
// List files/directories with the word 'purchase' in the name, in the :code:`scripts` directory // List files/directories with the word 'purchase' in the name, in the :code:`scripts` directory
$ ls scripts | grep purchase $ ls scripts -l --grep purchase
lscpu lscpu

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

@ -108,6 +108,7 @@
"cy:run": "cypress run", "cy:run": "cypress run",
"doc": "npx api-extractor run && npx api-documenter markdown && rm input/bitburner.api.json && rm -r input", "doc": "npx api-extractor run && npx api-documenter markdown && rm input/bitburner.api.json && rm -r input",
"format": "prettier --write .", "format": "prettier --write .",
"format:report": "prettier -c .",
"start": "http-server -p 8000", "start": "http-server -p 8000",
"start:dev": "webpack-dev-server --progress --env.devServer --mode development", "start:dev": "webpack-dev-server --progress --env.devServer --mode development",
"start:dev-fast": "webpack-dev-server --progress --env.devServer --mode development --fast true", "start:dev-fast": "webpack-dev-server --progress --env.devServer --mode development --fast true",

@ -58,7 +58,9 @@ function bitNodeFinishedState(): boolean {
const wd = GetServer(SpecialServers.WorldDaemon); const wd = GetServer(SpecialServers.WorldDaemon);
if (!(wd instanceof Server)) return false; if (!(wd instanceof Server)) return false;
if (wd.backdoorInstalled) return true; if (wd.backdoorInstalled) return true;
return Player.bladeburner !== null && Player.bladeburner.blackops.hasOwnProperty(BlackOperationNames.OperationDaedalus); return (
Player.bladeburner !== null && Player.bladeburner.blackops.hasOwnProperty(BlackOperationNames.OperationDaedalus)
);
} }
function hasAccessToSF(player: PlayerObject, bn: number): boolean { function hasAccessToSF(player: PlayerObject, bn: number): boolean {
@ -564,7 +566,7 @@ export const achievements: IMap<Achievement> = {
...achievementData["SLEEVE_8"], ...achievementData["SLEEVE_8"],
Icon: "SLEEVE8", Icon: "SLEEVE8",
Visible: () => hasAccessToSF(Player, 10), Visible: () => hasAccessToSF(Player, 10),
Condition: () => Player.sleeves.length === 8, Condition: () => Player.sleeves.length === 8 && Player.sourceFileLvl(10) === 3,
}, },
INDECISIVE: { INDECISIVE: {
...achievementData["INDECISIVE"], ...achievementData["INDECISIVE"],

File diff suppressed because it is too large Load Diff

@ -14,7 +14,6 @@ import Tooltip from "@mui/material/Tooltip";
import { Settings } from "../../Settings/Settings"; import { Settings } from "../../Settings/Settings";
import Button from "@mui/material/Button"; import Button from "@mui/material/Button";
const useStyles = makeStyles(() => const useStyles = makeStyles(() =>
createStyles({ createStyles({
portal: { portal: {
@ -70,7 +69,7 @@ function BitNodePortal(props: IPortalProps): React.ReactElement {
if (props.level === 2) { if (props.level === 2) {
cssClass = classes.level2; cssClass = classes.level2;
} }
cssClass = `${classes.portal} ${cssClass}` cssClass = `${classes.portal} ${cssClass}`;
return ( return (
<> <>
@ -86,12 +85,10 @@ function BitNodePortal(props: IPortalProps): React.ReactElement {
} }
> >
{Settings.DisableASCIIArt ? ( {Settings.DisableASCIIArt ? (
<Button <Button onClick={() => setPortalOpen(true)} sx={{ m: 2 }} aria-description={bitNode.desc}>
onClick={() => setPortalOpen(true)} <Typography>
sx={{ m: 2 }} BitNode-{bitNode.number.toString()}: {bitNode.name}
aria-description={bitNode.desc} </Typography>
>
<Typography>BitNode-{bitNode.number.toString()}: {bitNode.name}</Typography>
</Button> </Button>
) : ( ) : (
<IconButton <IconButton
@ -114,9 +111,7 @@ function BitNodePortal(props: IPortalProps): React.ReactElement {
flume={props.flume} flume={props.flume}
/> />
{Settings.DisableASCIIArt && ( {Settings.DisableASCIIArt && <br />}
<br/>
)}
</> </>
); );
} }
@ -173,19 +168,29 @@ export function BitverseRoot(props: IProps): React.ReactElement {
if (Settings.DisableASCIIArt) { if (Settings.DisableASCIIArt) {
return ( return (
<> <>
{Object.values(BitNodes).filter((node) => { {Object.values(BitNodes)
.filter((node) => {
console.log(node.desc); console.log(node.desc);
return node.desc !== 'COMING SOON'; return node.desc !== "COMING SOON";
}).map((node) => { })
.map((node) => {
return ( return (
<BitNodePortal key={node.number} n={node.number} level={nextSourceFileFlags[node.number]} enter={enter} flume={props.flume} destroyedBitNode={destroyed} /> <BitNodePortal
) key={node.number}
n={node.number}
level={nextSourceFileFlags[node.number]}
enter={enter}
flume={props.flume}
destroyedBitNode={destroyed}
/>
);
})} })}
<br /> <br />
<br /> <br />
<br /> <br />
<br /> <br />
<CinematicText lines={[ <CinematicText
lines={[
"> Many decades ago, a humanoid extraterrestrial species which we call the Enders descended on the Earth...violently", "> Many decades ago, a humanoid extraterrestrial species which we call the Enders descended on the Earth...violently",
"> Our species fought back, but it was futile. The Enders had technology far beyond our own...", "> Our species fought back, but it was futile. The Enders had technology far beyond our own...",
"> Instead of killing every last one of us, the human race was enslaved...", "> Instead of killing every last one of us, the human race was enslaved...",
@ -207,10 +212,11 @@ export function BitverseRoot(props: IProps): React.ReactElement {
"> Welcome to the Bitverse...", "> Welcome to the Bitverse...",
"> ", "> ",
"> (Enter a new BitNode using the image above)", "> (Enter a new BitNode using the image above)",
]} /> ]}
/>
</> </>
) );
} else { }
return ( return (
// prettier-ignore // prettier-ignore
<> <>
@ -268,6 +274,4 @@ export function BitverseRoot(props: IProps): React.ReactElement {
]} /> ]} />
</> </>
); );
}
return <></>;
} }

@ -318,9 +318,8 @@ export class Bladeburner implements IBladeburner {
if (this.contracts.hasOwnProperty(name)) { if (this.contracts.hasOwnProperty(name)) {
action.name = name; action.name = name;
return action; return action;
} else {
return null;
} }
return null;
case "operation": case "operation":
case "operations": case "operations":
case "op": case "op":
@ -329,9 +328,8 @@ export class Bladeburner implements IBladeburner {
if (this.operations.hasOwnProperty(name)) { if (this.operations.hasOwnProperty(name)) {
action.name = name; action.name = name;
return action; return action;
} else {
return null;
} }
return null;
case "blackoperation": case "blackoperation":
case "black operation": case "black operation":
case "black operations": case "black operations":
@ -343,9 +341,8 @@ export class Bladeburner implements IBladeburner {
if (BlackOperations.hasOwnProperty(name)) { if (BlackOperations.hasOwnProperty(name)) {
action.name = name; action.name = name;
return action; return action;
} else {
return null;
} }
return null;
case "general": case "general":
case "general action": case "general action":
case "gen": case "gen":
@ -1529,7 +1526,8 @@ export class Bladeburner implements IBladeburner {
this.startAction(player, this.action); this.startAction(player, this.action);
if (this.logging.general) { if (this.logging.general) {
this.log( this.log(
`Rested in Hyperbolic Regeneration Chamber. Restored ${BladeburnerConstants.HrcHpGain `Rested in Hyperbolic Regeneration Chamber. Restored ${
BladeburnerConstants.HrcHpGain
} HP and gained ${numeralWrapper.formatStamina(staminaGain)} stamina`, } HP and gained ${numeralWrapper.formatStamina(staminaGain)} stamina`,
); );
} }
@ -1578,7 +1576,9 @@ export class Bladeburner implements IBladeburner {
if (factionExists(bladeburnersFactionName)) { if (factionExists(bladeburnersFactionName)) {
const bladeburnerFac = Factions[bladeburnersFactionName]; const bladeburnerFac = Factions[bladeburnersFactionName];
if (!(bladeburnerFac instanceof Faction)) { if (!(bladeburnerFac instanceof Faction)) {
throw new Error(`Could not properly get ${FactionNames.Bladeburners} Faction object in ${FactionNames.Bladeburners} UI Overview Faction button`); throw new Error(
`Could not properly get ${FactionNames.Bladeburners} Faction object in ${FactionNames.Bladeburners} UI Overview Faction button`,
);
} }
if (bladeburnerFac.isMember) { if (bladeburnerFac.isMember) {
const favorBonus = 1 + bladeburnerFac.favor / 100; const favorBonus = 1 + bladeburnerFac.favor / 100;

@ -1,5 +1,6 @@
import React, { useState, useRef, useEffect } from "react"; import React, { useState, useRef, useEffect } from "react";
import { IBladeburner } from "../IBladeburner"; import { IBladeburner } from "../IBladeburner";
import { KEY } from "../../utils/helpers/keyCodes";
import { IPlayer } from "../../PersonObjects/IPlayer"; import { IPlayer } from "../../PersonObjects/IPlayer";
import Paper from "@mui/material/Paper"; import Paper from "@mui/material/Paper";
@ -76,7 +77,7 @@ export function Console(props: IProps): React.ReactElement {
}, []); }, []);
function handleKeyDown(event: React.KeyboardEvent<HTMLInputElement>): void { function handleKeyDown(event: React.KeyboardEvent<HTMLInputElement>): void {
if (event.keyCode === 13) { if (event.key === KEY.ENTER) {
event.preventDefault(); event.preventDefault();
if (command.length > 0) { if (command.length > 0) {
props.bladeburner.postToConsole("> " + command); props.bladeburner.postToConsole("> " + command);
@ -88,7 +89,7 @@ export function Console(props: IProps): React.ReactElement {
const consoleHistory = props.bladeburner.consoleHistory; const consoleHistory = props.bladeburner.consoleHistory;
if (event.keyCode === 38) { if (event.key === KEY.S) {
// up // up
let i = consoleHistoryIndex; let i = consoleHistoryIndex;
const len = consoleHistory.length; const len = consoleHistory.length;
@ -108,7 +109,7 @@ export function Console(props: IProps): React.ReactElement {
setCommand(prevCommand); setCommand(prevCommand);
} }
if (event.keyCode === 40) { if (event.key === KEY.DOWNARROW) {
const i = consoleHistoryIndex; const i = consoleHistoryIndex;
const len = consoleHistory.length; const len = consoleHistory.length;

@ -64,6 +64,9 @@ export function UnlockUpgrade(corporation: ICorporation, upgrade: CorporationUnl
if (corporation.funds < upgrade[1]) { if (corporation.funds < upgrade[1]) {
throw new Error("Insufficient funds"); throw new Error("Insufficient funds");
} }
if(corporation.unlockUpgrades[upgrade[0]] === 1){
throw new Error(`You have already unlocked the ${upgrade[2]} upgrade!`);
}
corporation.unlock(upgrade); corporation.unlock(upgrade);
} }

@ -8,6 +8,7 @@ import Button from "@mui/material/Button";
import TextField from "@mui/material/TextField"; import TextField from "@mui/material/TextField";
import { BuyBackShares } from '../Actions'; import { BuyBackShares } from '../Actions';
import { dialogBoxCreate } from '../../ui/React/DialogBox'; import { dialogBoxCreate } from '../../ui/React/DialogBox';
import { KEY } from "../../utils/helpers/keyCodes";
interface IProps { interface IProps {
open: boolean; open: boolean;
@ -69,7 +70,7 @@ export function BuybackSharesModal(props: IProps): React.ReactElement {
} }
function onKeyDown(event: React.KeyboardEvent<HTMLInputElement>): void { function onKeyDown(event: React.KeyboardEvent<HTMLInputElement>): void {
if (event.keyCode === 13) buy(); if (event.key === KEY.ENTER) buy();
} }
return ( return (

@ -11,6 +11,7 @@ import TextField from "@mui/material/TextField";
import MenuItem from "@mui/material/MenuItem"; import MenuItem from "@mui/material/MenuItem";
import Box from "@mui/material/Box"; import Box from "@mui/material/Box";
import Select, { SelectChangeEvent } from "@mui/material/Select"; import Select, { SelectChangeEvent } from "@mui/material/Select";
import { KEY } from "../../utils/helpers/keyCodes";
interface IProps { interface IProps {
setDivisionName: (name: string) => void; setDivisionName: (name: string) => void;
@ -53,7 +54,7 @@ export function ExpandIndustryTab(props: IProps): React.ReactElement {
} }
function onKeyDown(event: React.KeyboardEvent<HTMLInputElement>): void { function onKeyDown(event: React.KeyboardEvent<HTMLInputElement>): void {
if (event.keyCode === 13) newIndustry(); if (event.key === KEY.ENTER) newIndustry();
} }
function onIndustryChange(event: SelectChangeEvent<string>): void { function onIndustryChange(event: SelectChangeEvent<string>): void {

@ -7,6 +7,7 @@ import Typography from "@mui/material/Typography";
import Button from "@mui/material/Button"; import Button from "@mui/material/Button";
import TextField from "@mui/material/TextField"; import TextField from "@mui/material/TextField";
import Box from "@mui/material/Box"; import Box from "@mui/material/Box";
import { KEY } from "../../utils/helpers/keyCodes";
interface IProps { interface IProps {
open: boolean; open: boolean;
@ -45,7 +46,7 @@ export function GoPublicModal(props: IProps): React.ReactElement {
} }
function onKeyDown(event: React.KeyboardEvent<HTMLInputElement>): void { function onKeyDown(event: React.KeyboardEvent<HTMLInputElement>): void {
if (event.keyCode === 13) goPublic(); if (event.key === KEY.ENTER) goPublic();
} }
function onChange(event: React.ChangeEvent<HTMLInputElement>): void { function onChange(event: React.ChangeEvent<HTMLInputElement>): void {

@ -7,6 +7,7 @@ import { useCorporation } from "./Context";
import Typography from "@mui/material/Typography"; import Typography from "@mui/material/Typography";
import TextField from "@mui/material/TextField"; import TextField from "@mui/material/TextField";
import Button from "@mui/material/Button"; import Button from "@mui/material/Button";
import { KEY } from "../../utils/helpers/keyCodes";
interface IProps { interface IProps {
open: boolean; open: boolean;
onClose: () => void; onClose: () => void;
@ -32,7 +33,7 @@ export function IssueDividendsModal(props: IProps): React.ReactElement {
} }
function onKeyDown(event: React.KeyboardEvent<HTMLInputElement>): void { function onKeyDown(event: React.KeyboardEvent<HTMLInputElement>): void {
if (event.keyCode === 13) issueDividends(); if (event.key === KEY.ENTER) issueDividends();
} }
function onChange(event: React.ChangeEvent<HTMLInputElement>): void { function onChange(event: React.ChangeEvent<HTMLInputElement>): void {

@ -8,6 +8,7 @@ import { useCorporation } from "./Context";
import Typography from "@mui/material/Typography"; import Typography from "@mui/material/Typography";
import TextField from "@mui/material/TextField"; import TextField from "@mui/material/TextField";
import Button from "@mui/material/Button"; import Button from "@mui/material/Button";
import { KEY } from "../../utils/helpers/keyCodes";
interface IEffectTextProps { interface IEffectTextProps {
shares: number | null; shares: number | null;
@ -93,7 +94,7 @@ export function IssueNewSharesModal(props: IProps): React.ReactElement {
} }
function onKeyDown(event: React.KeyboardEvent<HTMLInputElement>): void { function onKeyDown(event: React.KeyboardEvent<HTMLInputElement>): void {
if (event.keyCode === 13) issueNewShares(); if (event.key === KEY.ENTER) issueNewShares();
} }
function onChange(event: React.ChangeEvent<HTMLInputElement>): void { function onChange(event: React.ChangeEvent<HTMLInputElement>): void {

@ -5,6 +5,7 @@ import { Modal } from "../../ui/React/Modal";
import Typography from "@mui/material/Typography"; import Typography from "@mui/material/Typography";
import Button from "@mui/material/Button"; import Button from "@mui/material/Button";
import TextField from "@mui/material/TextField"; import TextField from "@mui/material/TextField";
import { KEY } from "../../utils/helpers/keyCodes";
interface IProps { interface IProps {
open: boolean; open: boolean;
@ -25,7 +26,7 @@ export function LimitProductProductionModal(props: IProps): React.ReactElement {
} }
function onKeyDown(event: React.KeyboardEvent<HTMLInputElement>): void { function onKeyDown(event: React.KeyboardEvent<HTMLInputElement>): void {
if (event.keyCode === 13) limitProductProduction(); if (event.key === KEY.ENTER) limitProductProduction();
} }
function onChange(event: React.ChangeEvent<HTMLInputElement>): void { function onChange(event: React.ChangeEvent<HTMLInputElement>): void {

@ -9,6 +9,7 @@ import TextField from "@mui/material/TextField";
import Button from "@mui/material/Button"; import Button from "@mui/material/Button";
import MenuItem from "@mui/material/MenuItem"; import MenuItem from "@mui/material/MenuItem";
import Select, { SelectChangeEvent } from "@mui/material/Select"; import Select, { SelectChangeEvent } from "@mui/material/Select";
import { KEY } from "../../utils/helpers/keyCodes";
interface IProps { interface IProps {
open: boolean; open: boolean;
@ -165,7 +166,7 @@ export function MakeProductModal(props: IProps): React.ReactElement {
} }
function onKeyDown(event: React.KeyboardEvent<HTMLInputElement>): void { function onKeyDown(event: React.KeyboardEvent<HTMLInputElement>): void {
if (event.keyCode === 13) makeProduct(); if (event.key === KEY.ENTER) makeProduct();
} }
return ( return (

@ -10,6 +10,7 @@ import { useCorporation, useDivision } from "./Context";
import Typography from "@mui/material/Typography"; import Typography from "@mui/material/Typography";
import TextField from "@mui/material/TextField"; import TextField from "@mui/material/TextField";
import Button from "@mui/material/Button"; import Button from "@mui/material/Button";
import { KEY } from "../../utils/helpers/keyCodes";
interface IBulkPurchaseTextProps { interface IBulkPurchaseTextProps {
warehouse: Warehouse; warehouse: Warehouse;
@ -68,7 +69,7 @@ function BulkPurchaseSection(props: IBPProps): React.ReactElement {
} }
function onKeyDown(event: React.KeyboardEvent<HTMLInputElement>): void { function onKeyDown(event: React.KeyboardEvent<HTMLInputElement>): void {
if (event.keyCode === 13) bulkPurchase(); if (event.key === KEY.ENTER) bulkPurchase();
} }
function onChange(event: React.ChangeEvent<HTMLInputElement>): void { function onChange(event: React.ChangeEvent<HTMLInputElement>): void {
@ -123,7 +124,7 @@ export function PurchaseMaterialModal(props: IProps): React.ReactElement {
} }
function onKeyDown(event: React.KeyboardEvent<HTMLInputElement>): void { function onKeyDown(event: React.KeyboardEvent<HTMLInputElement>): void {
if (event.keyCode === 13) purchaseMaterial(); if (event.key === KEY.ENTER) purchaseMaterial();
} }
function onChange(event: React.ChangeEvent<HTMLInputElement>): void { function onChange(event: React.ChangeEvent<HTMLInputElement>): void {

@ -6,6 +6,7 @@ import { Modal } from "../../ui/React/Modal";
import Typography from "@mui/material/Typography"; import Typography from "@mui/material/Typography";
import TextField from "@mui/material/TextField"; import TextField from "@mui/material/TextField";
import Button from "@mui/material/Button"; import Button from "@mui/material/Button";
import { KEY } from "../../utils/helpers/keyCodes";
function initialPrice(mat: Material): string { function initialPrice(mat: Material): string {
let val = mat.sCost ? mat.sCost + "" : ""; let val = mat.sCost ? mat.sCost + "" : "";
@ -46,7 +47,7 @@ export function SellMaterialModal(props: IProps): React.ReactElement {
} }
function onKeyDown(event: React.KeyboardEvent<HTMLInputElement>): void { function onKeyDown(event: React.KeyboardEvent<HTMLInputElement>): void {
if (event.keyCode === 13) sellMaterial(); if (event.key === KEY.ENTER) sellMaterial();
} }
return ( return (

@ -9,6 +9,7 @@ import TextField from "@mui/material/TextField";
import Button from "@mui/material/Button"; import Button from "@mui/material/Button";
import FormControlLabel from "@mui/material/FormControlLabel"; import FormControlLabel from "@mui/material/FormControlLabel";
import Switch from "@mui/material/Switch"; import Switch from "@mui/material/Switch";
import { KEY } from "../../utils/helpers/keyCodes";
function initialPrice(product: Product): string { function initialPrice(product: Product): string {
let val = product.sCost ? product.sCost + "" : ""; let val = product.sCost ? product.sCost + "" : "";
@ -58,7 +59,7 @@ export function SellProductModal(props: IProps): React.ReactElement {
} }
function onKeyDown(event: React.KeyboardEvent<HTMLInputElement>): void { function onKeyDown(event: React.KeyboardEvent<HTMLInputElement>): void {
if (event.keyCode === 13) sellProduct(); if (event.key === KEY.ENTER) sellProduct();
} }
return ( return (

@ -10,6 +10,7 @@ import TextField from "@mui/material/TextField";
import Button from "@mui/material/Button"; import Button from "@mui/material/Button";
import { Money } from "../../ui/React/Money"; import { Money } from "../../ui/React/Money";
import { SellShares } from "../Actions"; import { SellShares } from "../Actions";
import { KEY } from "../../utils/helpers/keyCodes";
interface IProps { interface IProps {
open: boolean; open: boolean;
onClose: () => void; onClose: () => void;
@ -68,7 +69,7 @@ export function SellSharesModal(props: IProps): React.ReactElement {
} }
function onKeyDown(event: React.KeyboardEvent<HTMLInputElement>): void { function onKeyDown(event: React.KeyboardEvent<HTMLInputElement>): void {
if (event.keyCode === 13) sell(); if (event.key === KEY.ENTER) sell();
} }
return ( return (

@ -10,6 +10,7 @@ import Typography from "@mui/material/Typography";
import Button from "@mui/material/Button"; import Button from "@mui/material/Button";
import TextField from "@mui/material/TextField"; import TextField from "@mui/material/TextField";
import Box from "@mui/material/Box"; import Box from "@mui/material/Box";
import { KEY } from "../../utils/helpers/keyCodes";
interface IProps { interface IProps {
open: boolean; open: boolean;
@ -57,7 +58,7 @@ export function ThrowPartyModal(props: IProps): React.ReactElement {
} }
function onKeyDown(event: React.KeyboardEvent<HTMLInputElement>): void { function onKeyDown(event: React.KeyboardEvent<HTMLInputElement>): void {
if (event.keyCode === 13) throwParty(); if (event.key === KEY.ENTER) throwParty();
} }
return ( return (

@ -29,67 +29,51 @@ export function Effect(tpe: FragmentType): string {
switch (tpe) { switch (tpe) {
case FragmentType.HackingChance: { case FragmentType.HackingChance: {
return "+x% hack() success chance"; return "+x% hack() success chance";
break;
} }
case FragmentType.HackingSpeed: { case FragmentType.HackingSpeed: {
return "+x% faster hack(), grow(), and weaken()"; return "+x% faster hack(), grow(), and weaken()";
break;
} }
case FragmentType.HackingMoney: { case FragmentType.HackingMoney: {
return "+x% hack() power"; return "+x% hack() power";
break;
} }
case FragmentType.HackingGrow: { case FragmentType.HackingGrow: {
return "+x% grow() power"; return "+x% grow() power";
break;
} }
case FragmentType.Hacking: { case FragmentType.Hacking: {
return "+x% hacking skill"; return "+x% hacking skill";
break;
} }
case FragmentType.Strength: { case FragmentType.Strength: {
return "+x% strength skill"; return "+x% strength skill";
break;
} }
case FragmentType.Defense: { case FragmentType.Defense: {
return "+x% defense skill"; return "+x% defense skill";
break;
} }
case FragmentType.Dexterity: { case FragmentType.Dexterity: {
return "+x% dexterity skill"; return "+x% dexterity skill";
break;
} }
case FragmentType.Agility: { case FragmentType.Agility: {
return "+x% agility skill"; return "+x% agility skill";
break;
} }
case FragmentType.Charisma: { case FragmentType.Charisma: {
return "+x% charisma skill"; return "+x% charisma skill";
break;
} }
case FragmentType.HacknetMoney: { case FragmentType.HacknetMoney: {
return "+x% hacknet production"; return "+x% hacknet production";
break;
} }
case FragmentType.HacknetCost: { case FragmentType.HacknetCost: {
return "x% cheaper hacknet cost"; return "x% cheaper hacknet cost";
break;
} }
case FragmentType.Rep: { case FragmentType.Rep: {
return "+x% reputation from factions and companies"; return "+x% reputation from factions and companies";
break;
} }
case FragmentType.WorkMoney: { case FragmentType.WorkMoney: {
return "+x% work money"; return "+x% work money";
break;
} }
case FragmentType.Crime: { case FragmentType.Crime: {
return "+x% crime money"; return "+x% crime money";
break;
} }
case FragmentType.Bladeburner: { case FragmentType.Bladeburner: {
return "+x% all bladeburner stats"; return "+x% all bladeburner stats";
break;
} }
} }
throw new Error("Calling effect for fragment type that doesn't have an effect " + tpe); throw new Error("Calling effect for fragment type that doesn't have an effect " + tpe);

@ -6,6 +6,7 @@ import { Modal } from "../../ui/React/Modal";
import { use } from "../../ui/Context"; import { use } from "../../ui/Context";
import Typography from "@mui/material/Typography"; import Typography from "@mui/material/Typography";
import Button from "@mui/material/Button"; import Button from "@mui/material/Button";
import { KEY } from "../../utils/helpers/keyCodes";
import { FactionNames } from "../data/FactionNames"; import { FactionNames } from "../data/FactionNames";
interface IProps { interface IProps {
@ -38,7 +39,7 @@ export function CreateGangModal(props: IProps): React.ReactElement {
} }
function onKeyUp(event: React.KeyboardEvent): void { function onKeyUp(event: React.KeyboardEvent): void {
if (event.keyCode === 13) createGang(); if (event.key === KEY.ENTER) createGang();
} }
return ( return (

@ -7,12 +7,7 @@ import { useGang } from "./Context";
import Typography from "@mui/material/Typography"; import Typography from "@mui/material/Typography";
import Tooltip from "@mui/material/Tooltip"; import Tooltip from "@mui/material/Tooltip";
import { import { Table, TableBody, TableCell, TableRow } from "@mui/material";
Table,
TableBody,
TableCell,
TableRow,
} from "@mui/material";
import { numeralWrapper } from "../../ui/numeralFormat"; import { numeralWrapper } from "../../ui/numeralFormat";
import { GangMember } from "../GangMember"; import { GangMember } from "../GangMember";
@ -37,8 +32,6 @@ export function GangMemberStats(props: IProps): React.ReactElement {
cha: props.member.calculateAscensionMult(props.member.cha_asc_points), cha: props.member.calculateAscensionMult(props.member.cha_asc_points),
}; };
const gang = useGang(); const gang = useGang();
const data = [ const data = [
[`Money:`, <MoneyRate money={5 * props.member.calculateMoneyGain(gang)} />], [`Money:`, <MoneyRate money={5 * props.member.calculateMoneyGain(gang)} />],
@ -78,14 +71,38 @@ export function GangMemberStats(props: IProps): React.ReactElement {
</Typography> </Typography>
} }
> >
<Table sx={{ display: 'table', mb: 1, width: '100%' }}> <Table sx={{ display: "table", mb: 1, width: "100%" }}>
<TableBody> <TableBody>
<StatsRow name="Hacking" color={Settings.theme.hack} data={{ level: props.member.hack, exp: props.member.hack_exp }} /> <StatsRow
<StatsRow name="Strength" color={Settings.theme.combat} data={{ level: props.member.str, exp: props.member.str_exp }} /> name="Hacking"
<StatsRow name="Defense" color={Settings.theme.combat} data={{ level: props.member.def, exp: props.member.def_exp }} /> color={Settings.theme.hack}
<StatsRow name="Dexterity" color={Settings.theme.combat} data={{ level: props.member.dex, exp: props.member.dex_exp }} /> data={{ level: props.member.hack, exp: props.member.hack_exp }}
<StatsRow name="Agility" color={Settings.theme.combat} data={{ level: props.member.agi, exp: props.member.agi_exp }} /> />
<StatsRow name="Charisma" color={Settings.theme.cha} data={{ level: props.member.cha, exp: props.member.cha_exp }} /> <StatsRow
name="Strength"
color={Settings.theme.combat}
data={{ level: props.member.str, exp: props.member.str_exp }}
/>
<StatsRow
name="Defense"
color={Settings.theme.combat}
data={{ level: props.member.def, exp: props.member.def_exp }}
/>
<StatsRow
name="Dexterity"
color={Settings.theme.combat}
data={{ level: props.member.dex, exp: props.member.dex_exp }}
/>
<StatsRow
name="Agility"
color={Settings.theme.combat}
data={{ level: props.member.agi, exp: props.member.agi_exp }}
/>
<StatsRow
name="Charisma"
color={Settings.theme.cha}
data={{ level: props.member.cha, exp: props.member.cha_exp }}
/>
<TableRow> <TableRow>
<TableCell classes={{ root: classes.cellNone }}> <TableCell classes={{ root: classes.cellNone }}>
<br /> <br />

@ -8,6 +8,7 @@ import { useGang } from "./Context";
import Typography from "@mui/material/Typography"; import Typography from "@mui/material/Typography";
import TextField from "@mui/material/TextField"; import TextField from "@mui/material/TextField";
import Button from "@mui/material/Button"; import Button from "@mui/material/Button";
import { KEY } from "../../utils/helpers/keyCodes";
interface IRecruitPopupProps { interface IRecruitPopupProps {
open: boolean; open: boolean;
@ -34,7 +35,7 @@ export function RecruitModal(props: IRecruitPopupProps): React.ReactElement {
} }
function onKeyUp(event: React.KeyboardEvent<HTMLInputElement>): void { function onKeyUp(event: React.KeyboardEvent<HTMLInputElement>): void {
if (event.keyCode === 13) recruit(); if (event.key === KEY.ENTER) recruit();
} }
function onChange(event: React.ChangeEvent<HTMLInputElement>): void { function onChange(event: React.ChangeEvent<HTMLInputElement>): void {

@ -21,6 +21,13 @@ export function TaskSelector(props: IProps): React.ReactElement {
const gang = useGang(); const gang = useGang();
const [currentTask, setCurrentTask] = useState(props.member.task); const [currentTask, setCurrentTask] = useState(props.member.task);
const contextMember = gang.members.find(member => member.name == props.member.name)
if (contextMember &&
contextMember.task != currentTask
) {
setCurrentTask(contextMember.task)
}
function onChange(event: SelectChangeEvent<string>): void { function onChange(event: SelectChangeEvent<string>): void {
const task = event.target.value; const task = event.target.value;
props.member.assignToTask(task); props.member.assignToTask(task);

@ -10,6 +10,7 @@ import { use } from "../../ui/Context";
import Typography from "@mui/material/Typography"; import Typography from "@mui/material/Typography";
import TextField from "@mui/material/TextField"; import TextField from "@mui/material/TextField";
import Button from "@mui/material/Button"; import Button from "@mui/material/Button";
import { KEY } from "../../utils/helpers/keyCodes";
interface IProps { interface IProps {
open: boolean; open: boolean;
@ -29,7 +30,7 @@ export function PurchaseServerModal(props: IProps): React.ReactElement {
} }
function onKeyUp(event: React.KeyboardEvent<HTMLInputElement>): void { function onKeyUp(event: React.KeyboardEvent<HTMLInputElement>): void {
if (event.keyCode === 13) tryToPurchaseServer(); if (event.key === KEY.ENTER) tryToPurchaseServer();
} }
function onChange(event: React.ChangeEvent<HTMLInputElement>): void { function onChange(event: React.ChangeEvent<HTMLInputElement>): void {

@ -232,6 +232,8 @@ export const RamCosts: IMap<any> = {
connect: SF4Cost(RamCostConstants.ScriptSingularityFn1RamCost), connect: SF4Cost(RamCostConstants.ScriptSingularityFn1RamCost),
manualHack: SF4Cost(RamCostConstants.ScriptSingularityFn1RamCost), manualHack: SF4Cost(RamCostConstants.ScriptSingularityFn1RamCost),
installBackdoor: SF4Cost(RamCostConstants.ScriptSingularityFn1RamCost), installBackdoor: SF4Cost(RamCostConstants.ScriptSingularityFn1RamCost),
getDarkwebProgramCost: SF4Cost(RamCostConstants.ScriptSingularityFn1RamCost / 4),
getDarkwebPrograms: SF4Cost(RamCostConstants.ScriptSingularityFn1RamCost / 4),
getStats: SF4Cost(RamCostConstants.ScriptSingularityFn1RamCost / 4), getStats: SF4Cost(RamCostConstants.ScriptSingularityFn1RamCost / 4),
getCharacterInformation: SF4Cost(RamCostConstants.ScriptSingularityFn1RamCost / 4), getCharacterInformation: SF4Cost(RamCostConstants.ScriptSingularityFn1RamCost / 4),
hospitalize: SF4Cost(RamCostConstants.ScriptSingularityFn1RamCost / 4), hospitalize: SF4Cost(RamCostConstants.ScriptSingularityFn1RamCost / 4),

@ -144,7 +144,7 @@ export function NetscriptFunctions(workerScript: WorkerScript): NS {
const safeGetServer = function (hostname: string, callingFnName: string): BaseServer { const safeGetServer = function (hostname: string, callingFnName: string): BaseServer {
const server = GetServer(hostname); const server = GetServer(hostname);
if (server == null) { if (server == null) {
throw makeRuntimeErrorMsg(callingFnName, `Invalid hostname or IP: ${hostname}`); throw makeRuntimeErrorMsg(callingFnName, `Invalid hostname: ${hostname}`);
} }
return server; return server;
}; };
@ -539,6 +539,10 @@ export function NetscriptFunctions(workerScript: WorkerScript): NS {
const percentHacked = calculatePercentMoneyHacked(server, Player); const percentHacked = calculatePercentMoneyHacked(server, Player);
if (percentHacked === 0 || server.moneyAvailable === 0) {
return 0; // To prevent returning infinity below
}
return hackAmount / Math.floor(server.moneyAvailable * percentHacked); return hackAmount / Math.floor(server.moneyAvailable * percentHacked);
}, },
hackAnalyze: function (hostname: any): any { hackAnalyze: function (hostname: any): any {

@ -291,7 +291,7 @@ export function NetscriptCorporation(
lastCycleExpenses: division.lastCycleExpenses, lastCycleExpenses: division.lastCycleExpenses,
thisCycleRevenue: division.thisCycleRevenue, thisCycleRevenue: division.thisCycleRevenue,
thisCycleExpenses: division.thisCycleExpenses, thisCycleExpenses: division.thisCycleExpenses,
upgrades: division.upgrades, upgrades: division.upgrades.slice(),
cities: cities, cities: cities,
products: division.products === undefined ? [] : Object.keys(division.products), products: division.products === undefined ? [] : Object.keys(division.products),
}; };
@ -846,5 +846,9 @@ export function NetscriptCorporation(
const amountShares = helper.number("bribe", "amountShares", aamountShares); const amountShares = helper.number("bribe", "amountShares", aamountShares);
return bribe(factionName, amountCash, amountShares); return bribe(factionName, amountCash, amountShares);
}, },
getBonusTime: function (): number {
checkAccess("getBonusTime");
return Math.round(getCorporation().storedCycles / 5) * 1000;
}
}; };
} }

@ -3,7 +3,6 @@ import { IPlayer } from "../PersonObjects/IPlayer";
import { Exploit } from "../Exploits/Exploit"; import { Exploit } from "../Exploits/Exploit";
import * as bcrypt from "bcryptjs"; import * as bcrypt from "bcryptjs";
import { INetscriptHelper } from "./INetscriptHelper"; import { INetscriptHelper } from "./INetscriptHelper";
import { Augmentations } from "../Augmentation/Augmentations";
export interface INetscriptExtra { export interface INetscriptExtra {
heart: { heart: {

@ -43,6 +43,7 @@ import { calculateHackingTime } from "../Hacking";
import { Server } from "../Server/Server"; import { Server } from "../Server/Server";
import { netscriptCanHack } from "../Hacking/netscriptCanHack"; import { netscriptCanHack } from "../Hacking/netscriptCanHack";
import { FactionNames } from "../Faction/data/FactionNames"; import { FactionNames } from "../Faction/data/FactionNames";
import { FactionInfos } from "../Faction/FactionInfo";
export function NetscriptSingularity( export function NetscriptSingularity(
player: IPlayer, player: IPlayer,
@ -370,7 +371,8 @@ export function NetscriptSingularity(
if (player.city != CityName.Aevum) { if (player.city != CityName.Aevum) {
workerScript.log( workerScript.log(
"gymWorkout", "gymWorkout",
() => `You cannot workout at '${LocationName.AevumCrushFitnessGym}' because you are not in '${CityName.Aevum}'.`, () =>
`You cannot workout at '${LocationName.AevumCrushFitnessGym}' because you are not in '${CityName.Aevum}'.`,
); );
return false; return false;
} }
@ -382,7 +384,8 @@ export function NetscriptSingularity(
if (player.city != CityName.Aevum) { if (player.city != CityName.Aevum) {
workerScript.log( workerScript.log(
"gymWorkout", "gymWorkout",
() => `You cannot workout at '${LocationName.AevumSnapFitnessGym}' because you are not in '${CityName.Aevum}'.`, () =>
`You cannot workout at '${LocationName.AevumSnapFitnessGym}' because you are not in '${CityName.Aevum}'.`,
); );
return false; return false;
} }
@ -394,7 +397,8 @@ export function NetscriptSingularity(
if (player.city != CityName.Sector12) { if (player.city != CityName.Sector12) {
workerScript.log( workerScript.log(
"gymWorkout", "gymWorkout",
() => `You cannot workout at '${LocationName.Sector12IronGym}' because you are not in '${CityName.Sector12}'.`, () =>
`You cannot workout at '${LocationName.Sector12IronGym}' because you are not in '${CityName.Sector12}'.`,
); );
return false; return false;
} }
@ -406,7 +410,8 @@ export function NetscriptSingularity(
if (player.city != CityName.Sector12) { if (player.city != CityName.Sector12) {
workerScript.log( workerScript.log(
"gymWorkout", "gymWorkout",
() => `You cannot workout at '${LocationName.Sector12PowerhouseGym}' because you are not in '${CityName.Sector12}'.`, () =>
`You cannot workout at '${LocationName.Sector12PowerhouseGym}' because you are not in '${CityName.Sector12}'.`,
); );
return false; return false;
} }
@ -418,7 +423,8 @@ export function NetscriptSingularity(
if (player.city != CityName.Volhaven) { if (player.city != CityName.Volhaven) {
workerScript.log( workerScript.log(
"gymWorkout", "gymWorkout",
() => `You cannot workout at '${LocationName.VolhavenMilleniumFitnessGym}' because you are not in '${CityName.Volhaven}'.`, () =>
`You cannot workout at '${LocationName.VolhavenMilleniumFitnessGym}' because you are not in '${CityName.Volhaven}'.`,
); );
return false; return false;
} }
@ -476,7 +482,7 @@ export function NetscriptSingularity(
case CityName.Volhaven: case CityName.Volhaven:
if (player.money < CONSTANTS.TravelCost) { if (player.money < CONSTANTS.TravelCost) {
workerScript.log("travelToCity", () => "Not enough money to travel."); workerScript.log("travelToCity", () => "Not enough money to travel.");
return false return false;
} }
player.loseMoney(CONSTANTS.TravelCost, "other"); player.loseMoney(CONSTANTS.TravelCost, "other");
player.city = cityname; player.city = cityname;
@ -1033,93 +1039,12 @@ export function NetscriptSingularity(
const fac = Factions[name]; const fac = Factions[name];
// Arrays listing factions that allow each time of work // Arrays listing factions that allow each time of work
const hackAvailable = [
FactionNames.Illuminati as string,
FactionNames.Daedalus as string,
FactionNames.TheCovenant as string,
FactionNames.ECorp as string,
FactionNames.MegaCorp as string,
FactionNames.BachmanAssociates as string,
FactionNames.Bladeburners as string,
FactionNames.NWO as string,
FactionNames.ClarkeIncorporated as string,
FactionNames.OmniTekIncorporated as string,
FactionNames.FourSigma as string,
FactionNames.KuaiGongInternational as string,
FactionNames.FulcrumSecretTechnologies as string,
FactionNames.BitRunners as string,
FactionNames.TheBlackHand as string,
FactionNames.NiteSec as string,
FactionNames.Chongqing as string,
FactionNames.Sector12 as string,
FactionNames.NewTokyo as string,
FactionNames.Aevum as string,
FactionNames.Ishima as string,
FactionNames.Volhaven as string,
FactionNames.SpeakersForTheDead as string,
FactionNames.TheDarkArmy as string,
FactionNames.TheSyndicate as string,
FactionNames.Silhouette as string,
FactionNames.Netburners as string,
FactionNames.TianDiHui as string,
FactionNames.CyberSec as string,
];
const fdWkAvailable = [
FactionNames.Illuminati as string,
FactionNames.Daedalus as string,
FactionNames.TheCovenant as string,
FactionNames.ECorp as string,
FactionNames.MegaCorp as string,
FactionNames.BachmanAssociates as string,
FactionNames.Bladeburners as string,
FactionNames.NWO as string,
FactionNames.ClarkeIncorporated as string,
FactionNames.OmniTekIncorporated as string,
FactionNames.FourSigma as string,
FactionNames.KuaiGongInternational as string,
FactionNames.TheBlackHand as string,
FactionNames.Chongqing as string,
FactionNames.Sector12 as string,
FactionNames.NewTokyo as string,
FactionNames.Aevum as string,
FactionNames.Ishima as string,
FactionNames.Volhaven as string,
FactionNames.SpeakersForTheDead as string,
FactionNames.TheDarkArmy as string,
FactionNames.TheSyndicate as string,
FactionNames.Silhouette as string,
FactionNames.Tetrads as string,
FactionNames.SlumSnakes as string,
];
const scWkAvailable = [
FactionNames.ECorp as string,
FactionNames.MegaCorp as string,
FactionNames.BachmanAssociates as string,
FactionNames.Bladeburners as string,
FactionNames.NWO as string,
FactionNames.ClarkeIncorporated as string,
FactionNames.OmniTekIncorporated as string,
FactionNames.FourSigma as string,
FactionNames.KuaiGongInternational as string,
FactionNames.FulcrumSecretTechnologies as string,
FactionNames.Chongqing as string,
FactionNames.Sector12 as string,
FactionNames.NewTokyo as string,
FactionNames.Aevum as string,
FactionNames.Ishima as string,
FactionNames.Volhaven as string,
FactionNames.SpeakersForTheDead as string,
FactionNames.TheSyndicate as string,
FactionNames.Tetrads as string,
FactionNames.SlumSnakes as string,
FactionNames.TianDiHui as string,
];
switch (type.toLowerCase()) { switch (type.toLowerCase()) {
case "hacking": case "hacking":
case "hacking contracts": case "hacking contracts":
case "hackingcontracts": case "hackingcontracts":
if (!hackAvailable.includes(fac.name)) { if (!FactionInfos[fac.name].offerHackingWork) {
workerScript.log("workForFaction", () => `Faction '${fac.name}' do not need help with hacking contracts.`); workerScript.log("workForFaction", () => `Faction '${fac.name}' do not need help with hacking contracts.`);
return false; return false;
} }
@ -1136,7 +1061,7 @@ export function NetscriptSingularity(
case "field": case "field":
case "fieldwork": case "fieldwork":
case "field work": case "field work":
if (!fdWkAvailable.includes(fac.name)) { if (!FactionInfos[fac.name].offerFieldWork) {
workerScript.log("workForFaction", () => `Faction '${fac.name}' do not need help with field missions.`); workerScript.log("workForFaction", () => `Faction '${fac.name}' do not need help with field missions.`);
return false; return false;
} }
@ -1153,7 +1078,7 @@ export function NetscriptSingularity(
case "security": case "security":
case "securitywork": case "securitywork":
case "security work": case "security work":
if (!scWkAvailable.includes(fac.name)) { if (!FactionInfos[fac.name].offerSecurityWork) {
workerScript.log("workForFaction", () => `Faction '${fac.name}' do not need help with security work.`); workerScript.log("workForFaction", () => `Faction '${fac.name}' do not need help with security work.`);
return false; return false;
} }
@ -1325,5 +1250,49 @@ export function NetscriptSingularity(
return Object.assign({}, crime); return Object.assign({}, crime);
}, },
getDarkwebPrograms: function (): string[] {
helper.updateDynamicRam("getDarkwebPrograms", getRamCost(player, "getDarkwebPrograms"));
helper.checkSingularityAccess("getDarkwebPrograms");
// If we don't have Tor, log it and return [] (empty list)
if (!player.hasTorRouter()) {
workerScript.log("getDarkwebPrograms", () => "You do not have the TOR router.");
return [];
}
return Object.values(DarkWebItems).map((p) => p.program);
},
getDarkwebProgramCost: function (programName: any): any {
helper.updateDynamicRam("getDarkwebProgramCost", getRamCost(player, "getDarkwebProgramCost"));
helper.checkSingularityAccess("getDarkwebProgramCost");
// If we don't have Tor, log it and return -1
if (!player.hasTorRouter()) {
workerScript.log("getDarkwebProgramCost", () => "You do not have the TOR router.");
// returning -1 rather than throwing an error to be consistent with purchaseProgram
// which returns false if tor has
return -1;
}
programName = programName.toLowerCase();
const item = Object.values(DarkWebItems).find((i) => i.program.toLowerCase() === programName);
// If the program doesn't exist, throw an error. The reasoning here is that the 99% case is that
// the player will be using this in automation scripts, and if they're asking for a program that
// doesn't exist, it's the first time they've run the script. So throw an error to let them know
// that they need to fix it.
if (item == null) {
throw helper.makeRuntimeErrorMsg(
"getDarkwebProgramCost",
`No such exploit ('${programName}') found on the darkweb! ` +
`\nThis function is not case-sensitive. Did you perhaps forget .exe at the end?`,
);
}
if (player.hasProgram(item.program)) {
workerScript.log("getDarkwebProgramCost", () => `You already have the '${item.program}' program`);
return 0;
}
return item.price;
},
}; };
} }

@ -30,6 +30,20 @@ export function NetscriptSleeve(player: IPlayer, workerScript: WorkerScript, hel
} }
}; };
const getSleeveStats = function (sleeveNumber: any): any {
const sl = player.sleeves[sleeveNumber];
return {
shock: 100 - sl.shock,
sync: sl.sync,
hacking: sl.hacking,
strength: sl.strength,
defense: sl.defense,
dexterity: sl.dexterity,
agility: sl.agility,
charisma: sl.charisma,
};
}
return { return {
getNumSleeves: function (): number { getNumSleeves: function (): number {
helper.updateDynamicRam("getNumSleeves", getRamCost(player, "sleeve", "getNumSleeves")); helper.updateDynamicRam("getNumSleeves", getRamCost(player, "sleeve", "getNumSleeves"));
@ -150,18 +164,7 @@ export function NetscriptSleeve(player: IPlayer, workerScript: WorkerScript, hel
helper.updateDynamicRam("getSleeveStats", getRamCost(player, "sleeve", "getSleeveStats")); helper.updateDynamicRam("getSleeveStats", getRamCost(player, "sleeve", "getSleeveStats"));
checkSleeveAPIAccess("getSleeveStats"); checkSleeveAPIAccess("getSleeveStats");
checkSleeveNumber("getSleeveStats", sleeveNumber); checkSleeveNumber("getSleeveStats", sleeveNumber);
return getSleeveStats(sleeveNumber)
const sl = player.sleeves[sleeveNumber];
return {
shock: 100 - sl.shock,
sync: sl.sync,
hacking: sl.hacking,
strength: sl.strength,
defense: sl.defense,
dexterity: sl.dexterity,
agility: sl.agility,
charisma: sl.charisma,
};
}, },
getTask: function (asleeveNumber: any = 0): { getTask: function (asleeveNumber: any = 0): {
task: string; task: string;
@ -289,7 +292,7 @@ export function NetscriptSleeve(player: IPlayer, workerScript: WorkerScript, hel
checkSleeveAPIAccess("purchaseSleeveAug"); checkSleeveAPIAccess("purchaseSleeveAug");
checkSleeveNumber("purchaseSleeveAug", sleeveNumber); checkSleeveNumber("purchaseSleeveAug", sleeveNumber);
if (player.sleeves[sleeveNumber].shock > 0){ if (getSleeveStats(sleeveNumber).shock > 0) {
throw helper.makeRuntimeErrorMsg("sleeve.purchaseSleeveAug", `Sleeve shock too high: Sleeve ${sleeveNumber}`); throw helper.makeRuntimeErrorMsg("sleeve.purchaseSleeveAug", `Sleeve shock too high: Sleeve ${sleeveNumber}`);
} }

@ -28,7 +28,7 @@ export function NetscriptUserInterface(
setTheme: function (newTheme: UserInterfaceTheme): void { setTheme: function (newTheme: UserInterfaceTheme): void {
helper.updateDynamicRam("setTheme", getRamCost(player, "ui", "setTheme")); helper.updateDynamicRam("setTheme", getRamCost(player, "ui", "setTheme"));
const hex = /^(#)((?:[A-Fa-f0-9]{3}){1,2})$/; const hex = /^(#)((?:[A-Fa-f0-9]{2}){3,4}|(?:[A-Fa-f0-9]{3}))$/;
const currentTheme = {...Settings.theme} const currentTheme = {...Settings.theme}
const errors: string[] = []; const errors: string[] = [];
for (const key of Object.keys(newTheme)) { for (const key of Object.keys(newTheme)) {

@ -178,7 +178,7 @@ function startNetscript1Script(workerScript: WorkerScript): Promise<WorkerScript
const entry = ns[name]; const entry = ns[name];
if (typeof entry === "function") { if (typeof entry === "function") {
//Async functions need to be wrapped. See JS-Interpreter documentation //Async functions need to be wrapped. See JS-Interpreter documentation
if (["hack", "grow", "weaken", "sleep", "prompt", "manualHack", "scp", "write", "share"].includes(name)) { if (["hack", "grow", "weaken", "sleep", "prompt", "manualHack", "scp", "write", "share", "wget"].includes(name)) {
const tempWrapper = function (...args: any[]): void { const tempWrapper = function (...args: any[]): void {
const fnArgs = []; const fnArgs = [];
@ -199,7 +199,21 @@ 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)) console.error(err); if (!(err instanceof WorkerScript)) {
console.error(err);
const errorTextArray = err.split("|DELIMITER|");
const hostname = errorTextArray[1];
const scriptName = errorTextArray[2];
const errorMsg = errorTextArray[3];
let msg = `${scriptName}@${hostname}<br>`;
msg += "<br>";
msg += errorMsg;
dialogBoxCreate(msg);
workerScript.env.stopFlag = true;
workerScript.running = false;
killWorkerScript(workerScript);
return Promise.resolve(workerScript);
}
}); });
}; };
int.setProperty(scope, name, int.createAsyncFunction(tempWrapper)); int.setProperty(scope, name, int.createAsyncFunction(tempWrapper));
@ -725,7 +739,7 @@ export function runScriptFromScript(
`Cannot run script '${scriptname}' (t=${threads}) on '${server.hostname}' because there is not enough available RAM!`, `Cannot run script '${scriptname}' (t=${threads}) on '${server.hostname}' because there is not enough available RAM!`,
); );
return 0; return 0;
} else { }
// Able to run script // Able to run script
workerScript.log( workerScript.log(
caller, caller,
@ -737,8 +751,6 @@ export function runScriptFromScript(
return startWorkerScript(player, runningScriptObj, server, workerScript); return startWorkerScript(player, runningScriptObj, server, workerScript);
} }
break;
}
workerScript.log(caller, () => `Could not find script '${scriptname}' on '${server.hostname}'`); workerScript.log(caller, () => `Could not find script '${scriptname}' on '${server.hostname}'`);
return 0; return 0;

@ -25,26 +25,19 @@ export function getHackingWorkRepGain(p: IPlayer, f: Faction): number {
export function getFactionSecurityWorkRepGain(p: IPlayer, f: Faction): number { export function getFactionSecurityWorkRepGain(p: IPlayer, f: Faction): number {
const t = const t =
(0.9 * (0.9 *
(p.hacking / CONSTANTS.MaxSkillLevel + (p.strength + p.defense + p.dexterity + p.agility +
p.strength / CONSTANTS.MaxSkillLevel + (p.hacking + p.intelligence) * CalculateShareMult()
p.defense / CONSTANTS.MaxSkillLevel + )
p.dexterity / CONSTANTS.MaxSkillLevel + ) / CONSTANTS.MaxSkillLevel / 4.5;
p.agility / CONSTANTS.MaxSkillLevel +
p.intelligence / CONSTANTS.MaxSkillLevel)) /
4.5;
return t * p.faction_rep_mult * mult(f) * p.getIntelligenceBonus(1); return t * p.faction_rep_mult * mult(f) * p.getIntelligenceBonus(1);
} }
export function getFactionFieldWorkRepGain(p: IPlayer, f: Faction): number { export function getFactionFieldWorkRepGain(p: IPlayer, f: Faction): number {
const t = const t =
(0.9 * (0.9 *
(p.hacking / CONSTANTS.MaxSkillLevel + (p.strength + p.defense + p.dexterity + p.agility + p.charisma +
p.strength / CONSTANTS.MaxSkillLevel + (p.hacking + p.intelligence) * CalculateShareMult()
p.defense / CONSTANTS.MaxSkillLevel + )
p.dexterity / CONSTANTS.MaxSkillLevel + ) / CONSTANTS.MaxSkillLevel / 5.5;
p.agility / CONSTANTS.MaxSkillLevel +
p.charisma / CONSTANTS.MaxSkillLevel +
p.intelligence / CONSTANTS.MaxSkillLevel)) /
5.5;
return t * p.faction_rep_mult * mult(f) * p.getIntelligenceBonus(1); return t * p.faction_rep_mult * mult(f) * p.getIntelligenceBonus(1);
} }

@ -427,6 +427,4 @@ export async function calculateRamUsage(
console.error(e); console.error(e);
return { cost: RamCalculationErrorCode.SyntaxError }; return { cost: RamCalculationErrorCode.SyntaxError };
} }
return { cost: RamCalculationErrorCode.SyntaxError };
} }

@ -2289,6 +2289,67 @@ export interface Singularity {
* @returns True if the focus was changed. * @returns True if the focus was changed.
*/ */
setFocus(focus: boolean): boolean; setFocus(focus: boolean): boolean;
/**
* Get a list of programs offered on the dark web.
* @remarks
* RAM cost: 1 GB * 16/4/1
*
*
* This function allows the player to get a list of programs available for purchase
* on the dark web. Players MUST have purchased Tor to get the list of programs
* available. If Tor has not been purchased yet, this function will return an
* empty list.
*
* @example
* ```ts
* // NS1
* getDarkwebProgramsAvailable();
* // returns ['BruteSSH.exe', 'FTPCrack.exe'...etc]
* ```
* @example
* ```ts
* // NS2
* ns.getDarkwebProgramsAvailable();
* // returns ['BruteSSH.exe', 'FTPCrack.exe'...etc]
* ```
* @returns - a list of programs available for purchase on the dark web, or [] if Tor has not
* been purchased
*/
getDarkwebPrograms(): string[];
/**
* Check the price of an exploit on the dark web
* @remarks
* RAM cost: 0.5 GB * 16/4/1
*
*
* This function allows you to check the price of a darkweb exploit/program.
* You MUST have a TOR router in order to use this function. The price returned
* by this function is the same price you would see with buy -l from the terminal.
* Returns the cost of the program if it has not been purchased yet, 0 if it
* has already been purchased, or -1 if Tor has not been purchased (and thus
* the program/exploit is not available for purchase).
*
* If the program does not exist, an error is thrown.
*
*
* @example
* ```ts
* // NS1
* getDarkwebProgramCost("brutessh.exe");
* ```
* @example
* ```ts
* // NS2
* ns.getDarkwebProgramCost("brutessh.exe");
* ```
* @param programName - Name of program to check the price of
* @returns Price of the specified darkweb program
* (if not yet purchased), 0 if it has already been purchased, or -1 if Tor has not been
* purchased. Throws an error if the specified program/exploit does not exist
*/
getDarkwebProgramCost(programName: string): number;
} }
/** /**
@ -6665,6 +6726,16 @@ export interface Corporation extends WarehouseAPI, OfficeAPI {
* *
*/ */
sellShares(amount: number): void; sellShares(amount: number): void;
/**
* Get bonus time.
*
* Bonus time is accumulated when the game is offline or if the game is inactive in the browser.
*
* Bonus time makes the game progress faster.
*
* @returns Bonus time for the Corporation mechanic in milliseconds.
*/
getBonusTime(): number;
} }
/** /**

@ -42,8 +42,11 @@ function GetServerByHostname(hostname: string): BaseServer | null {
//Get server by IP or hostname. Returns null if invalid //Get server by IP or hostname. Returns null if invalid
export function GetServer(s: string): BaseServer | null { export function GetServer(s: string): BaseServer | null {
if (AllServers.hasOwnProperty(s)) {
const server = AllServers[s]; const server = AllServers[s];
if (server) return server; if (server) return server;
}
if (!isValidIPAddress(s)) { if (!isValidIPAddress(s)) {
return GetServerByHostname(s); return GetServerByHostname(s);
} }

@ -66,6 +66,199 @@ export function numCycleForGrowth(server: Server, growth: number, p: IPlayer, co
return cycles; return cycles;
} }
/**
* Replacement function for the above function that accounts for the +$1/thread functionality of grow
* with parameters that are the same (for compatibility), but functionality is slightly different.
* This function can ONLY be used to calculate the threads needed for a given server in its current state,
* and so wouldn't be appropriate to use for formulas.exe or ns.growthAnalyze (as those are meant to
* provide theoretical scenarios, or inverse hack respectively). Players COULD use this function with a
* custom server object with the correct moneyAvailable and moneyMax amounts, combined with a multiplier
* correctly calculated to bring the server to a new moneyAvailable (ie, passing in moneyAvailable 300 and x2
* when you want the number of threads required to grow that particular server from 300 to 600), and this
* function would pass back the correct number of threads. But the key thing is that it doesn't just
* inverse/undo a hack (since the amount hacked from/to matters, not just the multiplier).
* The above is also a rather unnecessarily obtuse way of thinking about it for a formulas.exe type of
* application, so another function with different parameters is provided for that case below this one.
* Instead this function is meant to hand-off from the old numCycleForGrowth function to the new one
* where used internally for pro-rating or the like. Where you have applied a grow and want to determine
* how many threads were needed for THAT SPECIFIC grow case using a multiplier.
* Ideally, this function, and the original function above will be depreciated to use the methodology
* and inputs of the new function below this one. Even for internal cases (it's actually easier to do so).
* @param server - Server being grown
* @param growth - How much the server money is expected to be multiplied by (e.g. 1.5 for *1.5 / +50%)
* @param p - Reference to Player object
* @returns Number of "growth cycles" needed
*/
export function numCycleForGrowthTransition(server: Server, growth: number, p: IPlayer, cores = 1): number {
return numCycleForGrowthCorrected(server, server.moneyAvailable * growth, server.moneyAvailable, p, cores);
}
/**
* This function calculates the number of threads needed to grow a server from one $amount to a higher $amount
* (ie, how many threads to grow this server from $200 to $600 for example). Used primarily for a formulas (or possibly growthAnalyze)
* type of application. It lets you "theorycraft" and easily ask what-if type questions. It's also the one that implements the
* main thread calculation algorithm, and so is the function all helper functions should call.
* It protects the inputs (so putting in INFINITY for targetMoney will use moneyMax, putting in a negative for start will use 0, etc.)
* @param server - Server being grown
* @param targetMoney - How much you want the server grown TO (not by), for instance, to grow from 200 to 600, input 600
* @param startMoney - How much you are growing the server from, for instance, to grow from 200 to 600, input 200
* @param p - Reference to Player object
* @returns Number of "growth cycles" needed
*/
export function numCycleForGrowthCorrected(server: Server, targetMoney: number, startMoney: number, p: IPlayer, cores = 1): number {
if (startMoney < 0) { startMoney = 0; } // servers "can't" have less than 0 dollars on them
if (targetMoney > server.moneyMax) { targetMoney = server.moneyMax; } // can't grow a server to more than its moneyMax
if (targetMoney <= startMoney) { return 0; } // no growth --> no threads
// exponential base adjusted by security
const adjGrowthRate = (1 + (CONSTANTS.ServerBaseGrowthRate - 1) / server.hackDifficulty);
const exponentialBase = Math.min(adjGrowthRate, CONSTANTS.ServerMaxGrowthRate); // cap growth rate
// total of all grow thread multipliers
const serverGrowthPercentage = server.serverGrowth / 100.0;
const coreMultiplier = 1 + ((cores - 1) / 16);
const threadMultiplier = serverGrowthPercentage * p.hacking_grow_mult * coreMultiplier * BitNodeMultipliers.ServerGrowthRate;
/* To understand what is done below we need to do some math. I hope the explanation is clear enough.
* First of, the names will be shortened for ease of manipulation:
* n:= targetMoney (n for new), o:= startMoney (o for old), b:= exponentialBase, t:= threadMultiplier, c:= cycles/threads
* c is what we are trying to compute.
*
* After growing, the money on a server is n = (o + c) * b^(c*t)
* c appears in an exponent and outside it, this is usually solved using the productLog/lambert's W special function
* this function will be noted W in the following
* The idea behind lambert's W function is W(x)*exp(W(x)) = x, or in other words, solving for y, y*exp(y) = x, as a function of x
* This function is provided in some advanced math library but we will compute it ourself here.
*
* Let's get back to solving the equation. It cannot be rewrote using W immediately because the base of the exponentiation is b
* b^(c*t) = exp(ln(b)*c*t) (this is how a^b is defined on reals, it matches the definition on integers)
* so n = (o + c) * exp(ln(b)*c*t) , W still cannot be used directly. We want to eliminate the other terms in 'o + c' and 'ln(b)*c*t'.
*
* A change of variable will do. The idea is to add an equation introducing a new variable (w here) in the form c = f(w) (for some f)
* With this equation we will eliminate all references to c, then solve for w and plug the result in the new equation to get c.
* The change of variable performed here should get rid of the unwanted terms mentionned above, c = w/(ln(b)*t) - o should help.
* This change of variable is allowed because whatever the value of c is, there is a value of w such that this equation holds:
* w = (c + o)*ln(b)*t (see how we used the terms we wanted to eliminate in order to build this variable change)
*
* We get n = (o + w/(ln(b)*t) - o) * exp(ln(b)*(w/(ln(b)*t) - o)*t) [ = w/(ln(b)*t) * exp(w - ln(b)*o*t) ]
* The change of variable exposed exp(w - o*ln(b)*t), we can rewrite that with exp(a - b) = exp(a)/exp(b) to isolate 'w*exp(w)'
* n = w/(ln(b)*t) * exp(w)/exp(ln(b)*o*t) [ = w*exp(w) / (ln(b) * t * b^(o*t)) ]
* Almost there, we just need to cancel the denominator on the right side of the equation:
* n * ln(b) * t * b^(o*t) = w*exp(w), Thus w = W(n * ln(b) * t * b^(o*t))
* Finally we invert the variable change: c = W(n * ln(b) * t * b^(o*t))/(ln(b)*t) - o
*
* There is still an issue left: b^(o*t) doesn't fit inside a double precision float
* because the typical amount of money on servers is arround 10^6~10^9
* We need to get an approximation of W without computing the power when o is huge
* Thankfully an approximation giving ~30% error uses log immediately so we will use
* W(n * ln(b) * t * b^(o*t)) ~= log(n * ln(b) * t * b^(o*t)) = log(n * ln(b) * t) + log(exp(ln(b) * o * t))
* = log(n * ln(b) * t) + ln(b) * o * t
* (thanks to Drak for the grow formula, f4113nb34st and Wolfram Alpha for the rewrite, dwRchyngqxs for the explanation)
*/
const x = threadMultiplier * Math.log(exponentialBase);
const y = startMoney * x + Math.log(targetMoney * x);
/* Code for the approximation of lambert's W function is adapted from
* https://git.savannah.gnu.org/cgit/gsl.git/tree/specfunc/lambert.c
* using the articles [1] https://doi.org/10.1007/BF02124750 (algorithm above)
* and [2] https://doi.org/10.1145/361952.361970 (initial approximation when x < 2.5)
*/
let w;
if (y < Math.log(2.5)) {
/* exp(y) can be safely computed without overflow.
* The relative error on the result is better when exp(y) < 2.5
* using Padé rational fraction approximation [2](5)
*/
const ey = Math.exp(y);
w = (ey + 4/3 * ey*ey) / (1 + 7/3 * ey + 5/6 * ey*ey);
} else {
/* obtain initial approximation from rough asymptotic [1](4.18)
* w = y [- log y when 0 <= y]
*/
w = y;
if (y > 0) w -= Math.log(y);
}
let cycles = w/x - startMoney;
/* Iterative refinement, the goal is to correct c until |(o + c) * b^(c*t) - n| < 1
* or the correction on the approximation is less than 1
* The Newton-Raphson method will be used, this method is a classic to find roots of functions
* (given f, find c such that f(c) = 0).
*
* The idea of this method is to take the horizontal position at which the horizontal axis
* intersects with of the tangent of the function's curve as the next approximation.
* It is equivalent to treating the curve as a line (it is called a first order approximation)
* If the current approximation is c then the new approximated value is c - f(c)/f'(c)
* (where f' is the derivative of f).
*
* In our case f(c) = (o + c) * b^(c*t) - n, f'(c) = d((o + c) * b^(c*t) - n)/dc
* = (ln(b)*t * (c + o) + 1) * b^(c*t)
* And the update step is c[new] = c[old] - ((o + c) * b^(c*t) - n)/((ln(b)*t * (o + c) + 1) * b^(c*t))
*
* The main question to ask when using this method is "does it converges?"
* (are the approximations getting better?), if it does then it does quickly.
* DOES IT CONVERGES? In the present case it does. The reason why doesn't help explaining the algorithm.
* If you are intrested then check out the wikipedia page.
*/
const bt = exponentialBase**threadMultiplier;
let corr = Infinity;
// Two sided error because we do not want to get stuck if the error stays on the wrong side
do {
// c should be above 0 so Halley's method can't be used, we have to stick to Newton-Raphson
const bct = bt**cycles;
const opc = startMoney + cycles;
const diff = opc * bct - targetMoney;
corr = diff / (opc * x + 1.0) / bct
cycles -= corr;
} while (Math.abs(corr) >= 1)
/* c is now within +/- 1 of the exact result.
* We want the ceiling of the exact result, so the floor if the approximation is above,
* the ceiling if the approximation is in the same unit as the exact result,
* and the ceiling + 1 if the approximation is below.
*/
const fca = Math.floor(cycles);
if (targetMoney <= (startMoney + fca)*Math.pow(exponentialBase, fca*threadMultiplier)) {
return fca;
}
const cca = Math.ceil(cycles);
if (targetMoney <= (startMoney + cca)*Math.pow(exponentialBase, cca*threadMultiplier)) {
return cca;
}
return cca + 1;
}
/**
* This function calculates the number of threads needed to grow a server based on a pre-hack money and hackAmt
* (ie, if you're hacking a server with $1e6 moneyAvail for 60%, this function will tell you how many threads to regrow it
* A good replacement for the current ns.growthAnalyze if you want players to have more control/responsibility
* @param server - Server being grown
* @param hackProp - the proportion of money hacked (total, not per thread, like 0.60 for hacking 60% of available money)
* @param prehackMoney - how much money the server had before being hacked (like 200000 for hacking a server that had $200000 on it at time of hacking)
* @param p - Reference to Player object
* @returns Number of "growth cycles" needed to reverse the described hack
*/
export function numCycleForGrowthByHackAmt(server: Server, hackProp: number, prehackMoney: number, p: IPlayer, cores = 1): number{
if (prehackMoney > server.moneyMax) prehackMoney = server.moneyMax;
const posthackMoney = Math.floor(prehackMoney * Math.min(1, Math.max(0, (1 - hackProp))));
return numCycleForGrowthCorrected(server, prehackMoney, posthackMoney, p, cores);
}
/**
* This function calculates the number of threads needed to grow a server based on an expected growth multiplier assuming it will max out
* (ie, if you expect to grow a server by 60% to reach maxMoney, this function will tell you how many threads to grow it)
* PROBABLY the best replacement for the current ns.growthAnalyze to maintain existing scripts
* @param server - Server being grown
* @param growth - How much the server is being grown by, as a multiple in DECIMAL form (e.g. 1.5 rather than 50). Infinity is acceptable.
* @param p - Reference to Player object
* @returns Number of "growth cycles" needed
*/
export function numCycleForGrowthByMultiplier(server: Server, growth: number, p: IPlayer, cores = 1): number{
if (growth < 1.0) growth = 1.0;
const targetMoney = server.moneyMax;
const startingMoney = server.moneyMax / growth;
return numCycleForGrowthCorrected(server, targetMoney, startingMoney, p, cores);
}
//Applied server growth for a single server. Returns the percentage growth //Applied server growth for a single server. Returns the percentage growth
export function processSingleServerGrowth(server: Server, threads: number, p: IPlayer, cores = 1): number { export function processSingleServerGrowth(server: Server, threads: number, p: IPlayer, cores = 1): number {
let serverGrowth = calculateServerGrowth(server, threads, p, cores); let serverGrowth = calculateServerGrowth(server, threads, p, cores);
@ -90,8 +283,8 @@ export function processSingleServerGrowth(server: Server, threads: number, p: IP
// if there was any growth at all, increase security // if there was any growth at all, increase security
if (oldMoneyAvailable !== server.moneyAvailable) { if (oldMoneyAvailable !== server.moneyAvailable) {
//Growing increases server security twice as much as hacking let usedCycles = numCycleForGrowthCorrected(server, server.moneyAvailable, oldMoneyAvailable, p, cores);
let usedCycles = numCycleForGrowth(server, server.moneyAvailable / oldMoneyAvailable, p, cores); // Growing increases server security twice as much as hacking
usedCycles = Math.min(Math.max(0, Math.ceil(usedCycles)), threads); usedCycles = Math.min(Math.max(0, Math.ceil(usedCycles)), threads);
server.fortify(2 * CONSTANTS.ServerFortifyAmount * usedCycles); server.fortify(2 * CONSTANTS.ServerFortifyAmount * usedCycles);
} }

@ -1,4 +1,5 @@
import React, { useState, useEffect } from "react"; import React, { useState, useEffect } from "react";
import { KEY } from "../../utils/helpers/keyCodes";
import clsx from "clsx"; import clsx from "clsx";
import { styled, Theme, CSSObject } from "@mui/material/styles"; import { styled, Theme, CSSObject } from "@mui/material/styles";
import createStyles from "@mui/styles/createStyles"; import createStyles from "@mui/styles/createStyles";
@ -53,7 +54,6 @@ import { Settings } from "../../Settings/Settings";
import { redPillFlag } from "../../RedPill"; import { redPillFlag } from "../../RedPill";
import { AugmentationNames } from "../../Augmentation/data/AugmentationNames"; import { AugmentationNames } from "../../Augmentation/data/AugmentationNames";
import { KEY } from "../../utils/helpers/keyCodes";
import { ProgramsSeen } from "../../Programs/ui/ProgramsRoot"; import { ProgramsSeen } from "../../Programs/ui/ProgramsRoot";
import { InvitationsSeen } from "../../Faction/ui/FactionsRoot"; import { InvitationsSeen } from "../../Faction/ui/FactionsRoot";
import { hash } from "../../hash/hash"; import { hash } from "../../hash/hash";
@ -276,54 +276,54 @@ export function SidebarRoot(props: IProps): React.ReactElement {
function handleShortcuts(this: Document, event: KeyboardEvent): any { function handleShortcuts(this: Document, event: KeyboardEvent): any {
if (Settings.DisableHotkeys) return; if (Settings.DisableHotkeys) return;
if ((props.player.isWorking && props.player.focus) || redPillFlag) return; if ((props.player.isWorking && props.player.focus) || redPillFlag) return;
if (event.keyCode == KEY.T && event.altKey) { if (event.key === "t" && event.altKey) {
event.preventDefault(); event.preventDefault();
clickTerminal(); clickTerminal();
} else if (event.keyCode === KEY.C && event.altKey) { } else if (event.key === KEY.C && event.altKey) {
event.preventDefault(); event.preventDefault();
clickStats(); clickStats();
} else if (event.keyCode === KEY.E && event.altKey) { } else if (event.key === KEY.E && event.altKey) {
event.preventDefault(); event.preventDefault();
clickScriptEditor(); clickScriptEditor();
} else if (event.keyCode === KEY.S && event.altKey) { } else if (event.key === KEY.S && event.altKey) {
event.preventDefault(); event.preventDefault();
clickActiveScripts(); clickActiveScripts();
} else if (event.keyCode === KEY.H && event.altKey) { } else if (event.key === KEY.H && event.altKey) {
event.preventDefault(); event.preventDefault();
clickHacknet(); clickHacknet();
} else if (event.keyCode === KEY.W && event.altKey) { } else if (event.key === KEY.W && event.altKey) {
event.preventDefault(); event.preventDefault();
clickCity(); clickCity();
} else if (event.keyCode === KEY.J && event.altKey && !event.ctrlKey && !event.metaKey && canJob) { } else if (event.key === KEY.J && event.altKey && !event.ctrlKey && !event.metaKey && canJob) {
// ctrl/cmd + alt + j is shortcut to open Chrome dev tools // ctrl/cmd + alt + j is shortcut to open Chrome dev tools
event.preventDefault(); event.preventDefault();
clickJob(); clickJob();
} else if (event.keyCode === KEY.R && event.altKey) { } else if (event.key === KEY.R && event.altKey) {
event.preventDefault(); event.preventDefault();
clickTravel(); clickTravel();
} else if (event.keyCode === KEY.P && event.altKey) { } else if (event.key === KEY.P && event.altKey) {
event.preventDefault(); event.preventDefault();
clickCreateProgram(); clickCreateProgram();
} else if (event.keyCode === KEY.F && event.altKey) { } else if (event.key === KEY.F && event.altKey) {
if (props.page == Page.Terminal && Settings.EnableBashHotkeys) { if (props.page == Page.Terminal && Settings.EnableBashHotkeys) {
return; return;
} }
event.preventDefault(); event.preventDefault();
clickFactions(); clickFactions();
} else if (event.keyCode === KEY.A && event.altKey) { } else if (event.key === KEY.A && event.altKey) {
event.preventDefault(); event.preventDefault();
clickAugmentations(); clickAugmentations();
} else if (event.keyCode === KEY.U && event.altKey) { } else if (event.key === KEY.U && event.altKey) {
event.preventDefault(); event.preventDefault();
clickTutorial(); clickTutorial();
} else if (event.keyCode === KEY.B && event.altKey && props.player.bladeburner) { } else if (event.key === KEY.B && event.altKey && props.player.bladeburner) {
event.preventDefault(); event.preventDefault();
clickBladeburner(); clickBladeburner();
} else if (event.keyCode === KEY.G && event.altKey && props.player.gang) { } else if (event.key === KEY.G && event.altKey && props.player.gang) {
event.preventDefault(); event.preventDefault();
clickGang(); clickGang();
} }
// if (event.keyCode === KEY.O && event.altKey) { // if (event.key === KEY.O && event.altKey) {
// event.preventDefault(); // event.preventDefault();
// gameOptionsBoxOpen(); // gameOptionsBoxOpen();
// } // }

@ -26,7 +26,7 @@ export const TerminalHelpText: string[] = [
" hostname Displays the hostname of the machine", " hostname Displays the hostname of the machine",
" kill [script/pid] [args...] Stops the specified script on the current server ", " kill [script/pid] [args...] Stops the specified script on the current server ",
" killall Stops all running scripts on the current machine", " killall Stops all running scripts on the current machine",
" ls [dir] [| grep pattern] Displays all files on the machine", " ls [dir] [--grep pattern] Displays all files on the machine",
" lscpu Displays the number of CPU cores on the machine", " lscpu Displays the number of CPU cores on the machine",
" mem [script] [-t n] Displays the amount of RAM required to run the script", " mem [script] [-t n] Displays the amount of RAM required to run the script",
" mv [src] [dest] Move/rename a text or script file", " mv [src] [dest] Move/rename a text or script file",
@ -295,28 +295,30 @@ export const HelpTexts: IMap<string[]> = {
" ", " ",
], ],
ls: [ ls: [
"Usage: ls [dir] [| grep pattern]", "Usage: ls [dir] [-l] [--grep pattern]",
" ", " ",
"The ls command, with no arguments, prints all files and directories on the current server's directory to the Terminal screen. ", "The ls command, with no arguments, prints all files and directories on the current server's directory to the Terminal screen. ",
"The files will be displayed in alphabetical order. ", "The files will be displayed in alphabetical order. ",
" ", " ",
"The 'dir' optional parameter can be used to display files/directories in another directory.", "The 'dir' optional parameter can be used to display files/directories in another directory.",
" ", " ",
"The '| grep pattern' optional parameter can be used to only display files whose filenames match the specified pattern.", "The '-l' optional parameter allows you to force each item onto a single line.",
" ",
"The '--grep pattern' optional parameter can be used to only display files whose filenames match the specified pattern.",
" ", " ",
"Examples:", "Examples:",
" ", " ",
"List all files with the '.script' extension in the current directory:", "List all files with the '.script' extension in the current directory:",
" ", " ",
" ls | grep .script", " ls -l --grep .script",
" ", " ",
"List all files with the '.js' extension in the root directory:", "List all files with the '.js' extension in the root directory:",
" ", " ",
" ls / | grep .js", " ls / -l --grep .js",
" ", " ",
"List all files with the word 'purchase' in the filename, in the 'scripts' directory:", "List all files with the word 'purchase' in the filename, in the 'scripts' directory:",
" ", " ",
" ls scripts | grep purchase", " ls scripts -l --grep purchase",
" ", " ",
], ],
lscpu: ["Usage: lscpu", " ", "Prints the number of CPU Cores the current server has", " "], lscpu: ["Usage: lscpu", " ", "Prints the number of CPU Cores the current server has", " "],

@ -620,7 +620,6 @@ export class Terminal implements ITerminal {
const n00dlesServ = GetServer("n00dles"); const n00dlesServ = GetServer("n00dles");
if (n00dlesServ == null) { if (n00dlesServ == null) {
throw new Error("Could not get n00dles server"); throw new Error("Could not get n00dles server");
return;
} }
switch (ITutorial.currStep) { switch (ITutorial.currStep) {
case iTutorialSteps.TerminalHelp: case iTutorialSteps.TerminalHelp:

@ -8,6 +8,7 @@ import { BaseServer } from "../../Server/BaseServer";
import { evaluateDirectoryPath, getFirstParentDirectory, isValidDirectoryPath } from "../DirectoryHelpers"; import { evaluateDirectoryPath, getFirstParentDirectory, isValidDirectoryPath } from "../DirectoryHelpers";
import { IRouter } from "../../ui/Router"; import { IRouter } from "../../ui/Router";
import { ITerminal } from "../ITerminal"; import { ITerminal } from "../ITerminal";
import * as libarg from "arg"
export function ls( export function ls(
terminal: ITerminal, terminal: ITerminal,
@ -16,45 +17,47 @@ export function ls(
server: BaseServer, server: BaseServer,
args: (string | number | boolean)[], args: (string | number | boolean)[],
): void { ): void {
let flags;
try {
flags = libarg({
'-l': Boolean,
'--grep': String,
'-g': '--grep',
},
{ argv: args }
)
} catch (e) {
// catch passing only -g / --grep with no string to use as the search
incorrectUsage()
return;
}
const filter = flags['--grep']
const numArgs = args.length; const numArgs = args.length;
function incorrectUsage(): void { function incorrectUsage(): void {
terminal.error("Incorrect usage of ls command. Usage: ls [dir] [| grep pattern]"); terminal.error("Incorrect usage of ls command. Usage: ls [dir] [-l] [-g, --grep pattern]");
} }
if (numArgs > 4 || numArgs === 2) { if (numArgs > 4) {
return incorrectUsage(); return incorrectUsage();
} }
// Grep
let filter = ""; // Grep
// Directory path // Directory path
let prefix = terminal.cwd(); let prefix = terminal.cwd();
if (!prefix.endsWith("/")) { if (!prefix.endsWith("/")) {
prefix += "/"; prefix += "/";
} }
// If there are 3+ arguments, then the last 3 must be for grep // If first arg doesn't contain a - it must be the file/folder
if (numArgs >= 3) { const dir = (args[0] && typeof args[0] == "string" && !args[0].startsWith("-")) ? args[0] : ""
if (args[numArgs - 2] !== "grep" || args[numArgs - 3] !== "|") { const newPath = evaluateDirectoryPath(dir + "", terminal.cwd());
return incorrectUsage(); prefix = newPath || "";
}
filter = args[numArgs - 1] + "";
}
// If the second argument is not a pipe, then it must be for listing a directory
if (numArgs >= 1 && args[0] !== "|") {
const newPath = evaluateDirectoryPath(args[0] + "", terminal.cwd());
prefix = newPath ? newPath : "";
if (prefix != null) {
if (!prefix.endsWith("/")) { if (!prefix.endsWith("/")) {
prefix += "/"; prefix += "/";
} }
if (!isValidDirectoryPath(prefix)) { if (!isValidDirectoryPath(prefix)) {
return incorrectUsage(); return incorrectUsage();
} }
}
}
// Root directory, which is the same as no 'prefix' at all // Root directory, which is the same as no 'prefix' at all
if (prefix === "/") { if (prefix === "/") {
@ -139,10 +142,9 @@ export function ls(
}), }),
)(); )();
const rowSplit = row const rowSplit = row.split("~");
.split(" ") let rowSplitArray = rowSplit.map((x) => [x.trim(), x.replace(x.trim(), "")]);
.map((x) => x.trim()) rowSplitArray = rowSplitArray.filter((x) => !!x[0]);
.filter((x) => !!x);
function onScriptLinkClick(filename: string): void { function onScriptLinkClick(filename: string): void {
if (player.getCurrentServer().hostname !== hostname) { if (player.getCurrentServer().hostname !== hostname) {
@ -156,24 +158,32 @@ export function ls(
return ( return (
<span className={classes.scriptLinksWrap}> <span className={classes.scriptLinksWrap}>
{rowSplit.map((rowItem) => ( {rowSplitArray.map((rowItem) => (
<span key={rowItem} className={classes.scriptLink} onClick={() => onScriptLinkClick(rowItem)}> <span>
{rowItem} <span key={rowItem[0]} className={classes.scriptLink} onClick={() => onScriptLinkClick(rowItem[0])}>
{rowItem[0]}
</span>
<span key={'s'+rowItem[0]}>
{rowItem[1]}
</span>
</span> </span>
))} ))}
</span> </span>
); );
} }
function postSegments(segments: string[], style?: any, linked?: boolean): void { function postSegments(segments: string[], flags: any, style?: any, linked?: boolean): void {
const maxLength = Math.max(...segments.map((s) => s.length)) + 1; const maxLength = Math.max(...segments.map((s) => s.length)) + 1;
const filesPerRow = Math.ceil(80 / maxLength); const filesPerRow = flags["-l"] === true ? 1 : Math.ceil(80 / maxLength);
for (let i = 0; i < segments.length; i++) { for (let i = 0; i < segments.length; i++) {
let row = ""; let row = "";
for (let col = 0; col < filesPerRow; col++) { for (let col = 0; col < filesPerRow; col++) {
if (!(i < segments.length)) break; if (!(i < segments.length)) break;
row += segments[i]; row += segments[i];
row += " ".repeat(maxLength * (col + 1) - row.length); row += " ".repeat(maxLength * (col + 1) - row.length);
if(linked) {
row += "~";
}
i++; i++;
} }
i--; i--;
@ -196,6 +206,6 @@ export function ls(
{ segments: allScripts, style: { color: "yellow", fontStyle: "bold" }, linked: true }, { segments: allScripts, style: { color: "yellow", fontStyle: "bold" }, linked: true },
].filter((g) => g.segments.length > 0); ].filter((g) => g.segments.length > 0);
for (let i = 0; i < groups.length; i++) { for (let i = 0; i < groups.length; i++) {
postSegments(groups[i].segments, groups[i].style, groups[i].linked); postSegments(groups[i].segments, flags, groups[i].style, groups[i].linked);
} }
} }

@ -48,7 +48,7 @@ export function TerminalInput({ terminal, router, player }: IProps): React.React
const terminalInput = useRef<HTMLInputElement>(null); const terminalInput = useRef<HTMLInputElement>(null);
const [value, setValue] = useState(command); const [value, setValue] = useState(command);
const [postUpdateValue, setPostUpdateValue] = useState<{postUpdate: () => void} | null>() const [postUpdateValue, setPostUpdateValue] = useState<{ postUpdate: () => void } | null>();
const [possibilities, setPossibilities] = useState<string[]>([]); const [possibilities, setPossibilities] = useState<string[]>([]);
const classes = useStyles(); const classes = useStyles();
@ -65,14 +65,14 @@ export function TerminalInput({ terminal, router, player }: IProps): React.React
postUpdateValue.postUpdate(); postUpdateValue.postUpdate();
setPostUpdateValue(null); setPostUpdateValue(null);
} }
}, [postUpdateValue]) }, [postUpdateValue]);
function saveValue(newValue: string, postUpdate?: () => void): void { function saveValue(newValue: string, postUpdate?: () => void): void {
command = newValue; command = newValue;
setValue(newValue); setValue(newValue);
if (postUpdate) { if (postUpdate) {
setPostUpdateValue({postUpdate}); setPostUpdateValue({ postUpdate });
} }
} }
@ -102,7 +102,7 @@ export function TerminalInput({ terminal, router, player }: IProps): React.React
// Move cursor to correct location // Move cursor to correct location
// foo bar |baz bum --> foo |baz bum // foo bar |baz bum --> foo |baz bum
const ref = terminalInput.current; const ref = terminalInput.current;
ref?.setSelectionRange(delStart+1, delStart+1); ref?.setSelectionRange(delStart + 1, delStart + 1);
}); });
return; return;
} }
@ -115,7 +115,7 @@ export function TerminalInput({ terminal, router, player }: IProps): React.React
// Move cursor to correct location // Move cursor to correct location
// foo bar |baz bum --> foo bar |bum // foo bar |baz bum --> foo bar |bum
const ref = terminalInput.current; const ref = terminalInput.current;
ref?.setSelectionRange(start, start) ref?.setSelectionRange(start, start);
}); });
return; return;
} }
@ -180,13 +180,13 @@ export function TerminalInput({ terminal, router, player }: IProps): React.React
useEffect(() => { useEffect(() => {
function keyDown(this: Document, event: KeyboardEvent): void { function keyDown(this: Document, event: KeyboardEvent): void {
if (terminal.contractOpen) return; if (terminal.contractOpen) return;
if (terminal.action !== null && event.keyCode === KEY.C && event.ctrlKey) { if (terminal.action !== null && event.key === KEY.C && event.ctrlKey) {
terminal.finishAction(router, player, true); terminal.finishAction(router, player, true);
return; return;
} }
const ref = terminalInput.current; const ref = terminalInput.current;
if (event.ctrlKey || event.metaKey) return; if (event.ctrlKey || event.metaKey) return;
if (event.keyCode === KEY.C && (event.ctrlKey || event.metaKey)) return; // trying to copy if (event.key === KEY.C && (event.ctrlKey || event.metaKey)) return; // trying to copy
if (ref) ref.focus(); if (ref) ref.focus();
} }
@ -196,7 +196,7 @@ export function TerminalInput({ terminal, router, player }: IProps): React.React
async function onKeyDown(event: React.KeyboardEvent<HTMLInputElement>): Promise<void> { async function onKeyDown(event: React.KeyboardEvent<HTMLInputElement>): Promise<void> {
// Run command. // Run command.
if (event.keyCode === KEY.ENTER && value !== "") { if (event.key === KEY.ENTER && value !== "") {
event.preventDefault(); event.preventDefault();
terminal.print(`[${player.getCurrentServer().hostname} ~${terminal.cwd()}]> ${value}`); terminal.print(`[${player.getCurrentServer().hostname} ~${terminal.cwd()}]> ${value}`);
terminal.executeCommands(router, player, value); terminal.executeCommands(router, player, value);
@ -205,7 +205,7 @@ export function TerminalInput({ terminal, router, player }: IProps): React.React
} }
// Autocomplete // Autocomplete
if (event.keyCode === KEY.TAB && value !== "") { if (event.key === KEY.TAB && value !== "") {
event.preventDefault(); event.preventDefault();
let copy = value; let copy = value;
@ -256,13 +256,13 @@ export function TerminalInput({ terminal, router, player }: IProps): React.React
} }
// Clear screen. // Clear screen.
if (event.keyCode === KEY.L && event.ctrlKey) { if (event.key === KEY.L && event.ctrlKey) {
event.preventDefault(); event.preventDefault();
terminal.clear(); terminal.clear();
} }
// Select previous command. // Select previous command.
if (event.keyCode === KEY.UPARROW || (Settings.EnableBashHotkeys && event.keyCode === KEY.P && event.ctrlKey)) { if (event.key === KEY.UPARROW || (Settings.EnableBashHotkeys && event.key === "p" && event.ctrlKey)) {
if (Settings.EnableBashHotkeys) { if (Settings.EnableBashHotkeys) {
event.preventDefault(); event.preventDefault();
} }
@ -290,7 +290,7 @@ export function TerminalInput({ terminal, router, player }: IProps): React.React
} }
// Select next command // Select next command
if (event.keyCode === KEY.DOWNARROW || (Settings.EnableBashHotkeys && event.keyCode === KEY.M && event.ctrlKey)) { if (event.key === KEY.DOWNARROW || (Settings.EnableBashHotkeys && event.key === "m" && event.ctrlKey)) {
if (Settings.EnableBashHotkeys) { if (Settings.EnableBashHotkeys) {
event.preventDefault(); event.preventDefault();
} }
@ -317,57 +317,57 @@ export function TerminalInput({ terminal, router, player }: IProps): React.React
// Extra Bash Emulation Hotkeys, must be enabled through options // Extra Bash Emulation Hotkeys, must be enabled through options
if (Settings.EnableBashHotkeys) { if (Settings.EnableBashHotkeys) {
if (event.keyCode === KEY.A && event.ctrlKey) { if (event.key === KEY.A && event.ctrlKey) {
event.preventDefault(); event.preventDefault();
moveTextCursor("home"); moveTextCursor("home");
} }
if (event.keyCode === KEY.E && event.ctrlKey) { if (event.key === KEY.E && event.ctrlKey) {
event.preventDefault(); event.preventDefault();
moveTextCursor("end"); moveTextCursor("end");
} }
if (event.keyCode === KEY.B && event.ctrlKey) { if (event.key === KEY.B && event.ctrlKey) {
event.preventDefault(); event.preventDefault();
moveTextCursor("prevchar"); moveTextCursor("prevchar");
} }
if (event.keyCode === KEY.B && event.altKey) { if (event.key === KEY.B && event.altKey) {
event.preventDefault(); event.preventDefault();
moveTextCursor("prevword"); moveTextCursor("prevword");
} }
if (event.keyCode === KEY.F && event.ctrlKey) { if (event.key === KEY.F && event.ctrlKey) {
event.preventDefault(); event.preventDefault();
moveTextCursor("nextchar"); moveTextCursor("nextchar");
} }
if (event.keyCode === KEY.F && event.altKey) { if (event.key === KEY.F && event.altKey) {
event.preventDefault(); event.preventDefault();
moveTextCursor("nextword"); moveTextCursor("nextword");
} }
if ((event.keyCode === KEY.H || event.keyCode === KEY.D) && event.ctrlKey) { if ((event.key === KEY.H || event.key === KEY.D) && event.ctrlKey) {
modifyInput("backspace"); modifyInput("backspace");
event.preventDefault(); event.preventDefault();
} }
if (event.keyCode === KEY.W && event.ctrlKey) { if (event.key === KEY.W && event.ctrlKey) {
event.preventDefault(); event.preventDefault();
modifyInput("deletewordbefore"); modifyInput("deletewordbefore");
} }
if (event.keyCode === KEY.D && event.altKey) { if (event.key === KEY.D && event.altKey) {
event.preventDefault(); event.preventDefault();
modifyInput("deletewordafter"); modifyInput("deletewordafter");
} }
if (event.keyCode === KEY.U && event.ctrlKey) { if (event.key === KEY.U && event.ctrlKey) {
event.preventDefault(); event.preventDefault();
modifyInput("clearbefore"); modifyInput("clearbefore");
} }
if (event.keyCode === KEY.K && event.ctrlKey) { if (event.key === KEY.K && event.ctrlKey) {
event.preventDefault(); event.preventDefault();
modifyInput("clearafter"); modifyInput("clearafter");
} }

@ -48,6 +48,7 @@ import { calculateAchievements } from "./Achievements/Achievements";
import React from "react"; import React from "react";
import { setupUncaughtPromiseHandler } from "./UncaughtPromiseHandler"; import { setupUncaughtPromiseHandler } from "./UncaughtPromiseHandler";
import { Typography } from "@mui/material";
const Engine: { const Engine: {
_lastUpdate: number; _lastUpdate: number;
@ -406,11 +407,21 @@ const Engine: {
() => () =>
AlertEvents.emit( AlertEvents.emit(
<> <>
Offline for {timeOfflineString}. While you were offline: <Typography>Offline for {timeOfflineString}. While you were offline:</Typography>
<ul> <ul>
<li>Your scripts generated{" "} <Money money={offlineHackingIncome} /></li> <li>
<li>Your Hacknet Nodes generated {hacknetProdInfo}</li> <Typography>
<li>You gained{" "} <Reputation reputation={offlineReputation} /> reputation divided amongst your factions</li> Your scripts generated <Money money={offlineHackingIncome} />
</Typography>
</li>
<li>
<Typography>Your Hacknet Nodes generated {hacknetProdInfo}</Typography>
</li>
<li>
<Typography>
You gained <Reputation reputation={offlineReputation} /> reputation divided amongst your factions
</Typography>
</li>
</ul> </ul>
</>, </>,
), ),

@ -3,7 +3,7 @@ import { EventEmitter } from "../../utils/EventEmitter";
import { Modal } from "./Modal"; import { Modal } from "./Modal";
import Typography from "@mui/material/Typography"; import Typography from "@mui/material/Typography";
import Box from "@mui/material/Box"; import Box from "@mui/material/Box";
import {sha256} from "js-sha256"; import { sha256 } from "js-sha256";
export const AlertEvents = new EventEmitter<[string | JSX.Element]>(); export const AlertEvents = new EventEmitter<[string | JSX.Element]>();
@ -23,8 +23,8 @@ export function AlertManager(): React.ReactElement {
i++; i++;
setAlerts((old) => { setAlerts((old) => {
const hash = getMessageHash(text); const hash = getMessageHash(text);
if (old.some(a => a.hash === hash)) { if (old.some((a) => a.hash === hash)) {
console.log('Duplicate message'); console.log("Duplicate message");
return old; return old;
} }
return [ return [
@ -51,7 +51,7 @@ export function AlertManager(): React.ReactElement {
}, []); }, []);
function getMessageHash(text: string | JSX.Element): string { function getMessageHash(text: string | JSX.Element): string {
if (typeof text === 'string') return sha256(text); if (typeof text === "string") return sha256(text);
return sha256(JSON.stringify(text.props)); return sha256(JSON.stringify(text.props));
} }

@ -37,7 +37,7 @@ export function CodingContractModal(): React.ReactElement {
// whatever ... // whatever ...
const value = (event.target as any).value; const value = (event.target as any).value;
if (event.keyCode === KEY.ENTER && value !== "") { if (event.key === KEY.ENTER && value !== "") {
event.preventDefault(); event.preventDefault();
props.onAttempt(answer); props.onAttempt(answer);
setAnswer(""); setAnswer("");

@ -6,6 +6,7 @@ import Button from "@mui/material/Button";
import Select, { SelectChangeEvent } from "@mui/material/Select"; import Select, { SelectChangeEvent } from "@mui/material/Select";
import TextField from "@mui/material/TextField"; import TextField from "@mui/material/TextField";
import MenuItem from "@mui/material/MenuItem"; import MenuItem from "@mui/material/MenuItem";
import { KEY } from "../../utils/helpers/keyCodes";
export const PromptEvent = new EventEmitter<[Prompt]>(); export const PromptEvent = new EventEmitter<[Prompt]>();
@ -93,7 +94,7 @@ function PromptMenuText({ resolve }: IContentProps): React.ReactElement {
const onKeyDown = (event: React.KeyboardEvent<HTMLInputElement>): void => { const onKeyDown = (event: React.KeyboardEvent<HTMLInputElement>): void => {
event.stopPropagation(); event.stopPropagation();
if (event.key === "Enter") { if (event.key === KEY.ENTER) {
event.preventDefault(); event.preventDefault();
submit(); submit();
} }

@ -37,23 +37,12 @@ export function WorkInProgressRoot(): React.ReactElement {
if (player.workType == CONSTANTS.WorkTypeFaction) { if (player.workType == CONSTANTS.WorkTypeFaction) {
const faction = Factions[player.currentWorkFactionName]; const faction = Factions[player.currentWorkFactionName];
if (!faction) {
return (
<Grid container direction="column" justifyContent="center" alignItems="center" style={{ minHeight: "100vh" }}>
<Grid item>
<Typography>
Something has gone wrong, you cannot work for {player.currentWorkFactionName || "(Faction not found)"} at
this time.
</Typography>
</Grid>
</Grid>
);
}
if (!faction) { if (!faction) {
return ( return (
<> <>
<Typography variant="h4" color="primary"> <Typography variant="h4" color="primary">
You have not joined {faction} yet! You have not joined {player.currentWorkFactionName || "(Faction not found)"} yet or cannot work at this time,
please try again if you think this should have worked
</Typography> </Typography>
<Button onClick={() => router.toFactions()}>Back to Factions</Button> <Button onClick={() => router.toFactions()}>Back to Factions</Button>
</> </>

@ -52,24 +52,25 @@ class NumeralFormatter {
} }
formatBigNumber(n: number): string { formatBigNumber(n: number): string {
return this.format(n, "0.000a"); return this.format(n, "0.[000]a");
} }
// TODO: leverage numeral.js to do it. This function also implies you can // TODO: leverage numeral.js to do it. This function also implies you can
// use this format in some text field but you can't. ( "1t" will parse but // use this format in some text field but you can't. ( "1t" will parse but
// "1s" will not) // "1s" will not)
formatReallyBigNumber(n: number, decimalPlaces = 3): string { formatReallyBigNumber(n: number, decimalPlaces = 3): string {
const nAbs = Math.abs(n);
if (n === Infinity) return "∞"; if (n === Infinity) return "∞";
for (let i = 0; i < extraFormats.length; i++) { for (let i = 0; i < extraFormats.length; i++) {
if (extraFormats[i] < n && n <= extraFormats[i] * 1000) { if (extraFormats[i] < nAbs && nAbs <= extraFormats[i] * 1000) {
return this.format(n / extraFormats[i], "0." + "0".repeat(decimalPlaces)) + extraNotations[i]; return this.format(n / extraFormats[i], "0." + "0".repeat(decimalPlaces)) + extraNotations[i];
} }
} }
if (Math.abs(n) < 1000) { if (nAbs < 1000) {
return this.format(n, "0." + "0".repeat(decimalPlaces)); return this.format(n, "0.[" + "0".repeat(decimalPlaces) + "]");
} }
const str = this.format(n, "0." + "0".repeat(decimalPlaces) + "a"); const str = this.format(n, "0.[" + "0".repeat(decimalPlaces) + "]a");
if (str === "NaNt") return this.format(n, "0." + " ".repeat(decimalPlaces) + "e+0"); if (str === "NaNt") return this.format(n, "0.[" + " ".repeat(decimalPlaces) + "]e+0");
return str; return str;
} }
@ -187,19 +188,56 @@ class NumeralFormatter {
return this.format(n, "0.00"); return this.format(n, "0.00");
} }
parseCustomLargeNumber(str: string): number {
const numericRegExp = new RegExp('^(\-?\\d+\\.?\\d*)([' + extraNotations.join("") + ']?)$');
const match = str.match(numericRegExp);
if (match == null) {
return NaN;
}
const [, number, notation] = match;
const notationIndex = extraNotations.indexOf(notation);
if (notationIndex === -1) {
return NaN;
}
return parseFloat(number) * extraFormats[notationIndex];
}
largestAbsoluteNumber(n1: number, n2 = 0, n3 = 0): number {
if(isNaN(n1)) n1=0;
if(isNaN(n2)) n2=0;
if(isNaN(n3)) n3=0;
const largestAbsolute = Math.max(Math.abs(n1), Math.abs(n2), Math.abs(n3));
switch(largestAbsolute) {
case Math.abs(n1): return n1;
case Math.abs(n2): return n2;
case Math.abs(n3): return n3;
}
return 0;
}
parseMoney(s: string): number { parseMoney(s: string): number {
// numeral library does not handle formats like 1e10 well (returns 110), // numeral library does not handle formats like 1s (returns 1) and 1e10 (returns 110) well,
// so if both return a valid number, return the biggest one // so if more then 1 return a valid number, return the one farthest from 0
const numeralValue = numeral(s).value(); const numeralValue = numeral(s).value();
const parsed = parseFloat(s); const parsed = parseFloat(s);
if (isNaN(parsed) && numeralValue === null) { const selfParsed = this.parseCustomLargeNumber(s);
// Check for one or more NaN values
if (isNaN(parsed) && numeralValue === null && isNaN(selfParsed)) { // 3x NaN
return NaN; return NaN;
} else if (isNaN(parsed)) { } else if (isNaN(parsed) && isNaN(selfParsed)) { // 2x NaN
return numeralValue; return numeralValue;
} else if (numeralValue === null) { } else if (numeralValue === null && isNaN(selfParsed)) { // 2x NaN
return parsed; return parsed;
} else { } else if (isNaN(parsed) && numeralValue === null) { // 2x NaN
return Math.max(numeralValue, parsed); return selfParsed;
} else if (isNaN(parsed)) { // 1x NaN
return this.largestAbsoluteNumber(numeralValue, selfParsed);
} else if (numeralValue === null) { // 1x NaN
return this.largestAbsoluteNumber(parsed, selfParsed);
} else if (isNaN(selfParsed)) { // 1x NaN
return this.largestAbsoluteNumber(numeralValue, parsed);
} else { // no NaN
return this.largestAbsoluteNumber(numeralValue, parsed, selfParsed);
} }
} }
} }

@ -1,51 +1,53 @@
import { IMap } from "../../types";
/** /**
* Keyboard key codes * Keyboard key codes
*/ */
export const KEY: IMap<number> = { export enum KEY {
CTRL: 17, //SHIFT: 16, // Check by `&& event.shiftKey`
DOWNARROW: 40, //CTRL: 17, // Check by `&& event.ctrlKey`
ENTER: 13, //ALT: 18, // Check by `&& event.altKey`
ESC: 27, ENTER = "Enter",
TAB: 9, ESC = "Escape",
UPARROW: 38, TAB = "Tab",
UPARROW = "ArrowUp",
DOWNARROW = "ArrowDown",
LEFTARROW = "ArrowLeft",
RIGHTARROW = "ArrowRight",
"0": 48, k0 = "0",
"1": 49, k1 = "1",
"2": 50, k2 = "2",
"3": 51, k3 = "3",
"4": 52, k4 = "4",
"5": 53, k5 = "5",
"6": 54, k6 = "6",
"7": 55, k7 = "7",
"8": 56, k8 = "8",
"9": 57, k9 = "9",
A: 65, A = "a",
B: 66, B = "b",
C: 67, C = "c",
D: 68, D = "d",
E: 69, E = "e",
F: 70, F = "f",
G: 71, G = "g",
H: 72, H = "h",
I: 73, I = "i",
J: 74, J = "j",
K: 75, K = "k",
L: 76, L = "l",
M: 77, M = "m",
N: 78, N = "n",
O: 79, O = "o",
P: 80, P = "p",
Q: 81, Q = "q",
R: 82, R = "r",
S: 83, S = "s",
T: 84, T = "t",
U: 85, U = "u",
V: 86, V = "v",
W: 87, W = "w",
X: 88, X = "x",
Y: 89, Y = "y",
Z: 90, Z = "z",
}; }

@ -1,5 +1,4 @@
// eslint-disable-next-line @typescript-eslint/no-unused-vars import { jest, describe, expect } from "@jest/globals";
import { jest, describe, expect, test } from "@jest/globals";
import { Player } from "../../../src/Player"; import { Player } from "../../../src/Player";
import { NetscriptFunctions } from "../../../src/NetscriptFunctions"; import { NetscriptFunctions } from "../../../src/NetscriptFunctions";
@ -704,6 +703,16 @@ describe("Netscript Dynamic RAM Calculation/Generation Tests", function () {
await testNonzeroDynamicRamCost(f); await testNonzeroDynamicRamCost(f);
}); });
it("getDarkwebProgramCost()", async function () {
const f = ["getDarkwebProgramCost"];
await testNonzeroDynamicRamCost(f);
});
it("getDarkwebPrograms()", async function () {
const f = ["getDarkwebPrograms"];
await testNonzeroDynamicRamCost(f);
});
it("getCharacterInformation()", async function () { it("getCharacterInformation()", async function () {
const f = ["getCharacterInformation"]; const f = ["getCharacterInformation"];
await testNonzeroDynamicRamCost(f); await testNonzeroDynamicRamCost(f);

@ -656,6 +656,16 @@ describe("Netscript Static RAM Calculation/Generation Tests", function () {
await expectNonZeroRamCost(f); await expectNonZeroRamCost(f);
}); });
it("getDarkwebPrograms()", async function () {
const f = ["getDarkwebPrograms"];
await expectNonZeroRamCost(f);
});
it("getDarkwebProgramCost()", async function () {
const f = ["getDarkwebProgramCost"];
await expectNonZeroRamCost(f);
});
it("upgradeHomeRam()", async function () { it("upgradeHomeRam()", async function () {
const f = ["upgradeHomeRam"]; const f = ["upgradeHomeRam"];
await expectNonZeroRamCost(f); await expectNonZeroRamCost(f);

@ -0,0 +1,247 @@
import { describe, expect, test } from "@jest/globals";
import { numeralWrapper } from "../../../src/ui/numeralFormat";
let decimalFormat = '0.[000000]';
describe('Numeral formatting for positive numbers', () => {
test('should not format too small numbers', () => {
expect(numeralWrapper.format(0.0000000001, decimalFormat)).toEqual('0');
expect(numeralWrapper.format(0.000000001, decimalFormat)).toEqual('0');
expect(numeralWrapper.format(0.00000001, decimalFormat)).toEqual('0');
expect(numeralWrapper.format(0.0000001, decimalFormat)).toEqual('0');
expect(numeralWrapper.format(0.000001, decimalFormat)).toEqual('0.000001');
expect(numeralWrapper.format(0.00001, decimalFormat)).toEqual('0.00001');
expect(numeralWrapper.format(0.0001, decimalFormat)).toEqual('0.0001');
expect(numeralWrapper.format(0.001, decimalFormat)).toEqual('0.001');
expect(numeralWrapper.format(0.01, decimalFormat)).toEqual('0.01');
expect(numeralWrapper.format(0.1, decimalFormat)).toEqual('0.1');
expect(numeralWrapper.format(1, decimalFormat)).toEqual('1');
});
test('should format big numbers in short format', () => {
expect(numeralWrapper.formatBigNumber(987654000000000000)).toEqual('987654t');
expect(numeralWrapper.formatBigNumber(987654300000000000)).toEqual('987654.3t');
expect(numeralWrapper.formatBigNumber(987654320000000000)).toEqual('987654.32t');
expect(numeralWrapper.formatBigNumber(987654321000000000)).toEqual('987654.321t');
expect(numeralWrapper.formatBigNumber(987654321900000000)).toEqual('987654.322t');
});
test('should format really big numbers in readable format', () => {
expect(numeralWrapper.formatReallyBigNumber(987)).toEqual('987');
expect(numeralWrapper.formatReallyBigNumber(987654)).toEqual('987.654k');
expect(numeralWrapper.formatReallyBigNumber(987654321)).toEqual('987.654m');
expect(numeralWrapper.formatReallyBigNumber(987654321987)).toEqual('987.654b');
expect(numeralWrapper.formatReallyBigNumber(987654321987654)).toEqual('987.654t');
expect(numeralWrapper.formatReallyBigNumber(987654321987654321)).toEqual('987.654q');
expect(numeralWrapper.formatReallyBigNumber(987654321987654321987)).toEqual('987.654Q');
expect(numeralWrapper.formatReallyBigNumber(987654321987654321987654)).toEqual('987.654s');
expect(numeralWrapper.formatReallyBigNumber(987654321987654321987654321)).toEqual('987.654S');
expect(numeralWrapper.formatReallyBigNumber(987654321987654321987654321987)).toEqual('987.654o');
expect(numeralWrapper.formatReallyBigNumber(987654321987654321987654321987654)).toEqual('987.654n');
});
test('should format even bigger really big numbers in scientific format', () => {
expect(numeralWrapper.formatReallyBigNumber(987654321987654321987654321987654321)).toEqual('9.877e+35');
expect(numeralWrapper.formatReallyBigNumber(9876543219876543219876543219876543219)).toEqual('9.877e+36');
expect(numeralWrapper.formatReallyBigNumber(98765432198765432198765432198765432198)).toEqual('9.877e+37');
});
test('should format percentage', () => {
expect(numeralWrapper.formatPercentage(1234.56789)).toEqual('123456.79%');
});
});
describe('Numeral formatting for negative numbers', () => {
test('should not format too small numbers', () => {
expect(numeralWrapper.format(-0.0000000001, decimalFormat)).toEqual('0');
expect(numeralWrapper.format(-0.000000001, decimalFormat)).toEqual('0');
expect(numeralWrapper.format(-0.00000001, decimalFormat)).toEqual('0');
expect(numeralWrapper.format(-0.0000001, decimalFormat)).toEqual('0');
expect(numeralWrapper.format(-0.000001, decimalFormat)).toEqual('-0.000001');
expect(numeralWrapper.format(-0.00001, decimalFormat)).toEqual('-0.00001');
expect(numeralWrapper.format(-0.0001, decimalFormat)).toEqual('-0.0001');
expect(numeralWrapper.format(-0.001, decimalFormat)).toEqual('-0.001');
expect(numeralWrapper.format(-0.01, decimalFormat)).toEqual('-0.01');
expect(numeralWrapper.format(-0.1, decimalFormat)).toEqual('-0.1');
expect(numeralWrapper.format(-1, decimalFormat)).toEqual('-1');
});
test('should format big numbers in short format', () => {
expect(numeralWrapper.formatBigNumber(-987654000000000000)).toEqual('-987654t');
expect(numeralWrapper.formatBigNumber(-987654300000000000)).toEqual('-987654.3t');
expect(numeralWrapper.formatBigNumber(-987654320000000000)).toEqual('-987654.32t');
expect(numeralWrapper.formatBigNumber(-987654321000000000)).toEqual('-987654.321t');
expect(numeralWrapper.formatBigNumber(-987654321900000000)).toEqual('-987654.322t');
});
test('should format really big numbers in readable format', () => {
expect(numeralWrapper.formatReallyBigNumber(-987)).toEqual('-987');
expect(numeralWrapper.formatReallyBigNumber(-987654)).toEqual('-987.654k');
expect(numeralWrapper.formatReallyBigNumber(-987654321)).toEqual('-987.654m');
expect(numeralWrapper.formatReallyBigNumber(-987654321987)).toEqual('-987.654b');
expect(numeralWrapper.formatReallyBigNumber(-987654321987654)).toEqual('-987.654t');
expect(numeralWrapper.formatReallyBigNumber(-987654321987654321)).toEqual('-987.654q');
expect(numeralWrapper.formatReallyBigNumber(-987654321987654321987)).toEqual('-987.654Q');
expect(numeralWrapper.formatReallyBigNumber(-987654321987654321987654)).toEqual('-987.654s');
expect(numeralWrapper.formatReallyBigNumber(-987654321987654321987654321)).toEqual('-987.654S');
expect(numeralWrapper.formatReallyBigNumber(-987654321987654321987654321987)).toEqual('-987.654o');
expect(numeralWrapper.formatReallyBigNumber(-987654321987654321987654321987654)).toEqual('-987.654n');
});
test('should format even bigger really big numbers in scientific format', () => {
expect(numeralWrapper.formatReallyBigNumber(-987654321987654321987654321987654321)).toEqual('-9.877e+35');
expect(numeralWrapper.formatReallyBigNumber(-9876543219876543219876543219876543219)).toEqual('-9.877e+36');
expect(numeralWrapper.formatReallyBigNumber(-98765432198765432198765432198765432198)).toEqual('-9.877e+37');
});
test('should format percentage', () => {
expect(numeralWrapper.formatPercentage(-1234.56789)).toEqual('-123456.79%');
});
});
describe('Numeral formatting of text', () => {
test('should filter non-numeric text', () => {
expect(numeralWrapper.format('abc')).toEqual('0');
expect(numeralWrapper.format('123abc')).toEqual('123');
expect(numeralWrapper.format('!3')).toEqual('3');
expect(numeralWrapper.format('3!')).toEqual('3');
expect(numeralWrapper.format('0.001', decimalFormat)).toEqual('0.001');
});
test('should not format too small numbers', () => {
expect(numeralWrapper.format('0.00000001', decimalFormat)).toEqual('0');
expect(numeralWrapper.format('0.0000001', decimalFormat)).toEqual('0');
expect(numeralWrapper.format('0.000001', decimalFormat)).toEqual('0.000001');
expect(numeralWrapper.format('0.00001', decimalFormat)).toEqual('0.00001');
expect(numeralWrapper.format('1', decimalFormat)).toEqual('1');
expect(numeralWrapper.format('-0.00000001', decimalFormat)).toEqual('0');
expect(numeralWrapper.format('-0.0000001', decimalFormat)).toEqual('0');
expect(numeralWrapper.format('-0.000001', decimalFormat)).toEqual('-0.000001');
expect(numeralWrapper.format('-0.00001', decimalFormat)).toEqual('-0.00001');
expect(numeralWrapper.format('-1', decimalFormat)).toEqual('-1');
});
test('should format big numbers in short format', () => {
expect(numeralWrapper.formatBigNumber('987654000000000000')).toEqual('987654t');
expect(numeralWrapper.formatBigNumber('987654300000000000')).toEqual('987654.3t');
expect(numeralWrapper.formatBigNumber('987654320000000000')).toEqual('987654.32t');
expect(numeralWrapper.formatBigNumber('987654321000000000')).toEqual('987654.321t');
expect(numeralWrapper.formatBigNumber('987654321900000000')).toEqual('987654.322t');
expect(numeralWrapper.formatBigNumber('-987654000000000000')).toEqual('-987654t');
expect(numeralWrapper.formatBigNumber('-987654300000000000')).toEqual('-987654.3t');
expect(numeralWrapper.formatBigNumber('-987654320000000000')).toEqual('-987654.32t');
expect(numeralWrapper.formatBigNumber('-987654321000000000')).toEqual('-987654.321t');
expect(numeralWrapper.formatBigNumber('-987654321900000000')).toEqual('-987654.322t');
});
test('should format really big numbers in readable format', () => {
expect(numeralWrapper.formatReallyBigNumber('987')).toEqual('987');
expect(numeralWrapper.formatReallyBigNumber('987654')).toEqual('987.654k');
expect(numeralWrapper.formatReallyBigNumber('987654321')).toEqual('987.654m');
expect(numeralWrapper.formatReallyBigNumber('987654321987')).toEqual('987.654b');
expect(numeralWrapper.formatReallyBigNumber('987654321987654')).toEqual('987.654t');
expect(numeralWrapper.formatReallyBigNumber('987654321987654321')).toEqual('987.654q');
expect(numeralWrapper.formatReallyBigNumber('987654321987654321987')).toEqual('987.654Q');
expect(numeralWrapper.formatReallyBigNumber('987654321987654321987654')).toEqual('987.654s');
expect(numeralWrapper.formatReallyBigNumber('987654321987654321987654321')).toEqual('987.654S');
expect(numeralWrapper.formatReallyBigNumber('987654321987654321987654321987')).toEqual('987.654o');
expect(numeralWrapper.formatReallyBigNumber('987654321987654321987654321987654')).toEqual('987.654n');
expect(numeralWrapper.formatReallyBigNumber('-987')).toEqual('-987');
expect(numeralWrapper.formatReallyBigNumber('-987654')).toEqual('-987.654k');
expect(numeralWrapper.formatReallyBigNumber('-987654321')).toEqual('-987.654m');
expect(numeralWrapper.formatReallyBigNumber('-987654321987')).toEqual('-987.654b');
expect(numeralWrapper.formatReallyBigNumber('-987654321987654')).toEqual('-987.654t');
expect(numeralWrapper.formatReallyBigNumber('-987654321987654321')).toEqual('-987.654q');
expect(numeralWrapper.formatReallyBigNumber('-987654321987654321987')).toEqual('-987.654Q');
expect(numeralWrapper.formatReallyBigNumber('-987654321987654321987654')).toEqual('-987.654s');
expect(numeralWrapper.formatReallyBigNumber('-987654321987654321987654321')).toEqual('-987.654S');
expect(numeralWrapper.formatReallyBigNumber('-987654321987654321987654321987')).toEqual('-987.654o');
expect(numeralWrapper.formatReallyBigNumber('-987654321987654321987654321987654')).toEqual('-987.654n');
});
test('should format even bigger really big numbers in scientific format', () => {
expect(numeralWrapper.formatReallyBigNumber('987654321987654321987654321987654321')).toEqual('9.877e+35');
expect(numeralWrapper.formatReallyBigNumber('9876543219876543219876543219876543219')).toEqual('9.877e+36');
expect(numeralWrapper.formatReallyBigNumber('98765432198765432198765432198765432198')).toEqual('9.877e+37');
expect(numeralWrapper.formatReallyBigNumber('-987654321987654321987654321987654321')).toEqual('-9.877e+35');
expect(numeralWrapper.formatReallyBigNumber('-9876543219876543219876543219876543219')).toEqual('-9.877e+36');
expect(numeralWrapper.formatReallyBigNumber('-98765432198765432198765432198765432198')).toEqual('-9.877e+37');
});
test('should format percentage', () => {
expect(numeralWrapper.formatPercentage('1234.56789')).toEqual('123456.79%');
expect(numeralWrapper.formatPercentage('-1234.56789')).toEqual('-123456.79%');
});
});
describe('Numeral formatting of scientific text', () => {
test('should format even bigger really big numbers in scientific format', () => {
// Accepted by numeral.js
expect(numeralWrapper.parseMoney('123')).toEqual(123);
expect(numeralWrapper.parseMoney('123.456')).toEqual(123.456);
expect(numeralWrapper.parseMoney('123k')).toEqual(123000);
expect(numeralWrapper.parseMoney('123.456k')).toEqual(123456);
expect(numeralWrapper.parseMoney('123m')).toEqual(123000000);
expect(numeralWrapper.parseMoney('123.456m')).toEqual(123456000);
expect(numeralWrapper.parseMoney('123b')).toEqual(123000000000);
expect(numeralWrapper.parseMoney('123.456b')).toEqual(123456000000);
expect(numeralWrapper.parseMoney('123t')).toEqual(123000000000000);
expect(numeralWrapper.parseMoney('123.456t')).toEqual(123456000000000);
// Custom formats, parseFloat has some rounding issues
expect(numeralWrapper.parseMoney('123q')).toBeCloseTo(123000000000000000);
expect(numeralWrapper.parseMoney('123.456q')).toBeCloseTo(123456000000000000);
expect(numeralWrapper.parseMoney('123Q')).toBeCloseTo(123000000000000000000);
expect(numeralWrapper.parseMoney('123.456Q')).toBeCloseTo(123456000000000000000);
expect(numeralWrapper.parseMoney('123s')).toBeCloseTo(123000000000000000000000);
expect(numeralWrapper.parseMoney('123.456s')).toBeCloseTo(123456000000000000000000);
expect(numeralWrapper.parseMoney('123S')).toBeCloseTo(123000000000000000000000000);
expect(numeralWrapper.parseMoney('123.456S')).toBeCloseTo(123456000000000000000000000);
// Larger numbers fail the test due to rounding issues
//expect(numeralWrapper.parseMoney('123o')).toBeCloseTo(123000000000000000000000000000);
//expect(numeralWrapper.parseMoney('123.456o')).toBeCloseTo(123456000000000000000000000000);
//expect(numeralWrapper.parseMoney('123n')).toBeCloseTo(123000000000000000000000000000000);
//expect(numeralWrapper.parseMoney('123.456n')).toBeCloseTo(123456000000000000000000000000000);
});
test('should format even bigger really big negative numbers in scientific format', () => {
// Accepted by numeral.js
expect(numeralWrapper.parseMoney('-123')).toEqual(-123);
expect(numeralWrapper.parseMoney('-123.456')).toEqual(-123.456);
expect(numeralWrapper.parseMoney('-123k')).toEqual(-123000);
expect(numeralWrapper.parseMoney('-123.456k')).toEqual(-123456);
expect(numeralWrapper.parseMoney('-123m')).toEqual(-123000000);
expect(numeralWrapper.parseMoney('-123.456m')).toEqual(-123456000);
expect(numeralWrapper.parseMoney('-123b')).toEqual(-123000000000);
expect(numeralWrapper.parseMoney('-123.456b')).toEqual(-123456000000);
expect(numeralWrapper.parseMoney('-123t')).toEqual(-123000000000000);
expect(numeralWrapper.parseMoney('-123.456t')).toEqual(-123456000000000);
// Custom formats, parseFloat has some rounding issues
expect(numeralWrapper.parseMoney('-123q')).toBeCloseTo(-123000000000000000);
expect(numeralWrapper.parseMoney('-123.456q')).toBeCloseTo(-123456000000000000);
expect(numeralWrapper.parseMoney('-123Q')).toBeCloseTo(-123000000000000000000);
expect(numeralWrapper.parseMoney('-123.456Q')).toBeCloseTo(-123456000000000000000);
expect(numeralWrapper.parseMoney('-123s')).toBeCloseTo(-123000000000000000000000);
expect(numeralWrapper.parseMoney('-123.456s')).toBeCloseTo(-123456000000000000000000);
expect(numeralWrapper.parseMoney('-123S')).toBeCloseTo(-123000000000000000000000000);
expect(numeralWrapper.parseMoney('-123.456S')).toBeCloseTo(-123456000000000000000000000);
// Larger numbers fail the test due to rounding issues
//expect(numeralWrapper.parseMoney('-123o')).toBeCloseTo(-123000000000000000000000000000);
//expect(numeralWrapper.parseMoney('-123.456o')).toBeCloseTo(-123456000000000000000000000000);
//expect(numeralWrapper.parseMoney('-123n')).toBeCloseTo(-123000000000000000000000000000000);
//expect(numeralWrapper.parseMoney('-123.456n')).toBeCloseTo(-123456000000000000000000000000000);
});
});
describe('Finding the number furthest away from 0', () => {
test('should work if all numbers are equal', () => {
expect(numeralWrapper.largestAbsoluteNumber(0, 0, 0)).toEqual(0);
expect(numeralWrapper.largestAbsoluteNumber(1, 1, 1)).toEqual(1);
expect(numeralWrapper.largestAbsoluteNumber(123, 123, 123)).toEqual(123);
expect(numeralWrapper.largestAbsoluteNumber(-1, -1, -1)).toEqual(-1);
expect(numeralWrapper.largestAbsoluteNumber(-123, -123, -123)).toEqual(-123);
});
test('should work for different positive numbers, and for the largest number in each spot', () => {
expect(numeralWrapper.largestAbsoluteNumber(1, 2, 3)).toEqual(3);
expect(numeralWrapper.largestAbsoluteNumber(456, 789, 123)).toEqual(789);
expect(numeralWrapper.largestAbsoluteNumber(789123, 123456, 456789)).toEqual(789123);
});
test('should work for different negative numbers, and for the smallest number in each spot', () => {
expect(numeralWrapper.largestAbsoluteNumber(-1, -2, -3)).toEqual(-3);
expect(numeralWrapper.largestAbsoluteNumber(-456, -789, -123)).toEqual(-789);
expect(numeralWrapper.largestAbsoluteNumber(-789123, -123456, -456789)).toEqual(-789123);
});
test('should work for combined positive and negative numbers', () => {
expect(numeralWrapper.largestAbsoluteNumber(1, -2, 3)).toEqual(3);
expect(numeralWrapper.largestAbsoluteNumber(-456, 789, -123)).toEqual(789);
expect(numeralWrapper.largestAbsoluteNumber(789123, -123456, -456789)).toEqual(789123);
});
test('Should return 0 for invalid input', () => {
expect(numeralWrapper.largestAbsoluteNumber('abc', undefined, null)).toEqual(0);
});
});