Merge branch 'dev' into add-simple-globs-nano-vim

This commit is contained in:
hydroflame
2022-01-15 17:38:25 -05:00
committed by GitHub
86 changed files with 1564 additions and 1090 deletions

2
.gitattributes vendored
View File

@ -1 +1 @@
* text=auto
* text=auto eol=lf

8
.github/ISSUE_TEMPLATE vendored Normal file
View File

@ -0,0 +1,8 @@
# DELETE THIS AFTER READING
# include (where relevant)
- [ ] Save file
- [ ] Minimal scripts to reproduce the issue
- [ ] Steps to reproduce
- [ ] Version of the game, e.g. Bitburner v1.3.0 (216bf616)

11
.github/PULL_REQUEST_TEMPLATE vendored Normal file
View File

@ -0,0 +1,11 @@
# DELETE THIS AFTER READING
# 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)

View File

@ -2,3 +2,14 @@ node_modules
package.json
dist
doc/build/
doc/source
.build
.package
editor.main.js
main.bundle.js
index.html
markdown
package.json
package.lock.json

View File

@ -1,5 +1,6 @@
{
"trailingComma": "all",
"endOfLine": "lf",
"tabWidth": 2,
"printWidth": 120
}

6
dist/bitburner.d.ts vendored
View File

@ -3848,7 +3848,7 @@ export declare interface NS extends Singularity {
* @param host - Host of target server.
* @returns Returns the amount of time in milliseconds it takes to execute the weaken Netscript function. Returns Infinity if called on a Hacknet Server.
*/
getWeakenTime(host: string): number;
getWeakenTime(host?: string): number;
/**
* Get the income of a script.
@ -3916,7 +3916,7 @@ export declare interface NS extends Singularity {
* @param args - Formating arguments.
* @returns Formated text.
*/
sprintf(format: string, ...args: string[]): string;
sprintf(format: string, ...args: any[]): string;
/**
* Format a string with an array of arguments.
@ -3928,7 +3928,7 @@ export declare interface NS extends Singularity {
* @param args - Formating arguments.
* @returns Formated text.
*/
vsprintf(format: string, args: string[]): string;
vsprintf(format: string, args: any[]): string;
/**
* Format a number

34
dist/vendor.bundle.js vendored

File diff suppressed because one or more lines are too long

View File

@ -39,8 +39,7 @@ There are two methods of obtaining Duplicate Sleeves:
1. Destroy BitNode-10. Each completion give you one additional Duplicate Sleeve
2. Purchase Duplicate Sleeves from :ref:`the faction The Covenant <gameplay_factions>`.
This is only available in BitNodes-10 and above, and is only available after defeating
BitNode-10 at least once. Sleeves purchased this way are **permanent** (they persist
This is only available in BitNodes-10. Sleeves purchased this way are **permanent** (they persist
through BitNodes). You can purchase up to 5 Duplicate Sleeves from The Covenant.
Synchronization

View File

@ -106,7 +106,7 @@ Check how much RAM a script requires to run with n threads
**nano [script]**
Create/Edit a script. The name of the script must end with a valid
extension: .script, .js, or .ns
extension: .script, or .js
**ps**

View File

@ -425,7 +425,7 @@ nano
$ nano [filename]
Opens up the specified file in the Text Editor. Only scripts (.script, .ns, .js) and
Opens up the specified file in the Text Editor. Only scripts (.script, .js) and
text files (.txt) can be edited. If the file does not already exist, then a new
empty file will be created.
@ -595,7 +595,7 @@ wget
$ wget [url] [target file]
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
The data can only be downloaded to a script (.script, .js) or a text file
(.txt). If the target file already exists, it will be overwritten by this command.
Note that will not be possible to download data from many websites because they

View File

@ -253,7 +253,6 @@ Here's what mine showed at the time I made this::
Take note of the following servers:
* |n00dles|
* |sigma-cosmetics|
* |joesguns|
* |nectar-net|

View File

@ -7,7 +7,7 @@ autocomplete() Netscript Function
:RAM cost: 0 GB
:param Object data: general data about the game you might want to autocomplete.
:param string[] args: current arguments. Minus `run script.ns`
:param string[] args: current arguments. Minus `run script.js`
data is an object with the following properties::
@ -34,6 +34,6 @@ autocomplete() Netscript Function
.. code-block:: bash
$ run demo.ns mega\t
$ run demo.js mega\t
// results in
$ run demo.ns megacorp
$ run demo.js megacorp

View File

@ -21,11 +21,11 @@ As of the time of writing this, a few browsers do not support `dynamic import <h
How to use ns2
----------------------
Working with ns2 scripts is the same as ns1 scripts. The only difference
is that ns2 scripts use the ".ns" or ".js" extension rather than ".script". E.g.::
is that ns2 scripts use the ".js" extension rather than ".script". E.g.::
$ nano foo.ns
$ run foo.ns -t 100 arg1 arg2 arg3
exec("foo.ns", "purchasedServer1", "100", "randomArg");
$ nano foo.js
$ run foo.js -t 100 arg1 arg2 arg3
exec("foo.js", "purchasedServer1", "100", "randomArg");
The caveat when using ns2 to write scripts is that your code must be
asynchronous. Furthermore, instead of using the global scope and executing your code
@ -66,9 +66,9 @@ Here is a summary of all rules you need to follow when writing Netscript JS code
* **Do not write any infinite loops without using a** :code:`sleep` **or one of the timed Netscript functions like** :code:`hack`. Doing so will freeze your game.
* Any global variable declared in a ns2 script is shared between all instances of that
script. For example, assume you write a script *foo.ns* and declared a global variable like so::
script. For example, assume you write a script *foo.js* and declared a global variable like so::
//foo.ns
//foo.js
let globalVariable;
export async function main(ns) {
@ -79,15 +79,15 @@ Here is a summary of all rules you need to follow when writing Netscript JS code
}
}
Then, you ran multiple instances of *foo.ns*::
Then, you ran multiple instances of *foo.js*::
$ run foo.ns 1
$ run foo.ns 1 2 3
$ run foo.ns 1 2 3 4 5
$ run foo.js 1
$ run foo.js 1 2 3
$ run foo.js 1 2 3 4 5
Then all three instances of foo.ns will share the same instance of :code:`globalVariable`.
Then all three instances of foo.js will share the same instance of :code:`globalVariable`.
(In this example, the value of :code:`globalVariable` will be set to 5 because the
last instance of *foo.ns* to run has 5 arguments. This means that all three instances of
last instance of *foo.js* to run has 5 arguments. This means that all three instances of
the script will repeatedly print the value 5).
These global variables can be thought of as `C++ static class members <https://www.tutorialspoint.com/cplusplus/cpp_static_members.htm>`_,
@ -117,7 +117,7 @@ early-hack-template.script
}
}
early-hack-template.ns
early-hack-template.js
.. code-block:: javascript
@ -151,8 +151,8 @@ You may have noticed that every new ns2 file will contains the following comment
* @param {NS} ns
**/
This comment is used to help the text editor autocomplete functions in the Netscript API. You can enabling it by pressing ctrl+space after `ns.`
This comment is used to help the text editor autocomplete functions in the Netscript API. You can enable it by pressing ctrl+space after `ns.`
.. image:: autocomplete.png
The comment can be safely removed but it is recommended to keep it as it will help you.
The comment can be safely removed but it is recommended to keep it as it will help you.

View File

@ -3,6 +3,9 @@ const greenworks = require("./greenworks");
const log = require("electron-log");
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
// here. Hey if it works it works.
const steamAchievements = greenworks.getAchievementNames();

View File

@ -6,6 +6,9 @@ const achievements = require("./achievements");
const menu = require("./menu");
const api = require("./api-server");
const cp = require("child_process");
const path = require("path");
const fs = require("fs");
const { fileURLToPath } = require("url");
const debug = process.argv.includes("--debug");
@ -24,15 +27,54 @@ async function createWindow(killall) {
window.show();
if (debug) window.webContents.openDevTools();
window.webContents.on("new-window", function (e, url) {
if (process.platform === "win32") {
cp.spawn("explorer", [url], { detached: true, stdio: "ignore" });
} else {
// make sure local urls stay in electron perimeter
if (url.substr(0, "file://".length) === "file://") {
return;
window.webContents.on("new-window", async function (e, url) {
// Let's make sure sure we have a proper url
let parsedUrl
try {
parsedUrl = new URL(url);
} catch (_) {
// This is an invalid url, let's just do nothing
log.warn(`Invalid url found: ${url}`)
e.preventDefault();
return;
}
// make sure local urls stay in electron perimeter
if (url.substr(0, "file://".length) === "file://") {
const requestedPath = fileURLToPath(url);
const appPath = path.parse(app.getAppPath());
const filePath = path.parse(requestedPath);
const isChild = filePath.dir.startsWith(appPath.dir);
// eslint-disable-next-line no-sync
const fileExists = fs.existsSync(requestedPath);
if (!isChild) {
// If we're not relative to our app's path let's abort
log.warn(`Requested path ${filePath.dir}${path.sep}${filePath.base} is not relative to the app: ${appPath.dir}${path.sep}${appPath.base}`)
e.preventDefault();
} else if (!fileExists) {
// If the file does not exist let's abort
log.warn(`Requested path ${filePath.dir}${path.sep}${filePath.base} does not exist`)
e.preventDefault();
}
return;
}
if (process.platform === "win32") {
// If we have parameters in the URL, explorer.exe won't register the URL and will open the file explorer instead.
let urlToOpen = parsedUrl.toString();
if (parsedUrl.search) {
log.log(`Cannot open a path with parameters: ${parsedUrl.search}`);
urlToOpen = urlToOpen.replace(parsedUrl.search, '');
// It would be possible to launch an URL with parameter using this, but it would mess up the process again...
// const escapedUri = parsedUrl.href.replace('&', '^&');
// cp.spawn("cmd.exe", ["/c", "start", escapedUri], { detached: true, stdio: "ignore" });
}
cp.spawn("explorer", [urlToOpen], { detached: true, stdio: "ignore" });
} else {
// and open every other protocols on the browser
utils.openExternal(url);
}

View File

@ -16,10 +16,18 @@ process.on('uncaughtException', function () {
process.exit(1);
});
if (greenworks.init()) {
log.info("Steam API has been initialized.");
} else {
log.warn("Steam API has failed to initialize.");
// We want to fail gracefully if we cannot connect to Steam
try {
if (greenworks.init()) {
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) {
@ -113,4 +121,13 @@ app.whenReady().then(async () => {
} else {
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']
});
}
});

View File

@ -48,6 +48,11 @@
</script>
<style>
html, body {
margin: 0;
padding: 0;
height: 100%;
}
body {
background-color: black;
}

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -9,15 +9,15 @@ Format a string.
<b>Signature:</b>
```typescript
sprintf(format: string, ...args: string[]): string;
sprintf(format: string, ...args: any[]): string;
```
## Parameters
| Parameter | Type | Description |
| --- | --- | --- |
| format | string | String to format. |
| args | string\[\] | Formating arguments. |
| Parameter | Type | Description |
| --------- | ------- | -------------------- |
| format | string | String to format. |
| args | any\[\] | Formating arguments. |
<b>Returns:</b>
@ -30,4 +30,3 @@ Formated text.
RAM cost: 0 GB
see: https://github.com/alexei/sprintf.js

View File

@ -9,15 +9,15 @@ Format a string with an array of arguments.
<b>Signature:</b>
```typescript
vsprintf(format: string, args: string[]): string;
vsprintf(format: string, args: any[]): string;
```
## Parameters
| Parameter | Type | Description |
| --- | --- | --- |
| format | string | String to format. |
| args | string\[\] | Formating arguments. |
| Parameter | Type | Description |
| --------- | ------- | -------------------- |
| format | string | String to format. |
| args | any\[\] | Formating arguments. |
<b>Returns:</b>
@ -30,4 +30,3 @@ Formated text.
RAM cost: 0 GB
see: https://github.com/alexei/sprintf.js

View File

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

View File

@ -16,7 +16,10 @@ interface UserInterface
| Method | Description |
| --- | --- |
| [getStyles()](./bitburner.userinterface.getstyles.md) | Get the current styles |
| [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 |
| [setStyles(newStyles)](./bitburner.userinterface.setstyles.md) | Sets the current styles |
| [setTheme(newTheme)](./bitburner.userinterface.settheme.md) | Sets the current theme |

View File

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

View File

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

View File

@ -209,7 +209,7 @@
},
"STOCK_1q": {
"ID": "STOCK_1q",
"Name": "Wolf of wall stree.",
"Name": "Wolf of wall street.",
"Description": "Make 1q on the stock market."
},
"DISCOUNT": {
@ -483,4 +483,5 @@
"Description": "Open the dev menu."
}
}
}
}

View File

@ -29,7 +29,7 @@ export function AchievementList({ achievements, playerAchievements }: IProps): J
const secret = data.filter(entry => !entry.unlockedOn && entry.achievement.Secret)
// Locked behind locked content (bitnode x)
const unavailable = data.filter(entry => !entry.unlockedOn && !entry.achievement.Secret && entry.achievement.Visible && entry.achievement.Visible());
const unavailable = data.filter(entry => !entry.unlockedOn && !entry.achievement.Secret && entry.achievement.Visible && !entry.achievement.Visible());
// Remaining achievements
const locked = data
@ -53,7 +53,7 @@ export function AchievementList({ achievements, playerAchievements }: IProps): J
</AccordionSummary>
<AccordionDetails sx={{ pt: 2 }}>
{unlocked.map(item => (
<AchievementEntry key={item.achievement.ID}
<AchievementEntry key={`unlocked_${item.achievement.ID}`}
achievement={item.achievement}
unlockedOn={item.unlockedOn}
cssFiltersUnlocked={cssPrimary}
@ -72,7 +72,7 @@ export function AchievementList({ achievements, playerAchievements }: IProps): J
</AccordionSummary>
<AccordionDetails sx={{ pt: 2 }}>
{locked.map(item => (
<AchievementEntry key={item.achievement.ID}
<AchievementEntry key={`locked_${item.achievement.ID}`}
achievement={item.achievement}
cssFiltersUnlocked={cssPrimary}
cssFiltersLocked={cssSecondary} />
@ -106,10 +106,10 @@ export function AchievementList({ achievements, playerAchievements }: IProps): J
<AccordionDetails>
<Typography color="secondary" sx={{ mt: 1 }}>
{secret.map(item => (
<>
<span key={`secret_${item.achievement.ID}`}>
<CorruptableText content={item.achievement.ID}></CorruptableText>
<br />
</>
</span>
))}
</Typography>
</AccordionDetails>

View File

@ -34,6 +34,7 @@ export interface Achievement {
Secret?: boolean;
Condition: () => boolean;
Visible?: () => boolean;
AdditionalUnlock?: string[]; // IDs of achievements that should be awarded when awarding this one
}
export interface PlayerAchievement {
@ -337,6 +338,7 @@ export const achievements: IMap<Achievement> = {
},
FIRST_HACKNET_NODE: {
...achievementData["FIRST_HACKNET_NODE"],
Icon: "node",
Condition: () => !hasHacknetServers(Player) && Player.hacknetNodes.length > 0,
},
"30_HACKNET_NODE": {
@ -509,12 +511,14 @@ export const achievements: IMap<Achievement> = {
Icon: "HASHNET",
Visible: () => hasAccessToSF(Player, 9),
Condition: () => hasHacknetServers(Player) && Player.hacknetNodes.length > 0,
AdditionalUnlock: [achievementData.FIRST_HACKNET_NODE.ID],
},
ALL_HACKNET_SERVER: {
...achievementData["ALL_HACKNET_SERVER"],
Icon: "HASHNETALL",
Visible: () => hasAccessToSF(Player, 9),
Condition: () => hasHacknetServers(Player) && Player.hacknetNodes.length === HacknetServerConstants.MaxServers,
AdditionalUnlock: [achievementData["30_HACKNET_NODE"].ID],
},
MAX_HACKNET_SERVER: {
...achievementData["MAX_HACKNET_SERVER"],
@ -536,12 +540,14 @@ export const achievements: IMap<Achievement> = {
}
return false;
},
AdditionalUnlock: [achievementData.MAX_HACKNET_NODE.ID],
},
HACKNET_SERVER_1B: {
...achievementData["HACKNET_SERVER_1B"],
Icon: "HASHNETMONEY",
Visible: () => hasAccessToSF(Player, 9),
Condition: () => hasHacknetServers(Player) && Player.moneySourceB.hacknet >= 1e9,
AdditionalUnlock: [achievementData.HACKNET_NODE_10M.ID],
},
MAX_CACHE: {
...achievementData["MAX_CACHE"],
@ -758,13 +764,14 @@ export const achievements: IMap<Achievement> = {
// { ID: "FLIGHT.EXE", Condition: () => Player.getHomeComputer().programs.includes(Programs.Flight.name) },
export function calculateAchievements(): void {
const availableAchievements = Object.values(achievements)
.filter((a) => a.Condition())
.map((a) => a.ID);
const playerAchievements = Player.achievements.map((a) => a.ID);
const newAchievements = availableAchievements.filter((a) => !playerAchievements.includes(a));
for (const id of newAchievements) {
const missingAchievements = Object.values(achievements)
.filter((a) => !playerAchievements.includes(a.ID) && a.Condition())
// callback returns array of achievement id and id of any in the additional list, flatmap means we have only a 1D array
.flatMap((a) => [a.ID, ...(a.AdditionalUnlock || [])]);
for (const id of missingAchievements) {
Player.giveAchievement(id);
}

View File

@ -15,24 +15,28 @@ const useStyles = makeStyles(() =>
createStyles({
level0: {
color: "red",
cursor: "pointer",
"&:hover": {
color: "#fff",
},
},
level1: {
color: "yellow",
cursor: "pointer",
"&:hover": {
color: "#fff",
},
},
level2: {
color: "#48d1cc",
cursor: "pointer",
"&:hover": {
color: "#fff",
},
},
level3: {
color: "blue",
cursor: "pointer",
"&:hover": {
color: "#fff",
},

View File

@ -12,7 +12,7 @@ export const ConsoleHelpText: {
} = {
helpList: [
"Use 'help [command]' to get more information about a particular Bladeburner console command.",
"",
" ",
" automate [var] [val] [hi/low] Configure simple automation for Bladeburner tasks",
" clear/cls Clear the console",
" 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",
" start [type] [name] Start a Bladeburner action/task",
" stop Stops your current Bladeburner action/task",
" ",
],
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 " +
"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.",
" ",
" 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 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:",
"",
" ",
" automate stamina 100 high",
" automate contract Tracking high",
" automate stamina 50 low",
" automate general 'Field Analysis' low",
"",
" ",
"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 " +
"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.",
" ",
],
clear: ["clear", "", "Clears the console"],
cls: ["cls", "", "Clears the console"],
clear: ["Usage: clear", " ", "Clears the console", " "],
cls: ["Usage: cls", " ", "Clears the console", " "],
help: [
"help [command]",
"",
"Usage: help [command]",
" ",
"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 " +
"about that particular command. For example:",
"",
" ",
" help automate",
"",
" ",
"will display specific information about using the automate console command",
" ",
],
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 " +
"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:",
"",
" ",
"[general, contracts, ops, blackops, events]",
"",
" ",
"The logging for these categories can be enabled or disabled like so:",
"",
" ",
" log dis contracts - Disables 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",
"",
" ",
"Logging can be universally enabled/disabled using the 'all' keyword:",
"",
" ",
" log dis all",
" log en all",
" ",
],
skill: [
"skill [action] [name]",
"",
"Usage: skill [action] [name]",
" ",
"Level or display information about your skills.",
"",
" ",
"To display information about all of your skills and your multipliers, use:",
"",
" ",
" skill list",
"",
" ",
"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 " +
"the name of the skill has whitespace, enclose the name of the skill in double quotation marks:",
"",
" ",
" skill list Reaper",
" skill list 'Digital Observer'",
"",
" ",
"This console command can also be used to level up skills:",
"",
" ",
" skill level [skill name]",
" ",
],
start: [
"start [type] [name]",
"",
"Usage: start [type] [name]",
" ",
"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 " +
"the name of the action has whitespace, enclose it in double quotation marks. " +
"Valid action types include:",
"",
" ",
"[general, contract, op, blackop]",
"",
" ",
"Examples:",
"",
" ",
" start contract Tracking",
" start op 'Undercover Operation'",
" ",
],
stop: ["stop", "", "Stop your current action and go idle."],
stop: ["Usage: stop", " ", "Stop your current action and go idle.", " "],
};

View File

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

View File

@ -23,7 +23,7 @@ const useStyles = makeStyles((theme: Theme) =>
width: "100%",
},
input: {
backgroundColor: "#000",
backgroundColor: theme.colors.backgroundsecondary,
},
nopadding: {
padding: theme.spacing(0),
@ -56,6 +56,7 @@ export function Console(props: IProps): React.ReactElement {
const classes = useStyles();
const [command, setCommand] = useState("");
const setRerender = useState(false)[1];
const consoleInput = useRef<HTMLInputElement>(null);
function handleCommandChange(event: React.ChangeEvent<HTMLInputElement>): void {
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 (
<Paper>
<Paper sx={{ p: 1 }}>
<Box sx={{
height: '60vh',
paddingBottom: '8px',
display: 'flex',
alignItems: 'stretch',
whiteSpace: 'pre-wrap',
}}>
}}
onClick={handleClick}>
<Box>
<Logs entries={[...props.bladeburner.consoleLogs]} />
</Box>
@ -149,6 +156,7 @@ export function Console(props: IProps): React.ReactElement {
autoFocus
tabIndex={1}
type="text"
inputRef={consoleInput}
value={command}
onChange={handleCommandChange}
onKeyDown={handleKeyDown}
@ -171,7 +179,7 @@ interface ILogProps {
entries: string[];
}
function Logs({entries}: ILogProps): React.ReactElement {
function Logs({ entries }: ILogProps): React.ReactElement {
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
@ -182,7 +190,7 @@ function Logs({entries}: ILogProps): React.ReactElement {
useEffect(() => {
scrollToBottom();
}, [entries]);
}, [entries.length]);
return (
<List sx={{ height: "100%", overflow: "auto", p: 1 }} ref={scrollHook}>

View File

@ -3,7 +3,6 @@ import { formatNumber, convertTimeMsToTimeElapsedString } from "../../utils/Stri
import { BladeburnerConstants } from "../data/Constants";
import { IPlayer } from "../../PersonObjects/IPlayer";
import { Money } from "../../ui/React/Money";
import { StatsTable } from "../../ui/React/StatsTable";
import { numeralWrapper } from "../../ui/numeralFormat";
import { Factions } from "../../Faction/Factions";
import { IRouter } from "../../ui/Router";
@ -44,138 +43,142 @@ export function Stats(props: IProps): React.ReactElement {
}
return (
<Paper sx={{ p: 1 }}>
<Box display="flex">
<Tooltip title={<Typography>Your rank within the Bladeburner division.</Typography>}>
<Typography>Rank: {formatNumber(props.bladeburner.rank, 2)}</Typography>
</Tooltip>
</Box>
<br />
<Box display="flex">
<Tooltip
title={
<Typography>
Performing actions will use up your stamina.
<br />
<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>
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>
}
>
<Paper sx={{ p: 1, overflowY: 'auto', overflowX: 'hidden', wordBreak: 'break-all' }}>
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 1, maxHeight: '60vh' }}>
<Box sx={{ alignSelf: 'flex-start', width: '100%' }}>
<Button onClick={() => setTravelOpen(true)} sx={{ width: '50%' }}>Travel</Button>
<Tooltip title={!inFaction ? <Typography>Rank 25 required.</Typography> : ""}>
<span>
<Button disabled={!inFaction} onClick={openFaction} sx={{ width: '50%' }}>
Faction
</Button>
</span>
</Tooltip>
<TravelModal open={travelOpen} onClose={() => setTravelOpen(false)} bladeburner={props.bladeburner} />
</Box>
<Box display="flex">
<Tooltip title={<Typography>Your rank within the Bladeburner division.</Typography>}>
<Typography>Rank: {formatNumber(props.bladeburner.rank, 2)}</Typography>
</Tooltip>
</Box>
<br />
<Box display="flex">
<Tooltip
title={
<Typography>
Bonus time:{" "}
{convertTimeMsToTimeElapsedString(
(props.bladeburner.storedCycles / BladeburnerConstants.CyclesPerSecond) * 1000,
)}
Performing actions will use up your stamina.
<br />
<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>
</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 />
</>
)}
<Typography>Skill Points: {formatNumber(props.bladeburner.skillPoints, 0)}</Typography>
<br />
<StatsTable
rows={[
["Aug. Success Chance mult: ", formatNumber(props.player.bladeburner_success_chance_mult * 100, 1) + "%"],
["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} />
Aug. Max Stamina mult: {formatNumber(props.player.bladeburner_max_stamina_mult * 100, 1)}%
<br />
Aug. Stamina Gain mult: {formatNumber(props.player.bladeburner_stamina_gain_mult * 100, 1)}%
<br />
Aug. Field Analysis mult: {formatNumber(props.player.bladeburner_analysis_mult * 100, 1)}%
</Typography>
</Box>
</Paper>
);
}

View File

@ -157,7 +157,7 @@ export const FactionInfos: IMap<FactionInfo> = {
(
<>
MegaCorp does what no other dares to do. We imagine. We create. We invent. We create what others have never even
dreamed of. Our work fills the world's needs for food, water, power, and transporation on an unprecendented
dreamed of. Our work fills the world's needs for food, water, power, and transportation on an unprecendented
scale, in ways that no other company can.
<br />
<br />

View File

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

View File

@ -182,7 +182,7 @@ export function HacknetNodeElem(props: IProps): React.ReactElement {
</TableCell>
<TableCell colSpan={2}>
<Typography>
<Money money={node.totalMoneyGenerated} player={props.player} /> (
<Money money={node.totalMoneyGenerated} /> (
<MoneyRate money={node.moneyGainRatePerSecond} />)
</Typography>
</TableCell>

View File

@ -27,6 +27,7 @@ import { GetServer } from "../../Server/AllServers";
import Typography from "@mui/material/Typography";
import Grid from "@mui/material/Grid";
import Button from "@mui/material/Button";
import { Box } from "@mui/material";
interface IProps {
player: IPlayer;
@ -136,7 +137,7 @@ export function HacknetRoot(props: IProps): React.ReactElement {
{hasHacknetServers(props.player) && <Button onClick={() => setOpen(true)}>Spend Hashes on Upgrades</Button>}
<Grid container>{nodes}</Grid>
<Box sx={{ display: 'grid', width: 'fit-content', gridTemplateColumns: 'repeat(3, 1fr)' }}>{nodes}</Box>
<HashUpgradeModal open={open} onClose={() => setOpen(false)} />
</>
);

View File

@ -114,7 +114,7 @@ export const Literatures: IMap<Literature> = {};
"as working for any military/defense organization or conducting any bioengineering, computing, or robotics related research.<br><br>" +
"Unfortunately, many believe that not all of the rogue MK-VI Synthoids from the Uprising were found and destroyed, " +
"and that many of them are blending in as normal humans in society today. In response, many nations have created " +
"Bladeburner divisions, special military branches that are tasked with investigating and dealing with any Synthoid threads.<br><br>" +
"Bladeburner divisions, special military branches that are tasked with investigating and dealing with any Synthoid threats.<br><br>" +
"To this day, tensions still exist between the remaining Synthoids and humans as a result of the Uprising.<br><br>" +
"Nobody knows what happened to the terrorist group Ascendis Totalis.";
Literatures[fn] = new Literature(title, fn, txt);
@ -309,7 +309,7 @@ export const Literatures: IMap<Literature> = {};
fn = LiteratureNames.CodedIntelligence;
txt =
"Tremendous progress has been made in the field of Artificial Intelligence over the past few decades. " +
"Our autonomous vehicles and transporation systems. The electronic personal assistants that control our everyday lives. " +
"Our autonomous vehicles and transportation systems. The electronic personal assistants that control our everyday lives. " +
"Medical, service, and manufacturing robots. All of these are examples of how far AI has come and how much it has " +
"improved our daily lives. However, the question still remains of whether AI will ever be advanced enough to re-create " +
"human intelligence.<br><br>" +

View File

@ -10,6 +10,7 @@ import { CoinFlip } from "../../Casino/CoinFlip";
import { Roulette } from "../../Casino/Roulette";
import { SlotMachine } from "../../Casino/SlotMachine";
import { IPlayer } from "../../PersonObjects/IPlayer";
import { Box } from "@mui/material";
enum GameType {
None = "none",
@ -33,15 +34,12 @@ export function CasinoLocation(props: IProps): React.ReactElement {
return (
<>
{game === GameType.None && (
<>
<Box sx={{ display: 'grid', width: 'fit-content' }}>
<Button onClick={() => updateGame(GameType.Coin)}>Play coin flip</Button>
<br />
<Button onClick={() => updateGame(GameType.Slots)}>Play slots</Button>
<br />
<Button onClick={() => updateGame(GameType.Roulette)}>Play roulette</Button>
<br />
<Button onClick={() => updateGame(GameType.Blackjack)}>Play blackjack</Button>
</>
</Box>
)}
{game !== GameType.None && (
<>

View File

@ -3,7 +3,7 @@
*
* This subcomponent renders all of the buttons for applying to jobs at a company
*/
import React, { useState } from "react";
import React, { useState, useEffect } from "react";
import Typography from "@mui/material/Typography";
import Button from "@mui/material/Button";
import Tooltip from "@mui/material/Tooltip";
@ -36,6 +36,11 @@ export function CompanyLocation(props: IProps): React.ReactElement {
function rerender(): void {
setRerender((old) => !old);
}
useEffect(() => {
const id = setInterval(rerender, 200);
return () => clearInterval(id);
}, []);
/**
* We'll keep a reference to the Company that this component is being rendered for,
* so we don't have to look it up every time
@ -221,108 +226,114 @@ export function CompanyLocation(props: IProps): React.ReactElement {
</Box>
<Typography>-------------------------</Typography>
<br />
<Button onClick={work}>Work</Button>
&nbsp;&nbsp;&nbsp;&nbsp;
<Button onClick={() => setQuitOpen(true)}>Quit</Button>
<QuitJobModal
locName={props.locName}
company={company}
onQuit={rerender}
open={quitOpen}
onClose={() => setQuitOpen(false)}
/>
</>
)}
<br />
{company.hasAgentPositions() && (
<ApplyToJobButton
company={company}
entryPosType={CompanyPositions[posNames.AgentCompanyPositions[0]]}
onClick={applyForAgentJob}
text={"Apply for Agent Job"}
/>
)}
{company.hasBusinessConsultantPositions() && (
<ApplyToJobButton
company={company}
entryPosType={CompanyPositions[posNames.BusinessConsultantCompanyPositions[0]]}
onClick={applyForBusinessConsultantJob}
text={"Apply for Business Consultant Job"}
/>
)}
{company.hasBusinessPositions() && (
<ApplyToJobButton
company={company}
entryPosType={CompanyPositions[posNames.BusinessCompanyPositions[0]]}
onClick={applyForBusinessJob}
text={"Apply for Business Job"}
/>
)}
{company.hasEmployeePositions() && (
<ApplyToJobButton
company={company}
entryPosType={CompanyPositions[posNames.MiscCompanyPositions[1]]}
onClick={applyForEmployeeJob}
text={"Apply to be an Employee"}
/>
)}
{company.hasEmployeePositions() && (
<ApplyToJobButton
company={company}
entryPosType={CompanyPositions[posNames.PartTimeCompanyPositions[1]]}
onClick={applyForPartTimeEmployeeJob}
text={"Apply to be a part-time Employee"}
/>
)}
{company.hasITPositions() && (
<ApplyToJobButton
company={company}
entryPosType={CompanyPositions[posNames.ITCompanyPositions[0]]}
onClick={applyForItJob}
text={"Apply for IT Job"}
/>
)}
{company.hasSecurityPositions() && (
<ApplyToJobButton
company={company}
entryPosType={CompanyPositions[posNames.SecurityCompanyPositions[2]]}
onClick={applyForSecurityJob}
text={"Apply for Security Job"}
/>
)}
{company.hasSoftwareConsultantPositions() && (
<ApplyToJobButton
company={company}
entryPosType={CompanyPositions[posNames.SoftwareConsultantCompanyPositions[0]]}
onClick={applyForSoftwareConsultantJob}
text={"Apply for Software Consultant Job"}
/>
)}
{company.hasSoftwarePositions() && (
<ApplyToJobButton
company={company}
entryPosType={CompanyPositions[posNames.SoftwareCompanyPositions[0]]}
onClick={applyForSoftwareJob}
text={"Apply for Software Job"}
/>
)}
{company.hasWaiterPositions() && (
<ApplyToJobButton
company={company}
entryPosType={CompanyPositions[posNames.MiscCompanyPositions[0]]}
onClick={applyForWaiterJob}
text={"Apply to be a Waiter"}
/>
)}
{company.hasWaiterPositions() && (
<ApplyToJobButton
company={company}
entryPosType={CompanyPositions[posNames.PartTimeCompanyPositions[0]]}
onClick={applyForPartTimeWaiterJob}
text={"Apply to be a part-time Waiter"}
/>
)}
{location.infiltrationData != null && <Button onClick={startInfiltration}>Infiltrate Company</Button>}
<Box sx={{ display: 'grid', width: 'fit-content' }}>
{isEmployedHere && (
<Box sx={{ display: 'grid', gridTemplateColumns: '1fr 1fr' }}>
<Button onClick={work}>Work</Button>
<Button onClick={() => setQuitOpen(true)}>Quit</Button>
<QuitJobModal
locName={props.locName}
company={company}
onQuit={rerender}
open={quitOpen}
onClose={() => setQuitOpen(false)}
/>
</Box>
)
}
{company.hasAgentPositions() && (
<ApplyToJobButton
company={company}
entryPosType={CompanyPositions[posNames.AgentCompanyPositions[0]]}
onClick={applyForAgentJob}
text={"Apply for Agent Job"}
/>
)}
{company.hasBusinessConsultantPositions() && (
<ApplyToJobButton
company={company}
entryPosType={CompanyPositions[posNames.BusinessConsultantCompanyPositions[0]]}
onClick={applyForBusinessConsultantJob}
text={"Apply for Business Consultant Job"}
/>
)}
{company.hasBusinessPositions() && (
<ApplyToJobButton
company={company}
entryPosType={CompanyPositions[posNames.BusinessCompanyPositions[0]]}
onClick={applyForBusinessJob}
text={"Apply for Business Job"}
/>
)}
{company.hasEmployeePositions() && (
<ApplyToJobButton
company={company}
entryPosType={CompanyPositions[posNames.MiscCompanyPositions[1]]}
onClick={applyForEmployeeJob}
text={"Apply to be an Employee"}
/>
)}
{company.hasEmployeePositions() && (
<ApplyToJobButton
company={company}
entryPosType={CompanyPositions[posNames.PartTimeCompanyPositions[1]]}
onClick={applyForPartTimeEmployeeJob}
text={"Apply to be a part-time Employee"}
/>
)}
{company.hasITPositions() && (
<ApplyToJobButton
company={company}
entryPosType={CompanyPositions[posNames.ITCompanyPositions[0]]}
onClick={applyForItJob}
text={"Apply for IT Job"}
/>
)}
{company.hasSecurityPositions() && (
<ApplyToJobButton
company={company}
entryPosType={CompanyPositions[posNames.SecurityCompanyPositions[2]]}
onClick={applyForSecurityJob}
text={"Apply for Security Job"}
/>
)}
{company.hasSoftwareConsultantPositions() && (
<ApplyToJobButton
company={company}
entryPosType={CompanyPositions[posNames.SoftwareConsultantCompanyPositions[0]]}
onClick={applyForSoftwareConsultantJob}
text={"Apply for Software Consultant Job"}
/>
)}
{company.hasSoftwarePositions() && (
<ApplyToJobButton
company={company}
entryPosType={CompanyPositions[posNames.SoftwareCompanyPositions[0]]}
onClick={applyForSoftwareJob}
text={"Apply for Software Job"}
/>
)}
{company.hasWaiterPositions() && (
<ApplyToJobButton
company={company}
entryPosType={CompanyPositions[posNames.MiscCompanyPositions[0]]}
onClick={applyForWaiterJob}
text={"Apply to be a Waiter"}
/>
)}
{company.hasWaiterPositions() && (
<ApplyToJobButton
company={company}
entryPosType={CompanyPositions[posNames.PartTimeCompanyPositions[0]]}
onClick={applyForPartTimeWaiterJob}
text={"Apply to be a part-time Waiter"}
/>
)}
{location.infiltrationData != null && <Button onClick={startInfiltration}>Infiltrate Company</Button>}
</Box>
</>
);
}

View File

@ -16,6 +16,7 @@ import { Server } from "../../Server/Server";
import { Money } from "../../ui/React/Money";
import { IRouter } from "../../ui/Router";
import { serverMetadata } from "../../Server/data/servers";
import { Box } from "@mui/material";
type IProps = {
loc: Location;
@ -56,7 +57,7 @@ export function GymLocation(props: IProps): React.ReactElement {
const cost = CONSTANTS.ClassGymBaseCost * calculateCost();
return (
<>
<Box sx={{ display: 'grid', width: 'fit-content' }}>
<Button onClick={trainStrength}>
Train Strength (<Money money={cost} player={props.p} /> / sec)
</Button>
@ -72,6 +73,6 @@ export function GymLocation(props: IProps): React.ReactElement {
<Button onClick={trainAgility}>
Train Agility (<Money money={cost} player={props.p} /> / sec)
</Button>
</>
</Box>
);
}

View File

@ -11,6 +11,7 @@ import { Crimes } from "../../Crime/Crimes";
import { numeralWrapper } from "../../ui/numeralFormat";
import { use } from "../../ui/Context";
import { Box } from "@mui/material";
export function SlumsLocation(): React.ReactElement {
const player = use.Player();
@ -113,73 +114,61 @@ export function SlumsLocation(): React.ReactElement {
const heistChance = Crimes.Heist.successRate(player);
return (
<>
<Box sx={{ display: 'grid', width: 'fit-content' }}>
<Tooltip title={<>Attempt to shoplift from a low-end retailer</>}>
<Button onClick={shoplift}>
Shoplift ({numeralWrapper.formatPercentage(shopliftChance)} chance of success)
</Button>
</Tooltip>
<br />
<Tooltip title={<>Attempt to commit armed robbery on a high-end store</>}>
<Button onClick={robStore}>
Rob store ({numeralWrapper.formatPercentage(robStoreChance)} chance of success)
</Button>
</Tooltip>
<br />
<Tooltip title={<>Attempt to mug a random person on the street</>}>
<Button onClick={mug}>Mug someone ({numeralWrapper.formatPercentage(mugChance)} chance of success)</Button>
</Tooltip>
<br />
<Tooltip title={<>Attempt to rob property from someone's house</>}>
<Button onClick={larceny}>Larceny ({numeralWrapper.formatPercentage(larcenyChance)} chance of success)</Button>
</Tooltip>
<br />
<Tooltip title={<>Attempt to deal drugs</>}>
<Button onClick={dealDrugs}>
Deal Drugs ({numeralWrapper.formatPercentage(drugsChance)} chance of success)
</Button>
</Tooltip>
<br />
<Tooltip title={<>Attempt to forge corporate bonds</>}>
<Button onClick={bondForgery}>
Bond Forgery ({numeralWrapper.formatPercentage(bondChance)} chance of success)
</Button>
</Tooltip>
<br />
<Tooltip title={<>Attempt to smuggle illegal arms into the city</>}>
<Button onClick={traffickArms}>
Traffick illegal Arms ({numeralWrapper.formatPercentage(armsChance)} chance of success)
</Button>
</Tooltip>
<br />
<Tooltip title={<>Attempt to murder a random person on the street</>}>
<Button onClick={homicide}>
Homicide ({numeralWrapper.formatPercentage(homicideChance)} chance of success)
</Button>
</Tooltip>
<br />
<Tooltip title={<>Attempt to commit grand theft auto</>}>
<Button onClick={grandTheftAuto}>
Grand theft Auto ({numeralWrapper.formatPercentage(gtaChance)} chance of success)
</Button>
</Tooltip>
<br />
<Tooltip title={<>Attempt to kidnap and ransom a high-profile-target</>}>
<Button onClick={kidnap}>
Kidnap and Ransom ({numeralWrapper.formatPercentage(kidnapChance)} chance of success)
</Button>
</Tooltip>
<br />
<Tooltip title={<>Attempt to assassinate a high-profile target</>}>
<Button onClick={assassinate}>
Assassinate ({numeralWrapper.formatPercentage(assassinateChance)} chance of success)
</Button>
</Tooltip>
<br />
<Tooltip title={<>Attempt to pull off the ultimate heist</>}>
<Button onClick={heist}>Heist ({numeralWrapper.formatPercentage(heistChance)} chance of success)</Button>
</Tooltip>
<br />
</>
</Box>
);
}

View File

@ -81,7 +81,7 @@ export function SpecialLocation(props: IProps): React.ReactElement {
return <></>;
}
const text = inBladeburner ? "Enter Bladeburner Headquarters" : "Apply to Bladeburner Division";
return <Button onClick={handleBladeburner}>{text}</Button>;
return <><br/><Button onClick={handleBladeburner}>{text}</Button></>;
}
function renderNoodleBar(): React.ReactElement {

View File

@ -18,6 +18,7 @@ import { Money } from "../../ui/React/Money";
import { use } from "../../ui/Context";
import { PurchaseServerModal } from "./PurchaseServerModal";
import { numeralWrapper } from "../../ui/numeralFormat";
import { Box } from "@mui/material";
interface IServerProps {
ram: number;
@ -70,7 +71,9 @@ export function TechVendorLocation(props: IProps): React.ReactElement {
return (
<>
<br />
{purchaseServerButtons}
<Box sx={{ display: 'grid', width: 'fit-content' }}>
{purchaseServerButtons}
</Box>
<br />
<Typography>
<i>"You can order bigger servers via scripts. We don't take custom orders in person."</i>

View File

@ -15,6 +15,7 @@ import { Server } from "../../Server/Server";
import { Money } from "../../ui/React/Money";
import { use } from "../../ui/Context";
import { Box } from "@mui/material";
type IProps = {
loc: Location;
@ -72,45 +73,40 @@ export function UniversityLocation(props: IProps): React.ReactElement {
const earnCharismaExpTooltip = `Gain charisma experience!`;
return (
<>
<Box sx={{ display: 'grid', width: 'fit-content' }}>
<Tooltip title={earnHackingExpTooltip}>
<Button onClick={study}>Study Computer Science (free)</Button>
</Tooltip>
<br />
<Tooltip title={earnHackingExpTooltip}>
<Button onClick={dataStructures}>
Take Data Structures course (
<Money money={dataStructuresCost} player={player} /> / sec)
</Button>
</Tooltip>
<br />
<Tooltip title={earnHackingExpTooltip}>
<Button onClick={networks}>
Take Networks course (
<Money money={networksCost} player={player} /> / sec)
</Button>
</Tooltip>
<br />
<Tooltip title={earnHackingExpTooltip}>
<Button onClick={algorithms}>
Take Algorithms course (
<Money money={algorithmsCost} player={player} /> / sec)
</Button>
</Tooltip>
<br />
<Tooltip title={earnCharismaExpTooltip}>
<Button onClick={management}>
Take Management course (
<Money money={managementCost} player={player} /> / sec)
</Button>
</Tooltip>
<br />
<Tooltip title={earnCharismaExpTooltip}>
<Button onClick={leadership}>
Take Leadership course (
<Money money={leadershipCost} player={player} /> / sec)
</Button>
</Tooltip>
</>
</Box>
);
}

View File

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

View File

@ -33,9 +33,9 @@ export class WorkerScript {
delay: number | null = null;
/**
* Holds the Promise resolve() function for when the script is "blocked" by an async op
* Holds the Promise reject() function while the script is "blocked" by an async op
*/
delayResolve?: () => void;
delayReject?: (reason?: any) => void;
/**
* Stores names of all functions that have logging disabled

View File

@ -138,8 +138,8 @@ function killNetscriptDelay(workerScript: WorkerScript): void {
if (workerScript instanceof WorkerScript) {
if (workerScript.delay) {
clearTimeout(workerScript.delay);
if (workerScript.delayResolve) {
workerScript.delayResolve();
if (workerScript.delayReject) {
workerScript.delayReject(workerScript);
}
}
}

View File

@ -3,12 +3,17 @@ import { GetServer } from "./Server/AllServers";
import { WorkerScript } from "./Netscript/WorkerScript";
export function netscriptDelay(time: number, workerScript: WorkerScript): Promise<void> {
return new Promise(function (resolve) {
return new Promise(function (resolve, reject) {
workerScript.delay = window.setTimeout(() => {
workerScript.delay = null;
resolve();
workerScript.delayReject = undefined;
if (workerScript.env.stopFlag)
reject(workerScript);
else
resolve();
}, time);
workerScript.delayResolve = resolve;
workerScript.delayReject = reject;
});
}

View File

@ -172,7 +172,7 @@ export function NetscriptFunctions(workerScript: WorkerScript): NS {
throw makeRuntimeRejectMsg(
workerScript,
`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`,
);
}
@ -335,16 +335,13 @@ export function NetscriptFunctions(workerScript: WorkerScript): NS {
workerScript.log(
"hack",
() =>
`Executing ${hostname} in ${convertTimeMsToTimeElapsedString(
`Executing on '${server.hostname}' in ${convertTimeMsToTimeElapsedString(
hackingTime * 1000,
true,
)} (t=${numeralWrapper.formatThreads(threads)})`,
);
return netscriptDelay(hackingTime * 1000, workerScript).then(function () {
if (workerScript.env.stopFlag) {
return Promise.reject(workerScript);
}
const hackChance = calculateHackingChance(server, Player);
const rand = Math.random();
let expGainedOnSuccess = calculateHackingExpGain(server, Player) * threads;
@ -352,7 +349,7 @@ export function NetscriptFunctions(workerScript: WorkerScript): NS {
if (rand < hackChance) {
// Success!
const percentHacked = calculatePercentMoneyHacked(server, Player);
let maxThreadNeeded = Math.ceil((1 / percentHacked) * (server.moneyAvailable / server.moneyMax));
let maxThreadNeeded = Math.ceil(1 / percentHacked);
if (isNaN(maxThreadNeeded)) {
// Server has a 'max money' of 0 (probably). We'll set this to an arbitrarily large value
maxThreadNeeded = 1e6;
@ -432,8 +429,10 @@ export function NetscriptFunctions(workerScript: WorkerScript): NS {
throw makeRuntimeErrorMsg(funcName, `${argName} should be a string`);
},
number: (funcName: string, argName: string, v: any): number => {
if (typeof v === "number") return v;
if (!isNaN(v) && !isNaN(parseFloat(v))) return parseFloat(v);
if (!isNaN(v)) {
if (typeof v === "number") return v;
if (!isNaN(parseFloat(v))) return parseFloat(v);
}
throw makeRuntimeErrorMsg(funcName, `${argName} should be a number`);
},
boolean: (v: any): boolean => {
@ -613,9 +612,6 @@ export function NetscriptFunctions(workerScript: WorkerScript): NS {
)} (t=${numeralWrapper.formatThreads(threads)}).`,
);
return netscriptDelay(growTime * 1000, workerScript).then(function () {
if (workerScript.env.stopFlag) {
return Promise.reject(workerScript);
}
const moneyBefore = server.moneyAvailable <= 0 ? 1 : server.moneyAvailable;
processSingleServerGrowth(server, threads, Player, host.cpuCores);
const moneyAfter = server.moneyAvailable;
@ -684,7 +680,6 @@ export function NetscriptFunctions(workerScript: WorkerScript): NS {
)} (t=${numeralWrapper.formatThreads(threads)})`,
);
return netscriptDelay(weakenTime * 1000, workerScript).then(function () {
if (workerScript.env.stopFlag) return Promise.reject(workerScript);
const host = GetServer(workerScript.hostname);
if (host === null) {
workerScript.log("weaken", () => "Server is null, did it die?");
@ -697,8 +692,7 @@ export function NetscriptFunctions(workerScript: WorkerScript): NS {
workerScript.log(
"weaken",
() =>
`'${server.hostname}' security level weakened to ${
server.hackDifficulty
`'${server.hostname}' security level weakened to ${server.hackDifficulty
}. Gained ${numeralWrapper.formatExp(expGain)} hacking exp (t=${numeralWrapper.formatThreads(threads)})`,
);
workerScript.scriptRef.onlineExpGained += expGain;
@ -1901,7 +1895,7 @@ export function NetscriptFunctions(workerScript: WorkerScript): NS {
const iport = helper.getValidPort("clearPort", port);
return iport.clear();
},
getPortHandle: function (port: any): any {
getPortHandle: function (port: any): IPort {
updateDynamicRam("getPortHandle", getRamCost(Player, "getPortHandle"));
const iport = helper.getValidPort("getPortHandle", port);
return iport;
@ -2011,7 +2005,7 @@ export function NetscriptFunctions(workerScript: WorkerScript): NS {
return calculateGrowTime(server, Player) * 1000;
},
getWeakenTime: function (hostname: any): any {
getWeakenTime: function (hostname: any = workerScript.hostname): any {
updateDynamicRam("getWeakenTime", getRamCost(Player, "getWeakenTime"));
const server = safeGetServer(hostname, "getWeakenTime");
if (!(server instanceof Server)) {

View File

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

View File

@ -311,9 +311,6 @@ export function NetscriptCorporation(
const job = helper.string("assignJob", "job", ajob);
const employee = getEmployee(divisionName, cityName, employeeName);
return netscriptDelay(1000, workerScript).then(function () {
if (workerScript.env.stopFlag) {
return Promise.reject(workerScript);
}
return Promise.resolve(AssignJob(employee, job));
});
},
@ -344,9 +341,6 @@ export function NetscriptCorporation(
(60 * 1000) / (player.hacking_speed_mult * calculateIntelligenceBonus(player.intelligence, 1)),
workerScript,
).then(function () {
if (workerScript.env.stopFlag) {
return Promise.reject(workerScript);
}
return Promise.resolve(ThrowParty(corporation, office, costPerEmployee));
});
},
@ -359,9 +353,6 @@ export function NetscriptCorporation(
(60 * 1000) / (player.hacking_speed_mult * calculateIntelligenceBonus(player.intelligence, 1)),
workerScript,
).then(function () {
if (workerScript.env.stopFlag) {
return Promise.reject(workerScript);
}
return Promise.resolve(BuyCoffee(corporation, getDivision(divisionName), getOffice(divisionName, cityName)));
});
},

View File

@ -111,7 +111,7 @@ export function NetscriptHacknet(player: IPlayer, workerScript: WorkerScript, he
}
const node = getHacknetNode(i, "upgradeCache");
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;
}
const res = purchaseCacheUpgrade(player, node, n);
@ -138,7 +138,7 @@ export function NetscriptHacknet(player: IPlayer, workerScript: WorkerScript, he
}
const node = getHacknetNode(i, "upgradeCache");
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 node.calculateCacheUpgradeCost(n);

View File

@ -611,9 +611,6 @@ export function NetscriptSingularity(
);
return netscriptDelay(installTime, workerScript).then(function () {
if (workerScript.env.stopFlag) {
return Promise.reject(workerScript);
}
workerScript.log("installBackdoor", () => `Successfully installed backdoor on '${server.hostname}'`);
server.backdoorInstalled = true;

View File

@ -38,9 +38,6 @@ export function NetscriptStanek(player: IPlayer, workerScript: WorkerScript, hel
if (!fragment) throw helper.makeRuntimeErrorMsg("stanek.charge", `No fragment with root (${rootX}, ${rootY}).`);
const time = staneksGift.inBonus() ? 200 : 1000;
return netscriptDelay(time, workerScript).then(function () {
if (workerScript.env.stopFlag) {
return Promise.reject(workerScript);
}
const charge = staneksGift.charge(player, fragment, workerScript.scriptRef.threads);
workerScript.log("stanek.charge", () => `Charged fragment for ${charge} charge.`);
return Promise.resolve();

View File

@ -2,10 +2,11 @@ import { INetscriptHelper } from "./INetscriptHelper";
import { WorkerScript } from "../Netscript/WorkerScript";
import { IPlayer } from "../PersonObjects/IPlayer";
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 { ThemeEvents } from "../ui/React/Theme";
import { defaultTheme } from "../Settings/Themes";
import { defaultStyles } from "../Settings/Styles";
export function NetscriptUserInterface(
player: IPlayer,
@ -18,6 +19,11 @@ export function NetscriptUserInterface(
return { ...Settings.theme };
},
getStyles: function (): IStyleSettings {
helper.updateDynamicRam("getStyles", getRamCost(player, "ui", "getStyles"));
return { ...Settings.styles };
},
setTheme: function (newTheme: UserInterfaceTheme): void {
helper.updateDynamicRam("setTheme", getRamCost(player, "ui", "setTheme"));
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 {
helper.updateDynamicRam("resetTheme", getRamCost(player, "ui", "resetTheme"));
Settings.theme = defaultTheme;
Settings.theme = { ...defaultTheme };
ThemeEvents.emit();
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`);
}
}
}

View File

@ -151,6 +151,24 @@ function _getScriptUrls(script: Script, scripts: Script[], seen: Script[]): Scri
start: node.source.range[0] + 1,
end: node.source.range[1] - 1
});
},
ExportNamedDeclaration(node: any) {
if (node.source) {
importNodes.push({
filename: node.source.value,
start: node.source.range[0] + 1,
end: node.source.range[1] - 1
});
}
},
ExportAllDeclaration(node: any) {
if (node.source) {
importNodes.push({
filename: node.source.value,
start: node.source.range[0] + 1,
end: node.source.range[1] - 1
});
}
}
});
// Sort the nodes from last start index to first. This replaces the last import with a blob first,

View File

@ -946,11 +946,6 @@ export function workForFaction(this: IPlayer, numCycles: number): boolean {
default:
break;
}
let favorMult = 1 + faction.favor / 100;
if (isNaN(favorMult)) {
favorMult = 1;
}
this.workRepGainRate *= favorMult;
this.workRepGainRate *= BitNodeMultipliers.FactionWorkRepGain;
//Cap the number of cycles being processed to whatever would put you at limit (20 hours)
@ -1821,7 +1816,7 @@ export function getNextCompanyPosition(
}
export function quitJob(this: IPlayer, company: string): void {
if (this.isWorking == true && this.workType == "Working for Company" && this.companyName == company) {
if (this.isWorking == true && this.workType.includes("Working for Company") && this.companyName == company) {
this.isWorking = false;
this.companyName = "";
}

View File

@ -2,7 +2,7 @@ import React, { useState, useEffect } from "react";
import { use } from "../../ui/Context";
import { getAvailableCreatePrograms } from "../ProgramHelpers";
import { Tooltip, Typography } from "@mui/material";
import { Box, Tooltip, Typography } from "@mui/material";
import Button from "@mui/material/Button";
export const ProgramsSeen: string[] = [];
@ -38,27 +38,28 @@ export function ProgramsRoot(): React.ReactElement {
time. Your progress will be saved and you can continue later.
</Typography>
{programs.map((program) => {
const create = program.create;
if (create === null) return <></>;
<Box sx={{ display: 'grid', width: 'fit-content' }}>
{programs.map((program) => {
const create = program.create;
if (create === null) return <></>;
return (
<React.Fragment key={program.name}>
<Tooltip title={create.tooltip}>
<Button
sx={{ my: 1 }}
onClick={(event) => {
if (!event.isTrusted) return;
player.startCreateProgramWork(router, program.name, create.time, create.level);
}}
>
{program.name}
</Button>
</Tooltip>
<br />
</React.Fragment>
);
})}
return (
<React.Fragment key={program.name}>
<Tooltip title={create.tooltip}>
<Button
sx={{ my: 1 }}
onClick={(event) => {
if (!event.isTrusted) return;
player.startCreateProgramWork(router, program.name, create.time, create.level);
}}
>
{program.name}
</Button>
</Tooltip>
</React.Fragment>
);
})}
</Box>
</>
);
}

View File

@ -43,10 +43,10 @@ class BitburnerSaveObject {
StaneksGiftSave = "";
SaveTimestamp = "";
getSaveString(): string {
getSaveString(excludeRunningScripts = false): string {
this.PlayerSave = JSON.stringify(Player);
this.AllServersSave = saveAllServers();
this.AllServersSave = saveAllServers(excludeRunningScripts);
this.CompaniesSave = JSON.stringify(Companies);
this.FactionsSave = JSON.stringify(Factions);
this.AliasesSave = JSON.stringify(Aliases);
@ -68,7 +68,7 @@ class BitburnerSaveObject {
}
saveGame(emitToastEvent = true): void {
const saveString = this.getSaveString();
const saveString = this.getSaveString(Settings.ExcludeRunningScriptsFromSave);
save(saveString)
.then(() => {
@ -80,7 +80,7 @@ class BitburnerSaveObject {
}
exportGame(): void {
const saveString = this.getSaveString();
const saveString = this.getSaveString(Settings.ExcludeRunningScriptsFromSave);
// Save file name is based on current timestamp and BitNode
const epochTime = Math.round(Date.now() / 1000);

View File

@ -118,7 +118,7 @@ export class Script {
*/
saveScript(player: IPlayer, filename: string, code: string, hostname: string, otherScripts: Script[]): void {
// Update code and filename
this.code = code.replace(/^\s+|\s+$/g, "");
this.code = Script.formatCode(code);
this.filename = filename;
this.server = hostname;
@ -158,6 +158,15 @@ export class Script {
s.rehash();
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;

View File

@ -109,6 +109,32 @@ interface RunningScript {
threads: number;
}
/**
* Interface of a netscript port
* @public
*/
export interface IPort {
/** write data to the port and removes and returns first element if full */
write: (value: any) => any;
/** add data to port if not full.
* @returns true if added and false if full and not added */
tryWrite: (value: any) => boolean;
/** reads and removes first element from port
* if no data in port returns "NULL PORT DATA"
*/
read: () => any;
/** reads first element without removing it from port
* if no data in port returns "NULL PORT DATA"
*/
peek: () => any;
/** check if port is full */
full: () => boolean;
/** check if port is empty */
empty: () => boolean;
/** removes all data from port */
clear: () => void;
}
/**
* Data representing the internal values of a crime.
* @public
@ -1776,7 +1802,7 @@ export interface Singularity {
* guarantee that your browser will follow that time limit.
*
* @param crime - Name of crime to attempt.
* @returns True if you successfully start working on the specified program, and false otherwise.
* @returns The number of milliseconds it takes to attempt the specified crime.
*/
commitCrime(crime: string): number;
@ -3854,6 +3880,36 @@ interface UserInterface {
* RAM cost: cost: 0 GB
*/
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;
}
/**
@ -5409,7 +5465,7 @@ export interface NS extends Singularity {
* @param port - Port number. Must be an integer between 1 and 20.
* @returns Data in the specified port.
*/
getPortHandle(port: number): any[];
getPortHandle(port: number): IPort;
/**
* Delete a file.
@ -5604,7 +5660,7 @@ export interface NS extends Singularity {
* @param args - Formating arguments.
* @returns Formated text.
*/
sprintf(format: string, ...args: string[]): string;
sprintf(format: string, ...args: any[]): string;
/**
* Format a string with an array of arguments.
@ -5616,7 +5672,7 @@ export interface NS extends Singularity {
* @param args - Formating arguments.
* @returns Formated text.
*/
vsprintf(format: string, args: string[]): string;
vsprintf(format: string, args: any[]): string;
/**
* Format a number
@ -6328,3 +6384,12 @@ interface UserInterfaceTheme {
backgroundsecondary: string;
button: string;
}
/**
* Interface Styles
* @internal
*/
interface IStyleSettings {
fontFamily: string;
lineHeight: number;
}

View File

@ -686,7 +686,9 @@ export function Root(props: IProps): React.ReactElement {
const serverScript = server.scripts.find((s) => s.filename === openScript.fileName);
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:
@ -846,7 +848,7 @@ export function Root(props: IProps): React.ReactElement {
<span style={{ color: Settings.theme.primary, fontSize: "20px", textAlign: "center" }}>
<Typography variant="h4">No open files</Typography>
<Typography variant="h5">
Use `nano FILENAME` in
Use <code>nano FILENAME</code> in
<br />
the terminal to open files
</Typography>

View File

@ -204,10 +204,14 @@ export function loadAllServers(saveString: string): void {
AllServers = JSON.parse(saveString, Reviver);
}
export function saveAllServers(): string {
export function saveAllServers(excludeRunningScripts = false): string {
const TempAllServers = JSON.parse(JSON.stringify(AllServers), Reviver);
for (const key in TempAllServers) {
const server = TempAllServers[key];
if (excludeRunningScripts) {
server.runningScripts = [];
continue;
}
for (let i = 0; i < server.runningScripts.length; ++i) {
const runningScriptObj = server.runningScripts[i];
runningScriptObj.logs.length = 0;

View File

@ -1,8 +1,10 @@
import { ISelfInitializer, ISelfLoading } from "../types";
import { OwnedAugmentationsOrderSetting, PurchaseAugmentationsOrderSetting } from "./SettingEnums";
import { defaultTheme, ITheme } from "./Themes";
import { defaultStyles, IStyleSettings } from "./Styles";
import { WordWrapOptions } from '../ScriptEditor/ui/Options';
import { defaultStyles } from "./Styles";
import { WordWrapOptions } from "../ScriptEditor/ui/Options";
import { OverviewSettings } from "../ui/React/Overview";
import { IStyleSettings } from "../ScriptEditor/NetscriptDefinitions";
/**
* Represents the default settings the player could customize.
@ -41,6 +43,11 @@ interface IDefaultSettings {
*/
DisableTextEffects: boolean;
/**
* Whether overview progress bars should be visible.
*/
DisableOverviewProgressBars: boolean;
/**
* Enable bash hotkeys
*/
@ -111,6 +118,11 @@ interface IDefaultSettings {
*/
SuppressSavedGameToast: boolean;
/*
* Whether the game should skip saving the running scripts for late game
*/
ExcludeRunningScriptsFromSave: boolean;
/*
* Theme colors
*/
@ -125,6 +137,11 @@ interface IDefaultSettings {
* Use GiB instead of GB
*/
UseIEC60027_2: boolean;
/*
* Character overview settings
*/
overview: OverviewSettings;
}
/**
@ -160,6 +177,7 @@ export const defaultSettings: IDefaultSettings = {
DisableASCIIArt: false,
DisableHotkeys: false,
DisableTextEffects: false,
DisableOverviewProgressBars: false,
EnableBashHotkeys: false,
TimestampsFormat: "",
Locale: "en",
@ -175,9 +193,11 @@ export const defaultSettings: IDefaultSettings = {
SuppressTIXPopup: false,
SuppressSavedGameToast: false,
UseIEC60027_2: false,
ExcludeRunningScriptsFromSave: false,
theme: defaultTheme,
styles: defaultStyles,
overview: { x: 0, y: 0, opened: true },
};
/**
@ -192,6 +212,7 @@ export const Settings: ISettings & ISelfInitializer & ISelfLoading = {
DisableASCIIArt: defaultSettings.DisableASCIIArt,
DisableHotkeys: defaultSettings.DisableHotkeys,
DisableTextEffects: defaultSettings.DisableTextEffects,
DisableOverviewProgressBars: defaultSettings.DisableOverviewProgressBars,
EnableBashHotkeys: defaultSettings.EnableBashHotkeys,
TimestampsFormat: defaultSettings.TimestampsFormat,
Locale: "en",
@ -209,14 +230,16 @@ export const Settings: ISettings & ISelfInitializer & ISelfLoading = {
SuppressTIXPopup: defaultSettings.SuppressTIXPopup,
SuppressSavedGameToast: defaultSettings.SuppressSavedGameToast,
UseIEC60027_2: defaultSettings.UseIEC60027_2,
ExcludeRunningScriptsFromSave: defaultSettings.ExcludeRunningScriptsFromSave,
MonacoTheme: "monokai",
MonacoInsertSpaces: false,
MonacoFontSize: 20,
MonacoVim: false,
MonacoWordWrap: 'off',
MonacoWordWrap: "off",
theme: { ...defaultTheme },
styles: { ...defaultStyles },
overview: defaultSettings.overview,
init() {
Object.assign(Settings, defaultSettings);
},
@ -226,6 +249,8 @@ export const Settings: ISettings & ISelfInitializer & ISelfLoading = {
delete save.theme;
Object.assign(Settings.styles, save.styles);
delete save.styles;
Object.assign(Settings.overview, save.overview);
delete save.overview;
Object.assign(Settings, save);
},
};

View File

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

View File

@ -69,29 +69,29 @@ const TemplatedHelpTexts: IMap<(command: string) => string[]> = {
export const HelpTexts: IMap<string[]> = {
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. ",
"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, ",
"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. ",
"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: ",
" ",
'alias worm="HTTPWorm.exe"',
' alias worm="HTTPWorm.exe"',
" ",
"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 ",
"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. ",
" ",
@ -102,15 +102,16 @@ export const HelpTexts: IMap<string[]> = {
" ",
],
analyze: [
"analyze",
"Usage: analyze",
" ",
"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 ",
"hacking-related information such as an estimated chance to successfully hack, an estimate of how much money is ",
"available on the server, etc.",
" ",
],
backdoor: [
"backdoor",
"Usage: backdoor",
" ",
"Install a backdoor on the current machine, grants a secret bonus depending on the machine.",
" ",
@ -118,7 +119,7 @@ export const HelpTexts: IMap<string[]> = {
" ",
],
buy: [
"buy [-l / -a / program]",
"Usage: buy [-l / -a / program]",
" ",
"Purchase a program through the Dark Web. Requires a TOR router to use.",
" ",
@ -128,65 +129,72 @@ export const HelpTexts: IMap<string[]> = {
"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.",
" ",
],
cat: [
"cat [file]",
"Usage: cat [file]",
" ",
"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 [dir]",
"Usage: cd [dir]",
" ",
"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:",
" ",
"cd scripts/hacking",
" cd scripts/hacking",
" ",
"cd /logs",
" cd /logs",
" ",
" cd ../",
" ",
"cd ../",
],
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 ",
"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: ",
" ",
"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: ",
" ",
"check foo.script 1 2 foodnstuff",
" check foo.script 1 2 foodnstuff",
" ",
],
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 ",
"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",
"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 ",
"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 [hostname]",
"Usage: connect [hostname]",
" ",
"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 ",
"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 [script/text file]",
"Usage: download [script/text file]",
" ",
"Downloads a script or text file to your computer (like your real life computer).",
" ",
@ -200,7 +208,7 @@ export const HelpTexts: IMap<string[]> = {
" ",
],
expr: [
"expr [mathematical expression]",
"Usage: expr [mathematical expression]",
" ",
"Evaluate a simple mathematical expression. Supports native JavaScript operators:",
" ",
@ -208,47 +216,49 @@ export const HelpTexts: IMap<string[]> = {
" ",
"Example:",
" ",
"expr 25 * 2 ** 10",
" expr 25 * 2 ** 10",
" ",
"Note that letters (non-digits) are not allowed and will be removed from the input.",
" ",
],
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 ",
"how much of it is being used.",
" ",
],
grow: [
"grow",
"",
"Usage: grow",
" ",
"Spoof transactions in the current server. Increasing the money available by hacking. Requires root access.",
"See the wiki page for hacking mechanics.",
" ",
],
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",
" ",
],
help: [
"help [command]",
"Usage: help [command]",
" ",
"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 ",
"more detailed information about the Terminal command. Examples: ",
" ",
"help alias",
" help alias",
" ",
" help scan-analyze",
" ",
"help scan-analyze",
],
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 [script name] [args...]",
" ",
"kill [pid]",
"Usage: kill [script name] [args...] or kill [pid",
" ",
"Kill the script specified by the script name and arguments OR by its PID.",
" ",
@ -257,24 +267,26 @@ export const HelpTexts: IMap<string[]> = {
"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:",
" ",
"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:",
" ",
"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",
" ",
],
killall: [
"killall",
"Usage: killall",
" ",
"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. ",
"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.",
" ",
],
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 files will be displayed in alphabetical order. ",
@ -287,34 +299,36 @@ export const HelpTexts: IMap<string[]> = {
" ",
"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:",
" ",
"ls / | grep .js",
" ls / | grep .js",
" ",
"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 [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 ",
"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:",
" ",
"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 ",
"above will print the amount of RAM needed to run 'foo.script' with 50 threads.",
" ",
],
mv: [
"mv [src] [dest]",
"Usage: mv [src] [dest]",
" ",
"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 ",
@ -324,22 +338,23 @@ export const HelpTexts: IMap<string[]> = {
"full filepath. ",
"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: TemplatedHelpTexts.scriptEditor('nano'),
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 [file]",
"Usage: rm [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",
" ",
],
run: [
"run [file name] [-t] [num threads] [args...]",
"Usage: run [file name] [-t] [num threads] [args...]",
" ",
"Execute a program, script or coding contract.",
" ",
@ -354,13 +369,14 @@ export const HelpTexts: IMap<string[]> = {
" ",
],
scan: [
"scan",
"Usage: scan",
" ",
"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.",
" ",
],
"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 ",
"'scan-analyze 1' will display information for the same servers that are shown by the 'scan' Terminal ",
@ -376,65 +392,70 @@ export const HelpTexts: IMap<string[]> = {
" ",
"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.",
" ",
],
scp: [
"scp [filename ...] [target server]",
"Usage: scp [filename ...] [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), ",
"and text files (.txt extension). ",
"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 [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 ",
"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: ",
" ",
"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: ",
" ",
"tail foo.script 10 50000",
" tail foo.script 10 50000",
" ",
],
top: [
"top",
"Usage: top",
" ",
"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.",
" ",
],
unalias: [
"unalias [alias name]",
"Usage: unalias [alias name]",
" ",
"Deletes the specified alias. Note that the double quotation marks are required. ",
" ",
"As an example, if an alias was declared using:",
" ",
'alias r="run"',
' alias r="run"',
" ",
"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'",
" ",
],
vim: TemplatedHelpTexts.scriptEditor('vim'),
weaken: [
"weaken",
"",
"Usage: weaken",
" ",
"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.",
" ",
],
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 ",
"be downloaded to a script (.script, .ns, .js) or a text file (.txt). If the file already exists, ",
@ -443,6 +464,7 @@ export const HelpTexts: IMap<string[]> = {
"Note that it will not be possible to download data from many websites because they do not allow ",
"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",
" ",
],
};

View File

@ -315,6 +315,7 @@ export class Terminal implements ITerminal {
this.print("Root Access: " + (hasAdminRights ? "YES" : "NO"));
this.print("Can run scripts on this host: " + (hasAdminRights ? "YES" : "NO"));
if (currServ instanceof Server) {
this.print("Backdoor: " + (currServ.backdoorInstalled ? "YES" : "NO"));
const hackingSkill = currServ.requiredHackingSkill;
this.print("Required hacking skill for hack() and backdoor: " + (!isHacknet ? hackingSkill : "N/A"));
const security = currServ.hackDifficulty;

View File

@ -5,8 +5,6 @@ import { BaseServer } from "../../Server/BaseServer";
import { getServerOnNetwork } from "../../Server/ServerHelpers";
import { GetServer } from "../../Server/AllServers";
import { Server } from "../../Server/Server";
import { Programs } from "src/Programs/Programs";
import { programsMetadata } from "src/Programs/data/ProgramsMetadata";
export function connect(
terminal: ITerminal,

View File

@ -3,6 +3,7 @@ import { IRouter } from "../../ui/Router";
import { IPlayer } from "../../PersonObjects/IPlayer";
import { BaseServer } from "../../Server/BaseServer";
import { numeralWrapper } from "../../ui/numeralFormat";
import { Settings } from "../../Settings/Settings";
export function mem(
terminal: ITerminal,
@ -40,8 +41,9 @@ export function mem(
);
const verboseEntries = script.ramUsageEntries?.sort((a, b) => b.cost - a.cost) ?? [];
const padding = Settings.UseIEC60027_2 ? 9 : 8;
for (const entry of verboseEntries) {
terminal.print(`${numeralWrapper.formatRAM(entry.cost * numThreads).padStart(8)} | ${entry.name} (${entry.type})`);
terminal.print(`${numeralWrapper.formatRAM(entry.cost * numThreads).padStart(padding)} | ${entry.name} (${entry.type})`);
}
if (ramUsage > 0 && verboseEntries.length === 0) {

View File

@ -41,7 +41,7 @@ export function runScript(
// Check if this script is already running
if (findRunningScript(scriptName, args, server) != null) {
terminal.error("This script is already running. Cannot run multiple instances");
terminal.error("This script is already running with the same args. Cannot run multiple instances with the same args");
return;
}

View File

@ -6,6 +6,7 @@ import { DarkWebItems } from "../DarkWeb/DarkWebItems";
import { IPlayer } from "../PersonObjects/IPlayer";
import { GetServer, GetAllServers } from "../Server/AllServers";
import { ParseCommand, ParseCommands } from "./Parser";
import { HelpTexts } from "./HelpText";
import { isScriptFilename } from "../Script/isScriptFilename";
import { compile } from "../NetscriptJSEvaluator";
import { Flags } from "../NetscriptFunctions/Flags";
@ -178,26 +179,8 @@ export async function determineAllPossibilitiesForTabCompletion(
return input.startsWith(t_cmd);
}
/**
* If the command starts with './' and the index == -1, then the user
* has input ./partialexecutablename so autocomplete the script or program.
* Put './' in front of each script/executable
*/
if (isCommand("./") && index == -1) {
//All programs and scripts
for (let i = 0; i < currServ.scripts.length; ++i) {
allPos.push("./" + currServ.scripts[i].filename);
}
//Programs are on home computer
for (let i = 0; i < homeComputer.programs.length; ++i) {
allPos.push("./" + homeComputer.programs[i]);
}
return allPos;
}
// Autocomplete the command
if (index === -1) {
if (index === -1 && !input.startsWith('./')) {
return commands.concat(Object.keys(Aliases)).concat(Object.keys(GlobalAliases));
}
@ -286,14 +269,22 @@ export async function determineAllPossibilitiesForTabCompletion(
}
async function scriptAutocomplete(): Promise<string[] | undefined> {
if (!isCommand("run") && !isCommand("tail") && !isCommand("kill")) return;
const commands = ParseCommands(input);
if (!isCommand("run") && !isCommand("tail") && !isCommand("kill") && !input.startsWith("./")) return;
let copy = input;
if (input.startsWith("./")) copy = "run " + input.slice(2);
const commands = ParseCommands(copy);
if (commands.length === 0) return;
const command = ParseCommand(commands[commands.length - 1]);
const filename = command[1] + "";
if (!isScriptFilename(filename)) return; // Not a script.
if (filename.endsWith(".script")) return; // Doesn't work with ns1.
const script = currServ.scripts.find((script) => script.filename === filename);
// Use regex to remove any leading './', and then check if it matches against
// the output of processFilepath or if it matches with a '/' prepended,
// this way autocomplete works inside of directories
const script = currServ.scripts.find((script) => {
const fn = filename.replace(/^\.\//g, '');
return (processFilepath(script.filename) === fn || script.filename === '/' + fn);
})
if (!script) return; // Doesn't exist.
if (!script.module) {
await compile(p, script, currServ.scripts);
@ -335,6 +326,35 @@ export async function determineAllPossibilitiesForTabCompletion(
const pos = await scriptAutocomplete();
if (pos) return pos;
// If input starts with './', essentially treat it as a slimmer
// invocation of `run`.
if (input.startsWith("./")) {
// All programs and scripts
for (const script of currServ.scripts) {
const res = processFilepath(script.filename);
if (res) {
allPos.push(res);
}
}
for (const program of currServ.programs) {
const res = processFilepath(program);
if (res) {
allPos.push(res);
}
}
// All coding contracts
for (const cct of currServ.contracts) {
const res = processFilepath(cct.fn);
if (res) {
allPos.push(res);
}
}
return allPos;
}
if (isCommand("run")) {
addAllScripts();
addAllPrograms();
@ -377,5 +397,11 @@ export async function determineAllPossibilitiesForTabCompletion(
addAllDirectories();
}
if (isCommand("help")) {
// Get names from here instead of commands array because some
// undocumented/nonexistent commands are in the array
return Object.keys(HelpTexts);
}
return allPos;
}

View File

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

View File

@ -24,7 +24,7 @@ function getDB(): Promise<IDBObjectStore> {
indexedDbRequest.onsuccess = function (this: IDBRequest<IDBDatabase>) {
const db = this.result;
if (!db) {
reject("database loadign result was undefined");
reject("database loading result was undefined");
return;
}
resolve(db.transaction(["savestring"], "readwrite").objectStore("savestring"));

View File

@ -52,6 +52,11 @@
</script>
<% } %>
<style>
html, body {
margin: 0;
padding: 0;
height: 100%;
}
body {
background-color: black;
}

View File

@ -5,6 +5,8 @@ import { TTheme as Theme, ThemeEvents, refreshTheme } from "./ui/React/Theme";
import { LoadingScreen } from "./ui/LoadingScreen";
import { initElectron } from "./Electron";
initElectron();
globalThis["React"] = React;
globalThis["ReactDOM"] = ReactDOM;
ReactDOM.render(
<Theme>
<LoadingScreen />

View File

@ -77,7 +77,6 @@ import { enterBitNode } from "../RedPill";
import { Context } from "./Context";
import { RecoveryMode, RecoveryRoot } from "./React/RecoveryRoot";
import { AchievementsRoot } from "../Achievements/AchievementsRoot";
import { Settings } from "../Settings/Settings";
const htmlLocation = location;
@ -93,6 +92,11 @@ const useStyles = makeStyles((theme: Theme) =>
"-ms-overflow-style": "none" /* for Internet Explorer, Edge */,
"scrollbar-width": "none" /* for Firefox */,
margin: theme.spacing(0),
flexGrow: 1,
display: "block",
padding: "8px",
minHeight: "100vh",
boxSizing: 'border-box',
},
}),
);
@ -187,7 +191,7 @@ export let Router: IRouter = {
},
toAchievements: () => {
throw new Error("Router called before initialization");
}
},
};
function determineStartPage(player: IPlayer): Page {
@ -198,7 +202,7 @@ function determineStartPage(player: IPlayer): Page {
export function GameRoot({ player, engine, terminal }: IProps): React.ReactElement {
const classes = useStyles();
const [{files, vim}, setEditorOptions] = useState({files: {}, vim: false})
const [{ files, vim }, setEditorOptions] = useState({ files: {}, vim: false });
const [page, setPage] = useState(determineStartPage(player));
const setRerender = useState(0)[1];
const [faction, setFaction] = useState<Faction>(
@ -315,7 +319,7 @@ export function GameRoot({ player, engine, terminal }: IProps): React.ReactEleme
mainPage = <BitverseRoot flume={flume} enter={enterBitNode} quick={quick} />;
withSidebar = false;
withPopups = false;
break
break;
}
case Page.Infiltration: {
mainPage = <InfiltrationRoot location={location} />;
@ -351,13 +355,15 @@ export function GameRoot({ player, engine, terminal }: IProps): React.ReactEleme
break;
}
case Page.ScriptEditor: {
mainPage = <ScriptEditorRoot
files={files}
hostname={player.getCurrentServer().hostname}
player={player}
router={Router}
vim={vim}
/>;
mainPage = (
<ScriptEditorRoot
files={files}
hostname={player.getCurrentServer().hostname}
player={player}
router={Router}
vim={vim}
/>
);
break;
}
case Page.ActiveScripts: {
@ -385,13 +391,15 @@ export function GameRoot({ player, engine, terminal }: IProps): React.ReactEleme
break;
}
case Page.Tutorial: {
mainPage = <TutorialRoot
reactivateTutorial={() => {
prestigeAugmentation();
Router.toTerminal();
iTutorialStart();
}}
/>;
mainPage = (
<TutorialRoot
reactivateTutorial={() => {
prestigeAugmentation();
Router.toTerminal();
iTutorialStart();
}}
/>
);
break;
}
case Page.DevMenu: {
@ -419,59 +427,65 @@ export function GameRoot({ player, engine, terminal }: IProps): React.ReactEleme
break;
}
case Page.StockMarket: {
mainPage = <StockMarketRoot
buyStockLong={buyStock}
buyStockShort={shortStock}
cancelOrder={cancelOrder}
eventEmitterForReset={eventEmitterForUiReset}
initStockMarket={initStockMarketFnForReact}
p={player}
placeOrder={placeOrder}
sellStockLong={sellStock}
sellStockShort={sellShort}
stockMarket={StockMarket}
/>;
mainPage = (
<StockMarketRoot
buyStockLong={buyStock}
buyStockShort={shortStock}
cancelOrder={cancelOrder}
eventEmitterForReset={eventEmitterForUiReset}
initStockMarket={initStockMarketFnForReact}
p={player}
placeOrder={placeOrder}
sellStockLong={sellStock}
sellStockShort={sellShort}
stockMarket={StockMarket}
/>
);
break;
}
case Page.City: {
mainPage = <LocationCity />;
break;
}
case Page.Job:
case Page.Job:
case Page.Location: {
mainPage = <GenericLocation loc={location} />;
break;
}
case Page.Options: {
mainPage = <GameOptionsRoot
player={player}
save={() => saveObject.saveGame()}
export={() => {
// Apply the export bonus before saving the game
onExport(player);
saveObject.exportGame()
}}
forceKill={killAllScripts}
softReset={() => {
dialogBoxCreate("Soft Reset!");
prestigeAugmentation();
Router.toTerminal();
}}
/>;
mainPage = (
<GameOptionsRoot
player={player}
save={() => saveObject.saveGame()}
export={() => {
// Apply the export bonus before saving the game
onExport(player);
saveObject.exportGame();
}}
forceKill={killAllScripts}
softReset={() => {
dialogBoxCreate("Soft Reset!");
prestigeAugmentation();
Router.toTerminal();
}}
/>
);
break;
}
case Page.Augmentations: {
mainPage = <AugmentationsRoot
exportGameFn={() => {
// Apply the export bonus before saving the game
onExport(player);
saveObject.exportGame();
}}
installAugmentationsFn={() => {
installAugmentations();
Router.toTerminal();
}}
/>;
mainPage = (
<AugmentationsRoot
exportGameFn={() => {
// Apply the export bonus before saving the game
onExport(player);
saveObject.exportGame();
}}
installAugmentationsFn={() => {
installAugmentations();
Router.toTerminal();
}}
/>
);
break;
}
case Page.Achievements: {
@ -479,12 +493,12 @@ export function GameRoot({ player, engine, terminal }: IProps): React.ReactEleme
break;
}
}
return (
<Context.Player.Provider value={player}>
<Context.Router.Provider value={Router}>
<SnackbarProvider>
<Overview>
<Overview mode={ITutorial.isRunning ? "tutorial" : "overview"}>
{!ITutorial.isRunning ? (
<CharacterOverview save={() => saveObject.saveGame()} killScripts={killAllScripts} />
) : (
@ -494,19 +508,19 @@ export function GameRoot({ player, engine, terminal }: IProps): React.ReactEleme
{withSidebar ? (
<Box display="flex" flexDirection="row" width="100%">
<SidebarRoot player={player} router={Router} page={page} />
<Box className={classes.root} flexGrow={1} display="block" px={1} min-height="100vh">
{mainPage}
</Box>
<Box className={classes.root}>{mainPage}</Box>
</Box>
) : mainPage }
) : (
<Box className={classes.root}>{mainPage}</Box>
)}
<Unclickable />
{withPopups && (
<>
<LogBoxManager />
<AlertManager />
<PromptManager />
<InvitationModal />
<Snackbar />
<LogBoxManager />
<AlertManager />
<PromptManager />
<InvitationModal />
<Snackbar />
</>
)}
</SnackbarProvider>

View File

@ -12,7 +12,6 @@ import { CopyableText } from "../React/CopyableText";
import ListItem from "@mui/material/ListItem";
import EqualizerIcon from "@mui/icons-material/Equalizer";
import LastPageIcon from "@mui/icons-material/LastPage";
import VisibilityOffIcon from "@mui/icons-material/VisibilityOff";
import HelpIcon from "@mui/icons-material/Help";
import AccountTreeIcon from "@mui/icons-material/AccountTree";
import StorageIcon from "@mui/icons-material/Storage";
@ -61,7 +60,7 @@ export function InteractiveTutorialRoot(): React.ReactElement {
This tutorial will show you the basics of the game. You may skip the tutorial at any time.
<br />
<br />
You can also click the eye symbol <VisibilityOffIcon /> to temporarily hide this tutorial.
You can also collapse this panel to temporarily hide this tutorial.
</Typography>
</>
),

View File

@ -7,6 +7,7 @@ import createStyles from "@mui/styles/createStyles";
import { numeralWrapper } from "../../ui/numeralFormat";
import { Reputation } from "./Reputation";
import { KillScriptsModal } from "./KillScriptsModal";
import { convertTimeMsToTimeElapsedString } from "../../utils/StringHelperFunctions";
import Table from "@mui/material/Table";
import TableBody from "@mui/material/TableBody";
@ -23,6 +24,9 @@ import { use } from "../Context";
import { StatsProgressOverviewCell } from "./StatsProgressBar";
import { BitNodeMultipliers } from "../../BitNode/BitNodeMultipliers";
import { Box, Tooltip } from "@mui/material";
import { CONSTANTS } from "../../Constants";
interface IProps {
save: () => void;
killScripts: () => void;
@ -74,95 +78,39 @@ function Bladeburner(): React.ReactElement {
);
}
function Work(): React.ReactElement {
const player = use.Player();
const router = use.Router();
interface WorkInProgressOverviewProps {
tooltip: React.ReactNode;
header: React.ReactNode;
children: React.ReactNode;
onClickFocus: () => void;
}
function WorkInProgressOverview({
tooltip,
children,
onClickFocus,
header,
}: WorkInProgressOverviewProps): React.ReactElement {
const classes = useStyles();
if (!player.isWorking || player.focus) return <></>;
if (player.className !== "") {
return (
<>
<TableRow>
<TableCell component="th" scope="row" colSpan={2} classes={{ root: classes.cellNone }}>
<Typography>Work&nbsp;in&nbsp;progress:</Typography>
</TableCell>
</TableRow>
<TableRow>
<TableCell component="th" scope="row" colSpan={2} classes={{ root: classes.cellNone }}>
<Typography>{player.className}</Typography>
</TableCell>
</TableRow>
<TableRow>
<TableCell component="th" scope="row" align="center" colSpan={2} classes={{ root: classes.cellNone }}>
<Button
onClick={() => {
player.startFocusing();
router.toWork();
}}
>
Focus
</Button>
</TableCell>
</TableRow>
</>
);
}
if (player.createProgramName !== "") {
return (
<>
<TableRow>
<TableCell component="th" scope="row" colSpan={2} classes={{ root: classes.cellNone }}>
<Typography>Work&nbsp;in&nbsp;progress:</Typography>
</TableCell>
</TableRow>
<TableRow>
<TableCell component="th" scope="row" colSpan={2} classes={{ root: classes.cellNone }}>
<Typography>
{player.createProgramName}{" "}
{((player.timeWorkedCreateProgram / player.timeNeededToCompleteWork) * 100).toFixed(2)}%
</Typography>
</TableCell>
</TableRow>
<TableRow>
<TableCell component="th" scope="row" align="center" colSpan={2} classes={{ root: classes.cellNone }}>
<Button
onClick={() => {
player.startFocusing();
router.toWork();
}}
>
Focus
</Button>
</TableCell>
</TableRow>
</>
);
}
return (
<>
<TableRow>
<TableCell component="th" scope="row" colSpan={2} classes={{ root: classes.cellNone }}>
<Typography>Work&nbsp;in&nbsp;progress:</Typography>
<TableCell component="th" scope="row" colSpan={2} classes={{ root: classes.workCell }}>
<Tooltip title={<>{tooltip}</>}>
<Typography className={classes.workHeader} sx={{ pt: 1, pb: 0.5 }}>
{header}
</Typography>
</Tooltip>
</TableCell>
</TableRow>
<TableRow>
<TableCell component="th" scope="row" colSpan={2} classes={{ root: classes.cellNone }}>
<Typography>
+<Reputation reputation={player.workRepGained} /> rep
</Typography>
<TableCell component="th" scope="row" colSpan={2} classes={{ root: classes.workCell }}>
<Typography className={classes.workSubtitles}>{children}</Typography>
</TableCell>
</TableRow>
<TableRow>
<TableCell component="th" scope="row" align="center" colSpan={2} classes={{ root: classes.cellNone }}>
<Button
onClick={() => {
player.startFocusing();
router.toWork();
}}
>
<Button sx={{ mt: 1 }} onClick={onClickFocus}>
Focus
</Button>
</TableCell>
@ -171,8 +119,91 @@ function Work(): React.ReactElement {
);
}
function Work(): React.ReactElement {
const player = use.Player();
const router = use.Router();
const onClickFocus = (): void => {
player.startFocusing();
router.toWork();
};
if (!player.isWorking || player.focus) return <></>;
let details = <></>;
let header = <></>;
let innerText = <></>;
if (player.workType === CONSTANTS.WorkTypeCompanyPartTime || player.workType === CONSTANTS.WorkTypeCompany) {
details = (
<>
{player.jobs[player.companyName]} at <strong>{player.companyName}</strong>
</>
);
header = (
<>
Working at <strong>{player.companyName}</strong>
</>
);
innerText = (
<>
+<Reputation reputation={player.workRepGained} /> rep
</>
);
} else if (player.workType === CONSTANTS.WorkTypeFaction) {
details = (
<>
{player.factionWorkType} for <strong>{player.currentWorkFactionName}</strong>
</>
);
header = (
<>
Working for <strong>{player.currentWorkFactionName}</strong>
</>
);
innerText = (
<>
+<Reputation reputation={player.workRepGained} /> rep
</>
);
} else if (player.workType === CONSTANTS.WorkTypeStudyClass) {
details = <>{player.workType}</>;
header = <>You are {player.className}</>;
innerText = <>{convertTimeMsToTimeElapsedString(player.timeWorked)}</>;
} else if (player.workType === CONSTANTS.WorkTypeCreateProgram) {
details = <>Coding {player.createProgramName}</>;
header = <>Creating a program</>;
innerText = (
<>
{player.createProgramName}{" "}
{((player.timeWorkedCreateProgram / player.timeNeededToCompleteWork) * 100).toFixed(2)}%
</>
);
}
return (
<WorkInProgressOverview tooltip={details} header={header} onClickFocus={onClickFocus}>
{innerText}
</WorkInProgressOverview>
);
}
const useStyles = makeStyles((theme: Theme) =>
createStyles({
workCell: {
textAlign: "center",
maxWidth: "200px",
borderBottom: "none",
padding: 0,
margin: 0,
},
workHeader: {
fontSize: "0.9rem",
},
workSubtitles: {
fontSize: "0.8rem",
},
cellNone: {
borderBottom: "none",
padding: 0,
@ -287,7 +318,9 @@ export function CharacterOverview({ save, killScripts }: IProps): React.ReactEle
</TableCell>
</TableRow>
<TableRow>
<StatsProgressOverviewCell progress={hackingProgress} color={theme.colors.hack} />
{!Settings.DisableOverviewProgressBars && (
<StatsProgressOverviewCell progress={hackingProgress} color={theme.colors.hack} />
)}
</TableRow>
<TableRow>
<TableCell component="th" scope="row" classes={{ root: classes.cell }}>
@ -314,7 +347,9 @@ export function CharacterOverview({ save, killScripts }: IProps): React.ReactEle
</TableCell>
</TableRow>
<TableRow>
<StatsProgressOverviewCell progress={strengthProgress} color={theme.colors.combat} />
{!Settings.DisableOverviewProgressBars && (
<StatsProgressOverviewCell progress={strengthProgress} color={theme.colors.combat} />
)}
</TableRow>
<TableRow>
@ -331,7 +366,9 @@ export function CharacterOverview({ save, killScripts }: IProps): React.ReactEle
</TableCell>
</TableRow>
<TableRow>
<StatsProgressOverviewCell progress={defenseProgress} color={theme.colors.combat} />
{!Settings.DisableOverviewProgressBars && (
<StatsProgressOverviewCell progress={defenseProgress} color={theme.colors.combat} />
)}
</TableRow>
<TableRow>
@ -348,7 +385,9 @@ export function CharacterOverview({ save, killScripts }: IProps): React.ReactEle
</TableCell>
</TableRow>
<TableRow>
<StatsProgressOverviewCell progress={dexterityProgress} color={theme.colors.combat} />
{!Settings.DisableOverviewProgressBars && (
<StatsProgressOverviewCell progress={dexterityProgress} color={theme.colors.combat} />
)}
</TableRow>
<TableRow>
@ -365,7 +404,9 @@ export function CharacterOverview({ save, killScripts }: IProps): React.ReactEle
</TableCell>
</TableRow>
<TableRow>
<StatsProgressOverviewCell progress={agilityProgress} color={theme.colors.combat} />
{!Settings.DisableOverviewProgressBars && (
<StatsProgressOverviewCell progress={agilityProgress} color={theme.colors.combat} />
)}
</TableRow>
<TableRow>
@ -382,7 +423,9 @@ export function CharacterOverview({ save, killScripts }: IProps): React.ReactEle
</TableCell>
</TableRow>
<TableRow>
<StatsProgressOverviewCell progress={charismaProgress} color={theme.colors.cha} />
{!Settings.DisableOverviewProgressBars && (
<StatsProgressOverviewCell progress={charismaProgress} color={theme.colors.cha} />
)}
</TableRow>
<Intelligence />
@ -406,21 +449,24 @@ export function CharacterOverview({ save, killScripts }: IProps): React.ReactEle
</TableRow>
<Work />
<Bladeburner />
<TableRow>
<TableCell align="center" classes={{ root: classes.cellNone }}>
<IconButton onClick={save}>
<SaveIcon color={Settings.AutosaveInterval !== 0 ? "primary" : "error"} />
</IconButton>
</TableCell>
<TableCell align="center" classes={{ root: classes.cellNone }}>
<IconButton onClick={() => setKillOpen(true)}>
<ClearAllIcon color="error" />
</IconButton>
</TableCell>
</TableRow>
</TableBody>
</Table>
<Box sx={{ display: "flex", borderTop: `1px solid ${Settings.theme.welllight}` }}>
<Box sx={{ display: "flex", flex: 1, justifyContent: "flex-start", alignItems: "center" }}>
<IconButton onClick={save}>
<Tooltip title="Save game">
<SaveIcon color={Settings.AutosaveInterval !== 0 ? "primary" : "error"} />
</Tooltip>
</IconButton>
</Box>
<Box sx={{ display: "flex", flex: 1, justifyContent: "flex-end", alignItems: "center" }}>
<IconButton onClick={() => setKillOpen(true)}>
<Tooltip title="Kill all running scripts">
<ClearAllIcon color="error" />
</Tooltip>
</IconButton>
</Box>
</Box>
<KillScriptsModal open={killOpen} onClose={() => setKillOpen(false)} killScripts={killScripts} />
</>
);

View File

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

View File

@ -8,8 +8,6 @@ import createStyles from "@mui/styles/createStyles";
import Typography from "@mui/material/Typography";
import Slider from "@mui/material/Slider";
import Grid from "@mui/material/Grid";
import FormControlLabel from "@mui/material/FormControlLabel";
import Switch from "@mui/material/Switch";
import Select, { SelectChangeEvent } from "@mui/material/Select";
import MenuItem from "@mui/material/MenuItem";
import Button from "@mui/material/Button";
@ -35,6 +33,7 @@ import { SnackbarEvents } from "./Snackbar";
import { Settings } from "../../Settings/Settings";
import { save, deleteGame } from "../../db";
import { formatTime } from "../../utils/helpers/formatTime";
import { OptionSwitch } from "./OptionSwitch";
const useStyles = makeStyles((theme: Theme) =>
createStyles({
@ -68,27 +67,8 @@ export function GameOptionsRoot(props: IProps): React.ReactElement {
const [logSize, setLogSize] = useState(Settings.MaxLogCapacity);
const [portSize, setPortSize] = useState(Settings.MaxPortCapacity);
const [terminalSize, setTerminalSize] = useState(Settings.MaxTerminalCapacity);
const [autosaveInterval, setAutosaveInterval] = useState(Settings.AutosaveInterval);
const [suppressMessages, setSuppressMessages] = useState(Settings.SuppressMessages);
const [suppressFactionInvites, setSuppressFactionInvites] = useState(Settings.SuppressFactionInvites);
const [suppressTravelConfirmations, setSuppressTravelConfirmations] = useState(Settings.SuppressTravelConfirmation);
const [suppressBuyAugmentationConfirmation, setSuppressBuyAugmentationConfirmation] = useState(
Settings.SuppressBuyAugmentationConfirmation,
);
const [suppressTIXPopup, setSuppressTIXPopup] = useState(Settings.SuppressTIXPopup);
const [suppressBladeburnerPopup, setSuppressBladeburnerPopup] = useState(Settings.SuppressBladeburnerPopup);
const [suppressSavedGameToast, setSuppresSavedGameToast] = useState(Settings.SuppressSavedGameToast);
const [disableHotkeys, setDisableHotkeys] = useState(Settings.DisableHotkeys);
const [disableASCIIArt, setDisableASCIIArt] = useState(Settings.DisableASCIIArt);
const [disableTextEffects, setDisableTextEffects] = useState(Settings.DisableTextEffects);
const [enableBashHotkeys, setEnableBashHotkeys] = useState(Settings.EnableBashHotkeys);
const [timestampFormat, setTimestampFormat] = useState(Settings.TimestampsFormat);
const [saveGameOnFileSave, setSaveGameOnFileSave] = useState(Settings.SaveGameOnFileSave);
const [useIEC60027_2, setUseIEC60027_2] = useState(Settings.UseIEC60027_2);
const [locale, setLocale] = useState(Settings.Locale);
const [diagnosticOpen, setDiagnosticOpen] = useState(false);
const [deleteGameOpen, setDeleteOpen] = useState(false);
@ -123,76 +103,15 @@ export function GameOptionsRoot(props: IProps): React.ReactElement {
Settings.AutosaveInterval = newValue as number;
}
function handleSuppressMessagesChange(event: React.ChangeEvent<HTMLInputElement>): void {
setSuppressMessages(event.target.checked);
Settings.SuppressMessages = event.target.checked;
}
function handleSuppressFactionInvitesChange(event: React.ChangeEvent<HTMLInputElement>): void {
setSuppressFactionInvites(event.target.checked);
Settings.SuppressFactionInvites = event.target.checked;
}
function handleSuppressTravelConfirmationsChange(event: React.ChangeEvent<HTMLInputElement>): void {
setSuppressTravelConfirmations(event.target.checked);
Settings.SuppressTravelConfirmation = event.target.checked;
}
function handleSuppressBuyAugmentationConfirmationChange(event: React.ChangeEvent<HTMLInputElement>): void {
setSuppressBuyAugmentationConfirmation(event.target.checked);
Settings.SuppressBuyAugmentationConfirmation = event.target.checked;
}
function handleSuppressTIXPopupChange(event: React.ChangeEvent<HTMLInputElement>): void {
setSuppressTIXPopup(event.target.checked);
Settings.SuppressTIXPopup = event.target.checked;
}
function handleSuppressBladeburnerPopupChange(event: React.ChangeEvent<HTMLInputElement>): void {
setSuppressBladeburnerPopup(event.target.checked);
Settings.SuppressBladeburnerPopup = event.target.checked;
}
function handleSuppressSavedGameToastChange(event: React.ChangeEvent<HTMLInputElement>): void {
setSuppresSavedGameToast(event.target.checked);
Settings.SuppressSavedGameToast = event.target.checked;
}
function handleDisableHotkeysChange(event: React.ChangeEvent<HTMLInputElement>): void {
setDisableHotkeys(event.target.checked);
Settings.DisableHotkeys = event.target.checked;
}
function handleDisableASCIIArtChange(event: React.ChangeEvent<HTMLInputElement>): void {
setDisableASCIIArt(event.target.checked);
Settings.DisableASCIIArt = event.target.checked;
}
function handleUseIEC60027_2Change(event: React.ChangeEvent<HTMLInputElement>): void {
setUseIEC60027_2(event.target.checked);
Settings.UseIEC60027_2 = event.target.checked;
}
function handleDisableTextEffectsChange(event: React.ChangeEvent<HTMLInputElement>): void {
setDisableTextEffects(event.target.checked);
Settings.DisableTextEffects = event.target.checked;
}
function handleLocaleChange(event: SelectChangeEvent<string>): void {
setLocale(event.target.value as string);
Settings.Locale = event.target.value as string;
}
function handleEnableBashHotkeysChange(event: React.ChangeEvent<HTMLInputElement>): void {
setEnableBashHotkeys(event.target.checked);
Settings.EnableBashHotkeys = event.target.checked;
}
function handleTimestampFormatChange(event: React.ChangeEvent<HTMLInputElement>): void {
setTimestampFormat(event.target.value);
Settings.TimestampsFormat = event.target.value;
}
function handleSaveGameOnFile(event: React.ChangeEvent<HTMLInputElement>): void {
setSaveGameOnFileSave(event.target.checked);
Settings.SaveGameOnFileSave = event.target.checked;
}
function startImport(): void {
if (!window.File || !window.FileReader || !window.FileList || !window.Blob) return;
@ -392,200 +311,128 @@ export function GameOptionsRoot(props: IProps): React.ReactElement {
/>
</ListItem>
<ListItem>
<FormControlLabel
control={<Switch checked={suppressMessages} onChange={handleSuppressMessagesChange} />}
label={
<Tooltip
title={
<Typography>
If this is set, then any messages you receive will not appear as popups on the screen. They will
still get sent to your home computer as '.msg' files and can be viewed with the 'cat' Terminal
command.
</Typography>
}
>
<Typography>Suppress story messages</Typography>
</Tooltip>
}
/>
<OptionSwitch checked={Settings.SuppressMessages}
onChange={(newValue) => Settings.SuppressMessages = newValue}
text="Suppress story messages"
tooltip={<>
If this is set, then any messages you receive will not appear as popups on the screen. They will
still get sent to your home computer as '.msg' files and can be viewed with the 'cat' Terminal
command.
</>} />
</ListItem>
<ListItem>
<FormControlLabel
control={<Switch checked={suppressFactionInvites} onChange={handleSuppressFactionInvitesChange} />}
label={
<Tooltip
title={
<Typography>
If this is set, then any faction invites you receive will not appear as popups on the screen.
Your outstanding faction invites can be viewed in the 'Factions' page.
</Typography>
}
>
<Typography>Suppress faction invites</Typography>
</Tooltip>
}
/>
<OptionSwitch checked={Settings.SuppressFactionInvites}
onChange={(newValue) => Settings.SuppressFactionInvites = newValue}
text="Suppress faction invites"
tooltip={<>
If this is set, then any faction invites you receive will not appear as popups on the screen.
Your outstanding faction invites can be viewed in the 'Factions' page.
</>} />
</ListItem>
<ListItem>
<FormControlLabel
control={
<Switch checked={suppressTravelConfirmations} onChange={handleSuppressTravelConfirmationsChange} />
}
label={
<Tooltip
title={
<Typography>
If this is set, the confirmation message before traveling will not show up. You will
automatically be deducted the travel cost as soon as you click.
</Typography>
}
>
<Typography>Suppress travel confirmations</Typography>
</Tooltip>
}
/>
<OptionSwitch checked={Settings.SuppressTravelConfirmation}
onChange={(newValue) => Settings.SuppressTravelConfirmation = newValue}
text="Suppress travel confirmations"
tooltip={<>
If this is set, the confirmation message before traveling will not show up. You will
automatically be deducted the travel cost as soon as you click.
</>} />
</ListItem>
<ListItem>
<FormControlLabel
control={
<Switch
checked={suppressBuyAugmentationConfirmation}
onChange={handleSuppressBuyAugmentationConfirmationChange}
/>
}
label={
<Tooltip
title={
<Typography>
If this is set, the confirmation message before buying augmentation will not show up.
</Typography>
}
>
<Typography>Suppress augmentations confirmation</Typography>
</Tooltip>
}
/>
<OptionSwitch checked={Settings.SuppressBuyAugmentationConfirmation}
onChange={(newValue) => Settings.SuppressBuyAugmentationConfirmation = newValue}
text="Suppress augmentations confirmation"
tooltip={<>
If this is set, the confirmation message before buying augmentation will not show up.
</>} />
</ListItem>
<ListItem>
<FormControlLabel
control={<Switch checked={suppressTIXPopup} onChange={handleSuppressTIXPopupChange} />}
label={
<Tooltip
title={<Typography>If this is set, the stock market will never create any popup.</Typography>}
>
<Typography>Suppress TIX messages</Typography>
</Tooltip>
}
/>
<OptionSwitch checked={Settings.SuppressTIXPopup}
onChange={(newValue) => Settings.SuppressTIXPopup = newValue}
text="Suppress TIX messages"
tooltip={<>
If this is set, the stock market will never create any popup.
</>} />
</ListItem>
{!!props.player.bladeburner && (
<ListItem>
<FormControlLabel
control={
<Switch checked={suppressBladeburnerPopup} onChange={handleSuppressBladeburnerPopupChange} />
}
label={
<Tooltip
title={
<Typography>
If this is set, then having your Bladeburner actions interrupted by being busy with something
else will not display a popup message.
</Typography>
}
>
<Typography>Suppress bladeburner popup</Typography>
</Tooltip>
}
/>
<OptionSwitch checked={Settings.SuppressBladeburnerPopup}
onChange={(newValue) => Settings.SuppressBladeburnerPopup = newValue}
text="Suppress bladeburner popup"
tooltip={<>
If this is set, then having your Bladeburner actions interrupted by being busy with something
else will not display a popup message.
</>} />
</ListItem>
)}
<ListItem>
<FormControlLabel
control={<Switch checked={suppressSavedGameToast} onChange={handleSuppressSavedGameToastChange} />}
label={
<Tooltip
title={
<Typography>If this is set, there will be no "Game Saved!" toast appearing after an auto-save.</Typography>
}
>
<Typography>Suppress Auto-Save Game Toast</Typography>
</Tooltip>
}
/>
<OptionSwitch checked={Settings.SuppressSavedGameToast}
onChange={(newValue) => Settings.SuppressSavedGameToast = newValue}
text="Suppress Auto-Save Game Toast"
tooltip={<>
If this is set, there will be no "Game Saved!" toast appearing after an auto-save.
</>} />
</ListItem>
<ListItem>
<FormControlLabel
control={<Switch checked={disableHotkeys} onChange={handleDisableHotkeysChange} />}
label={
<Tooltip
title={
<Typography>
If this is set, then most hotkeys (keyboard shortcuts) in the game are disabled. This includes
Terminal commands, hotkeys to navigate between different parts of the game, and the "Save and
Close (Ctrl + b)" hotkey in the Text Editor.
</Typography>
}
>
<Typography>Disable hotkeys</Typography>
</Tooltip>
}
/>
<OptionSwitch checked={Settings.DisableHotkeys}
onChange={(newValue) => Settings.DisableHotkeys = newValue}
text="Disable hotkeys"
tooltip={<>
If this is set, then most hotkeys (keyboard shortcuts) in the game are disabled. This includes
Terminal commands, hotkeys to navigate between different parts of the game, and the "Save and
Close (Ctrl + b)" hotkey in the Text Editor.
</>} />
</ListItem>
<ListItem>
<FormControlLabel
control={<Switch checked={disableASCIIArt} onChange={handleDisableASCIIArtChange} />}
label={
<Tooltip title={<Typography>If this is set all ASCII art will be disabled.</Typography>}>
<Typography>Disable ascii art</Typography>
</Tooltip>
}
/>
<OptionSwitch checked={Settings.DisableASCIIArt}
onChange={(newValue) => Settings.DisableASCIIArt = newValue}
text="Disable ascii art"
tooltip={<>
If this is set all ASCII art will be disabled.
</>} />
</ListItem>
<ListItem>
<FormControlLabel
control={<Switch checked={disableTextEffects} onChange={handleDisableTextEffectsChange} />}
label={
<Tooltip
title={
<Typography>
If this is set, text effects will not be displayed. This can help if text is difficult to read
in certain areas.
</Typography>
}
>
<Typography>Disable text effects</Typography>
</Tooltip>
}
/>
</ListItem>
<ListItem>
<FormControlLabel
control={<Switch checked={enableBashHotkeys} onChange={handleEnableBashHotkeysChange} />}
label={
<Tooltip
title={
<Typography>
Improved Bash emulation mode. Setting this to 1 enables several new Terminal shortcuts and
features that more closely resemble a real Bash-style shell. Note that when this mode is
enabled, the default browser shortcuts are overriden by the new Bash shortcuts.
</Typography>
}
>
<Typography>Enable bash hotkeys</Typography>
</Tooltip>
}
/>
<OptionSwitch checked={Settings.DisableTextEffects}
onChange={(newValue) => Settings.DisableTextEffects = newValue}
text="Disable text effects"
tooltip={<>
If this is set, text effects will not be displayed. This can help if text is difficult to read
in certain areas.
</>} />
</ListItem>
<ListItem>
<FormControlLabel
control={<Switch checked={useIEC60027_2} onChange={handleUseIEC60027_2Change} />}
label={
<Tooltip title={<Typography>If this is set all references to memory will use GiB instead of GB, in accordance with IEC 60027-2.</Typography>}>
<Typography>Use GiB instead of GB</Typography>
</Tooltip>
}
/>
<OptionSwitch checked={Settings.DisableOverviewProgressBars}
onChange={(newValue) => Settings.DisableOverviewProgressBars = newValue}
text="Disable Overview Progress Bars"
tooltip={<>
If this is set, the progress bars in the character overview will be hidden.
</>} />
</ListItem>
<ListItem>
<OptionSwitch checked={Settings.EnableBashHotkeys}
onChange={(newValue) => Settings.EnableBashHotkeys = newValue}
text="Enable bash hotkeys"
tooltip={<>
Improved Bash emulation mode. Setting this to 1 enables several new Terminal shortcuts and
features that more closely resemble a real Bash-style shell. Note that when this mode is
enabled, the default browser shortcuts are overriden by the new Bash shortcuts.
</>} />
</ListItem>
<ListItem>
<OptionSwitch checked={Settings.UseIEC60027_2}
onChange={(newValue) => Settings.UseIEC60027_2 = newValue}
text="Use GiB instead of GB"
tooltip={<>
If this is set all references to memory will use GiB instead of GB, in accordance with IEC 60027-2.
</>} />
</ListItem>
<ListItem>
<OptionSwitch checked={Settings.ExcludeRunningScriptsFromSave}
onChange={(newValue) => Settings.ExcludeRunningScriptsFromSave = newValue}
text="Exclude Running Scripts from Save"
tooltip={<>
If this is set, the save file will exclude all running scripts. This is only useful if your save is lagging a lot. You'll have to restart your script every time you launch the game.
</>} />
</ListItem>
<ListItem>
<Tooltip
@ -620,16 +467,12 @@ export function GameOptionsRoot(props: IProps): React.ReactElement {
</ListItem>
<ListItem>
<FormControlLabel
control={<Switch checked={saveGameOnFileSave} onChange={handleSaveGameOnFile} />}
label={
<Tooltip
title={<Typography>Save your game any time a file is saved in the script editor.</Typography>}
>
<Typography>Save game on file save</Typography>
</Tooltip>
}
/>
<OptionSwitch checked={Settings.SaveGameOnFileSave}
onChange={(newValue) => Settings.SaveGameOnFileSave = newValue}
text="Save game on file save"
tooltip={<>
Save your game any time a file is saved in the script editor.
</>} />
</ListItem>
<ListItem>
@ -686,19 +529,19 @@ export function GameOptionsRoot(props: IProps): React.ReactElement {
</>
)}
</Grid>
<Grid item xs={12} sm={6}>
<Box>
<Box sx={{ display: 'grid', width: 'fit-content', height: 'fit-content' }}>
<Box sx={{ display: 'grid', gridTemplateColumns: '1fr 1fr' }}>
<Button onClick={() => props.save()}>Save Game</Button>
<Button onClick={() => setDeleteOpen(true)}>Delete Game</Button>
</Box>
<Box>
<Box sx={{ display: 'grid', gridTemplateColumns: '1fr 1fr' }}>
<Tooltip title={<Typography>Export your game to a text file.</Typography>}>
<Button onClick={() => props.export()}>
<DownloadIcon color="primary" />
Export Game
</Button>
</Tooltip>
<Tooltip title={<Typography>Import your game from a text file.<br/>This will <strong>overwrite</strong> your current game. Back it up first!</Typography>}>
<Tooltip title={<Typography>Import your game from a text file.<br />This will <strong>overwrite</strong> your current game. Back it up first!</Typography>}>
<Button onClick={startImport}>
<UploadIcon color="primary" />
Import Game
@ -728,7 +571,7 @@ export function GameOptionsRoot(props: IProps): React.ReactElement {
}
/>
</Box>
<Box>
<Box sx={{ display: 'grid' }}>
<Tooltip
title={
<Typography>
@ -743,7 +586,7 @@ export function GameOptionsRoot(props: IProps): React.ReactElement {
<Button onClick={() => props.forceKill()}>Force kill all active scripts</Button>
</Tooltip>
</Box>
<Box>
<Box sx={{ display: 'grid', gridTemplateColumns: '1fr 1fr' }}>
<Tooltip
title={
<Typography>
@ -770,7 +613,7 @@ export function GameOptionsRoot(props: IProps): React.ReactElement {
<Button onClick={() => setDiagnosticOpen(true)}>Diagnose files</Button>
</Tooltip>
</Box>
<Box>
<Box sx={{ display: 'grid', gridTemplateColumns: '1fr 1fr' }}>
<Button onClick={() => setThemeEditorOpen(true)}>Theme editor</Button>
<Button onClick={() => setStyleEditorOpen(true)}>Style editor</Button>
</Box>
@ -794,7 +637,7 @@ export function GameOptionsRoot(props: IProps): React.ReactElement {
<Typography>Incremental game plaza</Typography>
</Link>
</Box>
</Grid>
</Box>
</Grid>
<FileDiagnosticModal open={diagnosticOpen} onClose={() => setDiagnosticOpen(false)} />
<ConfirmationModal

View File

@ -39,12 +39,14 @@ export function LogBoxManager(): React.ReactElement {
() =>
LogBoxEvents.subscribe((script: RunningScript) => {
const id = script.server + "-" + script.filename + script.args.map((x: any): string => `${x}`).join("-");
if (logs.find((l) => l.id === id)) return;
logs.push({
id: id,
script: script,
});
rerender();
if (logs.find((l) => l.id === id)) close(id);
Promise.resolve().then(() => {
logs.push({
id: id,
script: script,
});
rerender();
})
}),
[],
);

View File

@ -0,0 +1,30 @@
import { FormControlLabel, Switch, Tooltip, Typography } from "@mui/material";
import React, { useEffect, useState } from "react";
interface IProps {
checked: boolean;
onChange: (newValue: boolean, error?: string) => void;
text: React.ReactNode;
tooltip: React.ReactNode;
}
export function OptionSwitch({ checked, onChange, text, tooltip }: IProps): React.ReactElement {
const [value, setValue] = useState(checked);
function handleSwitchChange(event: React.ChangeEvent<HTMLInputElement>): void {
setValue(event.target.checked);
}
useEffect(() => onChange(value), [value]);
return (
<FormControlLabel
control={<Switch checked={value} onChange={handleSwitchChange} />}
label={
<Tooltip title={<Typography>{tooltip}</Typography>}>
<Typography>{text}</Typography>
</Tooltip>
}
/>
);
}

View File

@ -1,21 +1,19 @@
import React, { useState } from "react";
import React, { useState, useEffect, useRef } from "react";
import Draggable, { DraggableEventHandler } from "react-draggable";
import makeStyles from "@mui/styles/makeStyles";
import Collapse from "@mui/material/Collapse";
import Fab from "@mui/material/Fab";
import Paper from "@mui/material/Paper";
import VisibilityOffIcon from "@mui/icons-material/VisibilityOff";
import VisibilityIcon from "@mui/icons-material/Visibility";
import KeyboardArrowDownIcon from "@mui/icons-material/KeyboardArrowDown";
import KeyboardArrowUpIcon from "@mui/icons-material/KeyboardArrowUp";
import EqualizerIcon from "@mui/icons-material/Equalizer";
import SchoolIcon from "@mui/icons-material/School";
import { use } from "../Context";
import { Page } from "../Router";
import { Settings } from "../../Settings/Settings";
import { Box, Button, Typography } from "@mui/material";
import { debounce } from "lodash";
const useStyles = makeStyles({
visibilityToggle: {
backgroundColor: "transparent",
position: "absolute",
top: "100%",
right: 0,
},
overviewContainer: {
position: "fixed",
top: 0,
@ -25,31 +23,116 @@ const useStyles = makeStyles({
justifyContent: "flex-end",
flexDirection: "column",
},
header: {
cursor: "grab",
textAlign: "center",
display: "flex",
flexDirection: "row",
alignItems: "center",
},
visibilityToggle: {
padding: "2px",
minWidth: "inherit",
backgroundColor: "transparent",
border: "none",
"&:hover": {
backgroundColor: "rgba(255, 255, 255, 0.1)",
},
},
collapse: {
borderTop: `1px solid ${Settings.theme.welllight}`,
margin: "0 auto",
},
icon: {
fontSize: "24px",
},
});
interface IProps {
children: JSX.Element[] | JSX.Element | React.ReactElement[] | React.ReactElement;
mode: "tutorial" | "overview";
}
export function Overview({ children }: IProps): React.ReactElement {
const [open, setOpen] = useState(true);
export interface OverviewSettings {
opened: boolean;
x: number;
y: number;
}
export function Overview({ children, mode }: IProps): React.ReactElement {
const draggableRef = useRef<HTMLDivElement>(null);
const [open, setOpen] = useState(Settings.overview.opened);
const [x, setX] = useState(Settings.overview.x);
const [y, setY] = useState(Settings.overview.y);
const classes = useStyles();
const router = use.Router();
const CurrentIcon = open ? KeyboardArrowUpIcon : KeyboardArrowDownIcon;
const LeftIcon = mode === "tutorial" ? SchoolIcon : EqualizerIcon;
const header = mode === "tutorial" ? "Tutorial" : "Overview";
const handleStop: DraggableEventHandler = (e, data) => {
setX(data.x);
setY(data.y);
};
useEffect(() => {
Settings.overview = { x, y, opened: open };
}, [open, x, y]);
// Trigger fakeDrag once to make sure loaded data is not outside bounds
useEffect(() => fakeDrag(), []);
// And trigger fakeDrag when the window is resized
useEffect(() => {
window.addEventListener("resize", fakeDrag);
return () => {
window.removeEventListener("resize", fakeDrag);
};
}, []);
const fakeDrag = debounce((): void => {
const node = draggableRef?.current;
if (!node) return;
// No official way to trigger an onChange to recompute the bounds
// See: https://github.com/react-grid-layout/react-draggable/issues/363#issuecomment-947751127
triggerMouseEvent(node, "mouseover");
triggerMouseEvent(node, "mousedown");
triggerMouseEvent(document, "mousemove");
triggerMouseEvent(node, "mouseup");
triggerMouseEvent(node, "click");
}, 100);
const triggerMouseEvent = (node: HTMLDivElement | Document, eventType: string): void => {
const clickEvent = document.createEvent("MouseEvents");
clickEvent.initEvent(eventType, true, true);
node.dispatchEvent(clickEvent);
};
if (router.page() === Page.BitVerse || router.page() === Page.Loading || router.page() === Page.Recovery)
return <></>;
let icon;
if (open) {
icon = <VisibilityOffIcon color="primary" />;
} else {
icon = <VisibilityIcon color="primary" />;
}
return (
<Paper square classes={{ root: classes.overviewContainer }}>
<Collapse in={open}>{children}</Collapse>
<Fab size="small" classes={{ root: classes.visibilityToggle }} onClick={() => setOpen((old) => !old)}>
{icon}
</Fab>
</Paper>
<Draggable handle=".drag" bounds="body" onStop={handleStop} defaultPosition={{ x, y }}>
<Paper className={classes.overviewContainer} square>
<Box className="drag" onDoubleClick={() => setOpen((old) => !old)} ref={draggableRef}>
<Box className={classes.header}>
<LeftIcon color="secondary" className={classes.icon} sx={{ padding: "2px" }} />
<Typography flexGrow={1} color="secondary">
{header}
</Typography>
<Button variant="text" size="small" className={classes.visibilityToggle}>
{<CurrentIcon className={classes.icon} color="secondary" onClick={() => setOpen((old) => !old)} />}
</Button>
</Box>
</Box>
<Collapse in={open} className={classes.collapse}>
{children}
</Collapse>
</Paper>
</Draggable>
);
}

View File

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

View File

@ -0,0 +1,29 @@
// eslint-disable-next-line @typescript-eslint/no-unused-vars
import { jest, describe, expect, test } from "@jest/globals";
import { Script } from "../../src/Script/Script";
import { Player } from "../../src/Player";
jest.mock(`!!raw-loader!../NetscriptDefinitions.d.ts`, () => "", {
virtual: true,
});
const code = `/** @param {NS} ns **/
export async function main(ns) {
ns.print(ns.getWeakenTime('n00dles'));
}`;
describe("Validate Save Script Works", function () {
it("Save", function () {
const server = "home";
const filename = "test.js";
const player = Player;
const script = new Script();
script.saveScript(player, filename, code, server, []);
expect(script.filename).toEqual(filename)
expect(script.code).toEqual(code)
expect(script.server).toEqual(server)
});
});