This commit is contained in:
Jeffrey A. Robinson 2022-01-14 17:14:41 -08:00
commit 0002daa3e9
37 changed files with 689 additions and 524 deletions

8
.github/ISSUE_TEMPLATE vendored Normal file

@ -0,0 +1,8 @@
# include (where relevant)
You can delete this section
- [ ] Save file
- [ ] Minimal scripts to reproduce the issue
- [ ] Steps to reproduce
- [ ] Version of the game, e.g. Bitburner v1.3.0 (216bf616)

9
.github/PULL_REQUEST_TEMPLATE vendored Normal file

@ -0,0 +1,9 @@
# Documentation
- DO NOT CHANGE any markdown/\*.md, these files are autogenerated from NetscriptDefinitions.d.ts and will be overwritten
- DO NOT re-generate the documentation, makes it harder to review.
# Bug fix
- Include how it was tested
- Include screenshot / gif (if possible)

34
dist/vendor.bundle.js vendored

File diff suppressed because one or more lines are too long

@ -3,6 +3,9 @@ const greenworks = require("./greenworks");
const log = require("electron-log"); const log = require("electron-log");
function enableAchievementsInterval(window) { function enableAchievementsInterval(window) {
// If the Steam API could not be initialized on game start, we'll abort this.
if (global.greenworksError) return;
// This is backward but the game fills in an array called `document.achievements` and we retrieve it from // This is backward but the game fills in an array called `document.achievements` and we retrieve it from
// here. Hey if it works it works. // here. Hey if it works it works.
const steamAchievements = greenworks.getAchievementNames(); const steamAchievements = greenworks.getAchievementNames();

@ -16,10 +16,18 @@ process.on('uncaughtException', function () {
process.exit(1); process.exit(1);
}); });
if (greenworks.init()) { // We want to fail gracefully if we cannot connect to Steam
log.info("Steam API has been initialized."); try {
} else { if (greenworks.init()) {
log.warn("Steam API has failed to initialize."); log.info("Steam API has been initialized.");
} else {
const error = "Steam API has failed to initialize.";
log.warn(error);
global.greenworksError = error;
}
} catch (ex) {
log.warn(ex.message);
global.greenworksError = ex.message;
} }
function setStopProcessHandler(app, window, enabled) { function setStopProcessHandler(app, window, enabled) {
@ -113,4 +121,13 @@ app.whenReady().then(async () => {
} else { } else {
startWindow(process.argv.includes("--no-scripts")); startWindow(process.argv.includes("--no-scripts"));
} }
if (global.greenworksError) {
dialog.showMessageBox({
title: 'Bitburner',
message: 'Could not connect to Steam',
detail: `${global.greenworksError}\n\nYou won't be able to receive achievements until this is resolved and you restart the game.`,
type: 'warning', buttons: ['OK']
});
}
}); });

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

@ -0,0 +1,23 @@
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
[Home](./index.md) &gt; [bitburner](./bitburner.md) &gt; [UserInterface](./bitburner.userinterface.md) &gt; [getStyles](./bitburner.userinterface.getstyles.md)
## UserInterface.getStyles() method
Get the current styles
<b>Signature:</b>
```typescript
getStyles(): IStyleSettings;
```
<b>Returns:</b>
IStyleSettings
An object containing the player's styles
## Remarks
RAM cost: cost: 0 GB

@ -16,7 +16,10 @@ interface UserInterface
| Method | Description | | Method | Description |
| --- | --- | | --- | --- |
| [getStyles()](./bitburner.userinterface.getstyles.md) | Get the current styles |
| [getTheme()](./bitburner.userinterface.gettheme.md) | Get the current theme | | [getTheme()](./bitburner.userinterface.gettheme.md) | Get the current theme |
| [resetStyles()](./bitburner.userinterface.resetstyles.md) | Resets the player's styles to the default values |
| [resetTheme()](./bitburner.userinterface.resettheme.md) | Resets the player's theme to the default values | | [resetTheme()](./bitburner.userinterface.resettheme.md) | Resets the player's theme to the default values |
| [setStyles(newStyles)](./bitburner.userinterface.setstyles.md) | Sets the current styles |
| [setTheme(newTheme)](./bitburner.userinterface.settheme.md) | Sets the current theme | | [setTheme(newTheme)](./bitburner.userinterface.settheme.md) | Sets the current theme |

@ -0,0 +1,21 @@
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
[Home](./index.md) &gt; [bitburner](./bitburner.md) &gt; [UserInterface](./bitburner.userinterface.md) &gt; [resetStyles](./bitburner.userinterface.resetstyles.md)
## UserInterface.resetStyles() method
Resets the player's styles to the default values
<b>Signature:</b>
```typescript
resetStyles(): void;
```
<b>Returns:</b>
void
## Remarks
RAM cost: cost: 0 GB

@ -0,0 +1,38 @@
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
[Home](./index.md) &gt; [bitburner](./bitburner.md) &gt; [UserInterface](./bitburner.userinterface.md) &gt; [setStyles](./bitburner.userinterface.setstyles.md)
## UserInterface.setStyles() method
Sets the current styles
<b>Signature:</b>
```typescript
setStyles(newStyles: IStyleSettings): void;
```
## Parameters
| Parameter | Type | Description |
| --- | --- | --- |
| newStyles | IStyleSettings | |
<b>Returns:</b>
void
## Remarks
RAM cost: cost: 0 GB
## Example
Usage example (NS2)
```ts
const styles = ns.ui.getStyles();
styles.fontFamily = 'Comic Sans Ms';
ns.ui.setStyles(styles);
```

