Merge pull request #3369 from danielyxie/dev

fmt / fix sleep
This commit is contained in:
hydroflame 2022-04-06 19:43:57 -04:00 committed by GitHub
commit c94f31fc2a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
168 changed files with 3328 additions and 2941 deletions

@ -9,3 +9,6 @@
- Include how it was tested
- Include screenshot / gif (if possible)
Make sure you run `npm run format` and `npm run lint` before pushing.

@ -4,28 +4,28 @@ on:
workflow_dispatch:
inputs:
version:
description: 'Version (format: x.y.z)'
description: "Version (format: x.y.z)"
required: true
versionNumber:
description: 'Version Number (for saves migration)'
description: "Version Number (for saves migration)"
required: true
changelog:
description: 'Changelog (url that points to RAW markdown)'
default: ''
description: "Changelog (url that points to RAW markdown)"
default: ""
buildApp:
description: 'Include Application Build'
description: "Include Application Build"
type: boolean
default: 'true'
default: "true"
required: true
buildDoc:
description: 'Include Documentation Build'
description: "Include Documentation Build"
type: boolean
default: 'true'
default: "true"
required: true
prepareRelease:
description: 'Prepare Draft Release'
description: "Prepare Draft Release"
type: boolean
default: 'true'
default: "true"
required: true
jobs:
@ -46,7 +46,7 @@ jobs:
uses: actions/setup-node@v2
with:
node-version: 16.13.1
cache: 'npm'
cache: "npm"
- name: Install NPM dependencies for version updater
working-directory: ./tools/bump-version
run: npm ci

@ -3,10 +3,10 @@ on:
workflow_dispatch:
inputs:
fromCommit:
description: 'From Commit SHA (full-length)'
description: "From Commit SHA (full-length)"
required: true
toCommit:
description: 'To Commit SHA (full-length, if omitted will use latest)'
description: "To Commit SHA (full-length, if omitted will use latest)"
jobs:
fetchChangelog:
@ -17,7 +17,7 @@ jobs:
uses: actions/setup-node@v2
with:
node-version: 16.13.1
cache: 'npm'
cache: "npm"
- name: Install NPM dependencies
working-directory: ./tools/fetch-changelog
run: npm ci

@ -25,8 +25,8 @@ I need equations that test many different aspect of "math culture", it can be ch
All variable purchasing will be scriptable.
All equation must have:
- several variables that can be upgraded, at least 1 variable must be strategic in it's upgrading (upgrading too much can cause drop in performance)
- Some sort of math twist that requires some thinking, like (-2)^c1 alters between positive and negative.

@ -115,7 +115,6 @@ Fork and clone the repo
- Regularly rebase your branch against `dev` to make sure you have the latest updates pulled.
- When merging, always merge your branch into `dev`. When releasing a new update, then merge `dev` into `master`
## Running locally.
Install
@ -131,7 +130,6 @@ Inside the root of the repo run
After that you can open any browser and navigate to `localhost:8000` and play the game.
Saving a file will reload the game automatically.
### How to build the electron app
Tested on Node v16.13.1 (LTS) on Windows

@ -23,10 +23,11 @@ Yes, just export the save file from the options menu & import it in the other pl
## Game is stuck after running scripts!
You may have created an infinite loop with no sleep. You'll have to restart the game by killing all scripts.
* On Browser: Stick `?noScript` at the end of the URL
* On Steam:
* In the menu, "Reloads" -> "Reload & Kill All Scripts".
* If this does not work, when launching the game, use the kill all script options.
- On Browser: Stick `?noScript` at the end of the URL
- On Steam:
- In the menu, "Reloads" -> "Reload & Kill All Scripts".
- If this does not work, when launching the game, use the kill all script options.
---
@ -51,11 +52,13 @@ You can navigate to the game files by right-clicking the game in your library an
## Steam: Game won't launch
### **On Windows**
If the game is installed on a network drive, it will fail to start due to a [limitation in Chromium](https://github.com/electron/electron/issues/27356).
If you cannot move the game to another drive, you'll have to add the `--no-sandbox` launch option. In your Steam Library, Right click the game and hit "Properties". You'll see the launch option section in the "General" window.
### **On Linux**
The game is built natively, do not use Proton unless native does not work.
When launching the game, you will be prompted with three options. If the standard launch does not work, you may attempt the `--disable-seccomp-filter-sandbox` or `--no-sandbox` launch option. If this still does not work, the game should be able to start by launching it directly or through the terminal. See [How do I get to the game files?](#game-files).
@ -68,17 +71,17 @@ When launching the game, you will be prompted with three options. If the standar
You may want access the logs to get information about crashes or such.
* on Linux: `~/.config/bitburner/logs/main.log`
* on macOS: `~/Library/Logs/bitburner/main.log`
* on Windows: `%USERPROFILE%\AppData\Roaming\bitburner\logs\main.log`
- on Linux: `~/.config/bitburner/logs/main.log`
- on macOS: `~/Library/Logs/bitburner/main.log`
- on Windows: `%USERPROFILE%\AppData\Roaming\bitburner\logs\main.log`
### Config (using [electron-store](https://github.com/sindresorhus/electron-store#readme))
Configuration file will be written to disk in the application data directory.
* on Linux: `~/.config/bitburner/config.json`
* on macOS: `~/Library/Application\ Support/bitburner/config.json`
* on Windows: `%USERPROFILE%\AppData\Roaming\bitburner\config.json`
- on Linux: `~/.config/bitburner/config.json`
- on macOS: `~/Library/Application\ Support/bitburner/config.json`
- on Windows: `%USERPROFILE%\AppData\Roaming\bitburner\config.json`
---

@ -10,7 +10,7 @@ async function enableAchievementsInterval(window) {
// here. Hey if it works it works.
const steamAchievements = greenworks.getAchievementNames();
log.silly(`All Steam achievements ${JSON.stringify(steamAchievements)}`);
const playerAchieved = (await Promise.all(steamAchievements.map(checkSteamAchievement))).filter(name => !!name);
const playerAchieved = (await Promise.all(steamAchievements.map(checkSteamAchievement))).filter((name) => !!name);
log.debug(`Player has Steam achievements ${JSON.stringify(playerAchieved)}`);
const intervalID = setInterval(async () => {
try {
@ -26,7 +26,7 @@ async function enableAchievementsInterval(window) {
log.error(error);
// The interval probably did not get cleared after a window kill
log.warn('Clearing achievements timer');
log.warn("Clearing achievements timer");
clearInterval(intervalID);
return;
}
@ -36,10 +36,14 @@ async function enableAchievementsInterval(window) {
function checkSteamAchievement(name) {
return new Promise((resolve) => {
greenworks.getAchievement(name, playerHas => resolve(playerHas ? name : ""), err => {
greenworks.getAchievement(
name,
(playerHas) => resolve(playerHas ? name : ""),
(err) => {
log.warn(`Failed to get Steam achievement ${name} status: ${err}`);
resolve("");
});
},
);
});
}
@ -50,5 +54,6 @@ function disableAchievementsInterval(window) {
}
module.exports = {
enableAchievementsInterval, disableAchievementsInterval
}
enableAchievementsInterval,
disableAchievementsInterval,
};

@ -12,25 +12,27 @@ async function initialize(win) {
window = win;
server = http.createServer(async function (req, res) {
let body = "";
res.setHeader('Content-Type', 'application/json');
res.setHeader("Content-Type", "application/json");
req.on("data", (chunk) => {
body += chunk.toString(); // convert Buffer to string
});
req.on("end", async () => {
const providedToken = req.headers?.authorization?.replace('Bearer ', '') ?? '';
const providedToken = req.headers?.authorization?.replace("Bearer ", "") ?? "";
const isValid = providedToken === getAuthenticationToken();
if (isValid) {
log.debug('Valid authentication token');
log.debug("Valid authentication token");
} else {
log.log('Invalid authentication token');
log.log("Invalid authentication token");
res.writeHead(401);
res.end(JSON.stringify({
res.end(
JSON.stringify({
success: false,
msg: 'Invalid authentication token'
}));
msg: "Invalid authentication token",
}),
);
return;
}
@ -40,10 +42,12 @@ async function initialize(win) {
} catch (error) {
log.warn(`Invalid body data`);
res.writeHead(400);
res.end(JSON.stringify({
res.end(
JSON.stringify({
success: false,
msg: 'Invalid body data'
}));
msg: "Invalid body data",
}),
);
return;
}
@ -62,10 +66,12 @@ async function initialize(win) {
if (!data) {
log.warn(`Invalid script update request - No data`);
res.writeHead(400);
res.end(JSON.stringify({
res.end(
JSON.stringify({
success: false,
msg: 'Invalid script update request - No data'
}));
msg: "Invalid script update request - No data",
}),
);
return;
}
@ -84,19 +90,20 @@ async function initialize(win) {
log.warn(`Api Server Error`, result.msg);
}
res.end(JSON.stringify({
res.end(
JSON.stringify({
success: result.res,
msg: result.msg,
data: result.data
}));
data: result.data,
}),
);
});
});
const autostart = config.get('autostart', false);
const autostart = config.get("autostart", false);
if (autostart) {
try {
await enable()
await enable();
} catch (error) {
return Promise.reject(error);
}
@ -105,15 +112,14 @@ async function initialize(win) {
return Promise.resolve();
}
function enable() {
if (isListening()) {
log.warn('API server already listening');
log.warn("API server already listening");
return Promise.resolve();
}
const port = config.get('port', 9990);
const host = config.get('host', '127.0.0.1');
const port = config.get("port", 9990);
const host = config.get("host", "127.0.0.1");
log.log(`Starting http server on port ${port} - listening on ${host}`);
// https://stackoverflow.com/a/62289870
@ -125,13 +131,10 @@ function enable() {
resolve();
}
});
server.once('error', (err) => {
server.once("error", (err) => {
if (!startFinished) {
startFinished = true;
console.log(
'There was an error starting the server in the error listener:',
err
);
console.log("There was an error starting the server in the error listener:", err);
reject(err);
}
});
@ -140,11 +143,11 @@ function enable() {
function disable() {
if (!isListening()) {
log.warn('API server not listening');
log.warn("API server not listening");
return Promise.resolve();
}
log.log('Stopping http server');
log.log("Stopping http server");
return server.close();
}
@ -162,31 +165,35 @@ function isListening() {
function toggleAutostart() {
const newValue = !isAutostart();
config.set('autostart', newValue);
config.set("autostart", newValue);
log.log(`New autostart value is '${newValue}'`);
}
function isAutostart() {
return config.get('autostart');
return config.get("autostart");
}
function getAuthenticationToken() {
const token = config.get('token');
const token = config.get("token");
if (token) return token;
const newToken = generateToken();
config.set('token', newToken);
config.set("token", newToken);
return newToken;
}
function generateToken() {
const buffer = crypto.randomBytes(48);
return buffer.toString('base64')
return buffer.toString("base64");
}
module.exports = {
initialize,
enable, disable, toggleServer,
toggleAutostart, isAutostart,
getAuthenticationToken, isListening,
}
enable,
disable,
toggleServer,
toggleAutostart,
isAutostart,
getAuthenticationToken,
isListening,
};

@ -15,20 +15,20 @@ const debug = process.argv.includes("--debug");
async function createWindow(killall) {
const setStopProcessHandler = global.app_handlers.stopProcess;
app.setAppUserModelId("Bitburner")
app.setAppUserModelId("Bitburner");
let icon;
if (process.platform == 'linux') {
icon = path.join(__dirname, 'icon.png');
if (process.platform == "linux") {
icon = path.join(__dirname, "icon.png");
}
const tracker = windowTracker('main');
const tracker = windowTracker("main");
const window = new BrowserWindow({
icon,
show: false,
backgroundThrottling: false,
backgroundColor: "#000000",
title: 'Bitburner',
title: "Bitburner",
x: tracker.state.x,
y: tracker.state.y,
width: tracker.state.width,
@ -37,7 +37,7 @@ async function createWindow(killall) {
minHeight: 400,
webPreferences: {
nativeWindowOpen: true,
preload: path.join(__dirname, 'preload.js'),
preload: path.join(__dirname, "preload.js"),
},
});
@ -52,12 +52,12 @@ async function createWindow(killall) {
window.webContents.on("new-window", async function (e, url) {
// Let's make sure sure we have a proper url
let parsedUrl
let parsedUrl;
try {
parsedUrl = new URL(url);
} catch (_) {
// This is an invalid url, let's just do nothing
log.warn(`Invalid url found: ${url}`)
log.warn(`Invalid url found: ${url}`);
e.preventDefault();
return;
}
@ -74,11 +74,13 @@ async function createWindow(killall) {
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}`)
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`)
log.warn(`Requested path ${filePath.dir}${path.sep}${filePath.base} does not exist`);
e.preventDefault();
}
@ -90,7 +92,7 @@ async function createWindow(killall) {
let urlToOpen = parsedUrl.toString();
if (parsedUrl.search) {
log.log(`Cannot open a path with parameters: ${parsedUrl.search}`);
urlToOpen = urlToOpen.replace(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" });

@ -3,83 +3,91 @@
// found in the LICENSE file.
// The source code can be found in https://github.com/greenheartgames/greenworks
var fs = require('fs');
var fs = require("fs");
var greenworks;
if (process.platform == 'darwin') {
if (process.arch == 'x64')
greenworks = require('./lib/greenworks-osx64');
else if (process.arch == 'ia32')
greenworks = require('./lib/greenworks-osx32');
} else if (process.platform == 'win32') {
if (process.arch == 'x64')
greenworks = require('./lib/greenworks-win64');
else if (process.arch == 'ia32')
greenworks = require('./lib/greenworks-win32');
} else if (process.platform == 'linux') {
if (process.arch == 'x64')
greenworks = require('./lib/greenworks-linux64');
else if (process.arch == 'ia32')
greenworks = require('./lib/greenworks-linux32');
if (process.platform == "darwin") {
if (process.arch == "x64") greenworks = require("./lib/greenworks-osx64");
else if (process.arch == "ia32") greenworks = require("./lib/greenworks-osx32");
} else if (process.platform == "win32") {
if (process.arch == "x64") greenworks = require("./lib/greenworks-win64");
else if (process.arch == "ia32") greenworks = require("./lib/greenworks-win32");
} else if (process.platform == "linux") {
if (process.arch == "x64") greenworks = require("./lib/greenworks-linux64");
else if (process.arch == "ia32") greenworks = require("./lib/greenworks-linux32");
}
function error_process(err, error_callback) {
if (err && error_callback)
error_callback(err);
if (err && error_callback) error_callback(err);
}
greenworks.ugcGetItems = function(options, ugc_matching_type, ugc_query_type,
success_callback, error_callback) {
if (typeof options !== 'object') {
greenworks.ugcGetItems = function (options, ugc_matching_type, ugc_query_type, success_callback, error_callback) {
if (typeof options !== "object") {
error_callback = success_callback;
success_callback = ugc_query_type;
ugc_query_type = ugc_matching_type;
ugc_matching_type = options;
options = {
'app_id': greenworks.getAppId(),
'page_num': 1
}
}
greenworks._ugcGetItems(options, ugc_matching_type, ugc_query_type,
success_callback, error_callback);
app_id: greenworks.getAppId(),
page_num: 1,
};
}
greenworks._ugcGetItems(options, ugc_matching_type, ugc_query_type, success_callback, error_callback);
};
greenworks.ugcGetUserItems = function(options, ugc_matching_type,
ugc_list_sort_order, ugc_list, success_callback, error_callback) {
if (typeof options !== 'object') {
greenworks.ugcGetUserItems = function (
options,
ugc_matching_type,
ugc_list_sort_order,
ugc_list,
success_callback,
error_callback,
) {
if (typeof options !== "object") {
error_callback = success_callback;
success_callback = ugc_list;
ugc_list = ugc_list_sort_order;
ugc_list_sort_order = ugc_matching_type;
ugc_matching_type = options;
options = {
'app_id': greenworks.getAppId(),
'page_num': 1
}
}
greenworks._ugcGetUserItems(options, ugc_matching_type, ugc_list_sort_order,
ugc_list, success_callback, error_callback);
app_id: greenworks.getAppId(),
page_num: 1,
};
}
greenworks._ugcGetUserItems(
options,
ugc_matching_type,
ugc_list_sort_order,
ugc_list,
success_callback,
error_callback,
);
};
greenworks.ugcSynchronizeItems = function (options, sync_dir, success_callback,
error_callback) {
if (typeof options !== 'object') {
greenworks.ugcSynchronizeItems = function (options, sync_dir, success_callback, error_callback) {
if (typeof options !== "object") {
error_callback = success_callback;
success_callback = sync_dir;
sync_dir = options;
options = {
'app_id': greenworks.getAppId(),
'page_num': 1
}
}
greenworks._ugcSynchronizeItems(options, sync_dir, success_callback,
error_callback);
app_id: greenworks.getAppId(),
page_num: 1,
};
}
greenworks._ugcSynchronizeItems(options, sync_dir, success_callback, error_callback);
};
greenworks.publishWorkshopFile = function(options, file_path, image_path, title,
description, success_callback, error_callback) {
if (typeof options !== 'object') {
greenworks.publishWorkshopFile = function (
options,
file_path,
image_path,
title,
description,
success_callback,
error_callback,
) {
if (typeof options !== "object") {
error_callback = success_callback;
success_callback = description;
description = title;
@ -87,18 +95,24 @@ greenworks.publishWorkshopFile = function(options, file_path, image_path, title,
image_path = file_path;
file_path = options;
options = {
'app_id': greenworks.getAppId(),
'tags': []
}
}
greenworks._publishWorkshopFile(options, file_path, image_path, title,
description, success_callback, error_callback);
app_id: greenworks.getAppId(),
tags: [],
};
}
greenworks._publishWorkshopFile(options, file_path, image_path, title, description, success_callback, error_callback);
};
greenworks.updatePublishedWorkshopFile = function(options,
published_file_handle, file_path, image_path, title, description,
success_callback, error_callback) {
if (typeof options !== 'object') {
greenworks.updatePublishedWorkshopFile = function (
options,
published_file_handle,
file_path,
image_path,
title,
description,
success_callback,
error_callback,
) {
if (typeof options !== "object") {
error_callback = success_callback;
success_callback = description;
description = title;
@ -107,104 +121,166 @@ greenworks.updatePublishedWorkshopFile = function(options,
file_path = published_file_handle;
published_file_handle = options;
options = {
'tags': [] // No tags are set
}
}
greenworks._updatePublishedWorkshopFile(options, published_file_handle,
file_path, image_path, title, description, success_callback,
error_callback);
tags: [], // No tags are set
};
}
greenworks._updatePublishedWorkshopFile(
options,
published_file_handle,
file_path,
image_path,
title,
description,
success_callback,
error_callback,
);
};
// An utility function for publish related APIs.
// It processes remains steps after saving files to Steam Cloud.
function file_share_process(file_name, image_name, next_process_func,
error_callback, progress_callback) {
if (progress_callback)
progress_callback("Completed on saving files on Steam Cloud.");
greenworks.fileShare(file_name, function() {
greenworks.fileShare(image_name, function() {
function file_share_process(file_name, image_name, next_process_func, error_callback, progress_callback) {
if (progress_callback) progress_callback("Completed on saving files on Steam Cloud.");
greenworks.fileShare(
file_name,
function () {
greenworks.fileShare(
image_name,
function () {
next_process_func();
}, function(err) { error_process(err, error_callback); });
}, function(err) { error_process(err, error_callback); });
},
function (err) {
error_process(err, error_callback);
},
);
},
function (err) {
error_process(err, error_callback);
},
);
}
// Publishing user generated content(ugc) to Steam contains following steps:
// 1. Save file and image to Steam Cloud.
// 2. Share the file and image.
// 3. publish the file to workshop.
greenworks.ugcPublish = function(file_name, title, description, image_name,
success_callback, error_callback, progress_callback) {
greenworks.ugcPublish = function (
file_name,
title,
description,
image_name,
success_callback,
error_callback,
progress_callback,
) {
var publish_file_process = function () {
if (progress_callback)
progress_callback("Completed on sharing files.");
greenworks.publishWorkshopFile(file_name, image_name, title, description,
function(publish_file_id) { success_callback(publish_file_id); },
function(err) { error_process(err, error_callback); });
if (progress_callback) progress_callback("Completed on sharing files.");
greenworks.publishWorkshopFile(
file_name,
image_name,
title,
description,
function (publish_file_id) {
success_callback(publish_file_id);
},
function (err) {
error_process(err, error_callback);
},
);
};
greenworks.saveFilesToCloud(
[file_name, image_name],
function () {
file_share_process(file_name, image_name, publish_file_process, error_callback, progress_callback);
},
function (err) {
error_process(err, error_callback);
},
);
};
greenworks.saveFilesToCloud([file_name, image_name], function() {
file_share_process(file_name, image_name, publish_file_process,
error_callback, progress_callback);
}, function(err) { error_process(err, error_callback); });
}
// Update publish ugc steps:
// 1. Save new file and image to Steam Cloud.
// 2. Share file and images.
// 3. Update published file.
greenworks.ugcPublishUpdate = function(published_file_id, file_name, title,
description, image_name, success_callback, error_callback,
progress_callback) {
greenworks.ugcPublishUpdate = function (
published_file_id,
file_name,
title,
description,
image_name,
success_callback,
error_callback,
progress_callback,
) {
var update_published_file_process = function () {
if (progress_callback)
progress_callback("Completed on sharing files.");
greenworks.updatePublishedWorkshopFile(published_file_id,
file_name, image_name, title, description,
function() { success_callback(); },
function(err) { error_process(err, error_callback); });
if (progress_callback) progress_callback("Completed on sharing files.");
greenworks.updatePublishedWorkshopFile(
published_file_id,
file_name,
image_name,
title,
description,
function () {
success_callback();
},
function (err) {
error_process(err, error_callback);
},
);
};
greenworks.saveFilesToCloud([file_name, image_name], function() {
file_share_process(file_name, image_name, update_published_file_process,
error_callback, progress_callback);
}, function(err) { error_process(err, error_callback); });
}
greenworks.saveFilesToCloud(
[file_name, image_name],
function () {
file_share_process(file_name, image_name, update_published_file_process, error_callback, progress_callback);
},
function (err) {
error_process(err, error_callback);
},
);
};
// Greenworks Utils APIs implmentation.
greenworks.Utils.move = function(source_dir, target_dir, success_callback,
error_callback) {
greenworks.Utils.move = function (source_dir, target_dir, success_callback, error_callback) {
fs.rename(source_dir, target_dir, function (err) {
if (err) {
if (error_callback) error_callback(err);
return;
}
if (success_callback)
success_callback();
if (success_callback) success_callback();
});
}
};
greenworks.init = function () {
if (this.initAPI()) return true;
if (!this.isSteamRunning())
throw new Error("Steam initialization failed. Steam is not running.");
if (!this.isSteamRunning()) throw new Error("Steam initialization failed. Steam is not running.");
var appId;
try {
appId = fs.readFileSync('steam_appid.txt', 'utf8');
appId = fs.readFileSync("steam_appid.txt", "utf8");
} catch (e) {
throw new Error("Steam initialization failed. Steam is running," +
throw new Error(
"Steam initialization failed. Steam is running," +
"but steam_appid.txt is missing. Expected to find it in: " +
require('path').resolve('steam_appid.txt'));
require("path").resolve("steam_appid.txt"),
);
}
if (!/^\d+ *\r?\n?$/.test(appId)) {
throw new Error("Steam initialization failed. " +
throw new Error(
"Steam initialization failed. " +
"steam_appid.txt appears to be invalid; " +
"it should contain a numeric ID: " + appId);
"it should contain a numeric ID: " +
appId,
);
}
throw new Error("Steam initialization failed, but Steam is running, " +
throw new Error(
"Steam initialization failed, but Steam is running, " +
"and steam_appid.txt is present and valid." +
"Maybe that's not really YOUR app ID? " + appId.trim());
}
"Maybe that's not really YOUR app ID? " +
appId.trim(),
);
};
var EventEmitter = require('events').EventEmitter;
var EventEmitter = require("events").EventEmitter;
greenworks.__proto__ = EventEmitter.prototype;
EventEmitter.call(greenworks);
@ -212,6 +288,6 @@ greenworks._steam_events.on = function () {
greenworks.emit.apply(greenworks, arguments);
};
process.versions['greenworks'] = greenworks._version;
process.versions["greenworks"] = greenworks._version;
module.exports = greenworks;

@ -18,7 +18,7 @@ log.transports.console.level = config.get("console-log-level", "debug");
log.catchErrors();
log.info(`Started app: ${JSON.stringify(process.argv)}`);
process.on('uncaughtException', function () {
process.on("uncaughtException", function () {
// The exception will already have been logged by electron-log
process.exit(1);
});
@ -67,42 +67,43 @@ function setStopProcessHandler(app, window, enabled) {
// So we'll alert the player to close their browser.
if (global.app_playerOpenedExternalLink) {
await dialog.showMessageBox({
title: 'Bitburner',
message: 'You may have to close your browser to properly exit the game.',
detail: 'Steam will keep tracking Bitburner as "Running" if any process started within the game is still running.' +
' This includes launching an external link, which opens up your browser.',
type: 'warning', buttons: ['OK']
title: "Bitburner",
message: "You may have to close your browser to properly exit the game.",
detail:
'Steam will keep tracking Bitburner as "Running" if any process started within the game is still running.' +
" This includes launching an external link, which opens up your browser.",
type: "warning",
buttons: ["OK"],
});
}
// We'll try to execute javascript on the page to see if we're stuck
let canRunJS = false;
window.webContents.executeJavaScript('window.stop(); document.close()', true)
.then(() => canRunJS = true);
window.webContents.executeJavaScript("window.stop(); document.close()", true).then(() => (canRunJS = true));
setTimeout(() => {
// Wait a few milliseconds to prevent a race condition before loading the exit screen
window.webContents.stop();
window.loadFile("exit.html")
window.loadFile("exit.html");
}, 20);
// Wait 200ms, if the promise has not yet resolved, let's crash the process since we're possibly in a stuck scenario
setTimeout(() => {
if (!canRunJS) {
// We're stuck, let's crash the process
log.warn('Forcefully crashing the renderer process');
log.warn("Forcefully crashing the renderer process");
window.webContents.forcefullyCrashRenderer();
}
log.debug('Destroying the window');
log.debug("Destroying the window");
window.destroy();
}, 200);
}
};
const clearWindowHandler = () => {
window = null;
};
const stopProcessHandler = () => {
log.info('Quitting the app...');
log.info("Quitting the app...");
app.isQuiting = true;
app.quit();
process.exit(0);
@ -121,12 +122,12 @@ function setStopProcessHandler(app, window, enabled) {
const restoreNewest = config.get("onload-restore-newest", true);
if (restoreNewest && !isRestoreDisabled) {
try {
await storage.restoreIfNewerExists(window)
await storage.restoreIfNewerExists(window);
} catch (error) {
log.error("Could not restore newer file", error);
}
}
}
};
const receivedDisableRestoreHandler = async (event, arg) => {
if (!window) {
@ -140,7 +141,7 @@ function setStopProcessHandler(app, window, enabled) {
isRestoreDisabled = false;
log.debug("Re-enabling auto-restore");
}, arg.duration);
}
};
const receivedGameSavedHandler = async (event, arg) => {
if (!window) {
@ -164,10 +165,11 @@ function setStopProcessHandler(app, window, enabled) {
log.debug(`Auto-save to cloud disabled for save game under ${minimumPlaytime}ms (${playtime}ms)`);
}
}
}
};
const saveToCloud = debounce(async (save) => {
log.debug("Saving to Steam Cloud ...")
const saveToCloud = debounce(
async (save) => {
log.debug("Saving to Steam Cloud ...");
try {
const playerId = window.gameInfo.player.identifier;
await storage.pushGameSaveToSteamCloud(save, playerId);
@ -176,26 +178,33 @@ function setStopProcessHandler(app, window, enabled) {
log.error(error);
utils.writeToast(window, "Could not save to Steam Cloud.", "error", 5000);
}
}, config.get("cloud-save-min-time", 1000 * 60 * 15), { leading: true });
},
config.get("cloud-save-min-time", 1000 * 60 * 15),
{ leading: true },
);
const saveToDisk = debounce(async (save, fileName) => {
log.debug("Saving to Disk ...")
const saveToDisk = debounce(
async (save, fileName) => {
log.debug("Saving to Disk ...");
try {
const file = await storage.saveGameToDisk(window, { save, fileName });
log.silly(`Saved Game to '${file.replaceAll('\\', '\\\\')}'`);
log.silly(`Saved Game to '${file.replaceAll("\\", "\\\\")}'`);
} catch (error) {
log.error(error);
utils.writeToast(window, "Could not save to disk", "error", 5000);
}
}, config.get("disk-save-min-time", 1000 * 60 * 5), { leading: true });
},
config.get("disk-save-min-time", 1000 * 60 * 5),
{ leading: true },
);
if (enabled) {
log.debug("Adding closing handlers");
ipcMain.on("push-game-ready", receivedGameReadyHandler);
ipcMain.on("push-game-saved", receivedGameSavedHandler);
ipcMain.on("push-disable-restore", receivedDisableRestoreHandler)
ipcMain.on("push-disable-restore", receivedDisableRestoreHandler);
window.on("closed", clearWindowHandler);
window.on("close", closingWindowHandler)
window.on("close", closingWindowHandler);
app.on("window-all-closed", stopProcessHandler);
} else {
log.debug("Removing closing handlers");
@ -213,7 +222,7 @@ async function startWindow(noScript) {
global.app_handlers = {
stopProcess: setStopProcessHandler,
createWindow: startWindow,
}
};
app.whenReady().then(async () => {
log.info("Application is ready!");
@ -231,7 +240,8 @@ app.whenReady().then(async () => {
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']
type: "warning",
buttons: ["OK"],
});
}
}

@ -37,7 +37,7 @@ function getMenu(window) {
log.error(error);
utils.writeToast(window, "Could not load last save from disk", "error", 5000);
}
}
},
},
{
label: "Load From File",
@ -51,9 +51,7 @@ function getMenu(window) {
{ name: "Game Saves", extensions: ["json", "json.gz", "txt"] },
{ name: "All", extensions: ["*"] },
],
properties: [
"openFile", "dontAddToRecent",
]
properties: ["openFile", "dontAddToRecent"],
});
if (result.canceled) return;
const file = result.filePaths[0];
@ -65,7 +63,7 @@ function getMenu(window) {
log.error(error);
utils.writeToast(window, "Could not load save from disk", "error", 5000);
}
}
},
},
{
label: "Load From Steam Cloud",
@ -78,7 +76,7 @@ function getMenu(window) {
log.error(error);
utils.writeToast(window, "Could not load from Steam Cloud", "error", 5000);
}
}
},
},
{
type: "separator",
@ -89,8 +87,7 @@ function getMenu(window) {
checked: storage.isSaveCompressionEnabled(),
click: (menuItem) => {
storage.setSaveCompressionConfig(menuItem.checked);
utils.writeToast(window,
`${menuItem.checked ? "Enabled" : "Disabled"} Save Compression`, "info", 5000);
utils.writeToast(window, `${menuItem.checked ? "Enabled" : "Disabled"} Save Compression`, "info", 5000);
refreshMenu(window);
},
},
@ -100,8 +97,7 @@ function getMenu(window) {
checked: storage.isAutosaveEnabled(),
click: (menuItem) => {
storage.setAutosaveConfig(menuItem.checked);
utils.writeToast(window,
`${menuItem.checked ? "Enabled" : "Disabled"} Auto-Save to Disk`, "info", 5000);
utils.writeToast(window, `${menuItem.checked ? "Enabled" : "Disabled"} Auto-Save to Disk`, "info", 5000);
refreshMenu(window);
},
},
@ -112,8 +108,12 @@ function getMenu(window) {
checked: storage.isCloudEnabled(),
click: (menuItem) => {
storage.setCloudEnabledConfig(menuItem.checked);
utils.writeToast(window,
`${menuItem.checked ? "Enabled" : "Disabled"} Auto-Save to Steam Cloud`, "info", 5000);
utils.writeToast(
window,
`${menuItem.checked ? "Enabled" : "Disabled"} Auto-Save to Steam Cloud`,
"info",
5000,
);
refreshMenu(window);
},
},
@ -123,8 +123,12 @@ function getMenu(window) {
checked: config.get("onload-restore-newest", true),
click: (menuItem) => {
config.set("onload-restore-newest", menuItem.checked);
utils.writeToast(window,
`${menuItem.checked ? "Enabled" : "Disabled"} Restore Newest on Load`, "info", 5000);
utils.writeToast(
window,
`${menuItem.checked ? "Enabled" : "Disabled"} Restore Newest on Load`,
"info",
5000,
);
refreshMenu(window);
},
},
@ -153,7 +157,7 @@ function getMenu(window) {
label: "Open Data Directory",
click: () => shell.openPath(app.getPath("userData")),
},
]
],
},
{
type: "separator",
@ -162,7 +166,7 @@ function getMenu(window) {
label: "Quit",
click: () => app.quit(),
},
]
],
},
{
label: "Edit",
@ -210,29 +214,29 @@ function getMenu(window) {
label: "API Server",
submenu: [
{
label: api.isListening() ? 'Disable Server' : 'Enable Server',
click: (async () => {
label: api.isListening() ? "Disable Server" : "Enable Server",
click: async () => {
let success = false;
try {
await api.toggleServer();
success = true;
} catch (error) {
log.error(error);
utils.showErrorBox('Error Toggling Server', error);
utils.showErrorBox("Error Toggling Server", error);
}
if (success && api.isListening()) {
utils.writeToast(window, "Started API Server", "success");
} else if (success && !api.isListening()) {
utils.writeToast(window, "Stopped API Server", "success");
} else {
utils.writeToast(window, 'Error Toggling Server', "error");
utils.writeToast(window, "Error Toggling Server", "error");
}
refreshMenu(window);
})
},
},
{
label: api.isAutostart() ? 'Disable Autostart' : 'Enable Autostart',
click: (async () => {
label: api.isAutostart() ? "Disable Autostart" : "Enable Autostart",
click: async () => {
api.toggleAutostart();
if (api.isAutostart()) {
utils.writeToast(window, "Enabled API Server Autostart", "success");
@ -240,42 +244,45 @@ function getMenu(window) {
utils.writeToast(window, "Disabled API Server Autostart", "success");
}
refreshMenu(window);
})
},
},
{
label: 'Copy Auth Token',
click: (async () => {
label: "Copy Auth Token",
click: async () => {
const token = api.getAuthenticationToken();
log.log('Wrote authentication token to clipboard');
log.log("Wrote authentication token to clipboard");
clipboard.writeText(token);
utils.writeToast(window, "Copied Authentication Token to Clipboard", "info");
})
},
},
{
type: 'separator',
type: "separator",
},
{
label: 'Information',
label: "Information",
click: () => {
dialog.showMessageBox({
type: 'info',
title: 'Bitburner > API Server Information',
message: 'The API Server is used to write script files to your in-game home.',
detail: 'There is an official Visual Studio Code extension that makes use of that feature.\n\n' +
'It allows you to write your script file in an external IDE and have them pushed over to the game automatically.\n' +
'If you want more information, head over to: https://github.com/bitburner-official/bitburner-vscode.',
buttons: ['Dismiss', 'Open Extension Link (GitHub)'],
dialog
.showMessageBox({
type: "info",
title: "Bitburner > API Server Information",
message: "The API Server is used to write script files to your in-game home.",
detail:
"There is an official Visual Studio Code extension that makes use of that feature.\n\n" +
"It allows you to write your script file in an external IDE and have them pushed over to the game automatically.\n" +
"If you want more information, head over to: https://github.com/bitburner-official/bitburner-vscode.",
buttons: ["Dismiss", "Open Extension Link (GitHub)"],
defaultId: 0,
cancelId: 0,
noLink: true,
}).then(({response}) => {
})
.then(({ response }) => {
if (response === 1) {
utils.openExternal('https://github.com/bitburner-official/bitburner-vscode');
utils.openExternal("https://github.com/bitburner-official/bitburner-vscode");
}
});
}
}
]
},
},
],
},
{
label: "Zoom",
@ -291,7 +298,7 @@ function getMenu(window) {
utils.setZoomFactor(window, newZoom);
refreshMenu(window);
} else {
log.log('Max zoom out')
log.log("Max zoom out");
utils.writeToast(window, "Cannot zoom in anymore", "warning");
}
},
@ -307,7 +314,7 @@ function getMenu(window) {
utils.setZoomFactor(window, newZoom);
refreshMenu(window);
} else {
log.log('Max zoom in')
log.log("Max zoom in");
utils.writeToast(window, "Cannot zoom out anymore", "warning");
}
},
@ -340,8 +347,8 @@ function getMenu(window) {
} catch (error) {
log.error(error);
}
}
}
},
},
],
},
]);
@ -352,5 +359,6 @@ function refreshMenu(window) {
}
module.exports = {
getMenu, refreshMenu,
}
getMenu,
refreshMenu,
};