@ -2582,7 +2582,6 @@ function installAugmentations(): boolean {
augmentationList += aug.name + level + "<br>"; augmentationList += aug.name + level + "<br>";
} }
Player.queuedAugmentations = []; Player.queuedAugmentations = [];
dialogBoxCreate( dialogBoxCreate(
"You slowly drift to sleep as scientists put you under in order " + "You slowly drift to sleep as scientists put you under in order " +
"to install the following Augmentations:<br>" + "to install the following Augmentations:<br>" +

@ -12,7 +12,7 @@ export const ConsoleHelpText: {
} = { } = {
helpList: [ helpList: [
"Use 'help [command]' to get more information about a particular Bladeburner console command.", "Use 'help [command]' to get more information about a particular Bladeburner console command.",
"", " ",
" automate [var] [val] [hi/low] Configure simple automation for Bladeburner tasks", " automate [var] [val] [hi/low] Configure simple automation for Bladeburner tasks",
" clear/cls Clear the console", " clear/cls Clear the console",
" help [cmd] Display this help text, or help text for a specific command", " help [cmd] Display this help text, or help text for a specific command",
@ -20,96 +20,103 @@ export const ConsoleHelpText: {
" skill [action] [name] Level or display info about your Bladeburner skills", " skill [action] [name] Level or display info about your Bladeburner skills",
" start [type] [name] Start a Bladeburner action/task", " start [type] [name] Start a Bladeburner action/task",
" stop Stops your current Bladeburner action/task", " stop Stops your current Bladeburner action/task",
" ",
], ],
automate: [ automate: [
"automate [var] [val] [hi/low]", "Usage: automate [var] [val] [hi/low]",
"", " ",
"A simple way to automate your Bladeburner actions. This console command can be used " + "A simple way to automate your Bladeburner actions. This console command can be used " +
"to automatically start an action when your stamina rises above a certain threshold, and " + "to automatically start an action when your stamina rises above a certain threshold, and " +
"automatically switch to another action when your stamina drops below another threshold.", "automatically switch to another action when your stamina drops below another threshold.",
" ",
" automate status - Check the current status of your automation and get a brief description of what it'll do", " automate status - Check the current status of your automation and get a brief description of what it'll do",
" automate en - Enable the automation feature", " automate en - Enable the automation feature",
" automate dis - Disable the automation feature", " automate dis - Disable the automation feature",
"", " ",
"There are four properties that must be set for this automation to work properly. Here is how to set them:", "There are four properties that must be set for this automation to work properly. Here is how to set them:",
"", " ",
" automate stamina 100 high", " automate stamina 100 high",
" automate contract Tracking high", " automate contract Tracking high",
" automate stamina 50 low", " automate stamina 50 low",
" automate general 'Field Analysis' low", " automate general 'Field Analysis' low",
"", " ",
"Using the four console commands above will set the automation to perform Tracking contracts " + "Using the four console commands above will set the automation to perform Tracking contracts " +
"if your stamina is 100 or higher, and then switch to Field Analysis if your stamina drops below " + "if your stamina is 100 or higher, and then switch to Field Analysis if your stamina drops below " +
"50. Note that when setting the action, the name of the action is CASE-SENSITIVE. It must " + "50. Note that when setting the action, the name of the action is CASE-SENSITIVE. It must " +
"exactly match whatever the name is in the UI.", "exactly match whatever the name is in the UI.",
" ",
], ],
clear: ["clear", "", "Clears the console"], clear: ["Usage: clear", " ", "Clears the console", " "],
cls: ["cls", "", "Clears the console"], cls: ["Usage: cls", " ", "Clears the console", " "],
help: [ help: [
"help [command]", "Usage: help [command]",
"", " ",
"Running 'help' with no arguments displays the general help text, which lists all console commands " + "Running 'help' with no arguments displays the general help text, which lists all console commands " +
"and a brief description of what they do. A command can be specified to get more specific help text " + "and a brief description of what they do. A command can be specified to get more specific help text " +
"about that particular command. For example:", "about that particular command. For example:",
"", " ",
" help automate", " help automate",
"", " ",
"will display specific information about using the automate console command", "will display specific information about using the automate console command",
" ",
], ],
log: [ log: [
"log [en/dis] [type]", "Usage: log [en/dis] [type]",
"", " ",
"Enable or disable logging. By default, the results of completing actions such as contracts/operations are logged " + "Enable or disable logging. By default, the results of completing actions such as contracts/operations are logged " +
"in the console. There are also random events that are logged in the console as well. The five categories of " + "in the console. There are also random events that are logged in the console as well. The five categories of " +
"things that get logged are:", "things that get logged are:",
"", " ",
"[general, contracts, ops, blackops, events]", "[general, contracts, ops, blackops, events]",
"", " ",
"The logging for these categories can be enabled or disabled like so:", "The logging for these categories can be enabled or disabled like so:",
"", " ",
" log dis contracts - Disables logging that occurs when contracts are completed", " log dis contracts - Disables logging that occurs when contracts are completed",
" log en contracts - Enables logging that occurs when contracts are completed", " log en contracts - Enables logging that occurs when contracts are completed",
" log dis events - Disables logging for Bladeburner random events", " log dis events - Disables logging for Bladeburner random events",
"", " ",
"Logging can be universally enabled/disabled using the 'all' keyword:", "Logging can be universally enabled/disabled using the 'all' keyword:",
"", " ",
" log dis all", " log dis all",
" log en all", " log en all",
" ",
], ],
skill: [ skill: [
"skill [action] [name]", "Usage: skill [action] [name]",
"", " ",
"Level or display information about your skills.", "Level or display information about your skills.",
"", " ",
"To display information about all of your skills and your multipliers, use:", "To display information about all of your skills and your multipliers, use:",
"", " ",
" skill list", " skill list",
"", " ",
"To display information about a specific skill, specify the name of the skill afterwards. " + "To display information about a specific skill, specify the name of the skill afterwards. " +
"Note that the name of the skill is case-sensitive. Enter it exactly as seen in the UI. If " + "Note that the name of the skill is case-sensitive. Enter it exactly as seen in the UI. If " +
"the name of the skill has whitespace, enclose the name of the skill in double quotation marks:", "the name of the skill has whitespace, enclose the name of the skill in double quotation marks:",
"", " ",
" skill list Reaper", " skill list Reaper",
" skill list 'Digital Observer'", " skill list 'Digital Observer'",
"", " ",
"This console command can also be used to level up skills:", "This console command can also be used to level up skills:",
"", " ",
" skill level [skill name]", " skill level [skill name]",
" ",
], ],
start: [ start: [
"start [type] [name]", "Usage: start [type] [name]",
"", " ",
"Start an action. An action is specified by its type and its name. The " + "Start an action. An action is specified by its type and its name. The " +
"name is case-sensitive. It must appear exactly as it does in the UI. If " + "name is case-sensitive. It must appear exactly as it does in the UI. If " +
"the name of the action has whitespace, enclose it in double quotation marks. " + "the name of the action has whitespace, enclose it in double quotation marks. " +
"Valid action types include:", "Valid action types include:",
"", " ",
"[general, contract, op, blackop]", "[general, contract, op, blackop]",
"", " ",
"Examples:", "Examples:",
"", " ",
" start contract Tracking", " start contract Tracking",
" start op 'Undercover Operation'", " start op 'Undercover Operation'",
" ",
], ],
stop: ["stop", "", "Stop your current action and go idle."], stop: ["Usage: stop", " ", "Stop your current action and go idle.", " "],
}; };

@ -4,7 +4,6 @@ import { Console } from "./Console";
import { AllPages } from "./AllPages"; import { AllPages } from "./AllPages";
import { use } from "../../ui/Context"; import { use } from "../../ui/Context";
import Grid from "@mui/material/Grid";
import Box from "@mui/material/Box"; import Box from "@mui/material/Box";
export function BladeburnerRoot(): React.ReactElement { export function BladeburnerRoot(): React.ReactElement {
@ -24,14 +23,10 @@ export function BladeburnerRoot(): React.ReactElement {
if (bladeburner === null) return <></>; if (bladeburner === null) return <></>;
return ( return (
<Box display="flex" flexDirection="column"> <Box display="flex" flexDirection="column">
<Grid container> <Box sx={{ display: "grid", gridTemplateColumns: "4fr 8fr", p: 1 }}>
<Grid item xs={6}> <Stats bladeburner={bladeburner} player={player} router={router} />
<Stats bladeburner={bladeburner} player={player} router={router} /> <Console bladeburner={bladeburner} player={player} />
</Grid> </Box>
<Grid item xs={6}>
<Console bladeburner={bladeburner} player={player} />
</Grid>
</Grid>
<AllPages bladeburner={bladeburner} player={player} /> <AllPages bladeburner={bladeburner} player={player} />
</Box> </Box>

@ -23,7 +23,7 @@ const useStyles = makeStyles((theme: Theme) =>
width: "100%", width: "100%",
}, },
input: { input: {
backgroundColor: "#000", backgroundColor: theme.colors.backgroundsecondary,
}, },
nopadding: { nopadding: {
padding: theme.spacing(0), padding: theme.spacing(0),
@ -56,6 +56,7 @@ export function Console(props: IProps): React.ReactElement {
const classes = useStyles(); const classes = useStyles();
const [command, setCommand] = useState(""); const [command, setCommand] = useState("");
const setRerender = useState(false)[1]; const setRerender = useState(false)[1];
const consoleInput = useRef<HTMLInputElement>(null);
function handleCommandChange(event: React.ChangeEvent<HTMLInputElement>): void { function handleCommandChange(event: React.ChangeEvent<HTMLInputElement>): void {
setCommand(event.target.value); setCommand(event.target.value);
@ -131,15 +132,21 @@ export function Console(props: IProps): React.ReactElement {
} }
} }
function handleClick(): void {
if (!consoleInput.current) return;
consoleInput.current.focus();
}
return ( return (
<Paper> <Paper sx={{ p: 1 }}>
<Box sx={{ <Box sx={{
height: '60vh', height: '60vh',
paddingBottom: '8px', paddingBottom: '8px',
display: 'flex', display: 'flex',
alignItems: 'stretch', alignItems: 'stretch',
whiteSpace: 'pre-wrap', whiteSpace: 'pre-wrap',
}}> }}
onClick={handleClick}>
<Box> <Box>
<Logs entries={[...props.bladeburner.consoleLogs]} /> <Logs entries={[...props.bladeburner.consoleLogs]} />
</Box> </Box>
@ -149,6 +156,7 @@ export function Console(props: IProps): React.ReactElement {
autoFocus autoFocus
tabIndex={1} tabIndex={1}
type="text" type="text"
inputRef={consoleInput}
value={command} value={command}
onChange={handleCommandChange} onChange={handleCommandChange}
onKeyDown={handleKeyDown} onKeyDown={handleKeyDown}
@ -171,7 +179,7 @@ interface ILogProps {
entries: string[]; entries: string[];
} }
function Logs({entries}: ILogProps): React.ReactElement { function Logs({ entries }: ILogProps): React.ReactElement {
const scrollHook = useRef<HTMLUListElement>(null); const scrollHook = useRef<HTMLUListElement>(null);
// TODO: Text gets shifted up as new entries appear, if the user scrolled up it should attempt to keep the text focused // TODO: Text gets shifted up as new entries appear, if the user scrolled up it should attempt to keep the text focused
@ -182,7 +190,7 @@ function Logs({entries}: ILogProps): React.ReactElement {
useEffect(() => { useEffect(() => {
scrollToBottom(); scrollToBottom();
}, [entries]); }, [entries.length]);
return ( return (
<List sx={{ height: "100%", overflow: "auto", p: 1 }} ref={scrollHook}> <List sx={{ height: "100%", overflow: "auto", p: 1 }} ref={scrollHook}>

@ -3,7 +3,6 @@ import { formatNumber, convertTimeMsToTimeElapsedString } from "../../utils/Stri
import { BladeburnerConstants } from "../data/Constants"; import { BladeburnerConstants } from "../data/Constants";
import { IPlayer } from "../../PersonObjects/IPlayer"; import { IPlayer } from "../../PersonObjects/IPlayer";
import { Money } from "../../ui/React/Money"; import { Money } from "../../ui/React/Money";
import { StatsTable } from "../../ui/React/StatsTable";
import { numeralWrapper } from "../../ui/numeralFormat"; import { numeralWrapper } from "../../ui/numeralFormat";
import { Factions } from "../../Faction/Factions"; import { Factions } from "../../Faction/Factions";
import { IRouter } from "../../ui/Router"; import { IRouter } from "../../ui/Router";
@ -44,138 +43,142 @@ export function Stats(props: IProps): React.ReactElement {
} }
return ( return (
<Paper sx={{ p: 1 }}> <Paper sx={{ p: 1, overflowY: 'auto', overflowX: 'hidden', wordBreak: 'break-all' }}>
<Box display="flex"> <Box sx={{ display: 'flex', flexDirection: 'column', gap: 1, maxHeight: '60vh' }}>
<Tooltip title={<Typography>Your rank within the Bladeburner division.</Typography>}> <Box sx={{ alignSelf: 'flex-start', width: '100%' }}>
<Typography>Rank: {formatNumber(props.bladeburner.rank, 2)}</Typography> <Button onClick={() => setTravelOpen(true)} sx={{ width: '50%' }}>Travel</Button>
</Tooltip> <Tooltip title={!inFaction ? <Typography>Rank 25 required.</Typography> : ""}>
</Box> <span>
<br /> <Button disabled={!inFaction} onClick={openFaction} sx={{ width: '50%' }}>
<Box display="flex"> Faction
<Tooltip </Button>
title={ </span>
<Typography> </Tooltip>
Performing actions will use up your stamina. <TravelModal open={travelOpen} onClose={() => setTravelOpen(false)} bladeburner={props.bladeburner} />
<br /> </Box>
<br /> <Box display="flex">
Your max stamina is determined primarily by your agility stat. <Tooltip title={<Typography>Your rank within the Bladeburner division.</Typography>}>
<br /> <Typography>Rank: {formatNumber(props.bladeburner.rank, 2)}</Typography>
<br /> </Tooltip>
Your stamina gain rate is determined by both your agility and your max stamina. Higher max stamina leads </Box>
to a higher gain rate. <br />
<br /> <Box display="flex">
<br /> <Tooltip
Once your stamina falls below 50% of its max value, it begins to negatively affect the success rate of title={
your contracts/operations. This penalty is shown in the overview panel. If the penalty is 15%, then this
means your success rate would be multipled by 85% (100 - 15).
<br />
<br />
Your max stamina and stamina gain rate can also be increased by training, or through skills and
Augmentation upgrades.
</Typography>
}
>
<Typography>
Stamina: {formatNumber(props.bladeburner.stamina, 3)} / {formatNumber(props.bladeburner.maxStamina, 3)}
</Typography>
</Tooltip>
</Box>
<br />
<Typography>
Stamina Penalty: {formatNumber((1 - props.bladeburner.calculateStaminaPenalty()) * 100, 1)}%
</Typography>
<br />
<Typography>Team Size: {formatNumber(props.bladeburner.teamSize, 0)}</Typography>
<Typography>Team Members Lost: {formatNumber(props.bladeburner.teamLost, 0)}</Typography>
<br />
<Typography>Num Times Hospitalized: {props.bladeburner.numHosp}</Typography>
<Typography>
Money Lost From Hospitalizations: <Money money={props.bladeburner.moneyLost} />
</Typography>
<br />
<Typography>Current City: {props.bladeburner.city}</Typography>
<Box display="flex">
<Tooltip
title={
<Typography>
This is your Bladeburner division's estimate of how many Synthoids exist in your current city. An accurate
population count increases success rate estimates.
</Typography>
}
>
<Typography>
Est. Synthoid Population: {numeralWrapper.formatPopulation(props.bladeburner.getCurrentCity().popEst)}
</Typography>
</Tooltip>
</Box>
<br />
<Box display="flex">
<Tooltip
title={
<Typography>
This is your Bladeburner divison's estimate of how many Synthoid communities exist in your current city.
</Typography>
}
>
<Typography>Synthoid Communities: {formatNumber(props.bladeburner.getCurrentCity().comms, 0)}</Typography>
</Tooltip>
</Box>
<br />
<Box display="flex">
<Tooltip
title={
<Typography>
The city's chaos level due to tensions and conflicts between humans and Synthoids. Having too high of a
chaos level can make contracts and operations harder.
</Typography>
}
>
<Typography>City Chaos: {formatNumber(props.bladeburner.getCurrentCity().chaos)}</Typography>
</Tooltip>
</Box>
<br />
{(props.bladeburner.storedCycles / BladeburnerConstants.CyclesPerSecond) * 1000 > 15000 && (
<>
<Box display="flex">
<Tooltip
title={
<Typography>
You gain bonus time while offline or when the game is inactive (e.g. when the tab is throttled by
browser). Bonus time makes the Bladeburner mechanic progress faster, up to 5x the normal speed.
</Typography>
}
>
<Typography> <Typography>
Bonus time:{" "} Performing actions will use up your stamina.
{convertTimeMsToTimeElapsedString( <br />
(props.bladeburner.storedCycles / BladeburnerConstants.CyclesPerSecond) * 1000, <br />
)} Your max stamina is determined primarily by your agility stat.
<br />
<br />
Your stamina gain rate is determined by both your agility and your max stamina. Higher max stamina leads
to a higher gain rate.
<br />
<br />
Once your stamina falls below 50% of its max value, it begins to negatively affect the success rate of
your contracts/operations. This penalty is shown in the overview panel. If the penalty is 15%, then this
means your success rate would be multipled by 85% (100 - 15).
<br />
<br />
Your max stamina and stamina gain rate can also be increased by training, or through skills and
Augmentation upgrades.
</Typography> </Typography>
</Tooltip> }
</Box> >
<Typography>
Stamina: {formatNumber(props.bladeburner.stamina, 3)} / {formatNumber(props.bladeburner.maxStamina, 3)}
</Typography>
</Tooltip>
</Box>
<br />
<Typography>
Stamina Penalty: {formatNumber((1 - props.bladeburner.calculateStaminaPenalty()) * 100, 1)}%
</Typography>
<br />
<Typography>Team Size: {formatNumber(props.bladeburner.teamSize, 0)}</Typography>
<Typography>Team Members Lost: {formatNumber(props.bladeburner.teamLost, 0)}</Typography>
<br />
<Typography>Num Times Hospitalized: {props.bladeburner.numHosp}</Typography>
<Typography>
Money Lost From Hospitalizations: <Money money={props.bladeburner.moneyLost} />
</Typography>
<br />
<Typography>Current City: {props.bladeburner.city}</Typography>
<Box display="flex">
<Tooltip
title={
<Typography>
This is your Bladeburner division's estimate of how many Synthoids exist in your current city. An accurate
population count increases success rate estimates.
</Typography>
}
>
<Typography>
Est. Synthoid Population: {numeralWrapper.formatPopulation(props.bladeburner.getCurrentCity().popEst)}
</Typography>
</Tooltip>
</Box>
<br />
<Box display="flex">
<Tooltip
title={
<Typography>
This is your Bladeburner divison's estimate of how many Synthoid communities exist in your current city.
</Typography>
}
>
<Typography>Synthoid Communities: {formatNumber(props.bladeburner.getCurrentCity().comms, 0)}</Typography>
</Tooltip>
</Box>
<br />
<Box display="flex">
<Tooltip
title={
<Typography>
The city's chaos level due to tensions and conflicts between humans and Synthoids. Having too high of a
chaos level can make contracts and operations harder.
</Typography>
}
>
<Typography>City Chaos: {formatNumber(props.bladeburner.getCurrentCity().chaos)}</Typography>
</Tooltip>
</Box>
<br />
{(props.bladeburner.storedCycles / BladeburnerConstants.CyclesPerSecond) * 1000 > 15000 && (
<>
<Box display="flex">
<Tooltip
title={
<Typography>
You gain bonus time while offline or when the game is inactive (e.g. when the tab is throttled by
browser). Bonus time makes the Bladeburner mechanic progress faster, up to 5x the normal speed.
</Typography>
}
>
<Typography>
Bonus time:{" "}
{convertTimeMsToTimeElapsedString(
(props.bladeburner.storedCycles / BladeburnerConstants.CyclesPerSecond) * 1000,
)}
</Typography>
</Tooltip>
</Box>
<br />
</>
)}
<Typography>Skill Points: {formatNumber(props.bladeburner.skillPoints, 0)}</Typography>
<br />
<Typography>
Aug. Success Chance mult: {formatNumber(props.player.bladeburner_success_chance_mult * 100, 1)}%
<br /> <br />
</> Aug. Max Stamina mult: {formatNumber(props.player.bladeburner_max_stamina_mult * 100, 1)}%
)} <br />
<Typography>Skill Points: {formatNumber(props.bladeburner.skillPoints, 0)}</Typography> Aug. Stamina Gain mult: {formatNumber(props.player.bladeburner_stamina_gain_mult * 100, 1)}%
<br /> <br />
<StatsTable Aug. Field Analysis mult: {formatNumber(props.player.bladeburner_analysis_mult * 100, 1)}%
rows={[ </Typography>
["Aug. Success Chance mult: ", formatNumber(props.player.bladeburner_success_chance_mult * 100, 1) + "%"], </Box>
["Aug. Max Stamina mult: ", formatNumber(props.player.bladeburner_max_stamina_mult * 100, 1) + "%"],
["Aug. Stamina Gain mult: ", formatNumber(props.player.bladeburner_stamina_gain_mult * 100, 1) + "%"],
["Aug. Field Analysis mult: ", formatNumber(props.player.bladeburner_analysis_mult * 100, 1) + "%"],
]}
/>
<br />
<Button onClick={() => setTravelOpen(true)}>Travel</Button>
<Tooltip title={!inFaction ? <Typography>Rank 25 required.</Typography> : ""}>
<span>
<Button disabled={!inFaction} onClick={openFaction}>
Faction
</Button>
</span>
</Tooltip>
<TravelModal open={travelOpen} onClose={() => setTravelOpen(false)} bladeburner={props.bladeburner} />
</Paper> </Paper>
); );
} }

@ -15,6 +15,7 @@ import { hasAugmentationPrereqs } from "../FactionHelpers";
import { use } from "../../ui/Context"; import { use } from "../../ui/Context";
import { Reputation } from "../../ui/React/Reputation"; import { Reputation } from "../../ui/React/Reputation";
import { Favor } from "../../ui/React/Favor"; import { Favor } from "../../ui/React/Favor";
import { numeralWrapper } from "../../ui/numeralFormat";
import Box from "@mui/material/Box"; import Box from "@mui/material/Box";
import Button from "@mui/material/Button"; import Button from "@mui/material/Button";
@ -203,7 +204,7 @@ export function AugmentationsPage(props: IProps): React.ReactElement {
</Typography> </Typography>
} }
> >
<Typography>Price multiplier: x {mult.toFixed(3)}</Typography> <Typography>Price multiplier: x {numeralWrapper.formatMultiplier(mult)}</Typography>
</Tooltip> </Tooltip>
</Box> </Box>
<Button onClick={() => switchSortOrder(PurchaseAugmentationsOrderSetting.Cost)}>Sort by Cost</Button> <Button onClick={() => switchSortOrder(PurchaseAugmentationsOrderSetting.Cost)}>Sort by Cost</Button>

@ -364,6 +364,9 @@ export const RamCosts: IMap<any> = {
getTheme: 0, getTheme: 0,
setTheme: 0, setTheme: 0,
resetTheme: 0, resetTheme: 0,
getStyles: 0,
setStyles: 0,
resetStyles: 0,
}, },
heart: { heart: {

@ -172,7 +172,7 @@ export function NetscriptFunctions(workerScript: WorkerScript): NS {
throw makeRuntimeRejectMsg( throw makeRuntimeRejectMsg(
workerScript, workerScript,
`Invalid scriptArgs argument passed into getRunningScript() from ${callingFnName}(). ` + `Invalid scriptArgs argument passed into getRunningScript() from ${callingFnName}(). ` +
`This is probably a bug. Please report to game developer`, `This is probably a bug. Please report to game developer`,
); );
} }
@ -432,8 +432,7 @@ export function NetscriptFunctions(workerScript: WorkerScript): NS {
throw makeRuntimeErrorMsg(funcName, `${argName} should be a string`); throw makeRuntimeErrorMsg(funcName, `${argName} should be a string`);
}, },
number: (funcName: string, argName: string, v: any): number => { number: (funcName: string, argName: string, v: any): number => {
if (!isNaN(v)) if (!isNaN(v)) {
{
if (typeof v === "number") return v; if (typeof v === "number") return v;
if (!isNaN(parseFloat(v))) return parseFloat(v); if (!isNaN(parseFloat(v))) return parseFloat(v);
} }
@ -700,8 +699,7 @@ export function NetscriptFunctions(workerScript: WorkerScript): NS {
workerScript.log( workerScript.log(
"weaken", "weaken",
() => () =>
`'${server.hostname}' security level weakened to ${ `'${server.hostname}' security level weakened to ${server.hackDifficulty
server.hackDifficulty
}. Gained ${numeralWrapper.formatExp(expGain)} hacking exp (t=${numeralWrapper.formatThreads(threads)})`, }. Gained ${numeralWrapper.formatExp(expGain)} hacking exp (t=${numeralWrapper.formatThreads(threads)})`,
); );
workerScript.scriptRef.onlineExpGained += expGain; workerScript.scriptRef.onlineExpGained += expGain;

@ -53,23 +53,22 @@ export function NetscriptCodingContract(
const serv = helper.getServer(hostname, "codingcontract.attempt"); const serv = helper.getServer(hostname, "codingcontract.attempt");
if (contract.isSolution(answer)) { if (contract.isSolution(answer)) {
const reward = player.gainCodingContractReward(creward, contract.getDifficulty()); const reward = player.gainCodingContractReward(creward, contract.getDifficulty());
workerScript.log("attempt", () => `Successfully completed Coding Contract '${filename}'. Reward: ${reward}`); workerScript.log("codingcontract.attempt", () => `Successfully completed Coding Contract '${filename}'. Reward: ${reward}`);
serv.removeContract(filename); serv.removeContract(filename);
return returnReward ? reward : true; return returnReward ? reward : true;
} else { } else {
++contract.tries; ++contract.tries;
if (contract.tries >= contract.getMaxNumTries()) { if (contract.tries >= contract.getMaxNumTries()) {
workerScript.log( workerScript.log(
"attempt", "codingcontract.attempt",
() => `Coding Contract attempt '${filename}' failed. Contract is now self-destructing`, () => `Coding Contract attempt '${filename}' failed. Contract is now self-destructing`,
); );
serv.removeContract(filename); serv.removeContract(filename);
} else { } else {
workerScript.log( workerScript.log(
"attempt", "codingcontract.attempt",
() => () =>
`Coding Contract attempt '${filename}' failed. ${ `Coding Contract attempt '${filename}' failed. ${contract.getMaxNumTries() - contract.tries
contract.getMaxNumTries() - contract.tries
} attempts remaining.`, } attempts remaining.`,
); );
} }

@ -111,7 +111,7 @@ export function NetscriptHacknet(player: IPlayer, workerScript: WorkerScript, he
} }
const node = getHacknetNode(i, "upgradeCache"); const node = getHacknetNode(i, "upgradeCache");
if (!(node instanceof HacknetServer)) { if (!(node instanceof HacknetServer)) {
workerScript.log("upgradeCache", () => "Can only be called on hacknet servers"); workerScript.log("hacknet.upgradeCache", () => "Can only be called on hacknet servers");
return false; return false;
} }
const res = purchaseCacheUpgrade(player, node, n); const res = purchaseCacheUpgrade(player, node, n);
@ -138,7 +138,7 @@ export function NetscriptHacknet(player: IPlayer, workerScript: WorkerScript, he
} }
const node = getHacknetNode(i, "upgradeCache"); const node = getHacknetNode(i, "upgradeCache");
if (!(node instanceof HacknetServer)) { if (!(node instanceof HacknetServer)) {
workerScript.log("getCacheUpgradeCost", () => "Can only be called on hacknet servers"); workerScript.log("hacknet.getCacheUpgradeCost", () => "Can only be called on hacknet servers");
return -1; return -1;
} }
return node.calculateCacheUpgradeCost(n); return node.calculateCacheUpgradeCost(n);

@ -2,10 +2,11 @@ import { INetscriptHelper } from "./INetscriptHelper";
import { WorkerScript } from "../Netscript/WorkerScript"; import { WorkerScript } from "../Netscript/WorkerScript";
import { IPlayer } from "../PersonObjects/IPlayer"; import { IPlayer } from "../PersonObjects/IPlayer";
import { getRamCost } from "../Netscript/RamCostGenerator"; import { getRamCost } from "../Netscript/RamCostGenerator";
import { UserInterface as IUserInterface, UserInterfaceTheme } from "../ScriptEditor/NetscriptDefinitions"; import { IStyleSettings, UserInterface as IUserInterface, UserInterfaceTheme } from "../ScriptEditor/NetscriptDefinitions";
import { Settings } from "../Settings/Settings"; import { Settings } from "../Settings/Settings";
import { ThemeEvents } from "../ui/React/Theme"; import { ThemeEvents } from "../ui/React/Theme";
import { defaultTheme } from "../Settings/Themes"; import { defaultTheme } from "../Settings/Themes";
import { defaultStyles } from "../Settings/Styles";
export function NetscriptUserInterface( export function NetscriptUserInterface(
player: IPlayer, player: IPlayer,
@ -18,6 +19,11 @@ export function NetscriptUserInterface(
return { ...Settings.theme }; return { ...Settings.theme };
}, },
getStyles: function (): IStyleSettings {
helper.updateDynamicRam("getStyles", getRamCost(player, "ui", "getStyles"));
return { ...Settings.styles };
},
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]{3}){1,2})$/;
@ -43,11 +49,41 @@ export function NetscriptUserInterface(
} }
}, },
setStyles: function (newStyles: IStyleSettings): void {
helper.updateDynamicRam("setStyles", getRamCost(player, "ui", "setStyles"));
const currentStyles = {...Settings.styles}
const errors: string[] = [];
for (const key of Object.keys(newStyles)) {
if (!((currentStyles as any)[key])) {
// Invalid key
errors.push(`Invalid key "${key}"`);
} else {
(currentStyles as any)[key] = (newStyles as any)[key];
}
}
if (errors.length === 0) {
Object.assign(Settings.styles, currentStyles);
ThemeEvents.emit();
workerScript.log("ui.setStyles", () => `Successfully set styles`);
} else {
workerScript.log("ui.setStyles", () => `Failed to set styles. Errors: ${errors.join(', ')}`);
}
},
resetTheme: function (): void { resetTheme: function (): void {
helper.updateDynamicRam("resetTheme", getRamCost(player, "ui", "resetTheme")); helper.updateDynamicRam("resetTheme", getRamCost(player, "ui", "resetTheme"));
Settings.theme = defaultTheme; Settings.theme = { ...defaultTheme };
ThemeEvents.emit(); ThemeEvents.emit();
workerScript.log("ui.resetTheme", () => `Reinitialized theme to default`); workerScript.log("ui.resetTheme", () => `Reinitialized theme to default`);
}, },
resetStyles: function (): void {
helper.updateDynamicRam("resetStyles", getRamCost(player, "ui", "resetStyles"));
Settings.styles = { ...defaultStyles };
ThemeEvents.emit();
workerScript.log("ui.resetStyles", () => `Reinitialized styles to default`);
}
} }
} }

@ -946,11 +946,6 @@ export function workForFaction(this: IPlayer, numCycles: number): boolean {
default: default:
break; break;
} }
let favorMult = 1 + faction.favor / 100;
if (isNaN(favorMult)) {
favorMult = 1;
}
this.workRepGainRate *= favorMult;
this.workRepGainRate *= BitNodeMultipliers.FactionWorkRepGain; this.workRepGainRate *= BitNodeMultipliers.FactionWorkRepGain;
//Cap the number of cycles being processed to whatever would put you at limit (20 hours) //Cap the number of cycles being processed to whatever would put you at limit (20 hours)

@ -161,8 +161,6 @@ export function prestigeAugmentation(): void {
resetPidCounter(); resetPidCounter();
ProgramsSeen.splice(0, ProgramsSeen.length); ProgramsSeen.splice(0, ProgramsSeen.length);
InvitationsSeen.splice(0, InvitationsSeen.length); InvitationsSeen.splice(0, InvitationsSeen.length);
Router.clearHistory();
} }
// Prestige by destroying Bit Node and gaining a Source File // Prestige by destroying Bit Node and gaining a Source File
@ -308,7 +306,5 @@ export function prestigeSourceFile(flume: boolean): void {
// Gain int exp // Gain int exp
if (SourceFileFlags[5] !== 0 && !flume) Player.gainIntelligenceExp(300); if (SourceFileFlags[5] !== 0 && !flume) Player.gainIntelligenceExp(300);
Router.clearHistory();
resetPidCounter(); resetPidCounter();
} }

@ -118,7 +118,7 @@ export class Script {
*/ */
saveScript(player: IPlayer, filename: string, code: string, hostname: string, otherScripts: Script[]): void { saveScript(player: IPlayer, filename: string, code: string, hostname: string, otherScripts: Script[]): void {
// Update code and filename // Update code and filename
this.code = code.replace(/^\s+|\s+$/g, ""); this.code = Script.formatCode(this.code);
this.filename = filename; this.filename = filename;
this.server = hostname; this.server = hostname;
@ -158,6 +158,15 @@ export class Script {
s.rehash(); s.rehash();
return s; return s;
} }
/**
* Formats code: Removes the starting & trailing whitespace
* @param {string} code - The code to format
* @returns The formatted code
*/
static formatCode(code: string): string {
return code.replace(/^\s+|\s+$/g, "");
}
} }
Reviver.constructors.Script = Script; Reviver.constructors.Script = Script;

@ -119,7 +119,7 @@ export interface IPort {
/** add data to port if not full. /** add data to port if not full.
* @returns true if added and false if full and not added */ * @returns true if added and false if full and not added */
tryWrite: (value: any) => boolean; tryWrite: (value: any) => boolean;
/** reads and removes first element from port /** reads and removes first element from port
* if no data in port returns "NULL PORT DATA" * if no data in port returns "NULL PORT DATA"
*/ */
read: () => any; read: () => any;
@ -3880,6 +3880,36 @@ interface UserInterface {
* RAM cost: cost: 0 GB * RAM cost: cost: 0 GB
*/ */
resetTheme(): void; resetTheme(): void;
/**
* Get the current styles
* @remarks
* RAM cost: cost: 0 GB
*
* @returns An object containing the player's styles
*/
getStyles(): IStyleSettings;
/**
* Sets the current styles
* @remarks
* RAM cost: cost: 0 GB
* @example
* Usage example (NS2)
* ```ts
* const styles = ns.ui.getStyles();
* styles.fontFamily = 'Comic Sans Ms';
* ns.ui.setStyles(styles);
* ```
*/
setStyles(newStyles: IStyleSettings): void;
/**
* Resets the player's styles to the default values
* @remarks
* RAM cost: cost: 0 GB
*/
resetStyles(): void;
} }
/** /**
@ -6354,3 +6384,12 @@ interface UserInterfaceTheme {
backgroundsecondary: string; backgroundsecondary: string;
button: string; button: string;
} }
/**
* Interface Styles
* @internal
*/
interface IStyleSettings {
fontFamily: string;
lineHeight: number;
}

@ -686,7 +686,9 @@ export function Root(props: IProps): React.ReactElement {
const serverScript = server.scripts.find((s) => s.filename === openScript.fileName); const serverScript = server.scripts.find((s) => s.filename === openScript.fileName);
if (serverScript === undefined) return " *"; if (serverScript === undefined) return " *";
return serverScript.code !== openScript.code ? " *" : ""; // The server code is stored with its starting & trailing whitespace removed
const openScriptFormatted = Script.formatCode(openScript.code);
return serverScript.code !== openScriptFormatted ? " *" : "";
} }
// Toolbars are roughly 112px: // Toolbars are roughly 112px:
@ -846,7 +848,7 @@ export function Root(props: IProps): React.ReactElement {
<span style={{ color: Settings.theme.primary, fontSize: "20px", textAlign: "center" }}> <span style={{ color: Settings.theme.primary, fontSize: "20px", textAlign: "center" }}>
<Typography variant="h4">No open files</Typography> <Typography variant="h4">No open files</Typography>
<Typography variant="h5"> <Typography variant="h5">
Use `nano FILENAME` in Use <code>nano FILENAME</code> in
<br /> <br />
the terminal to open files the terminal to open files
</Typography> </Typography>

@ -1,9 +1,10 @@
import { ISelfInitializer, ISelfLoading } from "../types"; import { ISelfInitializer, ISelfLoading } from "../types";
import { OwnedAugmentationsOrderSetting, PurchaseAugmentationsOrderSetting } from "./SettingEnums"; import { OwnedAugmentationsOrderSetting, PurchaseAugmentationsOrderSetting } from "./SettingEnums";
import { defaultTheme, ITheme } from "./Themes"; import { defaultTheme, ITheme } from "./Themes";
import { defaultStyles, IStyleSettings } from "./Styles"; import { defaultStyles } from "./Styles";
import { WordWrapOptions } from "../ScriptEditor/ui/Options"; import { WordWrapOptions } from "../ScriptEditor/ui/Options";
import { OverviewSettings } from "../ui/React/Overview"; import { OverviewSettings } from "../ui/React/Overview";
import { IStyleSettings } from "../ScriptEditor/NetscriptDefinitions";
/** /**
* Represents the default settings the player could customize. * Represents the default settings the player could customize.

@ -1,9 +1,4 @@
import React from "react"; import { IStyleSettings } from "../ScriptEditor/NetscriptDefinitions";
export interface IStyleSettings {
fontFamily: React.CSSProperties["fontFamily"];
lineHeight: React.CSSProperties["lineHeight"];
}
export const defaultStyles: IStyleSettings = { export const defaultStyles: IStyleSettings = {
lineHeight: 1.5, lineHeight: 1.5,

@ -3,73 +3,74 @@ import { IMap } from "../types";
export const TerminalHelpText: string[] = [ export const TerminalHelpText: string[] = [
"Type 'help name' to learn more about the command ", "Type 'help name' to learn more about the command ",
"", " ",
'alias [-g] [name="value"] Create or display Terminal aliases', ' alias [-g] [name="value"] Create or display Terminal aliases',
"analyze Get information about the current machine ", " analyze Get information about the current machine ",
"backdoor Install a backdoor on the current machine ", " backdoor Install a backdoor on the current machine ",
"buy [-l/program] Purchase a program through the Dark Web", " buy [-l/-a/program] Purchase a program through the Dark Web",
"cat [file] Display a .msg, .lit, or .txt file", " cat [file] Display a .msg, .lit, or .txt file",
"cd [dir] Change to a new directory", " cd [dir] Change to a new directory",
"check [script] [args...] Print a script's logs to Terminal", " check [script] [args...] Print a script's logs to Terminal",
"clear Clear all text on the terminal ", " clear Clear all text on the terminal ",
"cls See 'clear' command ", " cls See 'clear' command ",
"connect [hostname] Connects to a remote server", " connect [hostname] Connects to a remote server",
"cp [src] [dst] Copy a file", " cp [src] [dst] Copy a file",
"download [script/text file] Downloads scripts or text files to your computer", " download [script/text file] Downloads scripts or text files to your computer",
"expr [math expression] Evaluate a mathematical expression", " expr [math expression] Evaluate a mathematical expression",
"free Check the machine's memory (RAM) usage", " free Check the machine's memory (RAM) usage",
"grow Spoof money in a servers bank account, increasing the amount available.", " grow Spoof money in a servers bank account, increasing the amount available.",
"hack Hack the current machine", " hack Hack the current machine",
"help [command] Display this help text, or the help text for a command", " help [command] Display this help text, or the help text for a command",
"home Connect to home computer", " home Connect to home computer",
"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",
"nano [file ...] Text editor - Open up and edit one or more scripts or text files", " nano [file ...] Text editor - Open up and edit one or more scripts or text files",
"ps Display all scripts that are currently running", " ps Display all scripts that are currently running",
"rm [file] Delete a file from the server", " rm [file] Delete a file from the server",
"run [name] [-t n] [--tail] [args...] Execute a program or script", " run [name] [-t n] [--tail] [args...] Execute a program or script",
"scan Prints all immediately-available network connections", " scan Prints all immediately-available network connections",
"scan-analyze [d] [-a] Prints info for all servers up to <i>d</i> nodes away", " scan-analyze [d] [-a] Prints info for all servers up to <i>d</i> nodes away",
"scp [file ...] [server] Copies a file to a destination server", " scp [file ...] [server] Copies a file to a destination server",
"sudov Shows whether you have root access on this computer", " sudov Shows whether you have root access on this computer",
"tail [script] [args...] Displays dynamic logs for the specified script", " tail [script] [args...] Displays dynamic logs for the specified script",
"top Displays all running scripts and their RAM usage", " top Displays all running scripts and their RAM usage",
"unalias [alias name] Deletes the specified alias", " unalias [alias name] Deletes the specified alias",
"vim [file ...] Text editor - Open up and edit one or more scripts or text files in vim mode", " vim [file ...] Text editor - Open up and edit one or more scripts or text files in vim mode",
"weaken Reduce the security of the current machine", " weaken Reduce the security of the current machine",
"wget [url] [target file] Retrieves code/text from a web server", " wget [url] [target file] Retrieves code/text from a web server",
" ",
]; ];
export const HelpTexts: IMap<string[]> = { export const HelpTexts: IMap<string[]> = {
alias: [ alias: [
'alias [-g] [name="value"] ', 'Usage: alias [-g] [name="value"] ',
" ", " ",
"Create or display aliases. An alias enables a replacement of a word with another string. ", "Create or display aliases. An alias enables a replacement of a word with another string. ",
"It can be used to abbreviate a commonly used command, or commonly used parts of a command. The NAME ", "It can be used to abbreviate a commonly used command, or commonly used parts of a command. The NAME ",
"of an alias defines the word that will be replaced, while the VALUE defines what it will be replaced by. For example, ", "of an alias defines the word that will be replaced, while the VALUE defines what it will be replaced by. For example, ",
"you could create the alias 'nuke' for the Terminal command 'run NUKE.exe' using the following: ", "you could create the alias 'nuke' for the Terminal command 'run NUKE.exe' using the following: ",
" ", " ",
'alias nuke="run NUKE.exe"', ' alias nuke="run NUKE.exe"',
" ", " ",
"Then, to run the NUKE.exe program you would just have to enter 'nuke' in Terminal rather than the full command. ", "Then, to run the NUKE.exe program you would just have to enter 'nuke' in Terminal rather than the full command. ",
"It is important to note that 'default' aliases will only be substituted for the first word of a Terminal command. For ", "It is important to note that 'default' aliases will only be substituted for the first word of a Terminal command. For ",
"example, if the following alias was set: ", "example, if the following alias was set: ",
" ", " ",
'alias worm="HTTPWorm.exe"', ' alias worm="HTTPWorm.exe"',
" ", " ",
"and then you tried to run the following terminal command: ", "and then you tried to run the following terminal command: ",
" ", " ",
"run worm", " run worm",
" ", " ",
"This would fail because the worm alias is not the first word of a Terminal command. To allow an alias to be substituted ", "This would fail because the worm alias is not the first word of a Terminal command. To allow an alias to be substituted ",
"anywhere in a Terminal command, rather than just the first word, you must set it to be a global alias using the -g flag: ", "anywhere in a Terminal command, rather than just the first word, you must set it to be a global alias using the -g flag: ",
" ", " ",
'alias -g worm="HTTPWorm.exe"', ' alias -g worm="HTTPWorm.exe"',
" ", " ",
"Now, the 'worm' alias will be substituted anytime it shows up as an individual word in a Terminal command. ", "Now, the 'worm' alias will be substituted anytime it shows up as an individual word in a Terminal command. ",
" ", " ",
@ -80,15 +81,16 @@ export const HelpTexts: IMap<string[]> = {
" ", " ",
], ],
analyze: [ analyze: [
"analyze", "Usage: analyze",
" ", " ",
"Prints details and statistics about the current server. The information that is printed includes basic ", "Prints details and statistics about the current server. The information that is printed includes basic ",
"server details such as the hostname, whether the player has root access, what ports are opened/closed, and also ", "server details such as the hostname, whether the player has root access, what ports are opened/closed, and also ",
"hacking-related information such as an estimated chance to successfully hack, an estimate of how much money is ", "hacking-related information such as an estimated chance to successfully hack, an estimate of how much money is ",
"available on the server, etc.", "available on the server, etc.",
" ",
], ],
backdoor: [ backdoor: [
"backdoor", "Usage: backdoor",
" ", " ",
"Install a backdoor on the current machine, grants a secret bonus depending on the machine.", "Install a backdoor on the current machine, grants a secret bonus depending on the machine.",
" ", " ",
@ -96,7 +98,7 @@ export const HelpTexts: IMap<string[]> = {
" ", " ",
], ],
buy: [ buy: [
"buy [-l / -a / program]", "Usage: buy [-l / -a / program]",
" ", " ",
"Purchase a program through the Dark Web. Requires a TOR router to use.", "Purchase a program through the Dark Web. Requires a TOR router to use.",
" ", " ",
@ -106,65 +108,72 @@ export const HelpTexts: IMap<string[]> = {
"If this command is ran with the '-a' flag, it will attempt to purchase all unowned programs.", "If this command is ran with the '-a' flag, it will attempt to purchase all unowned programs.",
" ", " ",
"Otherwise, the name of the program must be passed in as a parameter. This name is NOT case-sensitive.", "Otherwise, the name of the program must be passed in as a parameter. This name is NOT case-sensitive.",
" ",
], ],
cat: [ cat: [
"cat [file]", "Usage: cat [file]",
" ", " ",
"Display message (.msg), literature (.lit), or text (.txt) files. Examples:", "Display message (.msg), literature (.lit), or text (.txt) files. Examples:",
" ", " ",
"cat j1.msg", " cat j1.msg",
" ", " ",
"cat foo.lit", " cat foo.lit",
" ",
" cat servers.txt",
" ", " ",
"cat servers.txt",
], ],
cd: [ cd: [
"cd [dir]", "Usage: cd [dir]",
" ", " ",
"Change to the specified directory. Note that this works even for directories that don't exist. If you ", "Change to the specified directory. Note that this works even for directories that don't exist. If you ",
"change to a directory that does not exist, it will not be 'created'. Examples:", "change to a directory that does not exist, it will not be 'created'. Examples:",
" ", " ",
"cd scripts/hacking", " cd scripts/hacking",
" ", " ",
"cd /logs", " cd /logs",
" ",
" cd ../",
" ", " ",
"cd ../",
], ],
check: [ check: [
"check [script name] [args...]", "Usage: check [script name] [args...]",
" ", " ",
"Print the logs of the script specified by the script name and arguments to the Terminal. Each argument must be separated by ", "Print the logs of the script specified by the script name and arguments to the Terminal. Each argument must be separated by ",
"a space. Remember that a running script is uniquely ", "a space. Remember that a running script is uniquely ",
"identified both by its name and the arguments that are used to start it. So, if a script was ran with the following arguments: ", "identified both by its name and the arguments that are used to start it. So, if a script was ran with the following arguments: ",
" ", " ",
"run foo.script 1 2 foodnstuff", " run foo.script 1 2 foodnstuff",
" ", " ",
"Then to run the 'check' command on this script you would have to pass the same arguments in: ", "Then to run the 'check' command on this script you would have to pass the same arguments in: ",
" ", " ",
"check foo.script 1 2 foodnstuff", " check foo.script 1 2 foodnstuff",
" ",
], ],
clear: [ clear: [
"clear", "Usage: clear",
" ", " ",
"Clear the Terminal screen, deleting all of the text. Note that this does not delete the user's command history, so using the up ", "Clear the Terminal screen, deleting all of the text. Note that this does not delete the user's command history, so using the up ",
"and down arrow keys is still valid. Also note that this is permanent and there is no way to undo this. Synonymous with 'cls' command", "and down arrow keys is still valid. Also note that this is permanent and there is no way to undo this. Synonymous with 'cls' command",
" ",
], ],
cls: [ cls: [
"cls", "Usage: cls",
" ", " ",
"Clear the Terminal screen, deleting all of the text. Note that this does not delete the user's command history, so using the up ", "Clear the Terminal screen, deleting all of the text. Note that this does not delete the user's command history, so using the up ",
"and down arrow keys is still valid. Also note that this is permanent and there is no way to undo this. Synonymous with 'clear' command", "and down arrow keys is still valid. Also note that this is permanent and there is no way to undo this. Synonymous with 'clear' command",
" ",
], ],
connect: [ connect: [
"connect [hostname]", "Usage: connect [hostname]",
" ", " ",
"Connect to a remote server. The hostname or IP address of the remote server must be given as the argument ", "Connect to a remote server. The hostname or IP address of the remote server must be given as the argument ",
"to this command. Note that only servers that are immediately adjacent to the current server in the network can be connected to. To ", "to this command. Note that only servers that are immediately adjacent to the current server in the network can be connected to. To ",
"see which servers can be connected to, use the 'scan' command.", "see which servers can be connected to, use the 'scan' command.",
" ",
], ],
cp: ["cp [src] [dst]", " ", "Copy a file on this server. To copy a file to another server use scp."], cp: ["Usage: cp [src] [dst]", " ", "Copy a file on this server. To copy a file to another server use scp.", " "],
download: [ download: [
"download [script/text file]", "Usage: download [script/text file]",
" ", " ",
"Downloads a script or text file to your computer (like your real life computer).", "Downloads a script or text file to your computer (like your real life computer).",
" ", " ",
@ -178,7 +187,7 @@ export const HelpTexts: IMap<string[]> = {
" ", " ",
], ],
expr: [ expr: [
"expr [mathematical expression]", "Usage: expr [mathematical expression]",
" ", " ",
"Evaluate a simple mathematical expression. Supports native JavaScript operators:", "Evaluate a simple mathematical expression. Supports native JavaScript operators:",
" ", " ",
@ -186,47 +195,49 @@ export const HelpTexts: IMap<string[]> = {
" ", " ",
"Example:", "Example:",
" ", " ",
"expr 25 * 2 ** 10", " expr 25 * 2 ** 10",
" ", " ",
"Note that letters (non-digits) are not allowed and will be removed from the input.", "Note that letters (non-digits) are not allowed and will be removed from the input.",
" ",
], ],
free: [ free: [
"free", "Usage: free",
" ", " ",
"Displays the memory usage on the current machine. Print the amount of RAM that is available on the current server as well as ", "Displays the memory usage on the current machine. Print the amount of RAM that is available on the current server as well as ",
"how much of it is being used.", "how much of it is being used.",
" ",
], ],
grow: [ grow: [
"grow", "Usage: grow",
"", " ",
"Spoof transactions in the current server. Increasing the money available by hacking. Requires root access.", "Spoof transactions in the current server. Increasing the money available by hacking. Requires root access.",
"See the wiki page for hacking mechanics.", "See the wiki page for hacking mechanics.",
" ",
], ],
hack: [ hack: [
"hack", "Usage: hack",
" ", " ",
"Attempt to hack the current server. Requires root access in order to be run. See the wiki page for hacking mechanics", "Attempt to hack the current server. Requires root access in order to be run. See the wiki page for hacking mechanics",
" ", " ",
], ],
help: [ help: [
"help [command]", "Usage: help [command]",
" ", " ",
"Display Terminal help information. Without arguments, 'help' prints a list of all valid Terminal commands and a brief ", "Display Terminal help information. Without arguments, 'help' prints a list of all valid Terminal commands and a brief ",
"description of their functionality. You can also pass the name of a Terminal command as an argument to 'help' to print ", "description of their functionality. You can also pass the name of a Terminal command as an argument to 'help' to print ",
"more detailed information about the Terminal command. Examples: ", "more detailed information about the Terminal command. Examples: ",
" ", " ",
"help alias", " help alias",
" ",
" help scan-analyze",
" ", " ",
"help scan-analyze",
], ],
home: [ home: [
"home" + "Connect to your home computer. This will work no matter what server you are currently connected to.", "Usage: home", " ", "Connect to your home computer. This will work no matter what server you are currently connected to.", " ",
], ],
hostname: ["hostname", " ", "Prints the hostname of the current server"], hostname: ["Usage: hostname", " ", "Prints the hostname of the current server", " "],
kill: [ kill: [
"kill [script name] [args...]", "Usage: kill [script name] [args...] or kill [pid",
" ",
"kill [pid]",
" ", " ",
"Kill the script specified by the script name and arguments OR by its PID.", "Kill the script specified by the script name and arguments OR by its PID.",
" ", " ",
@ -235,24 +246,26 @@ export const HelpTexts: IMap<string[]> = {
"uniquely identified by both its name and the arguments that are used to start ", "uniquely identified by both its name and the arguments that are used to start ",
"it. So, if a script was ran with the following arguments:", "it. So, if a script was ran with the following arguments:",
" ", " ",
"run foo.script 1 sigma-cosmetics", " run foo.script 1 sigma-cosmetics",
" ", " ",
"Then to kill this script the same arguments would have to be used:", "Then to kill this script the same arguments would have to be used:",
" ", " ",
"kill foo.script 1 sigma-cosmetics", " kill foo.script 1 sigma-cosmetics",
" ", " ",
"If you are killing the script using its PID, then the PID argument must be numeric", "If you are killing the script using its PID, then the PID argument must be numeric",
" ",
], ],
killall: [ killall: [
"killall", "Usage: killall",
" ", " ",
"Kills all scripts on the current server. ", "Kills all scripts on the current server. ",
"Note that after the 'kill' command is issued for a script, it may take a while for the script to actually stop running. ", "Note that after the 'kill' command is issued for a script, it may take a while for the script to actually stop running. ",
"This will happen if the script is in the middle of a command such as grow() or weaken() that takes time to execute. ", "This will happen if the script is in the middle of a command such as grow() or weaken() that takes time to execute. ",
"The script will not be stopped/killed until after that time has elapsed.", "The script will not be stopped/killed until after that time has elapsed.",
" ",
], ],
ls: [ ls: [
"ls [dir] [| grep pattern]", "Usage: ls [dir] [| 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. ",
@ -265,34 +278,36 @@ export const HelpTexts: IMap<string[]> = {
" ", " ",
"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 | 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 / | 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 | grep purchase",
" ",
], ],
lscpu: ["lscpu", " ", "Prints the number of CPU Cores the current server has"], lscpu: ["Usage: lscpu", " ", "Prints the number of CPU Cores the current server has", " "],
mem: [ mem: [
"mem [script name] [-t num_threads]", "Usage: mem [script name] [-t num_threads]",
" ", " ",
"Displays the amount of RAM needed to run the specified script with a single thread. The command can also be used to print ", "Displays the amount of RAM needed to run the specified script with a single thread. The command can also be used to print ",
"the amount of RAM needed to run a script with multiple threads using the '-t' flag. If the '-t' flag is specified, then ", "the amount of RAM needed to run a script with multiple threads using the '-t' flag. If the '-t' flag is specified, then ",
"an argument for the number of threads must be passed in afterwards. Examples:", "an argument for the number of threads must be passed in afterwards. Examples:",
" ", " ",
"mem foo.script", " mem foo.script",
" ", " ",
"mem foo.script -t 50", " mem foo.script -t 50",
" ", " ",
"The first example above will print the amount of RAM needed to run 'foo.script' with a single thread. The second example ", "The first example above will print the amount of RAM needed to run 'foo.script' with a single thread. The second example ",
"above will print the amount of RAM needed to run 'foo.script' with 50 threads.", "above will print the amount of RAM needed to run 'foo.script' with 50 threads.",
" ",
], ],
mv: [ mv: [
"mv [src] [dest]", "Usage: mv [src] [dest]",
" ", " ",
"Move the source file to the specified destination. This can also be used to rename files. ", "Move the source file to the specified destination. This can also be used to rename files. ",
"This command only works for scripts and text files (.txt). This command CANNOT be used to ", "This command only works for scripts and text files (.txt). This command CANNOT be used to ",
@ -302,28 +317,31 @@ export const HelpTexts: IMap<string[]> = {
"full filepath. ", "full filepath. ",
"Examples: ", "Examples: ",
" ", " ",
"mv hacking-controller.script scripts/hacking-controller.script", " mv hacking-controller.script scripts/hacking-controller.script",
" ",
" mv myScript.js myOldScript.js",
" ", " ",
"mv myScript.js myOldScript.js",
], ],
nano: [ nano: [
"nano [file ...]", "Usage: nano [file ...]",
" ", " ",
"Opens up the specified file(s) in the Text Editor. Only scripts (.script) or text files (.txt) can be ", "Opens up the specified file(s) in the Text Editor. Only scripts (.script) or text files (.txt) can be ",
"edited using the Text Editor. If the file does not already exist, then a new, empty one ", "edited using the Text Editor. If the file does not already exist, then a new, empty one ",
"will be created", "will be created",
" ",
], ],
ps: ["ps", " ", "Prints all scripts that are running on the current server"], ps: ["Usage: ps", " ", "Prints all scripts that are running on the current server", " "],
rm: [ rm: [
"rm [file]", "Usage: rm [file]",
" ", " ",
"Removes the specified file from the current server. A file can be a script, a program, or a message file. ", "Removes the specified file from the current server. A file can be a script, a program, or a message file. ",
" ", " ",
"WARNING: This is permanent and cannot be undone", "WARNING: This is permanent and cannot be undone",
" ",
], ],
run: [ run: [
"run [file name] [-t] [num threads] [args...]", "Usage: run [file name] [-t] [num threads] [args...]",
" ", " ",
"Execute a program, script or coding contract.", "Execute a program, script or coding contract.",
" ", " ",
@ -338,13 +356,14 @@ export const HelpTexts: IMap<string[]> = {
" ", " ",
], ],
scan: [ scan: [
"scan", "Usage: scan",
" ", " ",
"Prints all immediately-available network connection. This will print a list of all servers that you can currently connect ", "Prints all immediately-available network connection. This will print a list of all servers that you can currently connect ",
"to using the 'connect' Terminal command.", "to using the 'connect' Terminal command.",
" ",
], ],
"scan-analyze": [ "scan-analyze": [
"scan-analyze [depth] [-a]", "Usage: scan-analyze [depth] [-a]",
" ", " ",
"Prints detailed information about all servers up to [depth] nodes away on the network. Calling ", "Prints detailed information about all servers up to [depth] nodes away on the network. Calling ",
"'scan-analyze 1' will display information for the same servers that are shown by the 'scan' Terminal ", "'scan-analyze 1' will display information for the same servers that are shown by the 'scan' Terminal ",
@ -360,71 +379,77 @@ export const HelpTexts: IMap<string[]> = {
" ", " ",
"By default, this command will not display servers that you have purchased. However, you can pass in the ", "By default, this command will not display servers that you have purchased. However, you can pass in the ",
"-a flag at the end of the command if you would like to enable that.", "-a flag at the end of the command if you would like to enable that.",
" ",
], ],
scp: [ scp: [
"scp [filename ...] [target server]", "Usage: scp [filename ...] [target server]",
" ", " ",
"Copies the specified file(s) from the current server to the target server. ", "Copies the specified file(s) from the current server to the target server. ",
"This command only works for script files (.script or .js extension), literature files (.lit extension), ", "This command only works for script files (.script or .js extension), literature files (.lit extension), ",
"and text files (.txt extension). ", "and text files (.txt extension). ",
"The second argument passed in must be the hostname or IP of the target server. Examples:", "The second argument passed in must be the hostname or IP of the target server. Examples:",
" ", " ",
"scp foo.script n00dles", " scp foo.script n00dles",
" ", " ",
"scp foo.script bar.script n00dles", " scp foo.script bar.script n00dles",
" ", " ",
], ],
sudov: ["sudov", " ", "Prints whether or not you have root access to the current machine"], sudov: ["Usage: sudov", " ", "Prints whether or not you have root access to the current machine", " "],
tail: [ tail: [
"tail [script name] [args...]", "Usage: tail [script name] [args...]",
" ", " ",
"Displays dynamic logs for the script specified by the script name and arguments. Each argument must be separated ", "Displays dynamic logs for the script specified by the script name and arguments. Each argument must be separated ",
"by a space. Remember that a running script is uniquely identified by both its name and the arguments that were used ", "by a space. Remember that a running script is uniquely identified by both its name and the arguments that were used ",
"to run it. So, if a script was ran with the following arguments: ", "to run it. So, if a script was ran with the following arguments: ",
" ", " ",
"run foo.script 10 50000", " run foo.script 10 50000",
" ", " ",
"Then in order to check its logs with 'tail' the same arguments must be used: ", "Then in order to check its logs with 'tail' the same arguments must be used: ",
" ", " ",
"tail foo.script 10 50000", " tail foo.script 10 50000",
" ",
], ],
top: [ top: [
"top", "Usage: top",
" ", " ",
"Prints a list of all scripts running on the current server as well as their thread count and how much ", "Prints a list of all scripts running on the current server as well as their thread count and how much ",
"RAM they are using in total.", "RAM they are using in total.",
" ",
], ],
unalias: [ unalias: [
"unalias [alias name]", "Usage: unalias [alias name]",
" ", " ",
"Deletes the specified alias. Note that the double quotation marks are required. ", "Deletes the specified alias. Note that the double quotation marks are required. ",
" ", " ",
"As an example, if an alias was declared using:", "As an example, if an alias was declared using:",
" ", " ",
'alias r="run"', ' alias r="run"',
" ", " ",
"Then it could be removed using:", "Then it could be removed using:",
" ", " ",
"unalias r", " unalias r",
" ", " ",
"It is not necessary to differentiate between global and non-global aliases when using 'unalias'", "It is not necessary to differentiate between global and non-global aliases when using 'unalias'",
" ",
], ],
vim: [ vim: [
"vim [file ...]", "Usage: vim [file ...]",
" ", " ",
"Opens up the specified file(s) in the Text Editor in vim mode. Only scripts (.script) or text files (.txt) can be ", "Opens up the specified file(s) in the Text Editor in vim mode. Only scripts (.script) or text files (.txt) can be ",
"edited using the Text Editor. If the file does not already exist, then a new, empty one ", "edited using the Text Editor. If the file does not already exist, then a new, empty one ",
"will be created", "will be created",
" ",
], ],
weaken: [ weaken: [
"weaken", "Usage: weaken",
"", " ",
"Reduces the security level of the current server. Decreasing the time it takes for all operations on this server.", "Reduces the security level of the current server. Decreasing the time it takes for all operations on this server.",
"Requires root access. See the wiki page for hacking mechanics.", "Requires root access. See the wiki page for hacking mechanics.",
" ",
], ],
wget: [ wget: [
"wget [url] [target file]", "Usage: wget [url] [target file]",
" ", " ",
"Retrieves data from a URL and downloads it to a file on the current server. The data can only ", "Retrieves data from a URL and downloads it to a file on the current server. The data can only ",
"be downloaded to a script (.script, .ns, .js) or a text file (.txt). If the file already exists, ", "be downloaded to a script (.script, .ns, .js) or a text file (.txt). If the file already exists, ",
@ -433,6 +458,7 @@ export const HelpTexts: IMap<string[]> = {
"Note that it will not be possible to download data from many websites because they do not allow ", "Note that it will not be possible to download data from many websites because they do not allow ",
"cross-origin resource sharing (CORS). Example:", "cross-origin resource sharing (CORS). Example:",
" ", " ",
"wget https://raw.githubusercontent.com/danielyxie/bitburner/master/README.md game_readme.txt", " wget https://raw.githubusercontent.com/danielyxie/bitburner/master/README.md game_readme.txt",
" ",
], ],
}; };

@ -95,20 +95,42 @@ export function TerminalRoot({ terminal, router, player }: IProps): React.ReactE
setKey((key) => key + 1); setKey((key) => key + 1);
} }
useEffect(() => TerminalEvents.subscribe(_.debounce(async () => rerender(), 25, { maxWait: 50 })), []); useEffect(() => {
useEffect(() => TerminalClearEvents.subscribe(_.debounce(async () => clear(), 25, { maxWait: 50 })), []); const debounced = _.debounce(async () => rerender(), 25, { maxWait: 50 });
const unsubscribe = TerminalEvents.subscribe(debounced);
return () => {
debounced.cancel();
unsubscribe();
}
}, []);
function doScroll(): void { useEffect(() => {
const debounced = _.debounce(async () => clear(), 25, { maxWait: 50 });
const unsubscribe = TerminalClearEvents.subscribe(debounced);
return () => {
debounced.cancel();
unsubscribe();
}
}, []);
function doScroll(): number | undefined {
const hook = scrollHook.current; const hook = scrollHook.current;
if (hook !== null) { if (hook !== null) {
setTimeout(() => hook.scrollIntoView(true), 50); return window.setTimeout(() => hook.scrollIntoView(true), 50);
} }
} }
doScroll(); doScroll();
useEffect(() => { useEffect(() => {
setTimeout(doScroll, 50); let scrollId: number;
const id = setTimeout(() => {
scrollId = doScroll() ?? 0;
}, 50);
return () => {
clearTimeout(id);
clearTimeout(scrollId);
}
}, []); }, []);
function lineClass(s: string): string { function lineClass(s: string): string {

@ -1,5 +1,5 @@
import React, { useState, useEffect } from "react"; import React, { useState, useEffect } from "react";
import { cloneDeep, isEqual } from "lodash";
import { IPlayer } from "../PersonObjects/IPlayer"; import { IPlayer } from "../PersonObjects/IPlayer";
import { IEngine } from "../IEngine"; import { IEngine } from "../IEngine";
import { ITerminal } from "../Terminal/ITerminal"; import { ITerminal } from "../Terminal/ITerminal";
@ -86,11 +86,6 @@ interface IProps {
engine: IEngine; engine: IEngine;
} }
interface PageHistoryEntry {
page: Page;
args: any[];
}
const useStyles = makeStyles((theme: Theme) => const useStyles = makeStyles((theme: Theme) =>
createStyles({ createStyles({
root: { root: {
@ -110,15 +105,6 @@ export let Router: IRouter = {
page: () => { page: () => {
throw new Error("Router called before initialization"); throw new Error("Router called before initialization");
}, },
previousPage: () => {
throw new Error("Router called before initialization");
},
clearHistory: () => {
throw new Error("Router called before initialization");
},
toPreviousPage: (): boolean => {
throw new Error("Router called before initialization");
},
toActiveScripts: () => { toActiveScripts: () => {
throw new Error("Router called before initialization"); throw new Error("Router called before initialization");
}, },
@ -217,9 +203,7 @@ function determineStartPage(player: IPlayer): Page {
export function GameRoot({ player, engine, terminal }: IProps): React.ReactElement { export function GameRoot({ player, engine, terminal }: IProps): React.ReactElement {
const classes = useStyles(); const classes = useStyles();
const [{ files, vim }, setEditorOptions] = useState({ files: {}, vim: false }); const [{ files, vim }, setEditorOptions] = useState({ files: {}, vim: false });
const startPage = determineStartPage(player); const [page, setPage] = useState(determineStartPage(player));
const [page, setPage] = useState(startPage);
const [pageHistory, setPageHistory] = useState<PageHistoryEntry[]>([{ page: startPage, args: [] }]);
const setRerender = useState(0)[1]; const setRerender = useState(0)[1];
const [faction, setFaction] = useState<Faction>( const [faction, setFaction] = useState<Faction>(
player.currentWorkFactionName ? Factions[player.currentWorkFactionName] : (undefined as unknown as Faction), player.currentWorkFactionName ? Factions[player.currentWorkFactionName] : (undefined as unknown as Faction),
@ -250,136 +234,70 @@ export function GameRoot({ player, engine, terminal }: IProps): React.ReactEleme
setTimeout(() => htmlLocation.reload(), 2000); setTimeout(() => htmlLocation.reload(), 2000);
} }
function setCurrentPage(page: Page, ...args: any): void {
const newPage = { page, args: cloneDeep(args) };
const previousPage = pageHistory[0];
const isDifferentThenPrevious = page !== previousPage?.page || !isEqual([...args], previousPage?.args);
if (isDifferentThenPrevious) {
const history = [
newPage,
...pageHistory
].slice(0, 20);
setPageHistory(history)
}
setPage(page)
}
function goBack(fallback: (...args: any[]) => void): void {
const [ , previousPage ] = pageHistory;
if (previousPage) {
const handler = pageToRouterMap[previousPage?.page];
handler(...previousPage.args);
} else {
if (fallback) fallback();
}
const [ , ...history] = pageHistory;
setPageHistory(cloneDeep(history));
}
const pageToRouterMap: { [key: number] : (...args: any[]) => void } = {
[Page.ActiveScripts]: Router.toActiveScripts,
[Page.Augmentations]: Router.toAugmentations,
[Page.Bladeburner]: Router.toBladeburner,
[Page.Stats]: Router.toStats,
[Page.Corporation]: Router.toCorporation,
[Page.CreateProgram]: Router.toCreateProgram,
[Page.DevMenu]: Router.toDevMenu,
[Page.Faction]: Router.toFaction,
[Page.Factions]: Router.toFactions,
[Page.Options]: Router.toGameOptions,
[Page.Gang]: Router.toGang,
[Page.Hacknet]: Router.toHacknetNodes,
[Page.Milestones]: Router.toMilestones,
[Page.Resleeves]: Router.toResleeves,
[Page.ScriptEditor]: Router.toScriptEditor,
[Page.Sleeves]: Router.toSleeves,
[Page.StockMarket]: Router.toStockMarket,
[Page.Terminal]: Router.toTerminal,
[Page.Tutorial]: Router.toTutorial,
[Page.Job]: Router.toJob,
[Page.City]: Router.toCity,
[Page.Travel]: Router.toTravel,
[Page.BitVerse]: Router.toBitVerse,
[Page.Infiltration]: Router.toInfiltration,
[Page.Work]: Router.toWork,
[Page.BladeburnerCinematic]: Router.toBladeburnerCinematic,
[Page.Location]: Router.toLocation,
[Page.StaneksGift]: Router.toStaneksGift,
[Page.Achievements]: Router.toAchievements,
}
Router = { Router = {
page: () => page, page: () => page,
previousPage: () => { toActiveScripts: () => setPage(Page.ActiveScripts),
const [ , previousPage] = pageHistory; toAugmentations: () => setPage(Page.Augmentations),
return previousPage?.page ?? -1; toBladeburner: () => setPage(Page.Bladeburner),
}, toStats: () => setPage(Page.Stats),
clearHistory: () => setPageHistory([]), toCorporation: () => setPage(Page.Corporation),
toPreviousPage: goBack, toCreateProgram: () => setPage(Page.CreateProgram),
toActiveScripts: () => setCurrentPage(Page.ActiveScripts), toDevMenu: () => setPage(Page.DevMenu),
toAugmentations: () => setCurrentPage(Page.Augmentations),
toBladeburner: () => setCurrentPage(Page.Bladeburner),
toStats: () => setCurrentPage(Page.Stats),
toCorporation: () => setCurrentPage(Page.Corporation),
toCreateProgram: () => setCurrentPage(Page.CreateProgram),
toDevMenu: () => setCurrentPage(Page.DevMenu),
toFaction: (faction?: Faction) => { toFaction: (faction?: Faction) => {
setCurrentPage(Page.Faction, faction); setPage(Page.Faction);
if (faction) setFaction(faction); if (faction) setFaction(faction);
}, },
toFactions: () => setCurrentPage(Page.Factions), toFactions: () => setPage(Page.Factions),
toGameOptions: () => setCurrentPage(Page.Options), toGameOptions: () => setPage(Page.Options),
toGang: () => setCurrentPage(Page.Gang), toGang: () => setPage(Page.Gang),
toHacknetNodes: () => setCurrentPage(Page.Hacknet), toHacknetNodes: () => setPage(Page.Hacknet),
toMilestones: () => setCurrentPage(Page.Milestones), toMilestones: () => setPage(Page.Milestones),
toResleeves: () => setCurrentPage(Page.Resleeves), toResleeves: () => setPage(Page.Resleeves),
toScriptEditor: (files: Record<string, string>, options?: ScriptEditorRouteOptions) => { toScriptEditor: (files: Record<string, string>, options?: ScriptEditorRouteOptions) => {
setEditorOptions({ setEditorOptions({
files, files,
vim: !!options?.vim, vim: !!options?.vim,
}); });
setCurrentPage(Page.ScriptEditor, files, options); setPage(Page.ScriptEditor);
}, },
toSleeves: () => setCurrentPage(Page.Sleeves), toSleeves: () => setPage(Page.Sleeves),
toStockMarket: () => setCurrentPage(Page.StockMarket), toStockMarket: () => setPage(Page.StockMarket),
toTerminal: () => setCurrentPage(Page.Terminal), toTerminal: () => setPage(Page.Terminal),
toTutorial: () => setCurrentPage(Page.Tutorial), toTutorial: () => setPage(Page.Tutorial),
toJob: () => { toJob: () => {
setLocation(Locations[player.companyName]); setLocation(Locations[player.companyName]);
setCurrentPage(Page.Job); setPage(Page.Job);
}, },
toCity: () => { toCity: () => {
setCurrentPage(Page.City); setPage(Page.City);
}, },
toTravel: () => { toTravel: () => {
player.gotoLocation(LocationName.TravelAgency); player.gotoLocation(LocationName.TravelAgency);
setCurrentPage(Page.Travel); setPage(Page.Travel);
}, },
toBitVerse: (flume: boolean, quick: boolean) => { toBitVerse: (flume: boolean, quick: boolean) => {
setFlume(flume); setFlume(flume);
setQuick(quick); setQuick(quick);
setCurrentPage(Page.BitVerse, flume, quick); setPage(Page.BitVerse);
}, },
toInfiltration: (location: Location) => { toInfiltration: (location: Location) => {
setLocation(location); setLocation(location);
setCurrentPage(Page.Infiltration, location); setPage(Page.Infiltration);
},
toWork: () => {
setCurrentPage(Page.Work);
}, },
toWork: () => setPage(Page.Work),
toBladeburnerCinematic: () => { toBladeburnerCinematic: () => {
setCurrentPage(Page.BladeburnerCinematic); setPage(Page.BladeburnerCinematic);
setCinematicText(cinematicText); setCinematicText(cinematicText);
}, },
toLocation: (location: Location) => { toLocation: (location: Location) => {
setLocation(location); setLocation(location);
setCurrentPage(Page.Location, location); setPage(Page.Location);
}, },
toStaneksGift: () => { toStaneksGift: () => {
setCurrentPage(Page.StaneksGift); setPage(Page.StaneksGift);
}, },
toAchievements: () => { toAchievements: () => {
setCurrentPage(Page.Achievements); setPage(Page.Achievements);
}, },
}; };
@ -582,11 +500,7 @@ export function GameRoot({ player, engine, terminal }: IProps): React.ReactEleme
<SnackbarProvider> <SnackbarProvider>
<Overview mode={ITutorial.isRunning ? "tutorial" : "overview"}> <Overview mode={ITutorial.isRunning ? "tutorial" : "overview"}>
{!ITutorial.isRunning ? ( {!ITutorial.isRunning ? (
<CharacterOverview <CharacterOverview save={() => saveObject.saveGame()} killScripts={killAllScripts} />
save={() => saveObject.saveGame()}
killScripts={killAllScripts}
router={Router}
allowBackButton={withSidebar} />
) : ( ) : (
<InteractiveTutorialRoot /> <InteractiveTutorialRoot />
)} )}

@ -17,22 +17,19 @@ import Typography from "@mui/material/Typography";
import Button from "@mui/material/Button"; import Button from "@mui/material/Button";
import IconButton from "@mui/material/IconButton"; import IconButton from "@mui/material/IconButton";
import SaveIcon from "@mui/icons-material/Save"; import SaveIcon from "@mui/icons-material/Save";
import ArrowBackIcon from "@mui/icons-material/ArrowBack";
import ClearAllIcon from "@mui/icons-material/ClearAll"; import ClearAllIcon from "@mui/icons-material/ClearAll";
import { Settings } from "../../Settings/Settings"; import { Settings } from "../../Settings/Settings";
import { use } from "../Context"; import { use } from "../Context";
import { StatsProgressOverviewCell } from "./StatsProgressBar"; import { StatsProgressOverviewCell } from "./StatsProgressBar";
import { BitNodeMultipliers } from "../../BitNode/BitNodeMultipliers"; import { BitNodeMultipliers } from "../../BitNode/BitNodeMultipliers";
import { IRouter, Page } from "../Router";
import { Box, Tooltip } from "@mui/material"; import { Box, Tooltip } from "@mui/material";
import { CONSTANTS } from "../../Constants"; import { CONSTANTS } from "../../Constants";
interface IProps { interface IProps {
save: () => void; save: () => void;
killScripts: () => void; killScripts: () => void;
router: IRouter;
allowBackButton: boolean;
} }
function Intelligence(): React.ReactElement { function Intelligence(): React.ReactElement {
@ -239,7 +236,7 @@ const useStyles = makeStyles((theme: Theme) =>
export { useStyles as characterOverviewStyles }; export { useStyles as characterOverviewStyles };
export function CharacterOverview({ save, killScripts, router, allowBackButton }: IProps): React.ReactElement { export function CharacterOverview({ save, killScripts }: IProps): React.ReactElement {
const [killOpen, setKillOpen] = useState(false); const [killOpen, setKillOpen] = useState(false);
const player = use.Player(); const player = use.Player();
@ -278,9 +275,6 @@ export function CharacterOverview({ save, killScripts, router, allowBackButton }
player.charisma_mult * BitNodeMultipliers.CharismaLevelMultiplier, player.charisma_mult * BitNodeMultipliers.CharismaLevelMultiplier,
); );
const previousPageName =
router.previousPage() < 0 ? "" : Page[router.previousPage() ?? 0].replace(/([a-z])([A-Z])/g, "$1 $2");
return ( return (
<> <>
<Table sx={{ display: "block", m: 1 }}> <Table sx={{ display: "block", m: 1 }}>
@ -464,13 +458,6 @@ export function CharacterOverview({ save, killScripts, router, allowBackButton }
<SaveIcon color={Settings.AutosaveInterval !== 0 ? "primary" : "error"} /> <SaveIcon color={Settings.AutosaveInterval !== 0 ? "primary" : "error"} />
</Tooltip> </Tooltip>
</IconButton> </IconButton>
{allowBackButton && (
<IconButton disabled={!previousPageName} onClick={() => router.toPreviousPage()}>
<Tooltip title={previousPageName ? `Go back to "${previousPageName}"` : ""}>
<ArrowBackIcon />
</Tooltip>
</IconButton>
)}
</Box> </Box>
<Box sx={{ display: "flex", flex: 1, justifyContent: "flex-end", alignItems: "center" }}> <Box sx={{ display: "flex", flex: 1, justifyContent: "flex-end", alignItems: "center" }}>
<IconButton onClick={() => setKillOpen(true)}> <IconButton onClick={() => setKillOpen(true)}>

@ -25,20 +25,22 @@ export function CorruptableText(props: IProps): JSX.Element {
useEffect(() => { useEffect(() => {
let counter = 5; let counter = 5;
const id = setInterval(() => { const timers: number[] = [];
const intervalId = setInterval(() => {
counter--; counter--;
if (counter > 0) return; if (counter > 0) return;
counter = Math.random() * 5; counter = Math.random() * 5;
const index = Math.random() * content.length; const index = Math.random() * content.length;
const letter = content.charAt(index); const letter = content.charAt(index);
setContent((content) => replace(content, index, randomize(letter))); setContent((content) => replace(content, index, randomize(letter)));
setTimeout(() => { timers.push(window.setTimeout(() => {
setContent((content) => replace(content, index, letter)); setContent((content) => replace(content, index, letter));
}, 500); }, 500));
}, 20); }, 20);
return () => { return () => {
clearInterval(id); clearInterval(intervalId);
timers.forEach((timerId) => clearTimeout(timerId));
}; };
}, []); }, []);

@ -7,12 +7,13 @@ import Typography from "@mui/material/Typography";
import Paper from "@mui/material/Paper"; import Paper from "@mui/material/Paper";
import TextField from "@mui/material/TextField"; import TextField from "@mui/material/TextField";
import ReplyIcon from "@mui/icons-material/Reply"; import ReplyIcon from "@mui/icons-material/Reply";
import SaveIcon from '@mui/icons-material/Save'; import SaveIcon from "@mui/icons-material/Save";
import { ThemeEvents } from "./Theme"; import { ThemeEvents } from "./Theme";
import { Settings } from "../../Settings/Settings"; import { Settings } from "../../Settings/Settings";
import { IStyleSettings, defaultStyles } from "../../Settings/Styles"; import { defaultStyles } from "../../Settings/Styles";
import { Tooltip } from "@mui/material"; import { Tooltip } from "@mui/material";
import { IStyleSettings } from "../../ScriptEditor/NetscriptDefinitions";
interface IProps { interface IProps {
open: boolean; open: boolean;
@ -25,16 +26,16 @@ interface FontFamilyProps {
refreshId: number; refreshId: number;
} }
function FontFamilyField({ value, onChange, refreshId } : FontFamilyProps): React.ReactElement { function FontFamilyField({ value, onChange, refreshId }: FontFamilyProps): React.ReactElement {
const [errorText, setErrorText] = useState<string | undefined>(); const [errorText, setErrorText] = useState<string | undefined>();
const [fontFamily, setFontFamily] = useState<React.CSSProperties["fontFamily"]>(value); const [fontFamily, setFontFamily] = useState<React.CSSProperties["fontFamily"]>(value);
function update(newValue: React.CSSProperties["fontFamily"]): void { function update(newValue: React.CSSProperties["fontFamily"]): void {
setFontFamily(newValue); setFontFamily(newValue);
if (!newValue) { if (!newValue) {
setErrorText('Must have a value'); setErrorText("Must have a value");
} else { } else {
setErrorText(''); setErrorText("");
} }
} }
@ -55,7 +56,7 @@ function FontFamilyField({ value, onChange, refreshId } : FontFamilyProps): Reac
onChange={onTextChange} onChange={onTextChange}
fullWidth fullWidth
/> />
) );
} }
interface LineHeightProps { interface LineHeightProps {
@ -64,18 +65,18 @@ interface LineHeightProps {
refreshId: number; refreshId: number;
} }
function LineHeightField({ value, onChange, refreshId } : LineHeightProps): React.ReactElement { function LineHeightField({ value, onChange, refreshId }: LineHeightProps): React.ReactElement {
const [errorText, setErrorText] = useState<string | undefined>(); const [errorText, setErrorText] = useState<string | undefined>();
const [lineHeight, setLineHeight] = useState<React.CSSProperties["lineHeight"]>(value); const [lineHeight, setLineHeight] = useState<React.CSSProperties["lineHeight"]>(value);
function update(newValue: React.CSSProperties["lineHeight"]): void { function update(newValue: React.CSSProperties["lineHeight"]): void {
setLineHeight(newValue); setLineHeight(newValue);
if (!newValue) { if (!newValue) {
setErrorText('Must have a value'); setErrorText("Must have a value");
} else if (isNaN(Number(newValue))) { } else if (isNaN(Number(newValue))) {
setErrorText('Must be a number'); setErrorText("Must be a number");
} else { } else {
setErrorText(''); setErrorText("");
} }
} }
@ -95,7 +96,7 @@ function LineHeightField({ value, onChange, refreshId } : LineHeightProps): Reac
helperText={errorText} helperText={errorText}
onChange={onTextChange} onChange={onTextChange}
/> />
) );
} }
export function StyleEditorModal(props: IProps): React.ReactElement { export function StyleEditorModal(props: IProps): React.ReactElement {
@ -115,7 +116,7 @@ export function StyleEditorModal(props: IProps): React.ReactElement {
} }
function setDefaults(): void { function setDefaults(): void {
const styles = {...defaultStyles} const styles = { ...defaultStyles };
setCustomStyle(styles); setCustomStyle(styles);
persistToSettings(styles); persistToSettings(styles);
setRefreshId(refreshId + 1); setRefreshId(refreshId + 1);
@ -132,16 +133,23 @@ export function StyleEditorModal(props: IProps): React.ReactElement {
<Modal open={props.open} onClose={props.onClose}> <Modal open={props.open} onClose={props.onClose}>
<Typography variant="h6">Styles Editor</Typography> <Typography variant="h6">Styles Editor</Typography>
<Typography> <Typography>
WARNING: Changing styles <strong>may mess up</strong> the interface. Drastic changes are <strong>NOT recommended</strong>. WARNING: Changing styles <strong>may mess up</strong> the interface. Drastic changes are{" "}
<strong>NOT recommended</strong>.
</Typography> </Typography>
<Paper sx={{ p: 2, my: 2 }}> <Paper sx={{ p: 2, my: 2 }}>
<FontFamilyField value={customStyle.fontFamily} refreshId={refreshId} <FontFamilyField
onChange={(value, error) => update({ ...customStyle, fontFamily: value }, error)} /> value={customStyle.fontFamily}
refreshId={refreshId}
onChange={(value, error) => update({ ...customStyle, fontFamily: value as any }, error)}
/>
<br /> <br />
<LineHeightField value={customStyle.lineHeight} refreshId={refreshId} <LineHeightField
onChange={(value, error) => update({ ...customStyle, lineHeight: value }, error)} /> value={customStyle.lineHeight}
refreshId={refreshId}
onChange={(value, error) => update({ ...customStyle, lineHeight: value as any }, error)}
/>
<br /> <br />
<ButtonGroup sx={{ my: 1}}> <ButtonGroup sx={{ my: 1 }}>
<Button onClick={setDefaults} startIcon={<ReplyIcon />} color="secondary" variant="outlined"> <Button onClick={setDefaults} startIcon={<ReplyIcon />} color="secondary" variant="outlined">
Revert to Defaults Revert to Defaults
</Button> </Button>

@ -53,9 +53,6 @@ export interface IRouter {
// toRedPill(): void; // toRedPill(): void;
// toworkInProgress(): void; // toworkInProgress(): void;
page(): Page; page(): Page;
previousPage(): Page;
clearHistory(): void;
toPreviousPage(fallback?: (...args: any[]) => void): void;
toActiveScripts(): void; toActiveScripts(): void;
toAugmentations(): void; toAugmentations(): void;
toBitVerse(flume: boolean, quick: boolean): void; toBitVerse(flume: boolean, quick: boolean): void;

@ -36,12 +36,12 @@ export function WorkInProgressRoot(): React.ReactElement {
const faction = Factions[player.currentWorkFactionName]; const faction = Factions[player.currentWorkFactionName];
if (player.workType == CONSTANTS.WorkTypeFaction) { if (player.workType == CONSTANTS.WorkTypeFaction) {
function cancel(): void { function cancel(): void {
router.toFaction(faction);
player.finishFactionWork(true); player.finishFactionWork(true);
router.toPreviousPage(() => router.toFaction(faction));
} }
function unfocus(): void { function unfocus(): void {
router.toFaction(faction);
player.stopFocusing(); player.stopFocusing();
router.toPreviousPage(() => router.toFaction(faction));
} }
return ( return (
<Grid container direction="column" justifyContent="center" alignItems="center" style={{ minHeight: "100vh" }}> <Grid container direction="column" justifyContent="center" alignItems="center" style={{ minHeight: "100vh" }}>
@ -120,12 +120,13 @@ export function WorkInProgressRoot(): React.ReactElement {
if (player.className !== "") { if (player.className !== "") {
function cancel(): void { function cancel(): void {
player.finishClass(true); player.finishClass(true);
router.toPreviousPage(() => router.toCity()); router.toCity();
} }
function unfocus(): void { function unfocus(): void {
router.toFaction(faction);
router.toCity();
player.stopFocusing(); player.stopFocusing();
router.toPreviousPage(() => router.toCity());
} }
let stopText = ""; let stopText = "";
@ -211,11 +212,11 @@ export function WorkInProgressRoot(): React.ReactElement {
function cancel(): void { function cancel(): void {
player.finishWork(true); player.finishWork(true);
router.toPreviousPage(() => router.toJob()); router.toJob();
} }
function unfocus(): void { function unfocus(): void {
player.stopFocusing(); player.stopFocusing();
router.toPreviousPage(() => router.toJob()); router.toJob();
} }
const position = player.jobs[player.companyName]; const position = player.jobs[player.companyName];
@ -303,11 +304,11 @@ export function WorkInProgressRoot(): React.ReactElement {
if (player.workType == CONSTANTS.WorkTypeCompanyPartTime) { if (player.workType == CONSTANTS.WorkTypeCompanyPartTime) {
function cancel(): void { function cancel(): void {
player.finishWorkPartTime(true); player.finishWorkPartTime(true);
router.toPreviousPage(() => router.toJob()); router.toJob();
} }
function unfocus(): void { function unfocus(): void {
player.stopFocusing(); player.stopFocusing();
router.toPreviousPage(() => router.toJob()); router.toJob();
} }
const comp = Companies[player.companyName]; const comp = Companies[player.companyName];
let companyRep = 0; let companyRep = 0;
@ -439,11 +440,11 @@ export function WorkInProgressRoot(): React.ReactElement {
if (player.createProgramName !== "") { if (player.createProgramName !== "") {
function cancel(): void { function cancel(): void {
player.finishCreateProgramWork(true); player.finishCreateProgramWork(true);
router.toPreviousPage(() => router.toTerminal()); router.toTerminal();
} }
function unfocus(): void { function unfocus(): void {
router.toTerminal();
player.stopFocusing(); player.stopFocusing();
router.toPreviousPage(() => router.toTerminal());
} }
return ( return (
<Grid container direction="column" justifyContent="center" alignItems="center" style={{ minHeight: "100vh" }}> <Grid container direction="column" justifyContent="center" alignItems="center" style={{ minHeight: "100vh" }}>