@ -1,11 +1,10 @@
/* eslint-disable @typescript-eslint/no-var-requires */
const { ipcRenderer, contextBridge } = require('electron')
const { ipcRenderer, contextBridge } = require("electron");
const log = require("electron-log");
contextBridge.exposeInMainWorld(
"electronBridge", {
contextBridge.exposeInMainWorld("electronBridge", {
send: (channel, data) => {
log.log("Send on channel " + channel)
log.log("Send on channel " + channel);
// whitelist channels
let validChannels = [
"get-save-data-response",
@ -20,7 +19,7 @@ contextBridge.exposeInMainWorld(
}
},
receive: (channel, func) => {
log.log("Receive on channel " + channel)
log.log("Receive on channel " + channel);
let validChannels = [
"get-save-data-request",
"get-save-info-request",
@ -33,6 +32,5 @@ contextBridge.exposeInMainWorld(
// Deliberately strip event as it includes `sender`
ipcRenderer.on(channel, (event, ...args) => func(...args));
}
}
}
);
},
});

@ -16,9 +16,9 @@ const config = new Config();
// https://stackoverflow.com/a/69418940
const dirSize = async (directory) => {
const files = await fs.readdir(directory);
const stats = files.map(file => fs.stat(path.join(directory, file)));
const stats = files.map((file) => fs.stat(path.join(directory, file)));
return (await Promise.all(stats)).reduce((accumulator, { size }) => accumulator + size, 0);
}
};
const getDirFileStats = async (directory) => {
const files = await fs.readdir(directory);
@ -26,30 +26,31 @@ const getDirFileStats = async (directory) => {
const file = path.join(directory, f);
return fs.stat(file).then((stat) => ({ file, stat }));
});
const data = (await Promise.all(stats));
const data = await Promise.all(stats);
return data;
};
const getNewestFile = async (directory) => {
const data = await getDirFileStats(directory)
const data = await getDirFileStats(directory);
return data.sort((a, b) => b.stat.mtime.getTime() - a.stat.mtime.getTime())[0];
};
const getAllSaves = async (window) => {
const rootDirectory = await getSaveFolder(window, true);
const data = await fs.readdir(rootDirectory, { withFileTypes: true });
const savesPromises = data.filter((e) => e.isDirectory()).
map((dir) => path.join(rootDirectory, dir.name)).
map((dir) => getDirFileStats(dir));
const savesPromises = data
.filter((e) => e.isDirectory())
.map((dir) => path.join(rootDirectory, dir.name))
.map((dir) => getDirFileStats(dir));
const saves = await Promise.all(savesPromises);
const flat = flatten(saves);
return flat;
}
};
async function prepareSaveFolders(window) {
const rootFolder = await getSaveFolder(window, true);
const currentFolder = await getSaveFolder(window);
const backupsFolder = path.join(rootFolder, "/_backups")
const backupsFolder = path.join(rootFolder, "/_backups");
await prepareFolders(rootFolder, currentFolder, backupsFolder);
}
@ -60,7 +61,7 @@ async function prepareFolders(...folders) {
// eslint-disable-next-line no-await-in-loop
await fs.stat(folder);
} catch (error) {
if (error.code === 'ENOENT') {
if (error.code === "ENOENT") {
log.warn(`'${folder}' not found, creating it...`);
// eslint-disable-next-line no-await-in-loop
await fs.mkdir(folder);
@ -125,14 +126,14 @@ function isCloudEnabled() {
function saveCloudFile(name, content) {
return new Promise((resolve, reject) => {
greenworks.saveTextToFile(name, content, resolve, reject);
})
});
}
function getFirstCloudFile() {
const nbFiles = greenworks.getFileCount();
if (nbFiles === 0) throw new Error('No files in cloud');
if (nbFiles === 0) throw new Error("No files in cloud");
const file = greenworks.getFileNameAndSize(0);
log.silly(`Found ${nbFiles} files.`)
log.silly(`Found ${nbFiles} files.`);
log.silly(`First File: ${file.name} (${file.size} bytes)`);
return file.name;
}
@ -153,7 +154,7 @@ function deleteCloudFile() {
async function getSteamCloudQuota() {
return new Promise((resolve, reject) => {
greenworks.getCloudQuota(resolve, reject)
greenworks.getCloudQuota(resolve, reject);
});
}
@ -166,9 +167,9 @@ async function backupSteamDataToDisk(currentPlayerId) {
if (previousPlayerId !== currentPlayerId) {
const backupSave = await getSteamCloudSaveString();
const backupFile = path.join(app.getPath("userData"), "/saves/_backups", `${previousPlayerId}.json.gz`);
const buffer = Buffer.from(backupSave, 'base64').toString('utf8');
const buffer = Buffer.from(backupSave, "base64").toString("utf8");
saveContent = await gzip(buffer);
await fs.writeFile(backupFile, saveContent, 'utf8');
await fs.writeFile(backupFile, saveContent, "utf8");
log.debug(`Saved backup game to '${backupFile}`);
}
}
@ -219,7 +220,9 @@ async function saveGameToDisk(window, saveData) {
const remainingSpaceBytes = maxFolderSizeBytes - saveFolderSizeBytes;
log.debug(`Folder Usage: ${saveFolderSizeBytes} bytes`);
log.debug(`Folder Capacity: ${maxFolderSizeBytes} bytes`);
log.debug(`Remaining: ${remainingSpaceBytes} bytes (${(saveFolderSizeBytes / maxFolderSizeBytes * 100).toFixed(2)}% used)`)
log.debug(
`Remaining: ${remainingSpaceBytes} bytes (${((saveFolderSizeBytes / maxFolderSizeBytes) * 100).toFixed(2)}% used)`,
);
const shouldCompress = isSaveCompressionEnabled();
const fileName = saveData.fileName;
const file = path.join(currentFolder, fileName + (shouldCompress ? ".gz" : ""));
@ -227,10 +230,10 @@ async function saveGameToDisk(window, saveData) {
let saveContent = saveData.save;
if (shouldCompress) {
// Let's decode the base64 string so GZIP is more efficient.
const buffer = Buffer.from(saveContent, 'base64').toString('utf8');
const buffer = Buffer.from(saveContent, "base64").toString("utf8");
saveContent = await gzip(buffer);
}
await fs.writeFile(file, saveContent, 'utf8');
await fs.writeFile(file, saveContent, "utf8");
log.debug(`Saved Game to '${file}'`);
log.debug(`Save Size: ${saveContent.length} bytes`);
} catch (error) {
@ -240,7 +243,8 @@ async function saveGameToDisk(window, saveData) {
const fileStats = await getDirFileStats(currentFolder);
const oldestFiles = fileStats
.sort((a, b) => a.stat.mtime.getTime() - b.stat.mtime.getTime())
.map(f => f.file).filter(f => f !== file);
.map((f) => f.file)
.filter((f) => f !== file);
while (saveFolderSizeBytes > maxFolderSizeBytes && oldestFiles.length > 0) {
const fileToRemove = oldestFiles.shift();
@ -255,7 +259,12 @@ async function saveGameToDisk(window, saveData) {
// eslint-disable-next-line no-await-in-loop
saveFolderSizeBytes = await getFolderSizeInBytes(currentFolder);
log.debug(`Save Folder: ${saveFolderSizeBytes} bytes`);
log.debug(`Remaining: ${maxFolderSizeBytes - saveFolderSizeBytes} bytes (${(saveFolderSizeBytes / maxFolderSizeBytes * 100).toFixed(2)}% used)`)
log.debug(
`Remaining: ${maxFolderSizeBytes - saveFolderSizeBytes} bytes (${(
(saveFolderSizeBytes / maxFolderSizeBytes) *
100
).toFixed(2)}% used)`,
);
}
return file;
@ -271,13 +280,13 @@ async function loadLastFromDisk(window) {
async function loadFileFromDisk(path) {
const buffer = await fs.readFile(path);
let content;
if (path.endsWith('.gz')) {
if (path.endsWith(".gz")) {
const uncompressedBuffer = await gunzip(buffer);
content = uncompressedBuffer.toString('base64');
content = uncompressedBuffer.toString("base64");
log.debug(`Uncompressed file content (new size: ${content.length} bytes)`);
} else {
content = buffer.toString('utf8');
log.debug(`Loaded file with ${content.length} bytes`)
content = buffer.toString("utf8");
log.debug(`Loaded file with ${content.length} bytes`);
}
return content;
}
@ -293,10 +302,10 @@ function getSaveInformation(window, save) {
function getCurrentSave(window) {
return new Promise((resolve) => {
ipcMain.once('get-save-data-response', (event, data) => {
ipcMain.once("get-save-data-response", (event, data) => {
resolve(data);
});
window.webContents.send('get-save-data-request');
window.webContents.send("get-save-data-request");
});
}
@ -322,8 +331,7 @@ async function restoreIfNewerExists(window) {
}
try {
const saves = (await getAllSaves()).
sort((a, b) => b.stat.mtime.getTime() - a.stat.mtime.getTime());
const saves = (await getAllSaves()).sort((a, b) => b.stat.mtime.getTime() - a.stat.mtime.getTime());
if (saves.length > 0) {
disk.save = await loadFileFromDisk(saves[0].file);
disk.data = await getSaveInformation(window, disk.save);
@ -339,18 +347,17 @@ async function restoreIfNewerExists(window) {
log.info("No data to import");
} else if (!steam.data) {
// We'll just compare using the lastSave field for now.
log.debug('Best potential save match: Disk');
log.debug("Best potential save match: Disk");
bestMatch = disk;
} else if (!disk.data) {
log.debug('Best potential save match: Steam Cloud');
log.debug("Best potential save match: Steam Cloud");
bestMatch = steam;
} else if ((steam.data.lastSave >= disk.data.lastSave)
|| (steam.data.playtime + lowPlaytime > disk.data.playtime)) {
} else if (steam.data.lastSave >= disk.data.lastSave || steam.data.playtime + lowPlaytime > disk.data.playtime) {
// We want to prioritze steam data if the playtime is very close
log.debug('Best potential save match: Steam Cloud');
log.debug("Best potential save match: Steam Cloud");
bestMatch = steam;
} else {
log.debug('Best potential save match: disk');
log.debug("Best potential save match: disk");
bestMatch = disk;
}
if (bestMatch) {
@ -373,12 +380,24 @@ async function restoreIfNewerExists(window) {
}
module.exports = {
getCurrentSave, getSaveInformation,
restoreIfNewerExists, pushSaveGameForImport,
pushGameSaveToSteamCloud, getSteamCloudSaveString, getSteamCloudQuota, deleteCloudFile,
saveGameToDisk, loadLastFromDisk, loadFileFromDisk,
getSaveFolder, prepareSaveFolders, getAllSaves,
isCloudEnabled, setCloudEnabledConfig,
isAutosaveEnabled, setAutosaveConfig,
isSaveCompressionEnabled, setSaveCompressionConfig,
getCurrentSave,
getSaveInformation,
restoreIfNewerExists,
pushSaveGameForImport,
pushGameSaveToSteamCloud,
getSteamCloudSaveString,
getSteamCloudQuota,
deleteCloudFile,
saveGameToDisk,
loadLastFromDisk,
loadFileFromDisk,
getSaveFolder,
prepareSaveFolders,
getAllSaves,
isCloudEnabled,
setCloudEnabledConfig,
isAutosaveEnabled,
setAutosaveConfig,
isSaveCompressionEnabled,
setSaveCompressionConfig,
};

@ -9,39 +9,42 @@ const Config = require("electron-config");
const config = new Config();
function reloadAndKill(window, killScripts) {
const setStopProcessHandler = global.app_handlers.stopProcess
const setStopProcessHandler = global.app_handlers.stopProcess;
const createWindowHandler = global.app_handlers.createWindow;
log.info('Reloading & Killing all scripts...');
log.info("Reloading & Killing all scripts...");
setStopProcessHandler(app, window, false);
achievements.disableAchievementsInterval(window);
api.disable();
window.webContents.forcefullyCrashRenderer();
window.on('closed', () => {
window.on("closed", () => {
// Wait for window to be closed before opening the new one to prevent race conditions
log.debug('Opening new window');
log.debug("Opening new window");
createWindowHandler(killScripts);
})
});
window.close();
}
function promptForReload(window) {
detachUnresponsiveAppHandler(window);
dialog.showMessageBox({
type: 'error',
title: 'Bitburner > Application Unresponsive',
message: 'The application is unresponsive, possibly due to an infinite loop in your scripts.',
detail:' Did you forget a ns.sleep(x)?\n\n' +
'The application will be restarted for you, do you want to kill all running scripts?',
buttons: ['Restart', 'Cancel'],
dialog
.showMessageBox({
type: "error",
title: "Bitburner > Application Unresponsive",
message: "The application is unresponsive, possibly due to an infinite loop in your scripts.",
detail:
" Did you forget a ns.sleep(x)?\n\n" +
"The application will be restarted for you, do you want to kill all running scripts?",
buttons: ["Restart", "Cancel"],
defaultId: 0,
checkboxLabel: 'Kill all running scripts',
checkboxLabel: "Kill all running scripts",
checkboxChecked: true,
noLink: true,
}).then(({response, checkboxChecked}) => {
})
.then(({ response, checkboxChecked }) => {
if (response === 0) {
reloadAndKill(window, checkboxChecked);
} else {
@ -52,18 +55,15 @@ function promptForReload(window) {
function attachUnresponsiveAppHandler(window) {
window.unresponsiveHandler = () => promptForReload(window);
window.on('unresponsive', window.unresponsiveHandler);
window.on("unresponsive", window.unresponsiveHandler);
}
function detachUnresponsiveAppHandler(window) {
window.off('unresponsive', window.unresponsiveHandler);
window.off("unresponsive", window.unresponsiveHandler);
}
function showErrorBox(title, error) {
dialog.showErrorBox(
title,
`${error.name}\n\n${error.message}`
);
dialog.showErrorBox(title, `${error.name}\n\n${error.message}`);
}
function exportSaveFromIndexedDb() {
@ -71,15 +71,15 @@ function exportSaveFromIndexedDb() {
const dbRequest = indexedDB.open("bitburnerSave");
dbRequest.onsuccess = () => {
const db = dbRequest.result;
const transaction = db.transaction(['savestring'], "readonly");
const store = transaction.objectStore('savestring');
const request = store.get('save');
const transaction = db.transaction(["savestring"], "readonly");
const store = transaction.objectStore("savestring");
const request = store.get("save");
request.onsuccess = () => {
const file = new Blob([request.result], {type: 'text/plain'});
const file = new Blob([request.result], { type: "text/plain" });
const a = document.createElement("a");
const url = URL.createObjectURL(file);
a.href = url;
a.download = 'save.json';
a.download = "save.json";
document.body.appendChild(a);
a.click();
setTimeout(function () {
@ -87,24 +87,21 @@ function exportSaveFromIndexedDb() {
window.URL.revokeObjectURL(url);
resolve();
}, 0);
}
}
};
};
});
}
async function exportSave(window) {
await window.webContents
.executeJavaScript(`${exportSaveFromIndexedDb.toString()}; exportSaveFromIndexedDb();`, true);
await window.webContents.executeJavaScript(`${exportSaveFromIndexedDb.toString()}; exportSaveFromIndexedDb();`, true);
}
async function writeTerminal(window, message, type = null) {
await window.webContents
.executeJavaScript(`window.appNotifier.terminal("${message}", "${type}");`, true)
await window.webContents.executeJavaScript(`window.appNotifier.terminal("${message}", "${type}");`, true);
}
async function writeToast(window, message, type = "info", duration = 2000) {
await window.webContents
.executeJavaScript(`window.appNotifier.toast("${message}", "${type}", ${duration});`, true)
await window.webContents.executeJavaScript(`window.appNotifier.toast("${message}", "${type}", ${duration});`, true);
}
function openExternal(url) {
@ -113,7 +110,7 @@ function openExternal(url) {
}
function getZoomFactor() {
const configZoom = config.get('zoom', 1);
const configZoom = config.get("zoom", 1);
return configZoom;
}
@ -121,14 +118,20 @@ function setZoomFactor(window, zoom = null) {
if (zoom === null) {
zoom = 1;
} else {
config.set('zoom', zoom);
config.set("zoom", zoom);
}
window.webContents.setZoomFactor(zoom);
}
module.exports = {
reloadAndKill, showErrorBox, exportSave,
attachUnresponsiveAppHandler, detachUnresponsiveAppHandler,
openExternal, writeTerminal, writeToast,
getZoomFactor, setZoomFactor,
}
reloadAndKill,
showErrorBox,
exportSave,
attachUnresponsiveAppHandler,
detachUnresponsiveAppHandler,
openExternal,
writeTerminal,
writeToast,
getZoomFactor,
setZoomFactor,
};

@ -4,12 +4,11 @@ module.exports = {
transform: {
"^.+\\.(js|jsx|ts|tsx)$": "babel-jest",
},
testPathIgnorePatterns: [
'.cypress', 'node_modules', 'dist',
],
testPathIgnorePatterns: [".cypress", "node_modules", "dist"],
testEnvironment: "jsdom",
moduleNameMapper: {
"\\.(jpg|jpeg|png|gif|eot|otf|webp|svg|ttf|woff|woff2|mp4|webm|wav|mp3|m4a|aac|oga)$": "<rootDir>/test/__mocks__/fileMock.js",
"\\.(css|less)$": "<rootDir>/test/__mocks__/styleMock.js"
}
"\\.(jpg|jpeg|png|gif|eot|otf|webp|svg|ttf|woff|woff2|mp4|webm|wav|mp3|m4a|aac|oga)$":
"<rootDir>/test/__mocks__/fileMock.js",
"\\.(css|less)$": "<rootDir>/test/__mocks__/styleMock.js",
},
};

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

@ -3,7 +3,7 @@ import React from "react";
import { Box, Typography } from "@mui/material";
import { Achievement } from "./Achievements";
import { Settings } from "../Settings/Settings"
import { Settings } from "../Settings/Settings";
import { AchievementIcon } from "./AchievementIcon";
interface IProps {
@ -13,43 +13,58 @@ interface IProps {
cssFiltersLocked: string;
}
export function AchievementEntry({ achievement, unlockedOn, cssFiltersUnlocked, cssFiltersLocked }: IProps): JSX.Element {
export function AchievementEntry({
achievement,
unlockedOn,
cssFiltersUnlocked,
cssFiltersLocked,
}: IProps): JSX.Element {
if (!achievement) return <></>;
const isUnlocked = !!unlockedOn;
const mainColor = isUnlocked ? Settings.theme.primary : Settings.theme.secondarylight;
let achievedOn = '';
let achievedOn = "";
if (unlockedOn) {
achievedOn = new Date(unlockedOn).toLocaleString();
}
return (
<Box sx={{
border: `1px solid ${Settings.theme.well}`, mb: 2
}}>
<Box sx={{
display: 'flex',
flexDirection: 'row',
flexWrap: 'wrap',
}}>
<Box
sx={{
border: `1px solid ${Settings.theme.well}`,
mb: 2,
}}
>
<Box
sx={{
display: "flex",
flexDirection: "row",
flexWrap: "wrap",
}}
>
<AchievementIcon
achievement={achievement} unlocked={isUnlocked} size="72px"
colorFilters={isUnlocked ? cssFiltersUnlocked: cssFiltersLocked} />
<Box sx={{
display: 'flex',
flexDirection: 'column',
justifyContent: 'center',
px: 1
}}>
achievement={achievement}
unlocked={isUnlocked}
size="72px"
colorFilters={isUnlocked ? cssFiltersUnlocked : cssFiltersLocked}
/>
<Box
sx={{
display: "flex",
flexDirection: "column",
justifyContent: "center",
px: 1,
}}
>
<Typography variant="h6" sx={{ color: mainColor }}>
{achievement.Name}
</Typography>
<Typography variant="body2" sx={{ maxWidth: '500px', color: mainColor}}>
<Typography variant="body2" sx={{ maxWidth: "500px", color: mainColor }}>
{achievement.Description}
</Typography>
{isUnlocked && (
<Typography variant="caption" sx={{ fontSize: '12px', color: Settings.theme.primarydark }}>
<Typography variant="caption" sx={{ fontSize: "12px", color: Settings.theme.primarydark }}>
Acquired on {achievedOn}
</Typography>
)}

@ -3,7 +3,7 @@ import React, { useState } from "react";
import { Box } from "@mui/material";
import { Achievement } from "./Achievements";
import { Settings } from "../Settings/Settings"
import { Settings } from "../Settings/Settings";
interface IProps {
achievement: Achievement;
@ -16,20 +16,23 @@ export function AchievementIcon({ achievement, unlocked, colorFilters, size }: I
const [imgLoaded, setImgLoaded] = useState(false);
const mainColor = unlocked ? Settings.theme.primarydark : Settings.theme.secondarydark;
if (!achievement.Icon) return (<></>);
if (!achievement.Icon) return <></>;
return (
<Box
sx={{
border: `1px solid ${mainColor}`,
width: size, height: size,
width: size,
height: size,
m: 1,
visibility: imgLoaded ? 'visible' : 'hidden'
visibility: imgLoaded ? "visible" : "hidden",
}}
>
<img src={`dist/icons/achievements/${encodeURI(achievement.Icon)}.svg`}
<img
src={`dist/icons/achievements/${encodeURI(achievement.Icon)}.svg`}
style={{ filter: colorFilters, width: size, height: size }}
onLoad={() => setImgLoaded(true)}
alt={achievement.Name} />
alt={achievement.Name}
/>
</Box>
);
}

@ -4,7 +4,7 @@ import { Accordion, AccordionSummary, AccordionDetails, Box, Typography } from "
import { AchievementEntry } from "./AchievementEntry";
import { Achievement, PlayerAchievement } from "./Achievements";
import { Settings } from "../Settings/Settings"
import { Settings } from "../Settings/Settings";
import { getFiltersFromHex } from "../ThirdParty/colorUtils";
import { CorruptableText } from "../ui/React/CorruptableText";
@ -18,32 +18,39 @@ export function AchievementList({ achievements, playerAchievements }: IProps): J
const cssPrimary = getFiltersFromHex(Settings.theme.primary);
const cssSecondary = getFiltersFromHex(Settings.theme.secondary);
const data = achievements.map(achievement => ({
const data = achievements
.map((achievement) => ({
achievement,
unlockedOn: playerAchievements.find(playerAchievement => playerAchievement.ID === achievement.ID)?.unlockedOn,
})).sort((a, b) => (b.unlockedOn ?? 0) - (a.unlockedOn ?? 0));
unlockedOn: playerAchievements.find((playerAchievement) => playerAchievement.ID === achievement.ID)?.unlockedOn,
}))
.sort((a, b) => (b.unlockedOn ?? 0) - (a.unlockedOn ?? 0));
const unlocked = data.filter(entry => entry.unlockedOn);
const unlocked = data.filter((entry) => entry.unlockedOn);
// Hidden achievements
const secret = data.filter(entry => !entry.unlockedOn && entry.achievement.Secret)
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
.filter(entry => !unlocked.map(u => u.achievement.ID).includes(entry.achievement.ID))
.filter(entry => !secret.map(u => u.achievement.ID).includes(entry.achievement.ID))
.filter(entry => !unavailable.map(u => u.achievement.ID).includes(entry.achievement.ID));
.filter((entry) => !unlocked.map((u) => u.achievement.ID).includes(entry.achievement.ID))
.filter((entry) => !secret.map((u) => u.achievement.ID).includes(entry.achievement.ID))
.filter((entry) => !unavailable.map((u) => u.achievement.ID).includes(entry.achievement.ID));
return (
<Box sx={{ pr: 18, my: 2 }}>
<Box sx={{
display: 'flex',
flexDirection: 'column',
flexWrap: 'wrap',
}}>
<Box
sx={{
display: "flex",
flexDirection: "column",
flexWrap: "wrap",
}}
>
{unlocked.length > 0 && (
<Accordion defaultExpanded disableGutters square>
<AccordionSummary>
@ -52,12 +59,14 @@ export function AchievementList({ achievements, playerAchievements }: IProps): J
</Typography>
</AccordionSummary>
<AccordionDetails sx={{ pt: 2 }}>
{unlocked.map(item => (
<AchievementEntry key={`unlocked_${item.achievement.ID}`}
{unlocked.map((item) => (
<AchievementEntry
key={`unlocked_${item.achievement.ID}`}
achievement={item.achievement}
unlockedOn={item.unlockedOn}
cssFiltersUnlocked={cssPrimary}
cssFiltersLocked={cssSecondary} />
cssFiltersLocked={cssSecondary}
/>
))}
</AccordionDetails>
</Accordion>
@ -71,11 +80,13 @@ export function AchievementList({ achievements, playerAchievements }: IProps): J
</Typography>
</AccordionSummary>
<AccordionDetails sx={{ pt: 2 }}>
{locked.map(item => (
<AchievementEntry key={`locked_${item.achievement.ID}`}
{locked.map((item) => (
<AchievementEntry
key={`locked_${item.achievement.ID}`}
achievement={item.achievement}
cssFiltersUnlocked={cssPrimary}
cssFiltersLocked={cssSecondary} />
cssFiltersLocked={cssSecondary}
/>
))}
</AccordionDetails>
</Accordion>
@ -105,7 +116,7 @@ export function AchievementList({ achievements, playerAchievements }: IProps): J
</AccordionSummary>
<AccordionDetails>
<Typography color="secondary" sx={{ mt: 1 }}>
{secret.map(item => (
{secret.map((item) => (
<span key={`secret_${item.achievement.ID}`}>
<CorruptableText content={item.achievement.ID}></CorruptableText>
<br />

@ -1,9 +1,9 @@
# Adding Achievements
* Add a .svg in `./assets/Steam/achievements/real`
* Create the achievement in Steam Dev Portal
* Run `sh ./assets/Steam/achievements/pack-for-web.sh`
* Run `node ./tools/fetch-steam-achievements-data DEVKEYHERE`
* Get your key here: https://steamcommunity.com/dev/apikey
* Add an entry in `./src/Achievements/Achievements.ts` -> achievements
* Commit `./dist/icons/achievements` & `./src/Achievements/AchievementData.json`
- Add a .svg in `./assets/Steam/achievements/real`
- Create the achievement in Steam Dev Portal
- Run `sh ./assets/Steam/achievements/pack-for-web.sh`
- Run `node ./tools/fetch-steam-achievements-data DEVKEYHERE`
- Get your key here: https://steamcommunity.com/dev/apikey
- Add an entry in `./src/Achievements/Achievements.ts` -> achievements
- Commit `./dist/icons/achievements` & `./src/Achievements/AchievementData.json`

@ -134,9 +134,7 @@ export class Action implements IAction {
for (const decay of Object.keys(this.decays)) {
if (this.decays.hasOwnProperty(decay)) {
if (this.decays[decay] > 1) {
throw new Error(
`Invalid decays when constructing Action ${this.name}. Decay value cannot be greater than 1`,
);
throw new Error(`Invalid decays when constructing Action ${this.name}. Decay value cannot be greater than 1`);
}
}
}

@ -9,7 +9,7 @@ const actionNames : Array<string> = [
"Recruitment",
"Diplomacy",
"Hyperbolic Regeneration Chamber",
"Incite Violence"
"Incite Violence",
];
for (const actionName of actionNames) {

@ -7,8 +7,7 @@ export const Skills: IMap<Skill> = {};
(function () {
Skills[SkillNames.BladesIntuition] = new Skill({
name: SkillNames.BladesIntuition,
desc:
"Each level of this skill increases your success chance for all Contracts, Operations, and BlackOps by 3%",
desc: "Each level of this skill increases your success chance for all Contracts, Operations, and BlackOps by 3%",
baseCost: 3,
costInc: 2.1,
successChanceAll: 3,

@ -17,8 +17,8 @@ export const BlackOperations: {
Zenyatta, along with the rest of the PMC, is a Synthoid.
<br />
<br />
The goal of {BlackOperationNames.OperationTyphoon} is to find and eliminate Zenyatta and RedWater by any means necessary. After the
task is completed, the actions must be covered up from the general public.
The goal of {BlackOperationNames.OperationTyphoon} is to find and eliminate Zenyatta and RedWater by any means
necessary. After the task is completed, the actions must be covered up from the general public.
</>
),
},
@ -30,10 +30,10 @@ export const BlackOperations: {
supporter of Synthoid rights. He must be removed.
<br />
<br />
The goal of {BlackOperationNames.OperationZero} is to covertly infiltrate AeroCorp and uncover any incriminating evidence or
information against Watataki that will cause him to be removed from his position at AeroCorp. Incriminating
evidence can be fabricated as a last resort. Be warned that AeroCorp has some of the most advanced security
measures in the world.
The goal of {BlackOperationNames.OperationZero} is to covertly infiltrate AeroCorp and uncover any incriminating
evidence or information against Watataki that will cause him to be removed from his position at AeroCorp.
Incriminating evidence can be fabricated as a last resort. Be warned that AeroCorp has some of the most advanced
security measures in the world.
</>
),
},
@ -47,8 +47,8 @@ export const BlackOperations: {
<br />
<br />
Samizdat has done a good job of keeping hidden and anonymous. However, we've just received intelligence that
their base of operations is in {CityName.Ishima}'s underground sewer systems. Your task is to investigate the sewer
systems, and eliminate Samizdat. They must never publish anything again.
their base of operations is in {CityName.Ishima}'s underground sewer systems. Your task is to investigate the
sewer systems, and eliminate Samizdat. They must never publish anything again.
</>
),
},
@ -61,9 +61,9 @@ export const BlackOperations: {
dangerous.
<br />
<br />
Your goal is to enter and destroy the Bioengineering department's facility in {CityName.Aevum}. The task is not just to
retire the Synthoids there, but also to destroy any information or research at the facility that is relevant to
the Synthoids and their goals.
Your goal is to enter and destroy the Bioengineering department's facility in {CityName.Aevum}. The task is not
just to retire the Synthoids there, but also to destroy any information or research at the facility that is
relevant to the Synthoids and their goals.
</>
),
},
@ -96,10 +96,10 @@ export const BlackOperations: {
desc: (
<>
The CIA has just encountered a new security threat. A new criminal group, lead by a shadowy operative who calls
himself Juggernaut, has been smuggling drugs and weapons (including suspected bioweapons) into {CityName.Sector12}. We
also have reason to believe they tried to break into one of Universal Energy's facilities in order to cause a
city-wide blackout. The CIA suspects that Juggernaut is a heavily-augmented Synthoid, and have thus enlisted our
help.
himself Juggernaut, has been smuggling drugs and weapons (including suspected bioweapons) into{" "}
{CityName.Sector12}. We also have reason to believe they tried to break into one of Universal Energy's
facilities in order to cause a city-wide blackout. The CIA suspects that Juggernaut is a heavily-augmented
Synthoid, and have thus enlisted our help.
<br />
<br />
Your mission is to eradicate Juggernaut and his followers.
@ -109,13 +109,13 @@ export const BlackOperations: {
[BlackOperationNames.OperationRedDragon]: {
desc: (
<>
The {FactionNames.Tetrads} criminal organization is suspected of reverse-engineering the MK-VI Synthoid design. We believe they
altered and possibly improved the design and began manufacturing their own Synthoid models in order to bolster
their criminal activities.
The {FactionNames.Tetrads} criminal organization is suspected of reverse-engineering the MK-VI Synthoid design.
We believe they altered and possibly improved the design and began manufacturing their own Synthoid models in
order to bolster their criminal activities.
<br />
<br />
Your task is to infiltrate and destroy the {FactionNames.Tetrads}' base of operations in Los Angeles. Intelligence tells us
that their base houses one of their Synthoid manufacturing units.
Your task is to infiltrate and destroy the {FactionNames.Tetrads}' base of operations in Los Angeles.
Intelligence tells us that their base houses one of their Synthoid manufacturing units.
</>
),
},
@ -138,23 +138,24 @@ export const BlackOperations: {
[BlackOperationNames.OperationDeckard]: {
desc: (
<>
Despite your success in eliminating VitaLife's new android-replicating technology in {BlackOperationNames.OperationK}, we've
discovered that a small group of MK-VI Synthoids were able to make off with the schematics and design of the
technology before the Operation. It is almost a certainty that these Synthoids are some of the rogue MK-VI ones
from the Synthoid Uprising.
Despite your success in eliminating VitaLife's new android-replicating technology in{" "}
{BlackOperationNames.OperationK}, we've discovered that a small group of MK-VI Synthoids were able to make off
with the schematics and design of the technology before the Operation. It is almost a certainty that these
Synthoids are some of the rogue MK-VI ones from the Synthoid Uprising.
<br />
<br />
The goal of {BlackOperationNames.OperationDeckard} is to hunt down these Synthoids and retire them. I don't need to tell you how
critical this mission is.
The goal of {BlackOperationNames.OperationDeckard} is to hunt down these Synthoids and retire them. I don't need
to tell you how critical this mission is.
</>
),
},
[BlackOperationNames.OperationTyrell]: {
desc: (
<>
A week ago {FactionNames.BladeIndustries} reported a small break-in at one of their {CityName.Aevum} Augmentation storage facilities. We
figured out that {FactionNames.TheDarkArmy} was behind the heist, and didn't think any more of it. However, we've just
discovered that several known MK-VI Synthoids were part of that break-in group.
A week ago {FactionNames.BladeIndustries} reported a small break-in at one of their {CityName.Aevum}{" "}
Augmentation storage facilities. We figured out that {FactionNames.TheDarkArmy} was behind the heist, and didn't
think any more of it. However, we've just discovered that several known MK-VI Synthoids were part of that
break-in group.
<br />
<br />
We cannot have Synthoids upgrading their already-enhanced abilities with Augmentations. Your task is to hunt
@ -165,15 +166,17 @@ export const BlackOperations: {
[BlackOperationNames.OperationWallace]: {
desc: (
<>
Based on information gathered from {BlackOperationNames.OperationTyrell}, we've discovered that {FactionNames.TheDarkArmy} was well aware that
there were Synthoids amongst their ranks. Even worse, we believe that {FactionNames.TheDarkArmy} is working together with
other criminal organizations such as {FactionNames.TheSyndicate} and that they are planning some sort of large-scale takeover
of multiple major cities, most notably {CityName.Aevum}. We suspect that Synthoids have infiltrated the ranks of these
criminal factions and are trying to stage another Synthoid uprising.
Based on information gathered from {BlackOperationNames.OperationTyrell}, we've discovered that{" "}
{FactionNames.TheDarkArmy} was well aware that there were Synthoids amongst their ranks. Even worse, we believe
that {FactionNames.TheDarkArmy} is working together with other criminal organizations such as{" "}
{FactionNames.TheSyndicate} and that they are planning some sort of large-scale takeover of multiple major
cities, most notably {CityName.Aevum}. We suspect that Synthoids have infiltrated the ranks of these criminal
factions and are trying to stage another Synthoid uprising.
<br />
<br />
The best way to deal with this is to prevent it before it even happens. The goal of {BlackOperationNames.OperationWallace} is to
destroy {FactionNames.TheDarkArmy} and Syndicate factions in {CityName.Aevum} immediately. Leave no survivors.
The best way to deal with this is to prevent it before it even happens. The goal of{" "}
{BlackOperationNames.OperationWallace} is to destroy {FactionNames.TheDarkArmy} and Syndicate factions in{" "}
{CityName.Aevum} immediately. Leave no survivors.
</>
),
},
@ -193,18 +196,18 @@ export const BlackOperations: {
[BlackOperationNames.OperationHyron]: {
desc: (
<>
Our intelligence tells us that {FactionNames.FulcrumSecretTechnologies} is developing a quantum supercomputer using human brains as
core processors. This supercomputer is rumored to be able to store vast amounts of data and perform computations
unmatched by any other supercomputer on the planet. But more importantly, the use of organic human brains means
that the supercomputer may be able to reason abstractly and become self-aware.
Our intelligence tells us that {FactionNames.FulcrumSecretTechnologies} is developing a quantum supercomputer
using human brains as core processors. This supercomputer is rumored to be able to store vast amounts of data
and perform computations unmatched by any other supercomputer on the planet. But more importantly, the use of
organic human brains means that the supercomputer may be able to reason abstractly and become self-aware.
<br />
<br />
I do not need to remind you why sentient-level AIs pose a serious threat to all of mankind.
<br />
<br />
The research for this project is being conducted at one of {FactionNames.FulcrumSecretTechnologies} secret facilities in {CityName.Aevum},
codenamed 'Alpha Ranch'. Infiltrate the compound, delete and destroy the work, and then find and kill the
project lead.
The research for this project is being conducted at one of {FactionNames.FulcrumSecretTechnologies} secret
facilities in {CityName.Aevum}, codenamed 'Alpha Ranch'. Infiltrate the compound, delete and destroy the work,
and then find and kill the project lead.
</>
),
},
@ -213,8 +216,8 @@ export const BlackOperations: {
<>
DreamSense Technologies is an advertising company that uses special technology to transmit their ads into the
people's dreams and subconcious. They do this using broadcast transmitter towers. Based on information from our
agents and informants in {CityName.Chongqing}, we have reason to believe that one of the broadcast towers there has been
compromised by Synthoids and is being used to spread pro-Synthoid propaganda.
agents and informants in {CityName.Chongqing}, we have reason to believe that one of the broadcast towers there
has been compromised by Synthoids and is being used to spread pro-Synthoid propaganda.
<br />
<br />
The mission is to destroy this broadcast tower. Speed and stealth are of the utmost importance for this.
@ -224,36 +227,38 @@ export const BlackOperations: {
[BlackOperationNames.OperationIonStorm]: {
desc: (
<>
Our analysts have uncovered a gathering of MK-VI Synthoids that have taken up residence in the {CityName.Sector12} Slums.
We don't know if they are rogue Synthoids from the Uprising, but we do know that they have been stockpiling
weapons, money, and other resources. This makes them dangerous.
Our analysts have uncovered a gathering of MK-VI Synthoids that have taken up residence in the{" "}
{CityName.Sector12} Slums. We don't know if they are rogue Synthoids from the Uprising, but we do know that they
have been stockpiling weapons, money, and other resources. This makes them dangerous.
<br />
<br />
This is a full-scale assault operation to find and retire all of these Synthoids in the {CityName.Sector12} Slums.
This is a full-scale assault operation to find and retire all of these Synthoids in the {CityName.Sector12}{" "}
Slums.
</>
),
},
[BlackOperationNames.OperationAnnihilus]: {
desc: (
<>
Our superiors have ordered us to eradicate everything and everyone in an underground facility located in {CityName.Aevum}.
They tell us that the facility houses many dangerous Synthoids and belongs to a terrorist organization called
'{FactionNames.TheCovenant}'. We have no prior intelligence about this organization, so you are going in blind.
Our superiors have ordered us to eradicate everything and everyone in an underground facility located in{" "}
{CityName.Aevum}. They tell us that the facility houses many dangerous Synthoids and belongs to a terrorist
organization called '{FactionNames.TheCovenant}'. We have no prior intelligence about this organization, so you
are going in blind.
</>
),
},
[BlackOperationNames.OperationUltron]: {
desc: (
<>
{FactionNames.OmniTekIncorporated}, the original designer and manufacturer of Synthoids, has notified us of a malfunction in
their AI design. This malfunction, when triggered, causes MK-VI Synthoids to become radicalized and seek out the
destruction of humanity. They say that this bug affects all MK-VI Synthoids, not just the rogue ones from the
Uprising.
{FactionNames.OmniTekIncorporated}, the original designer and manufacturer of Synthoids, has notified us of a
malfunction in their AI design. This malfunction, when triggered, causes MK-VI Synthoids to become radicalized
and seek out the destruction of humanity. They say that this bug affects all MK-VI Synthoids, not just the rogue
ones from the Uprising.
<br />
<br />
{FactionNames.OmniTekIncorporated} has also told us they they believe someone has triggered this malfunction in a large group of MK-VI
Synthoids, and that these newly-radicalized Synthoids are now amassing in {CityName.Volhaven} to form a terrorist group
called Ultron.
{FactionNames.OmniTekIncorporated} has also told us they they believe someone has triggered this malfunction in
a large group of MK-VI Synthoids, and that these newly-radicalized Synthoids are now amassing in{" "}
{CityName.Volhaven} to form a terrorist group called Ultron.
<br />
<br />
Intelligence suggests Ultron is heavily armed and that their members are augmented. We believe Ultron is making

@ -1,4 +1,4 @@
import { CityName } from './../../Locations/data/CityNames';
import { CityName } from "./../../Locations/data/CityNames";
export const BladeburnerConstants: {
CityNames: string[];
CyclesPerSecond: number;
@ -28,7 +28,14 @@ export const BladeburnerConstants: {
HrcHpGain: number;
HrcStaminaGain: number;
} = {
CityNames: [CityName.Aevum, CityName.Chongqing, CityName.Sector12, CityName.NewTokyo, CityName.Ishima, CityName.Volhaven],
CityNames: [
CityName.Aevum,
CityName.Chongqing,
CityName.Sector12,
CityName.NewTokyo,
CityName.Ishima,
CityName.Volhaven,
],
CyclesPerSecond: 5, // Game cycle is 200 ms
StaminaGainPerSecond: 0.0085,

@ -18,7 +18,10 @@ export function BlackOpPage(props: IProps): React.ReactElement {
successively by completing the one before it.
<br />
<br />
<b>Your ultimate goal to climb through the ranks of {FactionNames.Bladeburners} is to complete all of the Black Ops.</b>
<b>
Your ultimate goal to climb through the ranks of {FactionNames.Bladeburners} is to complete all of the Black
Ops.
</b>
<br />
<br />
Like normal operations, you may use a team for Black Ops. Failing a black op will incur heavy HP and rank

@ -44,13 +44,15 @@ export function Stats(props: IProps): React.ReactElement {
}
return (
<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>
<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%' }}>
<Button disabled={!inFaction} onClick={openFaction} sx={{ width: "50%" }}>
Faction
</Button>
</span>
@ -110,8 +112,8 @@ export function Stats(props: IProps): React.ReactElement {
<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.
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>
}
>

@ -159,7 +159,7 @@ export function SlotMachine(props: IProps): React.ReactElement {
const copy = index.slice();
for (let i = 0; i < copy.length; i++) {
if (copy[i] === locks[i] && !stoppedOne) continue;
copy[i] = (copy[i] - 1 >= 0) ? copy[i] - 1 : symbols.length - 1;
copy[i] = copy[i] - 1 >= 0 ? copy[i] - 1 : symbols.length - 1;
stoppedOne = true;
}

@ -435,10 +435,10 @@ export class Industry implements IIndustry {
const popularityGain = corporation.getDreamSenseGain(),
awarenessGain = popularityGain * 4;
if (popularityGain > 0) {
const awareness = this.awareness + (awarenessGain * marketCycles);
const awareness = this.awareness + awarenessGain * marketCycles;
this.awareness = Math.min(awareness, Number.MAX_VALUE);
const popularity = this.popularity + (popularityGain * marketCycles);
const popularity = this.popularity + popularityGain * marketCycles;
this.popularity = Math.min(popularity, Number.MAX_VALUE);
}
@ -1188,7 +1188,8 @@ export class Industry implements IIndustry {
}
}
product.maxsll = 0.5 *
product.maxsll =
0.5 *
Math.pow(product.rat, 0.65) *
marketFactor *
corporation.getSalesMultiplier() *
@ -1281,10 +1282,10 @@ export class Industry implements IIndustry {
case 1: {
//AdVert.Inc,
const advMult = corporation.getAdvertisingMultiplier() * this.getAdvertisingMultiplier();
const awareness = (this.awareness + (3 * advMult)) * (1.01 * advMult);
const awareness = (this.awareness + 3 * advMult) * (1.01 * advMult);
this.awareness = Math.min(awareness, Number.MAX_VALUE);
const popularity = (this.popularity + (1 * advMult)) * ((1 + getRandomInt(1, 3) / 100) * advMult);
const popularity = (this.popularity + 1 * advMult) * ((1 + getRandomInt(1, 3) / 100) * advMult);
this.popularity = Math.min(popularity, Number.MAX_VALUE);
break;
}

@ -177,7 +177,7 @@ export class OfficeSpace {
let jobCount = this.employees.reduce((acc, employee) => (employee.pos === job ? acc + 1 : acc), 0);
for (const employee of this.employees) {
if (jobCount == amount) return true
if (jobCount == amount) return true;
if (employee.pos === EmployeePositions.Unassigned && jobCount <= amount) {
employee.pos = job;
jobCount++;

@ -1,4 +1,4 @@
import { CityName } from './../../Locations/data/CityNames';
import { CityName } from "./../../Locations/data/CityNames";
const CyclesPerMarketCycle = 50;
const AllCorporationStates = ["START", "PURCHASE", "PRODUCTION", "SALE", "EXPORT"];
export const CorporationConstants: {
@ -38,7 +38,14 @@ export const CorporationConstants: {
CyclesPerIndustryStateCycle: CyclesPerMarketCycle / AllCorporationStates.length,
SecsPerMarketCycle: CyclesPerMarketCycle / 5,
Cities: [CityName.Aevum, CityName.Chongqing, CityName.Sector12, CityName.NewTokyo, CityName.Ishima, CityName.Volhaven],
Cities: [
CityName.Aevum,
CityName.Chongqing,
CityName.Sector12,
CityName.NewTokyo,
CityName.Ishima,
CityName.Volhaven,
],
WarehouseInitialCost: 5e9, //Initial purchase cost of warehouse
WarehouseInitialSize: 100,
@ -74,16 +81,6 @@ export const CorporationConstants: {
"AI Cores",
"Real Estate",
],
FundingRoundShares: [
0.1,
0.35,
0.25,
0.2
],
FundingRoundMultiplier: [
4,
3,
3,
2.5
],
FundingRoundShares: [0.1, 0.35, 0.25, 0.2],
FundingRoundMultiplier: [4, 3, 3, 2.5],
};

@ -6,8 +6,8 @@ import { useCorporation } from "./Context";
import Typography from "@mui/material/Typography";
import Button from "@mui/material/Button";
import TextField from "@mui/material/TextField";
import { BuyBackShares } from '../Actions';
import { dialogBoxCreate } from '../../ui/React/DialogBox';
import { BuyBackShares } from "../Actions";
import { dialogBoxCreate } from "../../ui/React/DialogBox";
import { KEY } from "../../utils/helpers/keyCodes";
interface IProps {
@ -40,9 +40,8 @@ export function BuybackSharesModal(props: IProps): React.ReactElement {
function buy(): void {
if (disabled) return;
try {
BuyBackShares(corp, player, shares)
}
catch (err) {
BuyBackShares(corp, player, shares);
} catch (err) {
dialogBoxCreate(err + "");
}
props.onClose();

@ -30,7 +30,7 @@ export function CityTabs(props: IProps): React.ReactElement {
}
return (
<>
<Tabs variant="fullWidth" value={city} onChange={handleChange} sx={{ maxWidth: '65%' }}>
<Tabs variant="fullWidth" value={city} onChange={handleChange} sx={{ maxWidth: "65%" }}>
{Object.values(division.offices).map(
(office: OfficeSpace | 0) => office !== 0 && <Tab key={office.loc} label={office.loc} value={office.loc} />,
)}

@ -38,7 +38,7 @@ export function CorporationRoot(): React.ReactElement {
return (
<Context.Corporation.Provider value={corporation}>
<Tabs variant="scrollable" value={divisionName} onChange={handleChange} sx={{ maxWidth: '65%' }} scrollButtons>
<Tabs variant="scrollable" value={divisionName} onChange={handleChange} sx={{ maxWidth: "65%" }} scrollButtons>
<Tab label={corporation.name} value={"Overview"} />
{corporation.divisions.map((div) => (
<Tab key={div.name} label={div.name} value={div.name} />

@ -56,9 +56,11 @@ export function CreateCorporationModal(props: IProps): React.ReactElement {
return (
<Modal open={props.open} onClose={props.onClose}>
<Typography>
Would you like to start a corporation? This will require $150b for registration and initial funding. {player.bitNodeN === 3 && (`This $150b
Would you like to start a corporation? This will require $150b for registration and initial funding.{" "}
{player.bitNodeN === 3 &&
`This $150b
can either be self-funded, or you can obtain the seed money from the government in exchange for 500 million
shares`)}
shares`}
<br />
<br />
If you would like to start one, please enter a name for your corporation below:

@ -23,7 +23,9 @@ interface IProps {
// Create a popup that lets the player manage exports
export function ExportModal(props: IProps): React.ReactElement {
const corp = useCorporation();
const possibleDivisions = corp.divisions.filter((division: IIndustry) => isRelevantMaterial(props.mat.name, division));
const possibleDivisions = corp.divisions.filter((division: IIndustry) =>
isRelevantMaterial(props.mat.name, division),
);
if (possibleDivisions.length === 0) throw new Error("Export popup created with no divisions.");
const defaultDivision = possibleDivisions[0];
if (Object.keys(defaultDivision.warehouses).length === 0)

@ -17,7 +17,11 @@ interface IProps {
export function FindInvestorsModal(props: IProps): React.ReactElement {
const corporation = useCorporation();
const val = corporation.determineValuation();
if (corporation.fundingRound >= CorporationConstants.FundingRoundShares.length || corporation.fundingRound >= CorporationConstants.FundingRoundMultiplier.length) return <></>;
if (
corporation.fundingRound >= CorporationConstants.FundingRoundShares.length ||
corporation.fundingRound >= CorporationConstants.FundingRoundMultiplier.length
)
return <></>;
const percShares = CorporationConstants.FundingRoundShares[corporation.fundingRound];
const roundMultiplier = CorporationConstants.FundingRoundMultiplier[corporation.fundingRound];
const funding = val * percShares * roundMultiplier;

@ -18,7 +18,5 @@ export function IndustryProductEquation(props: IProps): React.ReactElement {
prod.push("Products");
}
return (
<MathJaxWrapper>{"\\(" + reqs.join("+") + `\\Rightarrow ` + prod.join("+") + "\\)"}</MathJaxWrapper>
);
return <MathJaxWrapper>{"\\(" + reqs.join("+") + `\\Rightarrow ` + prod.join("+") + "\\)"}</MathJaxWrapper>;
}

@ -42,9 +42,9 @@ interface IProps {
const useStyles = makeStyles(() =>
createStyles({
retainHeight: {
minHeight: '3em',
minHeight: "3em",
},
})
}),
);
function WarehouseRoot(props: IProps): React.ReactElement {
@ -127,27 +127,53 @@ function WarehouseRoot(props: IProps): React.ReactElement {
const mat = props.warehouse.materials[matName];
if (!MaterialSizes.hasOwnProperty(matName)) continue;
if (mat.qty === 0) continue;
breakdownItems.push(<>{matName}: {numeralWrapper.format(mat.qty * MaterialSizes[matName], "0,0.0")}</>);
breakdownItems.push(
<>
{matName}: {numeralWrapper.format(mat.qty * MaterialSizes[matName], "0,0.0")}
</>,
);
}
for (const prodName of Object.keys(division.products)) {
const prod = division.products[prodName];
if (prod === undefined) continue;
breakdownItems.push(<>{prodName}: {numeralWrapper.format(prod.data[props.warehouse.loc][0] * prod.siz, "0,0.0")}</>);
breakdownItems.push(
<>
{prodName}: {numeralWrapper.format(prod.data[props.warehouse.loc][0] * prod.siz, "0,0.0")}
</>,
);
}
let breakdown;
if (breakdownItems && breakdownItems.length > 0) {
breakdown = breakdownItems.reduce(
(previous: JSX.Element, current: JSX.Element): JSX.Element => previous && <>{previous}<br />{current}</> || <>{current}</>);
(previous: JSX.Element, current: JSX.Element): JSX.Element =>
(previous && (
<>
{previous}
<br />
{current}
</>
)) || <>{current}</>,
);
} else {
breakdown = <>No items in storage.</>
breakdown = <>No items in storage.</>;
}
return (
<Paper>
<Box display="flex" alignItems="center">
<Tooltip title={props.warehouse.sizeUsed !== 0 ? <Typography><>{breakdown}</></Typography> : ""}>
<Tooltip
title={
props.warehouse.sizeUsed !== 0 ? (
<Typography>
<>{breakdown}</>
</Typography>
) : (
""
)
}
>
<Typography color={props.warehouse.sizeUsed >= props.warehouse.size ? "error" : "primary"}>
Storage: {numeralWrapper.formatBigNumber(props.warehouse.sizeUsed)} /{" "}
{numeralWrapper.formatBigNumber(props.warehouse.size)}

@ -112,7 +112,7 @@ export function MaterialElem(props: IMaterialProps): React.ReactElement {
return (
<Paper>
<Box sx={{ display: 'grid', gridTemplateColumns: '2fr 1fr', m: '5px' }}>
<Box sx={{ display: "grid", gridTemplateColumns: "2fr 1fr", m: "5px" }}>
<Box>
<Tooltip
title={
@ -149,7 +149,7 @@ export function MaterialElem(props: IMaterialProps): React.ReactElement {
</Tooltip>
</Box>
<Box sx={{ "& button": { width: '100%' } }}>
<Box sx={{ "& button": { width: "100%" } }}>
<Tooltip
title={tutorial ? <Typography>Purchase your required materials to get production started!</Typography> : ""}
>

@ -89,7 +89,7 @@ export function Overview({ rerender }: IProps): React.ReactElement {
<StatsTable rows={multRows} />
<br />
<BonusTime />
<Box sx={{ display: 'grid', gridTemplateColumns: 'repeat(3, 1fr)', width: 'fit-content' }}>
<Box sx={{ display: "grid", gridTemplateColumns: "repeat(3, 1fr)", width: "fit-content" }}>
<Tooltip
title={
<Typography>

@ -16,7 +16,7 @@ import Box from "@mui/material/Box";
import Collapse from "@mui/material/Collapse";
import ExpandMore from "@mui/icons-material/ExpandMore";
import ExpandLess from "@mui/icons-material/ExpandLess";
import CheckIcon from '@mui/icons-material/Check';
import CheckIcon from "@mui/icons-material/Check";
interface INodeProps {
n: Node | null;
@ -66,19 +66,23 @@ function Upgrade({ n, division }: INodeProps): React.ReactElement {
>
{ele}
</Tooltip>
)
}
);
};
const but = (
<Box>
{wrapInTooltip(
<span>
<Button color={color} disabled={disabled && !n.researched} onClick={research}
style={{ width: '100%', textAlign: 'left', justifyContent: 'unset' }}
<Button
color={color}
disabled={disabled && !n.researched}
onClick={research}
style={{ width: "100%", textAlign: "left", justifyContent: "unset" }}
>
{n.researched && (<CheckIcon sx={{ mr: 1 }} />)}{n.text}
{n.researched && <CheckIcon sx={{ mr: 1 }} />}
{n.text}
</Button>
</span>
</span>,
)}
</Box>
);
@ -87,20 +91,29 @@ function Upgrade({ n, division }: INodeProps): React.ReactElement {
return (
<Box>
<Box display="flex" sx={{ border: '1px solid ' + Settings.theme.well }}>
<Box display="flex" sx={{ border: "1px solid " + Settings.theme.well }}>
{wrapInTooltip(
<span style={{ width: '100%' }}>
<Button color={color} disabled={disabled && !n.researched} onClick={research} sx={{
width: '100%',
textAlign: 'left',
justifyContent: 'unset',
borderColor: Settings.theme.button
}}>
{n.researched && (<CheckIcon sx={{ mr: 1 }} />)}{n.text}
<span style={{ width: "100%" }}>
<Button
color={color}
disabled={disabled && !n.researched}
onClick={research}
sx={{
width: "100%",
textAlign: "left",
justifyContent: "unset",
borderColor: Settings.theme.button,
}}
>
{n.researched && <CheckIcon sx={{ mr: 1 }} />}
{n.text}
</Button>
</span>
</span>,
)}
<Button onClick={() => setOpen((old) => !old)} sx={{ borderColor: Settings.theme.button, minWidth: 'fit-content' }}>
<Button
onClick={() => setOpen((old) => !old)}
sx={{ borderColor: Settings.theme.button, minWidth: "fit-content" }}
>
{open ? <ExpandLess color="primary" /> : <ExpandMore color="primary" />}
</Button>
</Box>

@ -51,7 +51,7 @@ export function SellSharesModal(props: IProps): React.ReactElement {
function sell(): void {
if (disabled) return;
try {
const profit = SellShares(corp, player, shares)
const profit = SellShares(corp, player, shares);
props.onClose();
dialogBoxCreate(
<>
@ -65,7 +65,6 @@ export function SellSharesModal(props: IProps): React.ReactElement {
} catch (err) {
dialogBoxCreate(err + "");
}
}
function onKeyDown(event: React.KeyboardEvent<HTMLInputElement>): void {

@ -5,5 +5,5 @@ export const StanekConstants: {
} = {
RAMBonus: 0.1,
BaseSize: 9,
MaxSize: 25
MaxSize: 25,
};

@ -107,7 +107,9 @@ export function buyAllDarkwebItems(): void {
}
if (cost > Player.money) {
Terminal.error("Not enough money to purchase remaining programs, " + numeralWrapper.formatMoney(cost) + " required");
Terminal.error(
"Not enough money to purchase remaining programs, " + numeralWrapper.formatMoney(cost) + " required",
);
return;
}

@ -8,8 +8,8 @@ import Typography from "@mui/material/Typography";
import Button from "@mui/material/Button";
import ButtonGroup from "@mui/material/ButtonGroup";
import { Tooltip } from "@mui/material";
import LockIcon from '@mui/icons-material/Lock';
import LockOpenIcon from '@mui/icons-material/LockOpen';
import LockIcon from "@mui/icons-material/Lock";
import LockOpenIcon from "@mui/icons-material/LockOpen";
import { IPlayer } from "../../PersonObjects/IPlayer";
import { achievements } from "../../Achievements/Achievements";
@ -21,26 +21,26 @@ interface IProps {
}
export function Achievements(props: IProps): React.ReactElement {
const [playerAchievement, setPlayerAchievements] = useState(props.player.achievements.map(m => m.ID));
const [playerAchievement, setPlayerAchievements] = useState(props.player.achievements.map((m) => m.ID));
function grantAchievement(id: string): void {
props.player.giveAchievement(id);
setPlayerAchievements(props.player.achievements.map(m => m.ID));
setPlayerAchievements(props.player.achievements.map((m) => m.ID));
}
function grantAllAchievements(): void {
Object.values(achievements).forEach(a => props.player.giveAchievement(a.ID));
setPlayerAchievements(props.player.achievements.map(m => m.ID));
Object.values(achievements).forEach((a) => props.player.giveAchievement(a.ID));
setPlayerAchievements(props.player.achievements.map((m) => m.ID));
}
function removeAchievement(id: string): void {
props.player.achievements = props.player.achievements.filter(a => a.ID !== id);
setPlayerAchievements(props.player.achievements.map(m => m.ID));
props.player.achievements = props.player.achievements.filter((a) => a.ID !== id);
setPlayerAchievements(props.player.achievements.map((m) => m.ID));
}
function clearAchievements(): void {
props.player.achievements = [];
setPlayerAchievements(props.player.achievements.map(m => m.ID));
setPlayerAchievements(props.player.achievements.map((m) => m.ID));
}
function disableEngine(): void {
@ -48,7 +48,7 @@ export function Achievements(props: IProps): React.ReactElement {
}
function enableEngine(): void {
props.engine.Counters.achievementsCounter = 0
props.engine.Counters.achievementsCounter = 0;
}
return (
@ -60,8 +60,7 @@ export function Achievements(props: IProps): React.ReactElement {
<table>
<tbody>
<tr>
<td>
</td>
<td></td>
<td>
<Typography>Achievements:</Typography>
</td>
@ -76,7 +75,8 @@ export function Achievements(props: IProps): React.ReactElement {
</tr>
{Object.values(achievements).map((i) => {
const achieved = playerAchievement.includes(i.ID);
return <tr key={"ach-" + i.ID}>
return (
<tr key={"ach-" + i.ID}>
<td>
{achieved ? (
<Tooltip title="Achieved">
@ -89,8 +89,16 @@ export function Achievements(props: IProps): React.ReactElement {
)}
</td>
<td>
<Tooltip title={<>{i.ID}<br />{i.Description}</>}>
<Typography color={achieved ? 'primary': 'secondary'}>{i.Name}:</Typography>
<Tooltip
title={
<>
{i.ID}
<br />
{i.Description}
</>
}
>
<Typography color={achieved ? "primary" : "secondary"}>{i.Name}:</Typography>
</Tooltip>
</td>
<td>
@ -100,6 +108,7 @@ export function Achievements(props: IProps): React.ReactElement {
</ButtonGroup>
</td>
</tr>
);
})}
</tbody>
</table>

@ -37,7 +37,7 @@ export function Factions(props: IProps): React.ReactElement {
}
function receiveAllInvites(): void {
Object.values(FactionNames).forEach(faction => props.player.receiveInvite(faction))
Object.values(FactionNames).forEach((faction) => props.player.receiveInvite(faction));
}
function modifyFactionRep(modifier: number): (x: number) => void {

@ -46,7 +46,7 @@ export function General(props: IProps): React.ReactElement {
}
useEffect(() => {
if (error) throw new ReferenceError('Manually thrown error');
if (error) throw new ReferenceError("Manually thrown error");
}, [error]);
return (

@ -85,8 +85,6 @@ export class FactionInfo {
}
}
/**
* A map of all factions and associated info to them.
*/
@ -143,8 +141,9 @@ export const FactionInfos: IMap<FactionInfo> = {
[FactionNames.ECorp]: new FactionInfo(
(
<>
{FactionNames.ECorp}'s mission is simple: to connect the world of today with the technology of tomorrow. With our wide range of
Internet-related software and commercial hardware, {FactionNames.ECorp} makes the world's information universally accessible.
{FactionNames.ECorp}'s mission is simple: to connect the world of today with the technology of tomorrow. With
our wide range of Internet-related software and commercial hardware, {FactionNames.ECorp} makes the world's
information universally accessible.
</>
),
[],
@ -159,12 +158,13 @@ export const FactionInfos: IMap<FactionInfo> = {
[FactionNames.MegaCorp]: new FactionInfo(
(
<>
{FactionNames.MegaCorp} does what no other dares to do. We imagine. We create. We invent. We create what others have never even
dreamed of. Our work fills the world's needs for food, water, power, and transportation on an unprecedented
scale, in ways that no other company can.
{FactionNames.MegaCorp} does what no other dares to do. We imagine. We create. We invent. We create what others
have never even dreamed of. Our work fills the world's needs for food, water, power, and transportation on an
unprecedented scale, in ways that no other company can.
<br />
<br />
In our labs and factories and on the ground with customers, {FactionNames.MegaCorp} is ushering in a new era for the world.
In our labs and factories and on the ground with customers, {FactionNames.MegaCorp} is ushering in a new era for
the world.
</>
),
[],
@ -194,7 +194,16 @@ export const FactionInfos: IMap<FactionInfo> = {
true,
),
[FactionNames.BladeIndustries]: new FactionInfo(<>Augmentation is Salvation.</>, [], true, true, true, true, false, true),
[FactionNames.BladeIndustries]: new FactionInfo(
<>Augmentation is Salvation.</>,
[],
true,
true,
true,
true,
false,
true,
),
[FactionNames.NWO]: new FactionInfo(
(
@ -486,12 +495,39 @@ export const FactionInfos: IMap<FactionInfo> = {
false,
),
[FactionNames.SlumSnakes]: new FactionInfo(<>{FactionNames.SlumSnakes} rule!</>, [], false, false, true, true, false, false),
[FactionNames.SlumSnakes]: new FactionInfo(
<>{FactionNames.SlumSnakes} rule!</>,
[],
false,
false,
true,
true,
false,
false,
),
// Earlygame factions - factions the player will prestige with early on that don't belong in other categories.
[FactionNames.Netburners]: new FactionInfo(<>{"~~//*>H4CK||3T 8URN3R5**>?>\\~~"}</>, [], true, true, false, false, false, false),
[FactionNames.Netburners]: new FactionInfo(
<>{"~~//*>H4CK||3T 8URN3R5**>?>\\~~"}</>,
[],
true,
true,
false,
false,
false,
false,
),
[FactionNames.TianDiHui]: new FactionInfo(<>Obey Heaven and work righteously.</>, [], true, true, false, true, false, false),
[FactionNames.TianDiHui]: new FactionInfo(
<>Obey Heaven and work righteously.</>,
[],
true,
true,
false,
true,
false,
false,
),
[FactionNames.CyberSec]: new FactionInfo(
(
@ -517,7 +553,8 @@ export const FactionInfos: IMap<FactionInfo> = {
It's too bad they won't live. But then again, who does?
<br />
<br />
Note that for this faction, reputation can only be gained through {FactionNames.Bladeburners} actions. Completing {FactionNames.Bladeburners}
Note that for this faction, reputation can only be gained through {FactionNames.Bladeburners} actions.
Completing {FactionNames.Bladeburners}
contracts/operations will increase your reputation.
</>
),

@ -59,9 +59,7 @@ export function Info(props: IProps): React.ReactElement {
</Typography>
<MathJaxWrapper>{"\\(\\huge{r = \\text{total faction reputation}}\\)"}</MathJaxWrapper>
<MathJaxWrapper>
{
"\\(\\huge{favor=1+\\left\\lfloor\\log_{1.02}\\left(\\frac{r+25000}{25500}\\right)\\right\\rfloor}\\)"
}
{"\\(\\huge{favor=1+\\left\\lfloor\\log_{1.02}\\left(\\frac{r+25000}{25500}\\right)\\right\\rfloor}\\)"}
</MathJaxWrapper>
</>
}
@ -86,7 +84,6 @@ export function Info(props: IProps): React.ReactElement {
<MathJaxWrapper>{"\\(\\huge{r = reputation}\\)"}</MathJaxWrapper>
<MathJaxWrapper>{"\\(\\huge{\\Delta r = \\Delta r \\times \\frac{100+favor}{100}}\\)"}</MathJaxWrapper>
</>
}
>

@ -1,4 +1,4 @@
import { FactionNames } from '../Faction/data/FactionNames';
import { FactionNames } from "../Faction/data/FactionNames";
import { Reviver } from "../utils/JSONReviver";
interface GangTerritory {

@ -1,4 +1,4 @@
import { FactionNames } from '../../Faction/data/FactionNames';
import { FactionNames } from "../../Faction/data/FactionNames";
export const PowerMultiplier: {
[key: string]: number | undefined;
} = {

@ -51,7 +51,7 @@ export function AscensionModal(props: IProps): React.ReactElement {
<br />
Charisma: x{numeralWrapper.format(res.cha, "0.000")}
<br />
</>
</>,
);
props.onClose();
}

@ -76,8 +76,8 @@ function UpgradeButton(props: IUpgradeButtonProps): React.ReactElement {
return (
<Tooltip title={<Typography dangerouslySetInnerHTML={{ __html: props.upg.desc }} />}>
<span>
<Button onClick={onClick} sx={{ display: 'flex', flexDirection: 'column', width: '100%', height: '100%' }}>
<Typography sx={{ display: 'block' }}>{props.upg.name}</Typography>
<Button onClick={onClick} sx={{ display: "flex", flexDirection: "column", width: "100%", height: "100%" }}>
<Typography sx={{ display: "block" }}>{props.upg.name}</Typography>
<Money money={gang.getUpgradeCost(props.upg)} />
</Button>
</span>
@ -113,8 +113,8 @@ function GangMemberUpgradePanel(props: IPanelProps): React.ReactElement {
const onChange = (event: SelectChangeEvent<string>): void => {
setCurrentCategory(event.target.value);
rerender()
}
rerender();
};
const weaponUpgrades = filterUpgrades(props.member.upgrades, UpgradeType.Weapon);
const armorUpgrades = filterUpgrades(props.member.upgrades, UpgradeType.Armor);
@ -123,11 +123,11 @@ function GangMemberUpgradePanel(props: IPanelProps): React.ReactElement {
const augUpgrades = filterUpgrades(props.member.augmentations, UpgradeType.Augmentation);
const categories: { [key: string]: (GangMemberUpgrade[] | UpgradeType)[] } = {
'Weapons': [weaponUpgrades, UpgradeType.Weapon],
'Armor': [armorUpgrades, UpgradeType.Armor],
'Vehicles': [vehicleUpgrades, UpgradeType.Vehicle],
'Rootkits': [rootkitUpgrades, UpgradeType.Rootkit],
'Augmentations': [augUpgrades, UpgradeType.Augmentation]
Weapons: [weaponUpgrades, UpgradeType.Weapon],
Armor: [armorUpgrades, UpgradeType.Armor],
Vehicles: [vehicleUpgrades, UpgradeType.Vehicle],
Rootkits: [rootkitUpgrades, UpgradeType.Rootkit],
Augmentations: [augUpgrades, UpgradeType.Augmentation],
};
const asc = {
@ -140,7 +140,7 @@ function GangMemberUpgradePanel(props: IPanelProps): React.ReactElement {
};
return (
<Paper>
<Box display="grid" sx={{ gridTemplateColumns: '1fr 1fr', m: 1, gap: 1 }}>
<Box display="grid" sx={{ gridTemplateColumns: "1fr 1fr", m: 1, gap: 1 }}>
<span>
<Typography variant="h5" color="primary">
{props.member.name} ({props.member.task})
@ -149,47 +149,70 @@ function GangMemberUpgradePanel(props: IPanelProps): React.ReactElement {
title={
<Typography>
Hk: x{numeralWrapper.formatMultiplier(props.member.hack_mult * asc.hack)}(x
{numeralWrapper.formatMultiplier(props.member.hack_mult)} Eq, x{numeralWrapper.formatMultiplier(asc.hack)}{" "}
Asc)
{numeralWrapper.formatMultiplier(props.member.hack_mult)} Eq, x
{numeralWrapper.formatMultiplier(asc.hack)} Asc)
<br />
St: x{numeralWrapper.formatMultiplier(props.member.str_mult * asc.str)}
(x{numeralWrapper.formatMultiplier(props.member.str_mult)} Eq, x{numeralWrapper.formatMultiplier(asc.str)}{" "}
Asc)
(x{numeralWrapper.formatMultiplier(props.member.str_mult)} Eq, x
{numeralWrapper.formatMultiplier(asc.str)} Asc)
<br />
Df: x{numeralWrapper.formatMultiplier(props.member.def_mult * asc.def)}
(x{numeralWrapper.formatMultiplier(props.member.def_mult)} Eq, x{numeralWrapper.formatMultiplier(asc.def)}{" "}
Asc)
(x{numeralWrapper.formatMultiplier(props.member.def_mult)} Eq, x
{numeralWrapper.formatMultiplier(asc.def)} Asc)
<br />
Dx: x{numeralWrapper.formatMultiplier(props.member.dex_mult * asc.dex)}
(x{numeralWrapper.formatMultiplier(props.member.dex_mult)} Eq, x{numeralWrapper.formatMultiplier(asc.dex)}{" "}
Asc)
(x{numeralWrapper.formatMultiplier(props.member.dex_mult)} Eq, x
{numeralWrapper.formatMultiplier(asc.dex)} Asc)
<br />
Ag: x{numeralWrapper.formatMultiplier(props.member.agi_mult * asc.agi)}
(x{numeralWrapper.formatMultiplier(props.member.agi_mult)} Eq, x{numeralWrapper.formatMultiplier(asc.agi)}{" "}
Asc)
(x{numeralWrapper.formatMultiplier(props.member.agi_mult)} Eq, x
{numeralWrapper.formatMultiplier(asc.agi)} Asc)
<br />
Ch: x{numeralWrapper.formatMultiplier(props.member.cha_mult * asc.cha)}
(x{numeralWrapper.formatMultiplier(props.member.cha_mult)} Eq, x{numeralWrapper.formatMultiplier(asc.cha)}{" "}
Asc)
(x{numeralWrapper.formatMultiplier(props.member.cha_mult)} Eq, x
{numeralWrapper.formatMultiplier(asc.cha)} Asc)
</Typography>
}
>
<Table>
<TableBody>
<StatsRow name="Hacking" color={Settings.theme.hack} data={{ level: props.member.hack, exp: props.member.hack_exp }} />
<StatsRow name="Strength" color={Settings.theme.combat} data={{ level: props.member.str, exp: props.member.str_exp }} />
<StatsRow name="Defense" color={Settings.theme.combat} data={{ level: props.member.def, exp: props.member.def_exp }} />
<StatsRow name="Dexterity" color={Settings.theme.combat} data={{ level: props.member.dex, exp: props.member.dex_exp }} />
<StatsRow name="Agility" color={Settings.theme.combat} data={{ level: props.member.agi, exp: props.member.agi_exp }} />
<StatsRow name="Charisma" color={Settings.theme.cha} data={{ level: props.member.cha, exp: props.member.cha_exp }} />
<StatsRow
name="Hacking"
color={Settings.theme.hack}
data={{ level: props.member.hack, exp: props.member.hack_exp }}
/>
<StatsRow
name="Strength"
color={Settings.theme.combat}
data={{ level: props.member.str, exp: props.member.str_exp }}
/>
<StatsRow
name="Defense"
color={Settings.theme.combat}
data={{ level: props.member.def, exp: props.member.def_exp }}
/>
<StatsRow
name="Dexterity"
color={Settings.theme.combat}
data={{ level: props.member.dex, exp: props.member.dex_exp }}
/>
<StatsRow
name="Agility"
color={Settings.theme.combat}
data={{ level: props.member.agi, exp: props.member.agi_exp }}
/>
<StatsRow
name="Charisma"
color={Settings.theme.cha}
data={{ level: props.member.cha, exp: props.member.cha_exp }}
/>
</TableBody>
</Table>
</Tooltip>
</span>
<span>
<Select onChange={onChange} value={currentCategory} sx={{ width: '100%', mb: 1 }}>
<Select onChange={onChange} value={currentCategory} sx={{ width: "100%", mb: 1 }}>
{Object.keys(categories).map((k, i) => (
<MenuItem key={i + 1} value={k}>
<Typography variant="h6">{k}</Typography>
@ -197,32 +220,22 @@ function GangMemberUpgradePanel(props: IPanelProps): React.ReactElement {
))}
</Select>
<Box sx={{ width: '100%' }}>
<Box sx={{ width: "100%" }}>
{(categories[currentCategory][0] as GangMemberUpgrade[]).length === 0 && (
<Typography>
All upgrades owned!
</Typography>
<Typography>All upgrades owned!</Typography>
)}
<Box display="grid" sx={{ gridTemplateColumns: '1fr 1fr' }}>
<Box display="grid" sx={{ gridTemplateColumns: "1fr 1fr" }}>
{(categories[currentCategory][0] as GangMemberUpgrade[]).map((upg) => (
<UpgradeButton
key={upg.name}
rerender={rerender}
member={props.member}
upg={upg}
/>
<UpgradeButton key={upg.name} rerender={rerender} member={props.member} upg={upg} />
))}
</Box>
<NextReveal
type={categories[currentCategory][1] as UpgradeType}
upgrades={props.member.upgrades}
/>
<NextReveal type={categories[currentCategory][1] as UpgradeType} upgrades={props.member.upgrades} />
</Box>
</span>
</Box>
<Typography sx={{ mx: 1 }}>Purchased Upgrades: </Typography>
<Box display="grid" sx={{ gridTemplateColumns: 'repeat(4, 1fr)', m: 1 }}>
<Box display="grid" sx={{ gridTemplateColumns: "repeat(4, 1fr)", m: 1 }}>
{props.member.upgrades.map((upg: string) => (
<PurchasedUpgrade key={upg} upgName={upg} />
))}
@ -238,13 +251,11 @@ export function EquipmentsSubpage(): React.ReactElement {
const gang = useGang();
const [filter, setFilter] = useState("");
const handleFilterChange = (event: React.ChangeEvent<HTMLInputElement>): void => {
setFilter(event.target.value.toLowerCase());
}
};
const members = gang.members
.filter((member) => member && member.name.toLowerCase().includes(filter));
const members = gang.members.filter((member) => member && member.name.toLowerCase().includes(filter));
return (
<>
@ -265,13 +276,13 @@ export function EquipmentsSubpage(): React.ReactElement {
autoFocus
InputProps={{
startAdornment: <SearchIcon />,
spellCheck: false
spellCheck: false,
}}
placeholder="Filter by member name"
sx={{ m: 1, width: '15%' }}
sx={{ m: 1, width: "15%" }}
/>
<Box display="grid" sx={{ gridTemplateColumns: '1fr 1fr', width: '100%' }}>
<Box display="grid" sx={{ gridTemplateColumns: "1fr 1fr", width: "100%" }}>
{members.map((member: GangMember) => (
<GangMemberUpgradePanel key={member.name} member={member} />
))}

@ -16,7 +16,7 @@ interface IProps {
export function GangMemberCard(props: IProps): React.ReactElement {
return (
<Box component={Paper} sx={{ width: 'auto' }}>
<Box component={Paper} sx={{ width: "auto" }}>
<Box sx={{ m: 1 }}>
<ListItemText primary={<b>{props.member.name}</b>} />
<GangMemberCardContent member={props.member} />

@ -26,15 +26,17 @@ export function GangMemberCardContent(props: IProps): React.ReactElement {
return (
<>
{props.member.canAscend() && (
<Box sx={{ display: 'flex', justifyContent: 'space-between', my: 1 }}>
<Button onClick={() => setAscendOpen(true)} style={{ flexGrow: 1, borderRightWidth: 0 }}>Ascend</Button>
<Box sx={{ display: "flex", justifyContent: "space-between", my: 1 }}>
<Button onClick={() => setAscendOpen(true)} style={{ flexGrow: 1, borderRightWidth: 0 }}>
Ascend
</Button>
<AscensionModal
open={ascendOpen}
onClose={() => setAscendOpen(false)}
member={props.member}
onAscend={() => setRerender((old) => !old)}
/>
<Button onClick={() => setHelpOpen(true)} style={{ width: 'fit-content', borderLeftWidth: 0 }}>
<Button onClick={() => setHelpOpen(true)} style={{ width: "fit-content", borderLeftWidth: 0 }}>
<HelpIcon />
</Button>
<StaticModal open={helpOpen} onClose={() => setHelpOpen(false)}>
@ -53,7 +55,7 @@ export function GangMemberCardContent(props: IProps): React.ReactElement {
</StaticModal>
</Box>
)}
<Box display="grid" sx={{ gridTemplateColumns: '1fr 1fr', width: '100%', gap: 1 }}>
<Box display="grid" sx={{ gridTemplateColumns: "1fr 1fr", width: "100%", gap: 1 }}>
<GangMemberStats member={props.member} />
<TaskSelector onTaskChange={() => setRerender((old) => !old)} member={props.member} />
</Box>

@ -20,7 +20,7 @@ export function GangMemberList(): React.ReactElement {
const handleFilterChange = (event: React.ChangeEvent<HTMLInputElement>): void => {
setFilter(event.target.value.toLowerCase());
}
};
const members = gang.members
.filter((member) => member && member.name.toLowerCase().includes(filter))
@ -38,23 +38,18 @@ export function GangMemberList(): React.ReactElement {
autoFocus
InputProps={{
startAdornment: <SearchIcon />,
spellCheck: false
spellCheck: false,
}}
placeholder="Filter by member name"
sx={{ m: 1, width: '15%' }}
sx={{ m: 1, width: "15%" }}
/>
<OptionSwitch
checked={ascendOnly}
onChange={(newValue) => (setAscendOnly(newValue))}
onChange={(newValue) => setAscendOnly(newValue)}
text="Show only ascendable"
tooltip={
<>
Filter the members list by whether or not the member
can be ascended.
</>
}
tooltip={<>Filter the members list by whether or not the member can be ascended.</>}
/>
<Box display="grid" sx={{ gridTemplateColumns: 'repeat(2, 1fr)' }}>
<Box display="grid" sx={{ gridTemplateColumns: "repeat(2, 1fr)" }}>
{members.map((member: GangMember) => (
<GangMemberCard key={member.name} member={member} />
))}

@ -32,7 +32,7 @@ export function GangRoot(): React.ReactElement {
return (
<Context.Gang.Provider value={gang}>
<Tabs variant="fullWidth" value={value} onChange={handleChange} sx={{ minWidth: 'fit-content', maxWidth: '45%' }}>
<Tabs variant="fullWidth" value={value} onChange={handleChange} sx={{ minWidth: "fit-content", maxWidth: "45%" }}>
<Tab label="Management" />
<Tab label="Equipment" />
<Tab label="Territory" />

@ -25,10 +25,10 @@ export function RecruitButton(props: IProps): React.ReactElement {
const respect = gang.getRespectNeededToRecruitMember();
return (
<Box display="flex" alignItems="center" sx={{ mx: 1 }}>
<Button disabled>
Recruit Gang Member
</Button>
<Typography sx={{ ml: 1 }}>{numeralWrapper.formatRespect(respect)} respect needed to recruit next member</Typography>
<Button disabled>Recruit Gang Member</Button>
<Typography sx={{ ml: 1 }}>
{numeralWrapper.formatRespect(respect)} respect needed to recruit next member
</Typography>
</Box>
);
}

@ -21,11 +21,9 @@ export function TaskSelector(props: IProps): React.ReactElement {
const gang = useGang();
const [currentTask, setCurrentTask] = useState(props.member.task);
const contextMember = gang.members.find(member => member.name == props.member.name)
if (contextMember &&
contextMember.task != currentTask
) {
setCurrentTask(contextMember.task)
const contextMember = gang.members.find((member) => member.name == props.member.name);
if (contextMember && contextMember.task != currentTask) {
setCurrentTask(contextMember.task);
}
function onChange(event: SelectChangeEvent<string>): void {
@ -39,7 +37,7 @@ export function TaskSelector(props: IProps): React.ReactElement {
return (
<Box>
<Select onChange={onChange} value={currentTask} sx={{ width: '100%' }}>
<Select onChange={onChange} value={currentTask} sx={{ width: "100%" }}>
<MenuItem key={0} value={"Unassigned"}>
Unassigned
</MenuItem>

@ -13,9 +13,7 @@ export const TerritoryInfoModal = ({ open, onClose }: IProps): React.ReactElemen
return (
<Modal open={open} onClose={onClose}>
<>
<Typography variant='h4'>
Clashing
</Typography>
<Typography variant="h4">Clashing</Typography>
<Typography>
Every ~20 seconds, your gang has a chance to 'clash' with other gangs. Your chance to win a clash depends on
your gang's power, which is listed in the display below. Your gang's power slowly accumulates over time. The
@ -29,21 +27,17 @@ export const TerritoryInfoModal = ({ open, onClose }: IProps): React.ReactElemen
gang.
</Typography>
<br />
<Typography variant='h4'>
Territory
</Typography>
<Typography variant="h4">Territory</Typography>
<Typography>
The amount of territory you have affects all aspects of your Gang members' production, including money, respect,
and wanted level. It is very beneficial to have high territory control.
The amount of territory you have affects all aspects of your Gang members' production, including money,
respect, and wanted level. It is very beneficial to have high territory control.
<br />
<br />
To increase your chances of winning territory, assign gang members to "Territory Warfare". This will build your
gang power. Then, enable "Engage in Territory Warfare" to start fighting over territory.
To increase your chances of winning territory, assign gang members to "Territory Warfare". This will build
your gang power. Then, enable "Engage in Territory Warfare" to start fighting over territory.
</Typography>
<br />
<Typography variant='h4'>
Territory Clash Chance
</Typography>
<Typography variant="h4">Territory Clash Chance</Typography>
<Typography>
This percentage represents the chance you have of 'clashing' with another gang. If you do not wish to
gain/lose territory, then keep this percentage at 0% by not engaging in territory warfare.
@ -51,4 +45,4 @@ export const TerritoryInfoModal = ({ open, onClose }: IProps): React.ReactElemen
</>
</Modal>
);
}
};

@ -3,16 +3,7 @@
*/
import React, { useState } from "react";
import {
Container,
Button,
Paper,
Box,
Tooltip,
Switch,
FormControlLabel,
Typography
} from "@mui/material";
import { Container, Button, Paper, Box, Tooltip, Switch, FormControlLabel, Typography } from "@mui/material";
import { Help } from "@mui/icons-material";
import { numeralWrapper } from "../../ui/numeralFormat";
@ -41,35 +32,51 @@ export function TerritorySubpage(): React.ReactElement {
</Button>
<Box component={Paper} sx={{ p: 1, mb: 1 }}>
<Typography variant="h6" sx={{ display: 'flex', alignItems: 'center', flexWrap: 'wrap' }}>
<Typography variant="h6" sx={{ display: "flex", alignItems: "center", flexWrap: "wrap" }}>
{gang.facName} (Your gang)
</Typography>
<FormControlLabel
control={<Switch
control={
<Switch
checked={gang.territoryWarfareEngaged}
onChange={(event) => (gang.territoryWarfareEngaged = event.target.checked)}
/>}
label={<Tooltip
title={<Typography>
/>
}
label={
<Tooltip
title={
<Typography>
Engaging in Territory Warfare sets your clash chance to 100%. Disengaging will cause your clash chance
to gradually decrease until it reaches 0%.
</Typography>}>
</Typography>
}
>
<Typography>Engage in Territory Warfare</Typography>
</Tooltip>} />
</Tooltip>
}
/>
<br />
<FormControlLabel
control={<Switch
control={
<Switch
checked={gang.notifyMemberDeath}
onChange={(event) => (gang.notifyMemberDeath = event.target.checked)}
/>}
label={<Tooltip
title={<Typography>
If this is enabled, then you will receive a pop-up notifying you whenever one of your Gang Members dies
in a territory clash.
</Typography>}>
/>
}
label={
<Tooltip
title={
<Typography>
If this is enabled, then you will receive a pop-up notifying you whenever one of your Gang Members
dies in a territory clash.
</Typography>
}
>
<Typography>Notify about Gang Member Deaths</Typography>
</Tooltip>} />
</Tooltip>
}
/>
<Typography>
<b>Territory Clash Chance:</b> {numeralWrapper.formatPercentage(gang.territoryClashChance, 3)} <br />
@ -77,7 +84,7 @@ export function TerritorySubpage(): React.ReactElement {
<b>Territory:</b> {formatTerritory(AllGangs[gang.facName].territory)}% <br />
</Typography>
</Box>
<Box sx={{ display: 'grid', gridTemplateColumns: 'repeat(3, 1fr)' }}>
<Box sx={{ display: "grid", gridTemplateColumns: "repeat(3, 1fr)" }}>
{gangNames.map((name) => (
<OtherGangTerritory key={name} name={name} />
))}
@ -109,7 +116,7 @@ function OtherGangTerritory(props: ITerritoryProps): React.ReactElement {
const clashVictoryChance = playerPower / (power + playerPower);
return (
<Box component={Paper} sx={{ p: 1 }}>
<Typography variant="h6" sx={{ display: 'flex', alignItems: 'center', flexWrap: 'wrap' }}>
<Typography variant="h6" sx={{ display: "flex", alignItems: "center", flexWrap: "wrap" }}>
{props.name}
</Typography>
<Typography>

@ -49,7 +49,8 @@ export function calculatePercentMoneyHacked(server: Server, player: IPlayer): nu
const difficultyMult = (100 - server.hackDifficulty) / 100;
const skillMult = (player.hacking - (server.requiredHackingSkill - 1)) / player.hacking;
const percentMoneyHacked = (difficultyMult * skillMult * player.hacking_money_mult * BitNodeMultipliers.ScriptHackMoney) / balanceFactor;
const percentMoneyHacked =
(difficultyMult * skillMult * player.hacking_money_mult * BitNodeMultipliers.ScriptHackMoney) / balanceFactor;
if (percentMoneyHacked < 0) {
return 0;
}

@ -21,14 +21,13 @@ import { Generic_fromJSON, Generic_toJSON, Reviver } from "../utils/JSONReviver"
import { ObjectValidator, minMax } from "../utils/Validator";
export class HacknetNode implements IHacknetNode {
static validationData: ObjectValidator<HacknetNode> = {
cores: minMax(1, 1, HacknetNodeConstants.MaxCores),
level: minMax(1, 1, HacknetNodeConstants.MaxLevel),
ram: minMax(1, 1, HacknetNodeConstants.MaxRam),
onlineTimeSeconds: minMax(0, 0, Infinity),
totalMoneyGenerated: minMax(0, 0, Infinity)
}
totalMoneyGenerated: minMax(0, 0, Infinity),
};
// Node's number of cores
cores = 1;

@ -137,7 +137,7 @@ export function HacknetRoot(props: IProps): React.ReactElement {
{hasHacknetServers(props.player) && <Button onClick={() => setOpen(true)}>Spend Hashes on Upgrades</Button>}
<Box sx={{ display: 'grid', width: 'fit-content', gridTemplateColumns: 'repeat(3, 1fr)' }}>{nodes}</Box>
<Box sx={{ display: "grid", width: "fit-content", gridTemplateColumns: "repeat(3, 1fr)" }}>{nodes}</Box>
<HashUpgradeModal open={open} onClose={() => setOpen(false)} />
</>
);

@ -1,4 +1,4 @@
import { CityName } from './../Locations/data/CityNames';
import { CityName } from "./../Locations/data/CityNames";
import { Literature } from "./Literature";
import { LiteratureNames } from "./data/LiteratureNames";
import { IMap } from "../types";

@ -34,7 +34,7 @@ export function CasinoLocation(props: IProps): React.ReactElement {
return (
<>
{game === GameType.None && (
<Box sx={{ display: 'grid', width: 'fit-content' }}>
<Box sx={{ display: "grid", width: "fit-content" }}>
<Button onClick={() => updateGame(GameType.Coin)}>Play coin flip</Button>
<Button onClick={() => updateGame(GameType.Slots)}>Play slots</Button>
<Button onClick={() => updateGame(GameType.Roulette)}>Play roulette</Button>

@ -228,9 +228,9 @@ export function CompanyLocation(props: IProps): React.ReactElement {
<br />
</>
)}
<Box sx={{ display: 'grid', width: 'fit-content' }}>
<Box sx={{ display: "grid", width: "fit-content" }}>
{isEmployedHere && (
<Box sx={{ display: 'grid', gridTemplateColumns: '1fr 1fr' }}>
<Box sx={{ display: "grid", gridTemplateColumns: "1fr 1fr" }}>
<Button onClick={work}>Work</Button>
<Button onClick={() => setQuitOpen(true)}>Quit</Button>
<QuitJobModal
@ -241,9 +241,7 @@ export function CompanyLocation(props: IProps): React.ReactElement {
onClose={() => setQuitOpen(false)}
/>
</Box>
)
}
)}
{company.hasAgentPositions() && (
<ApplyToJobButton
company={company}

@ -31,11 +31,7 @@ export function CoresButton(props: IProps): React.ReactElement {
}
return (
<Tooltip
title={
<MathJaxWrapper>{`\\(\\large{cost = 10^9 \\cdot 7.5 ^{\\text{cores}}}\\)`}</MathJaxWrapper>
}
>
<Tooltip title={<MathJaxWrapper>{`\\(\\large{cost = 10^9 \\cdot 7.5 ^{\\text{cores}}}\\)`}</MathJaxWrapper>}>
<span>
<br />
<Typography>

@ -96,8 +96,13 @@ export function GenericLocation({ loc }: IProps): React.ReactElement {
<Typography variant="h4" sx={{ mt: 1 }}>
{backdoorInstalled && !Settings.DisableTextEffects ? (
<Tooltip title={`Backdoor installed on ${loc.name}.`}>
<span><CorruptableText content={loc.name} /></span>
</Tooltip>) : loc.name}
<span>
<CorruptableText content={loc.name} />
</span>
</Tooltip>
) : (
loc.name
)}
</Typography>
{locContent}
</>

@ -59,7 +59,7 @@ export function GymLocation(props: IProps): React.ReactElement {
const cost = CONSTANTS.ClassGymBaseCost * calculateCost();
return (
<Box sx={{ display: 'grid', width: 'fit-content' }}>
<Box sx={{ display: "grid", width: "fit-content" }}>
<Button onClick={trainStrength}>
Train Strength (<Money money={cost} player={props.p} /> / sec)
</Button>

@ -32,9 +32,7 @@ export function RamButton(props: IProps): React.ReactElement {
return (
<Tooltip
title={
<MathJaxWrapper>{`\\(\\large{cost = 3.2 \\cdot 10^3 \\cdot 1.58^{log_2{(ram)}}}\\)`}</MathJaxWrapper>
}
title={<MathJaxWrapper>{`\\(\\large{cost = 3.2 \\cdot 10^3 \\cdot 1.58^{log_2{(ram)}}}\\)`}</MathJaxWrapper>}
>
<span>
<br />

@ -114,7 +114,7 @@ export function SlumsLocation(): React.ReactElement {
const heistChance = Crimes.Heist.successRate(player);
return (
<Box sx={{ display: 'grid', width: 'fit-content' }}>
<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)

@ -155,7 +155,11 @@ export function SpecialLocation(props: IProps): React.ReactElement {
if (!player.canAccessGrafting()) {
return <></>;
}
return <Button onClick={handleGrafting} sx={{ my: 5 }}>Enter the secret lab</Button>;
return (
<Button onClick={handleGrafting} sx={{ my: 5 }}>
Enter the secret lab
</Button>
);
}
function handleCotMG(): void {

@ -70,9 +70,7 @@ export function TechVendorLocation(props: IProps): React.ReactElement {
return (
<>
<br />
<Box sx={{ display: 'grid', width: 'fit-content' }}>
{purchaseServerButtons}
</Box>
<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>

@ -75,7 +75,7 @@ export function UniversityLocation(props: IProps): React.ReactElement {
const earnCharismaExpTooltip = `Gain charisma experience!`;
return (
<Box sx={{ display: 'grid', width: 'fit-content' }}>
<Box sx={{ display: "grid", width: "fit-content" }}>
<Tooltip title={earnHackingExpTooltip}>
<Button onClick={study}>Study Computer Science (free)</Button>
</Tooltip>

@ -1,4 +1,4 @@
import React from 'react';
import React from "react";
import { MathJax, MathJaxContext } from "better-react-mathjax";
interface IProps {
@ -10,5 +10,5 @@ export function MathJaxWrapper({ children }: IProps): React.ReactElement {
<MathJaxContext version={3} src={"dist/ext/MathJax-3.2.0/es5/tex-chtml.js"}>
<MathJax>{children}</MathJax>
</MathJaxContext>
)
);
}

@ -601,7 +601,7 @@ export function NetscriptFunctions(workerScript: WorkerScript): NS {
return calculateHackingChance(server, Player);
},
sleep: async function (_time: unknown = 0): Promise<void> {
sleep: async function (_time: unknown = 0): Promise<true> {
const time = helper.number("sleep", "time", _time);
updateDynamicRam("sleep", getRamCost(Player, "sleep"));
if (time === undefined) {
@ -609,17 +609,17 @@ export function NetscriptFunctions(workerScript: WorkerScript): NS {
}
workerScript.log("sleep", () => `Sleeping for ${time} milliseconds`);
return netscriptDelay(time, workerScript).then(function () {
return Promise.resolve();
return Promise.resolve(true);
});
},
asleep: function (_time: unknown = 0): Promise<void> {
asleep: function (_time: unknown = 0): Promise<true> {
const time = helper.number("asleep", "time", _time);
updateDynamicRam("asleep", getRamCost(Player, "asleep"));
if (time === undefined) {
throw makeRuntimeErrorMsg("asleep", "Takes 1 argument.");
}
workerScript.log("asleep", () => `Sleeping for ${time} milliseconds`);
return new Promise((resolve) => setTimeout(resolve, time));
return new Promise((resolve) => setTimeout(() => resolve(true), time));
},
grow: async function (
_hostname: unknown,

@ -2,7 +2,12 @@ import { INetscriptHelper } from "./INetscriptHelper";
import { WorkerScript } from "../Netscript/WorkerScript";
import { IPlayer } from "../PersonObjects/IPlayer";
import { getRamCost } from "../Netscript/RamCostGenerator";
import { GameInfo, IStyleSettings, UserInterface as IUserInterface, UserInterfaceTheme } from "../ScriptEditor/NetscriptDefinitions";
import {
GameInfo,
IStyleSettings,
UserInterface as IUserInterface,
UserInterfaceTheme,
} from "../ScriptEditor/NetscriptDefinitions";
import { Settings } from "../Settings/Settings";
import { ThemeEvents } from "../Themes/ui/Theme";
import { defaultTheme } from "../Themes/Themes";
@ -29,13 +34,13 @@ export function NetscriptUserInterface(
setTheme: function (newTheme: UserInterfaceTheme): void {
helper.updateDynamicRam("setTheme", getRamCost(player, "ui", "setTheme"));
const hex = /^(#)((?:[A-Fa-f0-9]{2}){3,4}|(?:[A-Fa-f0-9]{3}))$/;
const currentTheme = {...Settings.theme}
const currentTheme = { ...Settings.theme };
const errors: string[] = [];
for (const key of Object.keys(newTheme)) {
if (!currentTheme[key]) {
// Invalid key
errors.push(`Invalid key "${key}"`);
} else if (!hex.test(newTheme[key] ?? '')) {
} else if (!hex.test(newTheme[key] ?? "")) {
errors.push(`Invalid color "${key}": ${newTheme[key]}`);
} else {
currentTheme[key] = newTheme[key];
@ -47,17 +52,17 @@ export function NetscriptUserInterface(
ThemeEvents.emit();
workerScript.log("ui.setTheme", () => `Successfully set theme`);
} else {
workerScript.log("ui.setTheme", () => `Failed to set theme. Errors: ${errors.join(', ')}`);
workerScript.log("ui.setTheme", () => `Failed to set theme. Errors: ${errors.join(", ")}`);
}
},
setStyles: function (newStyles: IStyleSettings): void {
helper.updateDynamicRam("setStyles", getRamCost(player, "ui", "setStyles"));
const currentStyles = {...Settings.styles}
const currentStyles = { ...Settings.styles };
const errors: string[] = [];
for (const key of Object.keys(newStyles)) {
if (!((currentStyles as any)[key])) {
if (!(currentStyles as any)[key]) {
// Invalid key
errors.push(`Invalid key "${key}"`);
} else {
@ -70,7 +75,7 @@ export function NetscriptUserInterface(
ThemeEvents.emit();
workerScript.log("ui.setStyles", () => `Successfully set styles`);
} else {
workerScript.log("ui.setStyles", () => `Failed to set styles. Errors: ${errors.join(', ')}`);
workerScript.log("ui.setStyles", () => `Failed to set styles. Errors: ${errors.join(", ")}`);
}
},
@ -92,13 +97,15 @@ export function NetscriptUserInterface(
helper.updateDynamicRam("getGameInfo", getRamCost(player, "ui", "getGameInfo"));
const version = CONSTANTS.VersionString;
const commit = hash();
const platform = (navigator.userAgent.toLowerCase().indexOf(" electron/") > -1) ? 'Steam' : 'Browser';
const platform = navigator.userAgent.toLowerCase().indexOf(" electron/") > -1 ? "Steam" : "Browser";
const gameInfo = {
version, commit, platform,
}
version,
commit,
platform,
};
return gameInfo;
}
}
},
};
}

@ -178,7 +178,9 @@ function startNetscript1Script(workerScript: WorkerScript): Promise<WorkerScript
const entry = ns[name];
if (typeof entry === "function") {
//Async functions need to be wrapped. See JS-Interpreter documentation
if (["hack", "grow", "weaken", "sleep", "prompt", "manualHack", "scp", "write", "share", "wget"].includes(name)) {
if (
["hack", "grow", "weaken", "sleep", "prompt", "manualHack", "scp", "write", "share", "wget"].includes(name)
) {
const tempWrapper = function (...args: any[]): void {
const fnArgs = [];

@ -859,7 +859,7 @@ export class Sleeve extends Person {
* Returns boolean indicating success
*/
workForFaction(p: IPlayer, factionName: string, workType: string): boolean {
const faction = Factions[factionName]
const faction = Factions[factionName];
if (factionName === "" || !faction || !(faction instanceof Faction) || !p.factions.includes(factionName)) {
return false;
}

@ -1,4 +1,4 @@
import { FactionNames } from '../../Faction/data/FactionNames';
import { FactionNames } from "../../Faction/data/FactionNames";
import { Sleeve } from "./Sleeve";
import { IPlayer } from "../IPlayer";

@ -91,7 +91,8 @@ export function FAQModal({ open, onClose }: IProps): React.ReactElement {
<Typography variant="h4">Why can't I buy the X Augmentation for my sleeve?</Typography>
<br />
<Typography>
Certain Augmentations, like {FactionNames.Bladeburners}-specific ones and NeuroFlux Governor, are not available for sleeves.
Certain Augmentations, like {FactionNames.Bladeburners}-specific ones and NeuroFlux Governor, are not
available for sleeves.
</Typography>
<br />
<br />
@ -110,8 +111,8 @@ export function FAQModal({ open, onClose }: IProps): React.ReactElement {
<br />
<br />
<Typography>
Memory can only be increased by purchasing upgrades from {FactionNames.TheCovenant}. It is a persistent stat, meaning it
never gets resets back to 1. The maximum possible value for a sleeve's memory is 100.
Memory can only be increased by purchasing upgrades from {FactionNames.TheCovenant}. It is a persistent stat,
meaning it never gets resets back to 1. The maximum possible value for a sleeve's memory is 100.
</Typography>
</>
</Modal>

@ -91,11 +91,18 @@ export function SleeveAugmentationsModal(props: IProps): React.ReactElement {
{ownedAugNames.length > 0 && (
<>
<Typography sx={{ mx: 1 }}>Owned Augmentations:</Typography>
<Box display="grid" sx={{ gridTemplateColumns: 'repeat(5, 1fr)', m: 1 }}>
<Box display="grid" sx={{ gridTemplateColumns: "repeat(5, 1fr)", m: 1 }}>
{ownedAugNames.map((augName) => {
const aug = Augmentations[augName];
const info = typeof aug.info === "string" ? <span>{aug.info}</span> : aug.info
const tooltip = (<>{info}<br /><br />{aug.stats}</>);
const info = typeof aug.info === "string" ? <span>{aug.info}</span> : aug.info;
const tooltip = (
<>
{info}
<br />
<br />
{aug.stats}
</>
);
return (
<Tooltip key={augName} title={<Typography>{tooltip}</Typography>}>

@ -1,12 +1,6 @@
import React, { useState } from "react";
import {
Box,
Paper,
Typography,
Button,
Tooltip
} from "@mui/material";
import { Box, Paper, Typography, Button, Tooltip } from "@mui/material";
import { CONSTANTS } from "../../../Constants";
import { Crimes } from "../../../Crime/Crimes";
@ -137,12 +131,12 @@ export function SleeveElem(props: IProps): React.ReactElement {
}
return (
<Box component={Paper} sx={{ width: 'auto' }}>
<Box component={Paper} sx={{ width: "auto" }}>
<Box sx={{ m: 1 }}>
<Box display="grid" sx={{ gridTemplateColumns: '1fr 1fr', width: '100%', gap: 1 }}>
<Box display="grid" sx={{ gridTemplateColumns: "1fr 1fr", width: "100%", gap: 1 }}>
<Box>
<StatsElement sleeve={props.sleeve} />
<Box display="grid" sx={{ gridTemplateColumns: '1fr 1fr', width: '100%' }}>
<Box display="grid" sx={{ gridTemplateColumns: "1fr 1fr", width: "100%" }}>
<Button onClick={() => setStatsOpen(true)}>More Stats</Button>
<Button onClick={() => setEarningsOpen(true)}>More Earnings Info</Button>
<Tooltip title={player.money < CONSTANTS.TravelCost ? <Typography>Insufficient funds</Typography> : ""}>
@ -150,20 +144,22 @@ export function SleeveElem(props: IProps): React.ReactElement {
<Button
onClick={() => setTravelOpen(true)}
disabled={player.money < CONSTANTS.TravelCost}
sx={{ width: '100%', height: '100%' }}
sx={{ width: "100%", height: "100%" }}
>
Travel
</Button>
</span>
</Tooltip>
<Tooltip
title={props.sleeve.shock < 100 ? <Typography>Unlocked when sleeve has fully recovered</Typography> : ""}
title={
props.sleeve.shock < 100 ? <Typography>Unlocked when sleeve has fully recovered</Typography> : ""
}
>
<span>
<Button
onClick={() => setAugmentationsOpen(true)}
disabled={props.sleeve.shock < 100}
sx={{ width: '100%', height: '100%' }}
sx={{ width: "100%", height: "100%" }}
>
Manage Augmentations
</Button>
@ -175,7 +171,9 @@ export function SleeveElem(props: IProps): React.ReactElement {
<EarningsElement sleeve={props.sleeve} />
<Box>
<TaskSelector player={player} sleeve={props.sleeve} setABC={setABC} />
<Button onClick={setTask} sx={{ width: '100%' }}>Set Task</Button>
<Button onClick={setTask} sx={{ width: "100%" }}>
Set Task
</Button>
<Typography>{desc}</Typography>
<Typography>
{props.sleeve.currentTask === SleeveTaskType.Crime &&

@ -1,11 +1,6 @@
import React, { useState, useEffect } from "react";
import {
Box,
Typography,
Button,
Container
} from "@mui/material";
import { Box, Typography, Button, Container } from "@mui/material";
import { use } from "../../../ui/Context";
@ -38,14 +33,16 @@ export function SleeveRoot(): React.ReactElement {
<br />
<br />
</Typography>
</Container>
<Button onClick={() => setFAQOpen(true)}>FAQ</Button>
<Button href="https://bitburner.readthedocs.io/en/latest/advancedgameplay/sleeves.html#duplicate-sleeves" target="_blank">
<Button
href="https://bitburner.readthedocs.io/en/latest/advancedgameplay/sleeves.html#duplicate-sleeves"
target="_blank"
>
Wiki Documentation
</Button>
<Box display="grid" sx={{ gridTemplateColumns: 'repeat(2, 1fr)', mt: 1 }}>
<Box display="grid" sx={{ gridTemplateColumns: "repeat(2, 1fr)", mt: 1 }}>
{player.sleeves.map((sleeve, i) => (
<SleeveElem key={i} rerender={rerender} sleeve={sleeve} />
))}

@ -1,12 +1,6 @@
import React from "react";
import {
Typography,
Table,
TableBody,
TableCell,
TableRow,
} from "@mui/material";
import { Typography, Table, TableBody, TableCell, TableRow } from "@mui/material";
import { numeralWrapper } from "../../../ui/numeralFormat";
import { Settings } from "../../../Settings/Settings";
@ -28,29 +22,69 @@ export function StatsElement(props: IProps): React.ReactElement {
const classes = useStyles();
return (
<Table sx={{ display: 'table', mb: 1, width: '100%' }}>
<Table sx={{ display: "table", mb: 1, width: "100%" }}>
<TableBody>
<StatsRow name="City" color={Settings.theme.primary} data={{ content: props.sleeve.city }} />
<StatsRow name="HP" color={Settings.theme.hp}
data={{ content: `${numeralWrapper.formatHp(props.sleeve.hp)} / ${numeralWrapper.formatHp(props.sleeve.max_hp)}` }}
<StatsRow
name="HP"
color={Settings.theme.hp}
data={{
content: `${numeralWrapper.formatHp(props.sleeve.hp)} / ${numeralWrapper.formatHp(props.sleeve.max_hp)}`,
}}
/>
<StatsRow
name="Hacking"
color={Settings.theme.hack}
data={{ level: props.sleeve.hacking, exp: props.sleeve.hacking_exp }}
/>
<StatsRow
name="Strength"
color={Settings.theme.combat}
data={{ level: props.sleeve.strength, exp: props.sleeve.strength_exp }}
/>
<StatsRow
name="Defense"
color={Settings.theme.combat}
data={{ level: props.sleeve.defense, exp: props.sleeve.defense_exp }}
/>
<StatsRow
name="Dexterity"
color={Settings.theme.combat}
data={{ level: props.sleeve.dexterity, exp: props.sleeve.dexterity_exp }}
/>
<StatsRow
name="Agility"
color={Settings.theme.combat}
data={{ level: props.sleeve.agility, exp: props.sleeve.agility_exp }}
/>
<StatsRow
name="Charisma"
color={Settings.theme.cha}
data={{ level: props.sleeve.charisma, exp: props.sleeve.charisma_exp }}
/>
<StatsRow name="Hacking" color={Settings.theme.hack} data={{ level: props.sleeve.hacking, exp: props.sleeve.hacking_exp }} />
<StatsRow name="Strength" color={Settings.theme.combat} data={{ level: props.sleeve.strength, exp: props.sleeve.strength_exp }} />
<StatsRow name="Defense" color={Settings.theme.combat} data={{ level: props.sleeve.defense, exp: props.sleeve.defense_exp }} />
<StatsRow name="Dexterity" color={Settings.theme.combat} data={{ level: props.sleeve.dexterity, exp: props.sleeve.dexterity_exp }} />
<StatsRow name="Agility" color={Settings.theme.combat} data={{ level: props.sleeve.agility, exp: props.sleeve.agility_exp }} />
<StatsRow name="Charisma" color={Settings.theme.cha} data={{ level: props.sleeve.charisma, exp: props.sleeve.charisma_exp }} />
<TableRow>
<TableCell classes={{ root: classes.cellNone }}>
<br />
</TableCell>
</TableRow>
<StatsRow name="Shock" color={Settings.theme.primary} data={{ content: numeralWrapper.formatSleeveShock(100 - props.sleeve.shock) }} />
<StatsRow name="Sync" color={Settings.theme.primary} data={{ content: numeralWrapper.formatSleeveSynchro(props.sleeve.sync) }} />
<StatsRow name="Memory" color={Settings.theme.primary} data={{ content: numeralWrapper.formatSleeveMemory(props.sleeve.memory) }} />
<StatsRow
name="Shock"
color={Settings.theme.primary}
data={{ content: numeralWrapper.formatSleeveShock(100 - props.sleeve.shock) }}
/>
<StatsRow
name="Sync"
color={Settings.theme.primary}
data={{ content: numeralWrapper.formatSleeveSynchro(props.sleeve.sync) }}
/>
<StatsRow
name="Memory"
color={Settings.theme.primary}
data={{ content: numeralWrapper.formatSleeveMemory(props.sleeve.memory) }}
/>
</TableBody>
</Table>
)
);
}
export function EarningsElement(props: IProps): React.ReactElement {
@ -60,7 +94,12 @@ export function EarningsElement(props: IProps): React.ReactElement {
let data: any[][] = [];
if (props.sleeve.currentTask === SleeveTaskType.Crime) {
data = [
[`Money`, <><Money money={parseFloat(props.sleeve.currentTaskLocation)} /> (on success)</>],
[
`Money`,
<>
<Money money={parseFloat(props.sleeve.currentTaskLocation)} /> (on success)
</>,
],
[`Hacking Exp`, `${numeralWrapper.formatExp(props.sleeve.gainRatesForTask.hack)} (2x on success)`],
[`Strength Exp`, `${numeralWrapper.formatExp(props.sleeve.gainRatesForTask.str)} (2x on success)`],
[`Defense Exp`, `${numeralWrapper.formatExp(props.sleeve.gainRatesForTask.def)} (2x on success)`],
@ -85,13 +124,11 @@ export function EarningsElement(props: IProps): React.ReactElement {
}
return (
<Table sx={{ display: 'table', mb: 1, width: '100%', lineHeight: 0 }}>
<Table sx={{ display: "table", mb: 1, width: "100%", lineHeight: 0 }}>
<TableBody>
<TableRow>
<TableCell classes={{ root: classes.cellNone }}>
<Typography variant='h6'>
Earnings
</Typography>
<Typography variant="h6">Earnings</Typography>
</TableCell>
</TableRow>
{data.map(([a, b]) => (
@ -106,5 +143,5 @@ export function EarningsElement(props: IProps): React.ReactElement {
))}
</TableBody>
</Table>
)
);
}

@ -76,9 +76,9 @@ function possibleFactions(player: IPlayer, sleeve: Sleeve): string[] {
}
}
return factions.filter(faction => {
return factions.filter((faction) => {
const facInfo = Factions[faction].getInfo();
return facInfo.offerHackingWork || facInfo.offerFieldWork || facInfo.offerSecurityWork
return facInfo.offerHackingWork || facInfo.offerFieldWork || facInfo.offerSecurityWork;
});
}
@ -279,7 +279,7 @@ export function TaskSelector(props: IProps): React.ReactElement {
return (
<>
<Select onChange={onS0Change} value={s0} sx={{ width: '100%' }}>
<Select onChange={onS0Change} value={s0} sx={{ width: "100%" }}>
{validActions.map((task) => (
<MenuItem key={task} value={task}>
{task}
@ -288,7 +288,7 @@ export function TaskSelector(props: IProps): React.ReactElement {
</Select>
{!(details.first.length === 1 && details.first[0] === "------") && (
<>
<Select onChange={onS1Change} value={s1} sx={{ width: '100%' }}>
<Select onChange={onS1Change} value={s1} sx={{ width: "100%" }}>
{details.first.map((detail) => (
<MenuItem key={detail} value={detail}>
{detail}
@ -299,7 +299,7 @@ export function TaskSelector(props: IProps): React.ReactElement {
)}
{!(details2.length === 1 && details2[0] === "------") && (
<>
<Select onChange={onS2Change} value={s2} sx={{ width: '100%' }}>
<Select onChange={onS2Change} value={s2} sx={{ width: "100%" }}>
{details2.map((detail) => (
<MenuItem key={detail} value={detail}>
{detail}

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

@ -13,14 +13,14 @@ export function calculateSkillProgress(exp: number, mult = 1): ISkillProgress {
let baseExperience = calculateExp(currentSkill, mult);
if (baseExperience < 0) baseExperience = 0;
let nextExperience = calculateExp(nextSkill, mult)
let nextExperience = calculateExp(nextSkill, mult);
if (nextExperience < 0) nextExperience = 0;
const normalize = (value: number): number => ((value - baseExperience) * 100) / (nextExperience - baseExperience);
let progress = (nextExperience - baseExperience !== 0) ? normalize(exp) : 99.99;
let progress = nextExperience - baseExperience !== 0 ? normalize(exp) : 99.99;
// Clamp progress: When sleeves are working out, the player gets way too much progress
if (progress < 0) progress = 0
if (progress < 0) progress = 0;
if (progress > 100) progress = 100;
// Clamp floating point imprecisions
@ -37,8 +37,8 @@ export function calculateSkillProgress(exp: number, mult = 1): ISkillProgress {
nextExperience,
currentExperience,
remainingExperience,
progress
}
progress,
};
}
export interface ISkillProgress {
@ -54,9 +54,13 @@ export interface ISkillProgress {
export function getEmptySkillProgress(): ISkillProgress {
return {
currentSkill: 0, nextSkill: 0,
baseExperience: 0, experience: 0, nextExperience: 0,
currentExperience: 0, remainingExperience: 0,
currentSkill: 0,
nextSkill: 0,
baseExperience: 0,
experience: 0,
nextExperience: 0,
currentExperience: 0,
remainingExperience: 0,
progress: 0,
};
}

@ -1,13 +1,7 @@
import React, { useState, useEffect } from "react";
import { find } from "lodash";
import {
Box,
Typography,
Button,
Container,
Paper
} from "@mui/material";
import { Box, Typography, Button, Container, Paper } from "@mui/material";
import { Check, Lock, Create } from "@mui/icons-material";
import { use } from "../../ui/Context";
@ -26,7 +20,7 @@ export function ProgramsRoot(): React.ReactElement {
}
const programs = [...Object.values(Programs)]
.filter(prog => {
.filter((prog) => {
const create = prog.create;
if (create === null) return false;
if (prog.name === "b1t_flum3.exe") {
@ -38,7 +32,7 @@ export function ProgramsRoot(): React.ReactElement {
if (player.hasProgram(a.name)) return 1;
if (player.hasProgram(b.name)) return -1;
return (a.create?.level ?? 0) - (b.create?.level ?? 0);
})
});
useEffect(() => {
programs.forEach((p) => {
@ -54,11 +48,11 @@ export function ProgramsRoot(): React.ReactElement {
const getHackingLevelRemaining = (lvl: number): number => {
return Math.ceil(Math.max(lvl - (player.hacking + player.intelligence / 2), 0));
}
};
const getProgCompletion = (name: string): number => {
const programFile = find(player.getHomeComputer().programs, p => {
return (p.startsWith(name) && p.endsWith("%-INC"));
const programFile = find(player.getHomeComputer().programs, (p) => {
return p.startsWith(name) && p.endsWith("%-INC");
});
if (!programFile) return -1;
@ -69,7 +63,7 @@ export function ProgramsRoot(): React.ReactElement {
return -1;
}
return percComplete;
}
};
return (
<Container disableGutters maxWidth="lg" sx={{ mx: 0, mb: 10 }}>
@ -80,21 +74,26 @@ export function ProgramsRoot(): React.ReactElement {
time. Your progress will be saved and you can continue later.
</Typography>
<Box sx={{ display: 'grid', gridTemplateColumns: "repeat(3, 1fr)", my: 1 }}>
<Box sx={{ display: "grid", gridTemplateColumns: "repeat(3, 1fr)", my: 1 }}>
{programs.map((program) => {
const create = program.create;
if (create === null) return <></>;
const curCompletion = getProgCompletion(program.name);
return (
<Box component={Paper} sx={{ p: 1, opacity: player.hasProgram(program.name) ? 0.75 : 1 }} key={program.name}>
<Typography variant="h6" sx={{ display: 'flex', alignItems: 'center', flexWrap: 'wrap' }}>
{player.hasProgram(program.name) && <Check sx={{ mr: 1 }} /> ||
(create.req(player) && <Create sx={{ mr: 1 }} /> || <Lock sx={{ mr: 1 }} />)}
<Box
component={Paper}
sx={{ p: 1, opacity: player.hasProgram(program.name) ? 0.75 : 1 }}
key={program.name}
>
<Typography variant="h6" sx={{ display: "flex", alignItems: "center", flexWrap: "wrap" }}>
{(player.hasProgram(program.name) && <Check sx={{ mr: 1 }} />) ||
(create.req(player) && <Create sx={{ mr: 1 }} />) || <Lock sx={{ mr: 1 }} />}
{program.name}
</Typography>
{(!player.hasProgram(program.name) && create.req(player)) && <Button
sx={{ my: 1, width: '100%' }}
{!player.hasProgram(program.name) && create.req(player) && (
<Button
sx={{ my: 1, width: "100%" }}
onClick={(event) => {
if (!event.isTrusted) return;
player.startCreateProgramWork(program.name, create.time, create.level);
@ -103,18 +102,19 @@ export function ProgramsRoot(): React.ReactElement {
}}
>
Create program
</Button>}
{(player.hasProgram(program.name) || getHackingLevelRemaining(create.level) === 0) ||
</Button>
)}
{player.hasProgram(program.name) || getHackingLevelRemaining(create.level) === 0 || (
<Typography color={Settings.theme.hack}>
<b>Unlocks in:</b> {getHackingLevelRemaining(create.level)} hacking levels
</Typography>}
{(curCompletion !== -1) &&
</Typography>
)}
{curCompletion !== -1 && (
<Typography color={Settings.theme.infolight}>
<b>Current completion:</b> {curCompletion}%
</Typography>}
<Typography>
{create.tooltip}
</Typography>
)}
<Typography>{create.tooltip}</Typography>
</Box>
);
})}

@ -17,7 +17,7 @@ import { areImportsEquals } from "../Terminal/DirectoryHelpers";
import { IPlayer } from "../PersonObjects/IPlayer";
export interface RamUsageEntry {
type: 'ns' | 'dom' | 'fn' | 'misc';
type: "ns" | "dom" | "fn" | "misc";
name: string;
cost: number;
}
@ -139,7 +139,9 @@ async function parseOnlyRamCalculate(
// Finally, walk the reference map and generate a ram cost. The initial set of keys to scan
// are those that start with __SPECIAL_INITIAL_MODULE__.
let ram = RamCostConstants.ScriptBaseRamCost;
const detailedCosts: RamUsageEntry[] = [{ type: 'misc', name: 'baseCost', cost: RamCostConstants.ScriptBaseRamCost}];
const detailedCosts: RamUsageEntry[] = [
{ type: "misc", name: "baseCost", cost: RamCostConstants.ScriptBaseRamCost },
];
const unresolvedRefs = Object.keys(dependencyMap).filter((s) => s.startsWith(initialModule));
const resolvedRefs = new Set();
while (unresolvedRefs.length > 0) {
@ -149,19 +151,19 @@ async function parseOnlyRamCalculate(
// Check if this is one of the special keys, and add the appropriate ram cost if so.
if (ref === "hacknet" && !resolvedRefs.has("hacknet")) {
ram += RamCostConstants.ScriptHacknetNodesRamCost;
detailedCosts.push({ type: 'ns', name: 'hacknet', cost: RamCostConstants.ScriptHacknetNodesRamCost});
detailedCosts.push({ type: "ns", name: "hacknet", cost: RamCostConstants.ScriptHacknetNodesRamCost });
}
if (ref === "document" && !resolvedRefs.has("document")) {
ram += RamCostConstants.ScriptDomRamCost;
detailedCosts.push({ type: 'dom', name: 'document', cost: RamCostConstants.ScriptDomRamCost});
detailedCosts.push({ type: "dom", name: "document", cost: RamCostConstants.ScriptDomRamCost });
}
if (ref === "window" && !resolvedRefs.has("window")) {
ram += RamCostConstants.ScriptDomRamCost;
detailedCosts.push({ type: 'dom', name: 'window', cost: RamCostConstants.ScriptDomRamCost});
detailedCosts.push({ type: "dom", name: "window", cost: RamCostConstants.ScriptDomRamCost });
}
if (ref === "corporation" && !resolvedRefs.has("corporation")) {
ram += RamCostConstants.ScriptCorporationRamCost;
detailedCosts.push({ type: 'ns', name: 'corporation', cost: RamCostConstants.ScriptCorporationRamCost});
detailedCosts.push({ type: "ns", name: "corporation", cost: RamCostConstants.ScriptCorporationRamCost });
}
resolvedRefs.add(ref);
@ -203,7 +205,7 @@ async function parseOnlyRamCalculate(
// This accounts for namespaces (Bladeburner, CodingCpntract, etc.)
let func;
let refDetail = 'n/a';
let refDetail = "n/a";
if (ref in workerScript.env.vars.bladeburner) {
func = workerScript.env.vars.bladeburner[ref];
refDetail = `bladeburner.${ref}`;
@ -234,12 +236,12 @@ async function parseOnlyRamCalculate(
}
const fnRam = applyFuncRam(func);
ram += fnRam;
detailedCosts.push({ type: 'fn', name: refDetail, cost: fnRam});
detailedCosts.push({ type: "fn", name: refDetail, cost: fnRam });
} catch (error) {
continue;
}
}
return { cost: ram, entries: detailedCosts.filter(e => e.cost > 0) };
return { cost: ram, entries: detailedCosts.filter((e) => e.cost > 0) };
} catch (error) {
// console.info("parse or eval error: ", error);
// This is not unexpected. The user may be editing a script, and it may be in

@ -4642,7 +4642,7 @@ export interface NS extends Singularity {
* ```
* @returns
*/
sleep(millis: number): Promise<void>;
sleep(millis: number): Promise<true>;
/**
* Suspends the script for n milliseconds. Doesn't block with concurrent calls.
@ -4652,7 +4652,7 @@ export interface NS extends Singularity {
* @param millis - Number of milliseconds to sleep.
* @returns
*/
asleep(millis: number): Promise<void>;
asleep(millis: number): Promise<true>;
/**
* Prints one or move values or variables to the scripts logs.

@ -1,4 +1,4 @@
export type WordWrapOptions = 'on' | 'off' | 'bounded' | 'wordWrapColumn';
export type WordWrapOptions = "on" | "off" | "bounded" | "wordWrapColumn";
export interface Options {
theme: string;
insertSpaces: boolean;

@ -794,11 +794,11 @@ export function Root(props: IProps): React.ReactElement {
setFilter(event.target.value);
}
function handleExpandSearch(): void {
setFilter("")
setSearchExpanded(!searchExpanded)
setFilter("");
setSearchExpanded(!searchExpanded);
}
const filteredOpenScripts = Object.values(openScripts).filter(
(script) => (script.hostname.includes(filter) || script.fileName.includes(filter))
(script) => script.hostname.includes(filter) || script.fileName.includes(filter),
);
// Toolbars are roughly 112px:
@ -835,7 +835,7 @@ export function Root(props: IProps): React.ReactElement {
}}
>
<Tooltip title={"Search Open Scripts"}>
{searchExpanded ?
{searchExpanded ? (
<TextField
value={filter}
onChange={handleFilterChange}
@ -843,9 +843,14 @@ export function Root(props: IProps): React.ReactElement {
InputProps={{
startAdornment: <SearchIcon />,
spellCheck: false,
endAdornment: <CloseIcon onClick={handleExpandSearch} />
endAdornment: <CloseIcon onClick={handleExpandSearch} />,
}}
/> : <Button onClick={handleExpandSearch} ><SearchIcon /></Button>}
/>
) : (
<Button onClick={handleExpandSearch}>
<SearchIcon />
</Button>
)}
</Tooltip>
{filteredOpenScripts.map(({ fileName, hostname }, index) => {
const iconButtonStyle = {
@ -896,7 +901,7 @@ export function Root(props: IProps): React.ReactElement {
}}
style={{
maxWidth: `${tabTextWidth}px`,
minHeight: '38.5px',
minHeight: "38.5px",
overflow: "hidden",
...(currentScript?.fileName === filteredOpenScripts[index].fileName
? {

@ -260,7 +260,6 @@ export async function loadThemes(monaco: { editor: any }): Promise<void> {
token: "ns",
foreground: "FFB86C",
fontStyle: "italic",
},
{
token: "netscriptfunction",
@ -273,7 +272,7 @@ export async function loadThemes(monaco: { editor: any }): Promise<void> {
{
token: "type.identifier.js",
foreground: "7EE9FD",
fontStyle: "italic"
fontStyle: "italic",
},
{
token: "delimiter.square.js",
@ -281,7 +280,7 @@ export async function loadThemes(monaco: { editor: any }): Promise<void> {
},
{
token: "delimiter.parenthesis.js",
foreground: "FFD709"
foreground: "FFD709",
},
{
token: "delimiter.bracket.js",
@ -293,7 +292,7 @@ export async function loadThemes(monaco: { editor: any }): Promise<void> {
fontStyle: "italic",
},
],
"colors": {
colors: {
"editor.foreground": "#F8F8F2",
"editor.background": "#282A36",
"editorLineNumber.foreground": "#6272A4",
@ -402,6 +401,6 @@ export async function loadThemes(monaco: { editor: any }): Promise<void> {
"scrollbarSlider.background": "#4E566680",
"scrollbarSlider.activeBackground": "#747D9180",
"scrollbarSlider.hoverBackground": "#5A637580",
}
},
});
}

@ -105,19 +105,32 @@ export function numCycleForGrowthTransition(server: Server, growth: number, p: I
* @param p - Reference to Player object
* @returns Number of "growth cycles" needed
*/
export function numCycleForGrowthCorrected(server: Server, targetMoney: number, startMoney: number, p: IPlayer, cores = 1): number {
if (startMoney < 0) { startMoney = 0; } // servers "can't" have less than 0 dollars on them
if (targetMoney > server.moneyMax) { targetMoney = server.moneyMax; } // can't grow a server to more than its moneyMax
if (targetMoney <= startMoney) { return 0; } // no growth --> no threads
export function numCycleForGrowthCorrected(
server: Server,
targetMoney: number,
startMoney: number,
p: IPlayer,
cores = 1,
): number {
if (startMoney < 0) {
startMoney = 0;
} // servers "can't" have less than 0 dollars on them
if (targetMoney > server.moneyMax) {
targetMoney = server.moneyMax;
} // can't grow a server to more than its moneyMax
if (targetMoney <= startMoney) {
return 0;
} // no growth --> no threads
// exponential base adjusted by security
const adjGrowthRate = (1 + (CONSTANTS.ServerBaseGrowthRate - 1) / server.hackDifficulty);
const adjGrowthRate = 1 + (CONSTANTS.ServerBaseGrowthRate - 1) / server.hackDifficulty;
const exponentialBase = Math.min(adjGrowthRate, CONSTANTS.ServerMaxGrowthRate); // cap growth rate
// total of all grow thread multipliers
const serverGrowthPercentage = server.serverGrowth / 100.0;
const coreMultiplier = 1 + ((cores - 1) / 16);
const threadMultiplier = serverGrowthPercentage * p.hacking_grow_mult * coreMultiplier * BitNodeMultipliers.ServerGrowthRate;
const coreMultiplier = 1 + (cores - 1) / 16;
const threadMultiplier =
serverGrowthPercentage * p.hacking_grow_mult * coreMultiplier * BitNodeMultipliers.ServerGrowthRate;
/* To understand what is done below we need to do some math. I hope the explanation is clear enough.
* First of, the names will be shortened for ease of manipulation:
@ -169,7 +182,7 @@ export function numCycleForGrowthCorrected(server: Server, targetMoney: number,
* using Padé rational fraction approximation [2](5)
*/
const ey = Math.exp(y);
w = (ey + 4/3 * ey*ey) / (1 + 7/3 * ey + 5/6 * ey*ey);
w = (ey + (4 / 3) * ey * ey) / (1 + (7 / 3) * ey + (5 / 6) * ey * ey);
} else {
/* obtain initial approximation from rough asymptotic [1](4.18)
* w = y [- log y when 0 <= y]
@ -207,9 +220,9 @@ export function numCycleForGrowthCorrected(server: Server, targetMoney: number,
const bct = bt ** cycles;
const opc = startMoney + cycles;
const diff = opc * bct - targetMoney;
corr = diff / (opc * x + 1.0) / bct
corr = diff / (opc * x + 1.0) / bct;
cycles -= corr;
} while (Math.abs(corr) >= 1)
} while (Math.abs(corr) >= 1);
/* c is now within +/- 1 of the exact result.
* We want the ceiling of the exact result, so the floor if the approximation is above,
* the ceiling if the approximation is in the same unit as the exact result,
@ -236,9 +249,15 @@ export function numCycleForGrowthCorrected(server: Server, targetMoney: number,
* @param p - Reference to Player object
* @returns Number of "growth cycles" needed to reverse the described hack
*/
export function numCycleForGrowthByHackAmt(server: Server, hackProp: number, prehackMoney: number, p: IPlayer, cores = 1): number{
export function numCycleForGrowthByHackAmt(
server: Server,
hackProp: number,
prehackMoney: number,
p: IPlayer,
cores = 1,
): number {
if (prehackMoney > server.moneyMax) prehackMoney = server.moneyMax;
const posthackMoney = Math.floor(prehackMoney * Math.min(1, Math.max(0, (1 - hackProp))));
const posthackMoney = Math.floor(prehackMoney * Math.min(1, Math.max(0, 1 - hackProp)));
return numCycleForGrowthCorrected(server, prehackMoney, posthackMoney, p, cores);
}
@ -258,7 +277,6 @@ export function numCycleForGrowthByMultiplier(server: Server, growth: number, p:
return numCycleForGrowthCorrected(server, targetMoney, startingMoney, p, cores);
}
//Applied server growth for a single server. Returns the percentage growth
export function processSingleServerGrowth(server: Server, threads: number, p: IPlayer, cores = 1): number {
let serverGrowth = calculateServerGrowth(server, threads, p, cores);

@ -1,4 +1,4 @@
import { FactionNames } from '../../Faction/data/FactionNames';
import { FactionNames } from "../../Faction/data/FactionNames";
// tslint:disable:max-file-line-count
// This could actually be a JSON file as it should be constant metadata to be imported...

Some files were not shown because too many files have changed in this diff Show More