Merge branch 'dev' into bugfix/fix-tests

This commit is contained in:
hydroflame 2022-04-06 20:16:51 -04:00 committed by GitHub
commit b6d68f7353
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
192 changed files with 3339 additions and 2845 deletions

@ -9,3 +9,6 @@
- Include how it was tested - Include how it was tested
- Include screenshot / gif (if possible) - 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: workflow_dispatch:
inputs: inputs:
version: version:
description: 'Version (format: x.y.z)' description: "Version (format: x.y.z)"
required: true required: true
versionNumber: versionNumber:
description: 'Version Number (for saves migration)' description: "Version Number (for saves migration)"
required: true required: true
changelog: changelog:
description: 'Changelog (url that points to RAW markdown)' description: "Changelog (url that points to RAW markdown)"
default: '' default: ""
buildApp: buildApp:
description: 'Include Application Build' description: "Include Application Build"
type: boolean type: boolean
default: 'true' default: "true"
required: true required: true
buildDoc: buildDoc:
description: 'Include Documentation Build' description: "Include Documentation Build"
type: boolean type: boolean
default: 'true' default: "true"
required: true required: true
prepareRelease: prepareRelease:
description: 'Prepare Draft Release' description: "Prepare Draft Release"
type: boolean type: boolean
default: 'true' default: "true"
required: true required: true
jobs: jobs:
@ -46,7 +46,7 @@ jobs:
uses: actions/setup-node@v2 uses: actions/setup-node@v2
with: with:
node-version: 16.13.1 node-version: 16.13.1
cache: 'npm' cache: "npm"
- name: Install NPM dependencies for version updater - name: Install NPM dependencies for version updater
working-directory: ./tools/bump-version working-directory: ./tools/bump-version
run: npm ci run: npm ci

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

42
dist/vendor.bundle.js vendored

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

@ -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 variable purchasing will be scriptable.
All equation must have: 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) - 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. - 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. - 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` - When merging, always merge your branch into `dev`. When releasing a new update, then merge `dev` into `master`
## Running locally. ## Running locally.
Install 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. After that you can open any browser and navigate to `localhost:8000` and play the game.
Saving a file will reload the game automatically. Saving a file will reload the game automatically.
### How to build the electron app ### How to build the electron app
Tested on Node v16.13.1 (LTS) on Windows 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! ## 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. 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: - On Browser: Stick `?noScript` at the end of the URL
* In the menu, "Reloads" -> "Reload & Kill All Scripts". - On Steam:
* If this does not work, when launching the game, use the kill all script options. - 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 ## Steam: Game won't launch
### **On Windows** ### **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 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. 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** ### **On Linux**
The game is built natively, do not use Proton unless native does not work. 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). 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. You may want access the logs to get information about crashes or such.
* on Linux: `~/.config/bitburner/logs/main.log` - on Linux: `~/.config/bitburner/logs/main.log`
* on macOS: `~/Library/Logs/bitburner/main.log` - on macOS: `~/Library/Logs/bitburner/main.log`
* on Windows: `%USERPROFILE%\AppData\Roaming\bitburner\logs\main.log` - on Windows: `%USERPROFILE%\AppData\Roaming\bitburner\logs\main.log`
### Config (using [electron-store](https://github.com/sindresorhus/electron-store#readme)) ### Config (using [electron-store](https://github.com/sindresorhus/electron-store#readme))
Configuration file will be written to disk in the application data directory. Configuration file will be written to disk in the application data directory.
* on Linux: `~/.config/bitburner/config.json` - on Linux: `~/.config/bitburner/config.json`
* on macOS: `~/Library/Application\ Support/bitburner/config.json` - on macOS: `~/Library/Application\ Support/bitburner/config.json`
* on Windows: `%USERPROFILE%\AppData\Roaming\bitburner\config.json` - on Windows: `%USERPROFILE%\AppData\Roaming\bitburner\config.json`
--- ---

@ -3,6 +3,21 @@
Changelog Changelog
========= =========
v1.6.3 - 2022-04-01 Few stanek fixes
------------------------------------
Stanek Gift
* Has a minimum size of 2x3
* Active Fragment property 'avgCharge' renamed to 'highestCharge'
* Formula for fragment effect updated to make 561% more sense.
Now you can charge to your heart content.
* Logs for the 'chargeFragment' function updated.
Misc.
* Nerf noodle bar.
v1.6.0 - 2022-03-29 Grafting v1.6.0 - 2022-03-29 Grafting
---------------------------- ----------------------------

@ -66,7 +66,7 @@ documentation_title = '{0} Documentation'.format(project)
# The short X.Y version. # The short X.Y version.
version = '1.6' version = '1.6'
# The full version, including alpha/beta/rc tags. # The full version, including alpha/beta/rc tags.
release = '1.6.0' release = '1.6.3'
# The language for content autogenerated by Sphinx. Refer to documentation # The language for content autogenerated by Sphinx. Refer to documentation
# for a list of supported languages. # for a list of supported languages.

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

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

@ -15,19 +15,20 @@ const debug = process.argv.includes("--debug");
async function createWindow(killall) { async function createWindow(killall) {
const setStopProcessHandler = global.app_handlers.stopProcess; const setStopProcessHandler = global.app_handlers.stopProcess;
app.setAppUserModelId("Bitburner");
let icon; let icon;
if (process.platform == 'linux') { if (process.platform == "linux") {
icon = path.join(__dirname, 'icon.png'); icon = path.join(__dirname, "icon.png");
} }
const tracker = windowTracker('main'); const tracker = windowTracker("main");
const window = new BrowserWindow({ const window = new BrowserWindow({
icon, icon,
show: false, show: false,
backgroundThrottling: false, backgroundThrottling: false,
backgroundColor: "#000000", backgroundColor: "#000000",
title: 'Bitburner', title: "Bitburner",
x: tracker.state.x, x: tracker.state.x,
y: tracker.state.y, y: tracker.state.y,
width: tracker.state.width, width: tracker.state.width,
@ -36,7 +37,7 @@ async function createWindow(killall) {
minHeight: 400, minHeight: 400,
webPreferences: { webPreferences: {
nativeWindowOpen: true, nativeWindowOpen: true,
preload: path.join(__dirname, 'preload.js'), preload: path.join(__dirname, "preload.js"),
}, },
}); });
@ -51,12 +52,12 @@ async function createWindow(killall) {
window.webContents.on("new-window", async function (e, url) { window.webContents.on("new-window", async function (e, url) {
// Let's make sure sure we have a proper url // Let's make sure sure we have a proper url
let parsedUrl let parsedUrl;
try { try {
parsedUrl = new URL(url); parsedUrl = new URL(url);
} catch (_) { } catch (_) {
// This is an invalid url, let's just do nothing // This is an invalid url, let's just do nothing
log.warn(`Invalid url found: ${url}`) log.warn(`Invalid url found: ${url}`);
e.preventDefault(); e.preventDefault();
return; return;
} }
@ -73,11 +74,13 @@ async function createWindow(killall) {
if (!isChild) { if (!isChild) {
// If we're not relative to our app's path let's abort // 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(); e.preventDefault();
} else if (!fileExists) { } else if (!fileExists) {
// If the file does not exist let's abort // 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(); e.preventDefault();
} }
@ -89,7 +92,7 @@ async function createWindow(killall) {
let urlToOpen = parsedUrl.toString(); let urlToOpen = parsedUrl.toString();
if (parsedUrl.search) { if (parsedUrl.search) {
log.log(`Cannot open a path with parameters: ${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... // 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('&', '^&'); // const escapedUri = parsedUrl.href.replace('&', '^&');
// cp.spawn("cmd.exe", ["/c", "start", escapedUri], { detached: true, stdio: "ignore" }); // cp.spawn("cmd.exe", ["/c", "start", escapedUri], { detached: true, stdio: "ignore" });

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

@ -18,7 +18,7 @@ log.transports.console.level = config.get("console-log-level", "debug");
log.catchErrors(); log.catchErrors();
log.info(`Started app: ${JSON.stringify(process.argv)}`); 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 // The exception will already have been logged by electron-log
process.exit(1); process.exit(1);
}); });
@ -67,42 +67,43 @@ function setStopProcessHandler(app, window, enabled) {
// So we'll alert the player to close their browser. // So we'll alert the player to close their browser.
if (global.app_playerOpenedExternalLink) { if (global.app_playerOpenedExternalLink) {
await dialog.showMessageBox({ await dialog.showMessageBox({
title: 'Bitburner', title: "Bitburner",
message: 'You may have to close your browser to properly exit the game.', 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.' + detail:
' This includes launching an external link, which opens up your browser.', 'Steam will keep tracking Bitburner as "Running" if any process started within the game is still running.' +
type: 'warning', buttons: ['OK'] " 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 // We'll try to execute javascript on the page to see if we're stuck
let canRunJS = false; let canRunJS = false;
window.webContents.executeJavaScript('window.stop(); document.close()', true) window.webContents.executeJavaScript("window.stop(); document.close()", true).then(() => (canRunJS = true));
.then(() => canRunJS = true);
setTimeout(() => { setTimeout(() => {
// Wait a few milliseconds to prevent a race condition before loading the exit screen // Wait a few milliseconds to prevent a race condition before loading the exit screen
window.webContents.stop(); window.webContents.stop();
window.loadFile("exit.html") window.loadFile("exit.html");
}, 20); }, 20);
// Wait 200ms, if the promise has not yet resolved, let's crash the process since we're possibly in a stuck scenario // Wait 200ms, if the promise has not yet resolved, let's crash the process since we're possibly in a stuck scenario
setTimeout(() => { setTimeout(() => {
if (!canRunJS) { if (!canRunJS) {
// We're stuck, let's crash the process // 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(); window.webContents.forcefullyCrashRenderer();
} }
log.debug('Destroying the window'); log.debug("Destroying the window");
window.destroy(); window.destroy();
}, 200); }, 200);
} };
const clearWindowHandler = () => { const clearWindowHandler = () => {
window = null; window = null;
}; };
const stopProcessHandler = () => { const stopProcessHandler = () => {
log.info('Quitting the app...'); log.info("Quitting the app...");
app.isQuiting = true; app.isQuiting = true;
app.quit(); app.quit();
process.exit(0); process.exit(0);
@ -121,12 +122,12 @@ function setStopProcessHandler(app, window, enabled) {
const restoreNewest = config.get("onload-restore-newest", true); const restoreNewest = config.get("onload-restore-newest", true);
if (restoreNewest && !isRestoreDisabled) { if (restoreNewest && !isRestoreDisabled) {
try { try {
await storage.restoreIfNewerExists(window) await storage.restoreIfNewerExists(window);
} catch (error) { } catch (error) {
log.error("Could not restore newer file", error); log.error("Could not restore newer file", error);
} }
} }
} };
const receivedDisableRestoreHandler = async (event, arg) => { const receivedDisableRestoreHandler = async (event, arg) => {
if (!window) { if (!window) {
@ -140,7 +141,7 @@ function setStopProcessHandler(app, window, enabled) {
isRestoreDisabled = false; isRestoreDisabled = false;
log.debug("Re-enabling auto-restore"); log.debug("Re-enabling auto-restore");
}, arg.duration); }, arg.duration);
} };
const receivedGameSavedHandler = async (event, arg) => { const receivedGameSavedHandler = async (event, arg) => {
if (!window) { 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)`); log.debug(`Auto-save to cloud disabled for save game under ${minimumPlaytime}ms (${playtime}ms)`);
} }
} }
} };
const saveToCloud = debounce(async (save) => { const saveToCloud = debounce(
log.debug("Saving to Steam Cloud ...") async (save) => {
log.debug("Saving to Steam Cloud ...");
try { try {
const playerId = window.gameInfo.player.identifier; const playerId = window.gameInfo.player.identifier;
await storage.pushGameSaveToSteamCloud(save, playerId); await storage.pushGameSaveToSteamCloud(save, playerId);
@ -176,26 +178,33 @@ function setStopProcessHandler(app, window, enabled) {
log.error(error); log.error(error);
utils.writeToast(window, "Could not save to Steam Cloud.", "error", 5000); 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) => { const saveToDisk = debounce(
log.debug("Saving to Disk ...") async (save, fileName) => {
log.debug("Saving to Disk ...");
try { try {
const file = await storage.saveGameToDisk(window, { save, fileName }); const file = await storage.saveGameToDisk(window, { save, fileName });
log.silly(`Saved Game to '${file.replaceAll('\\', '\\\\')}'`); log.silly(`Saved Game to '${file.replaceAll("\\", "\\\\")}'`);
} catch (error) { } catch (error) {
log.error(error); log.error(error);
utils.writeToast(window, "Could not save to disk", "error", 5000); 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) { if (enabled) {
log.debug("Adding closing handlers"); log.debug("Adding closing handlers");
ipcMain.on("push-game-ready", receivedGameReadyHandler); ipcMain.on("push-game-ready", receivedGameReadyHandler);
ipcMain.on("push-game-saved", receivedGameSavedHandler); ipcMain.on("push-game-saved", receivedGameSavedHandler);
ipcMain.on("push-disable-restore", receivedDisableRestoreHandler) ipcMain.on("push-disable-restore", receivedDisableRestoreHandler);
window.on("closed", clearWindowHandler); window.on("closed", clearWindowHandler);
window.on("close", closingWindowHandler) window.on("close", closingWindowHandler);
app.on("window-all-closed", stopProcessHandler); app.on("window-all-closed", stopProcessHandler);
} else { } else {
log.debug("Removing closing handlers"); log.debug("Removing closing handlers");
@ -213,7 +222,7 @@ async function startWindow(noScript) {
global.app_handlers = { global.app_handlers = {
stopProcess: setStopProcessHandler, stopProcess: setStopProcessHandler,
createWindow: startWindow, createWindow: startWindow,
} };
app.whenReady().then(async () => { app.whenReady().then(async () => {
log.info("Application is ready!"); log.info("Application is ready!");
@ -231,7 +240,8 @@ app.whenReady().then(async () => {
title: "Bitburner", title: "Bitburner",
message: "Could not connect to Steam", 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.`, 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); log.error(error);
utils.writeToast(window, "Could not load last save from disk", "error", 5000); utils.writeToast(window, "Could not load last save from disk", "error", 5000);
} }
} },
}, },
{ {
label: "Load From File", label: "Load From File",
@ -51,9 +51,7 @@ function getMenu(window) {
{ name: "Game Saves", extensions: ["json", "json.gz", "txt"] }, { name: "Game Saves", extensions: ["json", "json.gz", "txt"] },
{ name: "All", extensions: ["*"] }, { name: "All", extensions: ["*"] },
], ],
properties: [ properties: ["openFile", "dontAddToRecent"],
"openFile", "dontAddToRecent",
]
}); });
if (result.canceled) return; if (result.canceled) return;
const file = result.filePaths[0]; const file = result.filePaths[0];
@ -65,7 +63,7 @@ function getMenu(window) {
log.error(error); log.error(error);
utils.writeToast(window, "Could not load save from disk", "error", 5000); utils.writeToast(window, "Could not load save from disk", "error", 5000);
} }
} },
}, },
{ {
label: "Load From Steam Cloud", label: "Load From Steam Cloud",
@ -78,7 +76,7 @@ function getMenu(window) {
log.error(error); log.error(error);
utils.writeToast(window, "Could not load from Steam Cloud", "error", 5000); utils.writeToast(window, "Could not load from Steam Cloud", "error", 5000);
} }
} },
}, },
{ {
type: "separator", type: "separator",
@ -89,8 +87,7 @@ function getMenu(window) {
checked: storage.isSaveCompressionEnabled(), checked: storage.isSaveCompressionEnabled(),
click: (menuItem) => { click: (menuItem) => {
storage.setSaveCompressionConfig(menuItem.checked); storage.setSaveCompressionConfig(menuItem.checked);
utils.writeToast(window, utils.writeToast(window, `${menuItem.checked ? "Enabled" : "Disabled"} Save Compression`, "info", 5000);
`${menuItem.checked ? "Enabled" : "Disabled"} Save Compression`, "info", 5000);
refreshMenu(window); refreshMenu(window);
}, },
}, },
@ -100,8 +97,7 @@ function getMenu(window) {
checked: storage.isAutosaveEnabled(), checked: storage.isAutosaveEnabled(),
click: (menuItem) => { click: (menuItem) => {
storage.setAutosaveConfig(menuItem.checked); storage.setAutosaveConfig(menuItem.checked);
utils.writeToast(window, utils.writeToast(window, `${menuItem.checked ? "Enabled" : "Disabled"} Auto-Save to Disk`, "info", 5000);
`${menuItem.checked ? "Enabled" : "Disabled"} Auto-Save to Disk`, "info", 5000);
refreshMenu(window); refreshMenu(window);
}, },
}, },
@ -112,8 +108,12 @@ function getMenu(window) {
checked: storage.isCloudEnabled(), checked: storage.isCloudEnabled(),
click: (menuItem) => { click: (menuItem) => {
storage.setCloudEnabledConfig(menuItem.checked); storage.setCloudEnabledConfig(menuItem.checked);
utils.writeToast(window, utils.writeToast(
`${menuItem.checked ? "Enabled" : "Disabled"} Auto-Save to Steam Cloud`, "info", 5000); window,
`${menuItem.checked ? "Enabled" : "Disabled"} Auto-Save to Steam Cloud`,
"info",
5000,
);
refreshMenu(window); refreshMenu(window);
}, },
}, },
@ -123,8 +123,12 @@ function getMenu(window) {
checked: config.get("onload-restore-newest", true), checked: config.get("onload-restore-newest", true),
click: (menuItem) => { click: (menuItem) => {
config.set("onload-restore-newest", menuItem.checked); config.set("onload-restore-newest", menuItem.checked);
utils.writeToast(window, utils.writeToast(
`${menuItem.checked ? "Enabled" : "Disabled"} Restore Newest on Load`, "info", 5000); window,
`${menuItem.checked ? "Enabled" : "Disabled"} Restore Newest on Load`,
"info",
5000,
);
refreshMenu(window); refreshMenu(window);
}, },
}, },
@ -153,7 +157,7 @@ function getMenu(window) {
label: "Open Data Directory", label: "Open Data Directory",
click: () => shell.openPath(app.getPath("userData")), click: () => shell.openPath(app.getPath("userData")),
}, },
] ],
}, },
{ {
type: "separator", type: "separator",
@ -162,7 +166,7 @@ function getMenu(window) {
label: "Quit", label: "Quit",
click: () => app.quit(), click: () => app.quit(),
}, },
] ],
}, },
{ {
label: "Edit", label: "Edit",
@ -210,29 +214,29 @@ function getMenu(window) {
label: "API Server", label: "API Server",
submenu: [ submenu: [
{ {
label: api.isListening() ? 'Disable Server' : 'Enable Server', label: api.isListening() ? "Disable Server" : "Enable Server",
click: (async () => { click: async () => {
let success = false; let success = false;
try { try {
await api.toggleServer(); await api.toggleServer();
success = true; success = true;
} catch (error) { } catch (error) {
log.error(error); log.error(error);
utils.showErrorBox('Error Toggling Server', error); utils.showErrorBox("Error Toggling Server", error);
} }
if (success && api.isListening()) { if (success && api.isListening()) {
utils.writeToast(window, "Started API Server", "success"); utils.writeToast(window, "Started API Server", "success");
} else if (success && !api.isListening()) { } else if (success && !api.isListening()) {
utils.writeToast(window, "Stopped API Server", "success"); utils.writeToast(window, "Stopped API Server", "success");
} else { } else {
utils.writeToast(window, 'Error Toggling Server', "error"); utils.writeToast(window, "Error Toggling Server", "error");
} }
refreshMenu(window); refreshMenu(window);
}) },
}, },
{ {
label: api.isAutostart() ? 'Disable Autostart' : 'Enable Autostart', label: api.isAutostart() ? "Disable Autostart" : "Enable Autostart",
click: (async () => { click: async () => {
api.toggleAutostart(); api.toggleAutostart();
if (api.isAutostart()) { if (api.isAutostart()) {
utils.writeToast(window, "Enabled API Server Autostart", "success"); utils.writeToast(window, "Enabled API Server Autostart", "success");
@ -240,42 +244,45 @@ function getMenu(window) {
utils.writeToast(window, "Disabled API Server Autostart", "success"); utils.writeToast(window, "Disabled API Server Autostart", "success");
} }
refreshMenu(window); refreshMenu(window);
}) },
}, },
{ {
label: 'Copy Auth Token', label: "Copy Auth Token",
click: (async () => { click: async () => {
const token = api.getAuthenticationToken(); const token = api.getAuthenticationToken();
log.log('Wrote authentication token to clipboard'); log.log("Wrote authentication token to clipboard");
clipboard.writeText(token); clipboard.writeText(token);
utils.writeToast(window, "Copied Authentication Token to Clipboard", "info"); utils.writeToast(window, "Copied Authentication Token to Clipboard", "info");
}) },
}, },
{ {
type: 'separator', type: "separator",
}, },
{ {
label: 'Information', label: "Information",
click: () => { click: () => {
dialog.showMessageBox({ dialog
type: 'info', .showMessageBox({
title: 'Bitburner > API Server Information', type: "info",
message: 'The API Server is used to write script files to your in-game home.', title: "Bitburner > API Server Information",
detail: 'There is an official Visual Studio Code extension that makes use of that feature.\n\n' + message: "The API Server is used to write script files to your in-game home.",
'It allows you to write your script file in an external IDE and have them pushed over to the game automatically.\n' + detail:
'If you want more information, head over to: https://github.com/bitburner-official/bitburner-vscode.', "There is an official Visual Studio Code extension that makes use of that feature.\n\n" +
buttons: ['Dismiss', 'Open Extension Link (GitHub)'], "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, defaultId: 0,
cancelId: 0, cancelId: 0,
noLink: true, noLink: true,
}).then(({response}) => { })
.then(({ response }) => {
if (response === 1) { if (response === 1) {
utils.openExternal('https://github.com/bitburner-official/bitburner-vscode'); utils.openExternal("https://github.com/bitburner-official/bitburner-vscode");
} }
}); });
} },
} },
] ],
}, },
{ {
label: "Zoom", label: "Zoom",
@ -291,7 +298,7 @@ function getMenu(window) {
utils.setZoomFactor(window, newZoom); utils.setZoomFactor(window, newZoom);
refreshMenu(window); refreshMenu(window);
} else { } else {
log.log('Max zoom out') log.log("Max zoom out");
utils.writeToast(window, "Cannot zoom in anymore", "warning"); utils.writeToast(window, "Cannot zoom in anymore", "warning");
} }
}, },
@ -307,7 +314,7 @@ function getMenu(window) {
utils.setZoomFactor(window, newZoom); utils.setZoomFactor(window, newZoom);
refreshMenu(window); refreshMenu(window);
} else { } else {
log.log('Max zoom in') log.log("Max zoom in");
utils.writeToast(window, "Cannot zoom out anymore", "warning"); utils.writeToast(window, "Cannot zoom out anymore", "warning");
} }
}, },
@ -340,8 +347,8 @@ function getMenu(window) {
} catch (error) { } catch (error) {
log.error(error); log.error(error);
} }
} },
} },
], ],
}, },
]); ]);
@ -352,5 +359,6 @@ function refreshMenu(window) {
} }
module.exports = { module.exports = {
getMenu, refreshMenu, getMenu,
} refreshMenu,
};

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

@ -16,9 +16,9 @@ const config = new Config();
// https://stackoverflow.com/a/69418940 // https://stackoverflow.com/a/69418940
const dirSize = async (directory) => { const dirSize = async (directory) => {
const files = await fs.readdir(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); return (await Promise.all(stats)).reduce((accumulator, { size }) => accumulator + size, 0);
} };
const getDirFileStats = async (directory) => { const getDirFileStats = async (directory) => {
const files = await fs.readdir(directory); const files = await fs.readdir(directory);
@ -26,30 +26,31 @@ const getDirFileStats = async (directory) => {
const file = path.join(directory, f); const file = path.join(directory, f);
return fs.stat(file).then((stat) => ({ file, stat })); return fs.stat(file).then((stat) => ({ file, stat }));
}); });
const data = (await Promise.all(stats)); const data = await Promise.all(stats);
return data; return data;
}; };
const getNewestFile = async (directory) => { 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]; return data.sort((a, b) => b.stat.mtime.getTime() - a.stat.mtime.getTime())[0];
}; };
const getAllSaves = async (window) => { const getAllSaves = async (window) => {
const rootDirectory = await getSaveFolder(window, true); const rootDirectory = await getSaveFolder(window, true);
const data = await fs.readdir(rootDirectory, { withFileTypes: true }); const data = await fs.readdir(rootDirectory, { withFileTypes: true });
const savesPromises = data.filter((e) => e.isDirectory()). const savesPromises = data
map((dir) => path.join(rootDirectory, dir.name)). .filter((e) => e.isDirectory())
map((dir) => getDirFileStats(dir)); .map((dir) => path.join(rootDirectory, dir.name))
.map((dir) => getDirFileStats(dir));
const saves = await Promise.all(savesPromises); const saves = await Promise.all(savesPromises);
const flat = flatten(saves); const flat = flatten(saves);
return flat; return flat;
} };
async function prepareSaveFolders(window) { async function prepareSaveFolders(window) {
const rootFolder = await getSaveFolder(window, true); const rootFolder = await getSaveFolder(window, true);
const currentFolder = await getSaveFolder(window); const currentFolder = await getSaveFolder(window);
const backupsFolder = path.join(rootFolder, "/_backups") const backupsFolder = path.join(rootFolder, "/_backups");
await prepareFolders(rootFolder, currentFolder, backupsFolder); await prepareFolders(rootFolder, currentFolder, backupsFolder);
} }
@ -60,7 +61,7 @@ async function prepareFolders(...folders) {
// eslint-disable-next-line no-await-in-loop // eslint-disable-next-line no-await-in-loop
await fs.stat(folder); await fs.stat(folder);
} catch (error) { } catch (error) {
if (error.code === 'ENOENT') { if (error.code === "ENOENT") {
log.warn(`'${folder}' not found, creating it...`); log.warn(`'${folder}' not found, creating it...`);
// eslint-disable-next-line no-await-in-loop // eslint-disable-next-line no-await-in-loop
await fs.mkdir(folder); await fs.mkdir(folder);
@ -125,14 +126,14 @@ function isCloudEnabled() {
function saveCloudFile(name, content) { function saveCloudFile(name, content) {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
greenworks.saveTextToFile(name, content, resolve, reject); greenworks.saveTextToFile(name, content, resolve, reject);
}) });
} }
function getFirstCloudFile() { function getFirstCloudFile() {
const nbFiles = greenworks.getFileCount(); 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); const file = greenworks.getFileNameAndSize(0);
log.silly(`Found ${nbFiles} files.`) log.silly(`Found ${nbFiles} files.`);
log.silly(`First File: ${file.name} (${file.size} bytes)`); log.silly(`First File: ${file.name} (${file.size} bytes)`);
return file.name; return file.name;
} }
@ -153,7 +154,7 @@ function deleteCloudFile() {
async function getSteamCloudQuota() { async function getSteamCloudQuota() {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
greenworks.getCloudQuota(resolve, reject) greenworks.getCloudQuota(resolve, reject);
}); });
} }
@ -166,9 +167,9 @@ async function backupSteamDataToDisk(currentPlayerId) {
if (previousPlayerId !== currentPlayerId) { if (previousPlayerId !== currentPlayerId) {
const backupSave = await getSteamCloudSaveString(); const backupSave = await getSteamCloudSaveString();
const backupFile = path.join(app.getPath("userData"), "/saves/_backups", `${previousPlayerId}.json.gz`); 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); saveContent = await gzip(buffer);
await fs.writeFile(backupFile, saveContent, 'utf8'); await fs.writeFile(backupFile, saveContent, "utf8");
log.debug(`Saved backup game to '${backupFile}`); log.debug(`Saved backup game to '${backupFile}`);
} }
} }
@ -219,7 +220,9 @@ async function saveGameToDisk(window, saveData) {
const remainingSpaceBytes = maxFolderSizeBytes - saveFolderSizeBytes; const remainingSpaceBytes = maxFolderSizeBytes - saveFolderSizeBytes;
log.debug(`Folder Usage: ${saveFolderSizeBytes} bytes`); log.debug(`Folder Usage: ${saveFolderSizeBytes} bytes`);
log.debug(`Folder Capacity: ${maxFolderSizeBytes} 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 shouldCompress = isSaveCompressionEnabled();
const fileName = saveData.fileName; const fileName = saveData.fileName;
const file = path.join(currentFolder, fileName + (shouldCompress ? ".gz" : "")); const file = path.join(currentFolder, fileName + (shouldCompress ? ".gz" : ""));
@ -227,10 +230,10 @@ async function saveGameToDisk(window, saveData) {
let saveContent = saveData.save; let saveContent = saveData.save;
if (shouldCompress) { if (shouldCompress) {
// Let's decode the base64 string so GZIP is more efficient. // 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); saveContent = await gzip(buffer);
} }
await fs.writeFile(file, saveContent, 'utf8'); await fs.writeFile(file, saveContent, "utf8");
log.debug(`Saved Game to '${file}'`); log.debug(`Saved Game to '${file}'`);
log.debug(`Save Size: ${saveContent.length} bytes`); log.debug(`Save Size: ${saveContent.length} bytes`);
} catch (error) { } catch (error) {
@ -240,7 +243,8 @@ async function saveGameToDisk(window, saveData) {
const fileStats = await getDirFileStats(currentFolder); const fileStats = await getDirFileStats(currentFolder);
const oldestFiles = fileStats const oldestFiles = fileStats
.sort((a, b) => a.stat.mtime.getTime() - b.stat.mtime.getTime()) .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) { while (saveFolderSizeBytes > maxFolderSizeBytes && oldestFiles.length > 0) {
const fileToRemove = oldestFiles.shift(); const fileToRemove = oldestFiles.shift();
@ -255,7 +259,12 @@ async function saveGameToDisk(window, saveData) {
// eslint-disable-next-line no-await-in-loop // eslint-disable-next-line no-await-in-loop
saveFolderSizeBytes = await getFolderSizeInBytes(currentFolder); saveFolderSizeBytes = await getFolderSizeInBytes(currentFolder);
log.debug(`Save Folder: ${saveFolderSizeBytes} bytes`); 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; return file;
@ -271,13 +280,13 @@ async function loadLastFromDisk(window) {
async function loadFileFromDisk(path) { async function loadFileFromDisk(path) {
const buffer = await fs.readFile(path); const buffer = await fs.readFile(path);
let content; let content;
if (path.endsWith('.gz')) { if (path.endsWith(".gz")) {
const uncompressedBuffer = await gunzip(buffer); const uncompressedBuffer = await gunzip(buffer);
content = uncompressedBuffer.toString('base64'); content = uncompressedBuffer.toString("base64");
log.debug(`Uncompressed file content (new size: ${content.length} bytes)`); log.debug(`Uncompressed file content (new size: ${content.length} bytes)`);
} else { } else {
content = buffer.toString('utf8'); content = buffer.toString("utf8");
log.debug(`Loaded file with ${content.length} bytes`) log.debug(`Loaded file with ${content.length} bytes`);
} }
return content; return content;
} }
@ -293,10 +302,10 @@ function getSaveInformation(window, save) {
function getCurrentSave(window) { function getCurrentSave(window) {
return new Promise((resolve) => { return new Promise((resolve) => {
ipcMain.once('get-save-data-response', (event, data) => { ipcMain.once("get-save-data-response", (event, data) => {
resolve(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 { try {
const saves = (await getAllSaves()). const saves = (await getAllSaves()).sort((a, b) => b.stat.mtime.getTime() - a.stat.mtime.getTime());
sort((a, b) => b.stat.mtime.getTime() - a.stat.mtime.getTime());
if (saves.length > 0) { if (saves.length > 0) {
disk.save = await loadFileFromDisk(saves[0].file); disk.save = await loadFileFromDisk(saves[0].file);
disk.data = await getSaveInformation(window, disk.save); disk.data = await getSaveInformation(window, disk.save);
@ -339,18 +347,17 @@ async function restoreIfNewerExists(window) {
log.info("No data to import"); log.info("No data to import");
} else if (!steam.data) { } else if (!steam.data) {
// We'll just compare using the lastSave field for now. // 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; bestMatch = disk;
} else if (!disk.data) { } else if (!disk.data) {
log.debug('Best potential save match: Steam Cloud'); log.debug("Best potential save match: Steam Cloud");
bestMatch = steam; bestMatch = steam;
} else if ((steam.data.lastSave >= disk.data.lastSave) } else if (steam.data.lastSave >= disk.data.lastSave || steam.data.playtime + lowPlaytime > disk.data.playtime) {
|| (steam.data.playtime + lowPlaytime > disk.data.playtime)) {
// We want to prioritze steam data if the playtime is very close // 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; bestMatch = steam;
} else { } else {
log.debug('Best potential save match: disk'); log.debug("Best potential save match: disk");
bestMatch = disk; bestMatch = disk;
} }
if (bestMatch) { if (bestMatch) {
@ -373,12 +380,24 @@ async function restoreIfNewerExists(window) {
} }
module.exports = { module.exports = {
getCurrentSave, getSaveInformation, getCurrentSave,
restoreIfNewerExists, pushSaveGameForImport, getSaveInformation,
pushGameSaveToSteamCloud, getSteamCloudSaveString, getSteamCloudQuota, deleteCloudFile, restoreIfNewerExists,
saveGameToDisk, loadLastFromDisk, loadFileFromDisk, pushSaveGameForImport,
getSaveFolder, prepareSaveFolders, getAllSaves, pushGameSaveToSteamCloud,
isCloudEnabled, setCloudEnabledConfig, getSteamCloudSaveString,
isAutosaveEnabled, setAutosaveConfig, getSteamCloudQuota,
isSaveCompressionEnabled, setSaveCompressionConfig, 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(); const config = new Config();
function reloadAndKill(window, killScripts) { function reloadAndKill(window, killScripts) {
const setStopProcessHandler = global.app_handlers.stopProcess const setStopProcessHandler = global.app_handlers.stopProcess;
const createWindowHandler = global.app_handlers.createWindow; const createWindowHandler = global.app_handlers.createWindow;
log.info('Reloading & Killing all scripts...'); log.info("Reloading & Killing all scripts...");
setStopProcessHandler(app, window, false); setStopProcessHandler(app, window, false);
achievements.disableAchievementsInterval(window); achievements.disableAchievementsInterval(window);
api.disable(); api.disable();
window.webContents.forcefullyCrashRenderer(); window.webContents.forcefullyCrashRenderer();
window.on('closed', () => { window.on("closed", () => {
// Wait for window to be closed before opening the new one to prevent race conditions // 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); createWindowHandler(killScripts);
}) });
window.close(); window.close();
} }
function promptForReload(window) { function promptForReload(window) {
detachUnresponsiveAppHandler(window); detachUnresponsiveAppHandler(window);
dialog.showMessageBox({ dialog
type: 'error', .showMessageBox({
title: 'Bitburner > Application Unresponsive', type: "error",
message: 'The application is unresponsive, possibly due to an infinite loop in your scripts.', title: "Bitburner > Application Unresponsive",
detail:' Did you forget a ns.sleep(x)?\n\n' + message: "The application is unresponsive, possibly due to an infinite loop in your scripts.",
'The application will be restarted for you, do you want to kill all running scripts?', detail:
buttons: ['Restart', 'Cancel'], " 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, defaultId: 0,
checkboxLabel: 'Kill all running scripts', checkboxLabel: "Kill all running scripts",
checkboxChecked: true, checkboxChecked: true,
noLink: true, noLink: true,
}).then(({response, checkboxChecked}) => { })
.then(({ response, checkboxChecked }) => {
if (response === 0) { if (response === 0) {
reloadAndKill(window, checkboxChecked); reloadAndKill(window, checkboxChecked);
} else { } else {
@ -52,18 +55,15 @@ function promptForReload(window) {
function attachUnresponsiveAppHandler(window) { function attachUnresponsiveAppHandler(window) {
window.unresponsiveHandler = () => promptForReload(window); window.unresponsiveHandler = () => promptForReload(window);
window.on('unresponsive', window.unresponsiveHandler); window.on("unresponsive", window.unresponsiveHandler);
} }
function detachUnresponsiveAppHandler(window) { function detachUnresponsiveAppHandler(window) {
window.off('unresponsive', window.unresponsiveHandler); window.off("unresponsive", window.unresponsiveHandler);
} }
function showErrorBox(title, error) { function showErrorBox(title, error) {
dialog.showErrorBox( dialog.showErrorBox(title, `${error.name}\n\n${error.message}`);
title,
`${error.name}\n\n${error.message}`
);
} }
function exportSaveFromIndexedDb() { function exportSaveFromIndexedDb() {
@ -71,15 +71,15 @@ function exportSaveFromIndexedDb() {
const dbRequest = indexedDB.open("bitburnerSave"); const dbRequest = indexedDB.open("bitburnerSave");
dbRequest.onsuccess = () => { dbRequest.onsuccess = () => {
const db = dbRequest.result; const db = dbRequest.result;
const transaction = db.transaction(['savestring'], "readonly"); const transaction = db.transaction(["savestring"], "readonly");
const store = transaction.objectStore('savestring'); const store = transaction.objectStore("savestring");
const request = store.get('save'); const request = store.get("save");
request.onsuccess = () => { 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 a = document.createElement("a");
const url = URL.createObjectURL(file); const url = URL.createObjectURL(file);
a.href = url; a.href = url;
a.download = 'save.json'; a.download = "save.json";
document.body.appendChild(a); document.body.appendChild(a);
a.click(); a.click();
setTimeout(function () { setTimeout(function () {
@ -87,24 +87,21 @@ function exportSaveFromIndexedDb() {
window.URL.revokeObjectURL(url); window.URL.revokeObjectURL(url);
resolve(); resolve();
}, 0); }, 0);
} };
} };
}); });
} }
async function exportSave(window) { async function exportSave(window) {
await window.webContents await window.webContents.executeJavaScript(`${exportSaveFromIndexedDb.toString()}; exportSaveFromIndexedDb();`, true);
.executeJavaScript(`${exportSaveFromIndexedDb.toString()}; exportSaveFromIndexedDb();`, true);
} }
async function writeTerminal(window, message, type = null) { async function writeTerminal(window, message, type = null) {
await window.webContents await window.webContents.executeJavaScript(`window.appNotifier.terminal("${message}", "${type}");`, true);
.executeJavaScript(`window.appNotifier.terminal("${message}", "${type}");`, true)
} }
async function writeToast(window, message, type = "info", duration = 2000) { async function writeToast(window, message, type = "info", duration = 2000) {
await window.webContents await window.webContents.executeJavaScript(`window.appNotifier.toast("${message}", "${type}", ${duration});`, true);
.executeJavaScript(`window.appNotifier.toast("${message}", "${type}", ${duration});`, true)
} }
function openExternal(url) { function openExternal(url) {
@ -113,7 +110,7 @@ function openExternal(url) {
} }
function getZoomFactor() { function getZoomFactor() {
const configZoom = config.get('zoom', 1); const configZoom = config.get("zoom", 1);
return configZoom; return configZoom;
} }
@ -121,14 +118,20 @@ function setZoomFactor(window, zoom = null) {
if (zoom === null) { if (zoom === null) {
zoom = 1; zoom = 1;
} else { } else {
config.set('zoom', zoom); config.set("zoom", zoom);
} }
window.webContents.setZoomFactor(zoom); window.webContents.setZoomFactor(zoom);
} }
module.exports = { module.exports = {
reloadAndKill, showErrorBox, exportSave, reloadAndKill,
attachUnresponsiveAppHandler, detachUnresponsiveAppHandler, showErrorBox,
openExternal, writeTerminal, writeToast, exportSave,
getZoomFactor, setZoomFactor, attachUnresponsiveAppHandler,
} detachUnresponsiveAppHandler,
openExternal,
writeTerminal,
writeToast,
getZoomFactor,
setZoomFactor,
};

@ -4,12 +4,11 @@ module.exports = {
transform: { transform: {
"^.+\\.(js|jsx|ts|tsx)$": "babel-jest", "^.+\\.(js|jsx|ts|tsx)$": "babel-jest",
}, },
testPathIgnorePatterns: [ testPathIgnorePatterns: [".cypress", "node_modules", "dist"],
'.cypress', 'node_modules', 'dist',
],
testEnvironment: "jsdom", testEnvironment: "jsdom",
moduleNameMapper: { moduleNameMapper: {
"\\.(jpg|jpeg|png|gif|eot|otf|webp|svg|ttf|woff|woff2|mp4|webm|wav|mp3|m4a|aac|oga)$": "<rootDir>/test/__mocks__/fileMock.js", "\\.(jpg|jpeg|png|gif|eot|otf|webp|svg|ttf|woff|woff2|mp4|webm|wav|mp3|m4a|aac|oga)$":
"\\.(css|less)$": "<rootDir>/test/__mocks__/styleMock.js" "<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 { Box, Typography } from "@mui/material";
import { Achievement } from "./Achievements"; import { Achievement } from "./Achievements";
import { Settings } from "../Settings/Settings" import { Settings } from "../Settings/Settings";
import { AchievementIcon } from "./AchievementIcon"; import { AchievementIcon } from "./AchievementIcon";
interface IProps { interface IProps {
@ -13,43 +13,58 @@ interface IProps {
cssFiltersLocked: string; 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 <></>; if (!achievement) return <></>;
const isUnlocked = !!unlockedOn; const isUnlocked = !!unlockedOn;
const mainColor = isUnlocked ? Settings.theme.primary : Settings.theme.secondarylight; const mainColor = isUnlocked ? Settings.theme.primary : Settings.theme.secondarylight;
let achievedOn = ''; let achievedOn = "";
if (unlockedOn) { if (unlockedOn) {
achievedOn = new Date(unlockedOn).toLocaleString(); achievedOn = new Date(unlockedOn).toLocaleString();
} }
return ( return (
<Box sx={{ <Box
border: `1px solid ${Settings.theme.well}`, mb: 2 sx={{
}}> border: `1px solid ${Settings.theme.well}`,
<Box sx={{ mb: 2,
display: 'flex', }}
flexDirection: 'row', >
flexWrap: 'wrap', <Box
}}> sx={{
display: "flex",
flexDirection: "row",
flexWrap: "wrap",
}}
>
<AchievementIcon <AchievementIcon
achievement={achievement} unlocked={isUnlocked} size="72px" achievement={achievement}
colorFilters={isUnlocked ? cssFiltersUnlocked: cssFiltersLocked} /> unlocked={isUnlocked}
<Box sx={{ size="72px"
display: 'flex', colorFilters={isUnlocked ? cssFiltersUnlocked : cssFiltersLocked}
flexDirection: 'column', />
justifyContent: 'center', <Box
px: 1 sx={{
}}> display: "flex",
flexDirection: "column",
justifyContent: "center",
px: 1,
}}
>
<Typography variant="h6" sx={{ color: mainColor }}> <Typography variant="h6" sx={{ color: mainColor }}>
{achievement.Name} {achievement.Name}
</Typography> </Typography>
<Typography variant="body2" sx={{ maxWidth: '500px', color: mainColor}}> <Typography variant="body2" sx={{ maxWidth: "500px", color: mainColor }}>
{achievement.Description} {achievement.Description}
</Typography> </Typography>
{isUnlocked && ( {isUnlocked && (
<Typography variant="caption" sx={{ fontSize: '12px', color: Settings.theme.primarydark }}> <Typography variant="caption" sx={{ fontSize: "12px", color: Settings.theme.primarydark }}>
Acquired on {achievedOn} Acquired on {achievedOn}
</Typography> </Typography>
)} )}

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

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

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

@ -94,7 +94,7 @@ function getRandomBonus(): any {
return bonuses[Math.floor(bonuses.length * randomNumber.random())]; return bonuses[Math.floor(bonuses.length * randomNumber.random())];
} }
export const generalAugmentations = [ export const initGeneralAugmentations = (): Augmentation[] => [
new Augmentation({ new Augmentation({
name: AugmentationNames.HemoRecirculator, name: AugmentationNames.HemoRecirculator,
moneyCost: 4.5e7, moneyCost: 4.5e7,
@ -1486,7 +1486,7 @@ export const generalAugmentations = [
}), }),
]; ];
export const bladeburnerAugmentations = [ export const initBladeburnerAugmentations = (): Augmentation[] => [
new Augmentation({ new Augmentation({
name: AugmentationNames.EsperEyewear, name: AugmentationNames.EsperEyewear,
repCost: 1.25e3, repCost: 1.25e3,
@ -1751,7 +1751,7 @@ export const bladeburnerAugmentations = [
}), }),
]; ];
export const churchOfTheMachineGodAugmentations = [ export const initChurchOfTheMachineGodAugmentations = (): Augmentation[] => [
new Augmentation({ new Augmentation({
name: AugmentationNames.StaneksGift1, name: AugmentationNames.StaneksGift1,
repCost: 0, repCost: 0,

@ -15,9 +15,9 @@ import { clearObject } from "../utils/helpers/clearObject";
import { FactionNames } from "../Faction/data/FactionNames"; import { FactionNames } from "../Faction/data/FactionNames";
import { import {
bladeburnerAugmentations, initBladeburnerAugmentations,
churchOfTheMachineGodAugmentations, initChurchOfTheMachineGodAugmentations,
generalAugmentations, initGeneralAugmentations,
initNeuroFluxGovernor, initNeuroFluxGovernor,
initUnstableCircadianModulator, initUnstableCircadianModulator,
} from "./AugmentationCreator"; } from "./AugmentationCreator";
@ -49,9 +49,9 @@ function createAugmentations(): void {
[ [
initNeuroFluxGovernor(), initNeuroFluxGovernor(),
initUnstableCircadianModulator(), initUnstableCircadianModulator(),
...generalAugmentations, ...initGeneralAugmentations(),
...(factionExists(FactionNames.Bladeburners) ? bladeburnerAugmentations : []), ...(factionExists(FactionNames.Bladeburners) ? initBladeburnerAugmentations() : []),
...(factionExists(FactionNames.ChurchOfTheMachineGod) ? churchOfTheMachineGodAugmentations : []), ...(factionExists(FactionNames.ChurchOfTheMachineGod) ? initChurchOfTheMachineGodAugmentations() : []),
].map(resetAugmentation); ].map(resetAugmentation);
} }
@ -97,7 +97,10 @@ export function updateAugmentationCosts(): void {
if (augmentationToUpdate.name === AugmentationNames.NeuroFluxGovernor) { if (augmentationToUpdate.name === AugmentationNames.NeuroFluxGovernor) {
updateNeuroFluxGovernorCosts(augmentationToUpdate); updateNeuroFluxGovernorCosts(augmentationToUpdate);
} else { } else {
augmentationToUpdate.baseCost = augmentationToUpdate.startingCost * getGenericAugmentationPriceMultiplier(); augmentationToUpdate.baseCost =
augmentationToUpdate.startingCost *
getGenericAugmentationPriceMultiplier() *
BitNodeMultipliers.AugmentationMoneyCost;
} }
} }
} }

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

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

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

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

@ -18,7 +18,10 @@ export function BlackOpPage(props: IProps): React.ReactElement {
successively by completing the one before it. successively by completing the one before it.
<br /> <br />
<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 />
<br /> <br />
Like normal operations, you may use a team for Black Ops. Failing a black op will incur heavy HP and rank 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 ( return (
<Paper sx={{ p: 1, overflowY: 'auto', overflowX: 'hidden', wordBreak: 'break-all' }}> <Paper sx={{ p: 1, overflowY: "auto", overflowX: "hidden", wordBreak: "break-all" }}>
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 1, maxHeight: '60vh' }}> <Box sx={{ display: "flex", flexDirection: "column", gap: 1, maxHeight: "60vh" }}>
<Box sx={{ alignSelf: 'flex-start', width: '100%' }}> <Box sx={{ alignSelf: "flex-start", width: "100%" }}>
<Button onClick={() => setTravelOpen(true)} sx={{ width: '50%' }}>Travel</Button> <Button onClick={() => setTravelOpen(true)} sx={{ width: "50%" }}>
Travel
</Button>
<Tooltip title={!inFaction ? <Typography>Rank 25 required.</Typography> : ""}> <Tooltip title={!inFaction ? <Typography>Rank 25 required.</Typography> : ""}>
<span> <span>
<Button disabled={!inFaction} onClick={openFaction} sx={{ width: '50%' }}> <Button disabled={!inFaction} onClick={openFaction} sx={{ width: "50%" }}>
Faction Faction
</Button> </Button>
</span> </span>
@ -110,8 +112,8 @@ export function Stats(props: IProps): React.ReactElement {
<Tooltip <Tooltip
title={ title={
<Typography> <Typography>
This is your Bladeburner division's estimate of how many Synthoids exist in your current city. An accurate This is your Bladeburner division's estimate of how many Synthoids exist in your current city. An
population count increases success rate estimates. accurate population count increases success rate estimates.
</Typography> </Typography>
} }
> >

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

@ -116,8 +116,8 @@ export const CONSTANTS: {
TotalNumBitNodes: number; TotalNumBitNodes: number;
LatestUpdate: string; LatestUpdate: string;
} = { } = {
VersionString: "1.6.0", VersionString: "1.6.3",
VersionNumber: 12, VersionNumber: 13,
// Speed (in ms) at which the main loop is updated // Speed (in ms) at which the main loop is updated
_idleSpeed: 200, _idleSpeed: 200,
@ -287,105 +287,19 @@ export const CONSTANTS: {
TotalNumBitNodes: 24, TotalNumBitNodes: 24,
LatestUpdate: ` LatestUpdate: `
v1.6.0 - 2022-03-29 Grafting v1.6.3 - 2022-04-01 Few stanek fixes
---------------------------- ----------------------------
** Vitalife secret lab ** Stanek Gift
* A new mechanic called Augmentation Grafting has been added. Resleeving has been removed. * Has a minimum size of 2x3
* Credit to @nickofolas for his incredible work. * Active Fragment property 'avgCharge' renamed to 'highestCharge'
* Formula for fragment effect updated to make 561% more sense.
Now you can charge to your heart content.
* Logs for the 'chargeFragment' function updated.
** Stanek ** Misc.
* BREAKING: Many functions in the stanek API were renamed in order to avoid name collision with things like Map.prototype.get
** UI **
* Major update to Sleeve, Gang UI, and Create Program (@nickofolas)
* re-add pre tags to support slash n in prompt (@jacktose)
* Tabelize linked output of 'ls' (@Master-Guy)
* Add the ability to filter open scripts (@phyzical)
* Add minHeight to editor tabs (@nickofolas)
* Properly expand gang equipment cards to fill entire screen (@nickofolas)
* Add shortcut to Faction augmentations page from FactionsRoot (@nickofolas)
* Fix extra space on editor tabs (@nickofolas)
* Present offline message as list (@DSteve595)
* add box showing remaining augments per faction (@jjayeon)
* Add tab switching support to vim mode (@JParisFerrer)
* Show current task on gang management screen (@zeddrak)
* Fix for ui of gang members current task when set via api (@phyzical)
* Don't hide irrelevant materials if their stock is not empty and hide irrelevant divisions from Export (@SagePtr)
* Fix regex to enable alpha transparency hex codes (8 digits) (@surdaft)
** API **
* Added dark web functions to ns api
* BREAKING: purchaseTor() should returns true if player already has Tor. (@DavidGrinberg, @waffleattack)
* Implement getBonusTime in Corporation API (@t-wolfeadam)
* Added functions to purchase TIX and WSI (@incubusnb)
* purchaseSleeveAug checks shock value (@incubusnb)
* Fix bug with hacknet api
* Fix spendHashes bug
* Added 0 cost of asleep() (@Master-Guy)
* Fix some misleading corporation errors (@TheRealMaxion)
* expose the inBladeburner on the player object (@phyzical)
* added ram charge for stanek width and height (@phyzical)
* Fix sufficient player money check to buy back shares. (@ChrissiQ)
* Fix Static Ram Circumventing for some NS functions (@CrafterKolyan)
* added CorporationSoftCap to NetscriptDefinitions (@phyzical)
* Added definition of autocomplete() 'data' argument. (@tigercat2000)
* Adding support for text/select options in Prompt command (@PhilipArmstead)
* Added the ability to exportGame via api (@phyzical)
** Arcade **
* Added an arcade to New Tokyo where you can play a 4 year old version of bitburner.
** Misc. **
* Add a warning triggered while auto-saves are off. (@MartinFournier)
* Log info for field analysis now displays actual rank gained. (@ApamNapat)
* Removed BladeburnerSkillCost from skill point cost description. (@ApamNapat)
* Fix handling for UpArrow in bladeburner console. (@dowinter)
* Add GitHub action to check PRs for generated files. (@MartinFournier)
* Cap Staneks gift at 25x25 to prevent crashes. (@waffleattack)
* Remove old & unused files from repository. (@MartinFournier)
* Factions on the factions screens are sorted by story progress / type. (@phyzical)
* Fix log manager not picking up new runs of scripts. (@phyzical)
* Added prettier to cicd.
* UI improvements (@phyzical)
* Documentation / Typos (@nanogyth, @Master-Guy, @incubusnb, @ApamNapat, @phyzical, @SagePtr)
* Give player code a copy of Division.upgrades instead of the live object (@Ornedan)
* Fix bug with small town achievement.
* Fix bug with purchaseSleeveAug (@phyzical)
* Check before unlocking corp upgrade (@gianfun)
* General codebase improvements. (@phyzical, @Master-Guy, @ApamNapat)
* Waiting on promises in NS1 no longer freezes the script. (@Master-Guy)
* Fix bug with missing ramcost for tFormat (@TheMas3212)
* Fix crash with new prompt
* Quick fix to prevent division by 0 in terminal (@Master-Guy)
* removed ip references (@phyzical, @Master-Guy)
* Terminal now supports 'ls -l'
* Fix negative number formatting (@Master-Guy)
* Fix unique ip generation (@InDieTasten)
* remove terminal command theme from docs (@phyzical)
* Fix 'Augmentations Left' with gang factions (@nickofolas)
* Attempt to fix 'bladeburner.process()' early routing issue (@MartinFournier)
* work in progress augment fix (@phyzical)
* Fixes missing space in Smart Supply (@TheRealMaxion)
* Change license to Apache 2 with Commons Clause
* updated regex sanitization (@mbrannen)
* Sleeve fix for when faction isnt found (@phyzical)
* Fix editor "close" naming (@phyzical)
* Fix bug with sleeves where some factions would be listed as workable. (@phyzical)
* Fix research tree of product industries post-prestige (@pd)
* Added a check for exisiting industry type before expanding (@phyzical)
* fix hackAnalyzeThreads returning infinity (@chrisrabe)
* Make growthAnalyze more accurate (@dwRchyngqxs)
* Add 'Zoom -> Reset Zoom' command to Steam (@smolgumball)
* Add hasOwnProperty check to GetServer (@SagePtr)
* Speed up employee productivity calculation (@pd)
* Field Work and Security Work benefit from 'share' (@SagePtr)
* Nerf noodle bar. * Nerf noodle bar.
`, `,
}; };

@ -17,7 +17,6 @@ import { Employee } from "./Employee";
import { IndustryUpgrades } from "./IndustryUpgrades"; import { IndustryUpgrades } from "./IndustryUpgrades";
import { ResearchMap } from "./ResearchMap"; import { ResearchMap } from "./ResearchMap";
import { isRelevantMaterial } from "./ui/Helpers"; import { isRelevantMaterial } from "./ui/Helpers";
import { CityName } from "src/Locations/data/CityNames";
export function NewIndustry(corporation: ICorporation, industry: string, name: string): void { export function NewIndustry(corporation: ICorporation, industry: string, name: string): void {
if (corporation.divisions.find(({ type }) => industry == type)) if (corporation.divisions.find(({ type }) => industry == type))

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

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

@ -1,4 +1,4 @@
import { CityName } from './../../Locations/data/CityNames'; import { CityName } from "./../../Locations/data/CityNames";
const CyclesPerMarketCycle = 50; const CyclesPerMarketCycle = 50;
const AllCorporationStates = ["START", "PURCHASE", "PRODUCTION", "SALE", "EXPORT"]; const AllCorporationStates = ["START", "PURCHASE", "PRODUCTION", "SALE", "EXPORT"];
export const CorporationConstants: { export const CorporationConstants: {
@ -38,7 +38,14 @@ export const CorporationConstants: {
CyclesPerIndustryStateCycle: CyclesPerMarketCycle / AllCorporationStates.length, CyclesPerIndustryStateCycle: CyclesPerMarketCycle / AllCorporationStates.length,
SecsPerMarketCycle: CyclesPerMarketCycle / 5, 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 WarehouseInitialCost: 5e9, //Initial purchase cost of warehouse
WarehouseInitialSize: 100, WarehouseInitialSize: 100,
@ -74,16 +81,6 @@ export const CorporationConstants: {
"AI Cores", "AI Cores",
"Real Estate", "Real Estate",
], ],
FundingRoundShares: [ FundingRoundShares: [0.1, 0.35, 0.25, 0.2],
0.1, FundingRoundMultiplier: [4, 3, 3, 2.5],
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 Typography from "@mui/material/Typography";
import Button from "@mui/material/Button"; import Button from "@mui/material/Button";
import TextField from "@mui/material/TextField"; import TextField from "@mui/material/TextField";
import { BuyBackShares } from '../Actions'; import { BuyBackShares } from "../Actions";
import { dialogBoxCreate } from '../../ui/React/DialogBox'; import { dialogBoxCreate } from "../../ui/React/DialogBox";
import { KEY } from "../../utils/helpers/keyCodes"; import { KEY } from "../../utils/helpers/keyCodes";
interface IProps { interface IProps {
@ -40,9 +40,8 @@ export function BuybackSharesModal(props: IProps): React.ReactElement {
function buy(): void { function buy(): void {
if (disabled) return; if (disabled) return;
try { try {
BuyBackShares(corp, player, shares) BuyBackShares(corp, player, shares);
} } catch (err) {
catch (err) {
dialogBoxCreate(err + ""); dialogBoxCreate(err + "");
} }
props.onClose(); props.onClose();

@ -30,7 +30,7 @@ export function CityTabs(props: IProps): React.ReactElement {
} }
return ( 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( {Object.values(division.offices).map(
(office: OfficeSpace | 0) => office !== 0 && <Tab key={office.loc} label={office.loc} value={office.loc} />, (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 ( return (
<Context.Corporation.Provider value={corporation}> <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"} /> <Tab label={corporation.name} value={"Overview"} />
{corporation.divisions.map((div) => ( {corporation.divisions.map((div) => (
<Tab key={div.name} label={div.name} value={div.name} /> <Tab key={div.name} label={div.name} value={div.name} />

@ -56,9 +56,11 @@ export function CreateCorporationModal(props: IProps): React.ReactElement {
return ( return (
<Modal open={props.open} onClose={props.onClose}> <Modal open={props.open} onClose={props.onClose}>
<Typography> <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 can either be self-funded, or you can obtain the seed money from the government in exchange for 500 million
shares`)} shares`}
<br /> <br />
<br /> <br />
If you would like to start one, please enter a name for your corporation below: 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 // Create a popup that lets the player manage exports
export function ExportModal(props: IProps): React.ReactElement { export function ExportModal(props: IProps): React.ReactElement {
const corp = useCorporation(); 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."); if (possibleDivisions.length === 0) throw new Error("Export popup created with no divisions.");
const defaultDivision = possibleDivisions[0]; const defaultDivision = possibleDivisions[0];
if (Object.keys(defaultDivision.warehouses).length === 0) if (Object.keys(defaultDivision.warehouses).length === 0)

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

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

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

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

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

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

@ -10,7 +10,7 @@ export interface IActiveFragmentParams {
export class ActiveFragment { export class ActiveFragment {
id: number; id: number;
avgCharge: number; highestCharge: number;
numCharge: number; numCharge: number;
rotation: number; rotation: number;
x: number; x: number;
@ -21,16 +21,16 @@ export class ActiveFragment {
this.id = params.fragment.id; this.id = params.fragment.id;
this.x = params.x; this.x = params.x;
this.y = params.y; this.y = params.y;
this.avgCharge = 0; this.highestCharge = 0;
this.numCharge = 0; this.numCharge = 0;
this.rotation = params.rotation; this.rotation = params.rotation;
} else { } else {
this.id = -1; this.id = -1;
this.x = -1; this.x = -1;
this.y = -1; this.y = -1;
this.avgCharge = -1; this.highestCharge = 0;
this.numCharge = -1; this.numCharge = 0;
this.rotation = -1; this.rotation = 0;
} }
} }
@ -71,7 +71,7 @@ export class ActiveFragment {
const fragment = FragmentById(this.id); const fragment = FragmentById(this.id);
if (fragment === null) throw new Error("ActiveFragment id refers to unknown Fragment."); if (fragment === null) throw new Error("ActiveFragment id refers to unknown Fragment.");
const c = new ActiveFragment({ x: this.x, y: this.y, rotation: this.rotation, fragment: fragment }); const c = new ActiveFragment({ x: this.x, y: this.y, rotation: this.rotation, fragment: fragment });
c.avgCharge = this.avgCharge; c.highestCharge = this.highestCharge;
c.numCharge = this.numCharge; c.numCharge = this.numCharge;
return c; return c;
} }

@ -61,7 +61,7 @@ export function Effect(tpe: FragmentType): string {
return "+x% hacknet production"; return "+x% hacknet production";
} }
case FragmentType.HacknetCost: { case FragmentType.HacknetCost: {
return "x% cheaper hacknet cost"; return "-x% cheaper hacknet cost";
} }
case FragmentType.Rep: { case FragmentType.Rep: {
return "+x% reputation from factions and companies"; return "+x% reputation from factions and companies";

@ -1,4 +1,4 @@
import { FactionNames } from '../Faction/data/FactionNames'; import { FactionNames } from "../Faction/data/FactionNames";
import { Fragment } from "./Fragment"; import { Fragment } from "./Fragment";
import { ActiveFragment } from "./ActiveFragment"; import { ActiveFragment } from "./ActiveFragment";
import { FragmentType } from "./FragmentType"; import { FragmentType } from "./FragmentType";
@ -19,19 +19,23 @@ export class StaneksGift implements IStaneksGift {
fragments: ActiveFragment[] = []; fragments: ActiveFragment[] = [];
baseSize(): number { baseSize(): number {
return StanekConstants.BaseSize + BitNodeMultipliers.StaneksGiftExtraSize + Player.sourceFileLvl(13) return StanekConstants.BaseSize + BitNodeMultipliers.StaneksGiftExtraSize + Player.sourceFileLvl(13);
} }
width(): number { width(): number {
return Math.min(Math.floor(this.baseSize() / 2 + 1),StanekConstants.MaxSize); return Math.max(2, Math.min(Math.floor(this.baseSize() / 2 + 1), StanekConstants.MaxSize));
} }
height(): number { height(): number {
return Math.min(Math.floor(this.baseSize() / 2 + 0.6),StanekConstants.MaxSize); return Math.max(3, Math.min(Math.floor(this.baseSize() / 2 + 0.6), StanekConstants.MaxSize));
} }
charge(player: IPlayer, af: ActiveFragment, threads: number): void { charge(player: IPlayer, af: ActiveFragment, threads: number): void {
af.avgCharge = (af.numCharge * af.avgCharge + threads) / (af.numCharge + 1); if (threads > af.highestCharge) {
af.numCharge++; af.numCharge = (af.highestCharge * af.numCharge) / threads + 1;
af.highestCharge = threads;
} else {
af.numCharge += threads / af.highestCharge;
}
const cotmg = Factions[FactionNames.ChurchOfTheMachineGod]; const cotmg = Factions[FactionNames.ChurchOfTheMachineGod];
cotmg.playerReputation += (player.faction_rep_mult * (Math.pow(threads, 0.95) * (cotmg.favor + 100))) / 1000; cotmg.playerReputation += (player.faction_rep_mult * (Math.pow(threads, 0.95) * (cotmg.favor + 100))) / 1000;
@ -66,7 +70,7 @@ export class StaneksGift implements IStaneksGift {
for (const neighboor of neighboors) { for (const neighboor of neighboors) {
boost *= neighboor.fragment().power; boost *= neighboor.fragment().power;
} }
return CalculateEffect(fragment.avgCharge, fragment.numCharge, fragment.fragment().power, boost); return CalculateEffect(fragment.highestCharge, fragment.numCharge, fragment.fragment().power, boost);
} }
canPlace(rootX: number, rootY: number, rotation: number, fragment: Fragment): boolean { canPlace(rootX: number, rootY: number, rotation: number, fragment: Fragment): boolean {
@ -126,7 +130,7 @@ export class StaneksGift implements IStaneksGift {
clearCharge(): void { clearCharge(): void {
this.fragments.forEach((f) => { this.fragments.forEach((f) => {
f.avgCharge = 0; f.highestCharge = 0;
f.numCharge = 0; f.numCharge = 0;
}); });
} }

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

@ -48,16 +48,18 @@ export function FragmentInspector(props: IProps): React.ReactElement {
} }
const f = props.fragment.fragment(); const f = props.fragment.fragment();
let charge = `${numeralWrapper.formatStaneksGiftCharge(props.fragment.avgCharge)} avg. * ${ let charge = numeralWrapper.formatStaneksGiftCharge(props.fragment.highestCharge * props.fragment.numCharge);
props.fragment.numCharge
} times`;
let effect = "N/A"; let effect = "N/A";
// Boosters and cooling don't deal with heat. // Boosters and cooling don't deal with heat.
if ([FragmentType.Booster, FragmentType.None, FragmentType.Delete].includes(f.type)) { if ([FragmentType.Booster, FragmentType.None, FragmentType.Delete].includes(f.type)) {
charge = "N/A"; charge = "N/A";
effect = `${f.power}x adjacent fragment power`; effect = `${f.power}x adjacent fragment power`;
} else { } else if (Effect(f.type).includes("+x%")) {
effect = Effect(f.type).replace(/-*x%/, numeralWrapper.formatPercentage(props.gift.effect(props.fragment) - 1)); effect = Effect(f.type).replace(/-*x%/, numeralWrapper.formatPercentage(props.gift.effect(props.fragment) - 1));
} else if (Effect(f.type).includes("-x%")) {
const effectAmt = props.gift.effect(props.fragment);
const perc = numeralWrapper.formatPercentage(1 - 1 / effectAmt);
effect = Effect(f.type).replace(/-x%/, perc);
} }
return ( return (

@ -107,7 +107,9 @@ export function buyAllDarkwebItems(): void {
} }
if (cost > Player.money) { 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; return;
} }

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

@ -37,7 +37,7 @@ export function Factions(props: IProps): React.ReactElement {
} }
function receiveAllInvites(): void { 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 { function modifyFactionRep(modifier: number): (x: number) => void {

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

@ -27,20 +27,20 @@ export function Stanek(): React.ReactElement {
function addCharge(): void { function addCharge(): void {
staneksGift.fragments.forEach((f) => { staneksGift.fragments.forEach((f) => {
f.avgCharge = 1e21; f.highestCharge = 1e21;
f.numCharge = 1e21; f.numCharge = 1e21;
}); });
} }
function modCharge(modify: number): (x: number) => void { function modCharge(modify: number): (x: number) => void {
return function (cycles: number): void { return function (cycles: number): void {
staneksGift.fragments.forEach((f) => (f.avgCharge += cycles * modify)); staneksGift.fragments.forEach((f) => (f.highestCharge += cycles * modify));
}; };
} }
function resetCharge(): void { function resetCharge(): void {
staneksGift.fragments.forEach((f) => { staneksGift.fragments.forEach((f) => {
f.avgCharge = 0; f.highestCharge = 0;
f.numCharge = 0; f.numCharge = 0;
}); });
} }

@ -85,8 +85,6 @@ export class FactionInfo {
} }
} }
/** /**
* A map of all factions and associated info to them. * 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]: new FactionInfo(
( (
<> <>
{FactionNames.ECorp}'s mission is simple: to connect the world of today with the technology of tomorrow. With our wide range of {FactionNames.ECorp}'s mission is simple: to connect the world of today with the technology of tomorrow. With
Internet-related software and commercial hardware, {FactionNames.ECorp} makes the world's information universally accessible. 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]: new FactionInfo(
( (
<> <>
{FactionNames.MegaCorp} does what no other dares to do. We imagine. We create. We invent. We create what others have never even {FactionNames.MegaCorp} does what no other dares to do. We imagine. We create. We invent. We create what others
dreamed of. Our work fills the world's needs for food, water, power, and transportation on an unprecedented have never even dreamed of. Our work fills the world's needs for food, water, power, and transportation on an
scale, in ways that no other company can. unprecedented scale, in ways that no other company can.
<br /> <br />
<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, 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( [FactionNames.NWO]: new FactionInfo(
( (
@ -486,12 +495,39 @@ export const FactionInfos: IMap<FactionInfo> = {
false, 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. // 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( [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? It's too bad they won't live. But then again, who does?
<br /> <br />
<br /> <br />
Note that for this faction, reputation can only be gained through {FactionNames.Bladeburners} actions. 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. contracts/operations will increase your reputation.
</> </>
), ),

@ -32,8 +32,6 @@ type IProps = {
export function AugmentationsPage(props: IProps): React.ReactElement { export function AugmentationsPage(props: IProps): React.ReactElement {
const player = use.Player(); const player = use.Player();
// Flag for whether the player has a gang with this faction
const isPlayersGang = player.inGang() && player.getGangName() === props.faction.name;
const setRerender = useState(false)[1]; const setRerender = useState(false)[1];

@ -2,8 +2,6 @@ import React, { useEffect, useState } from "react";
import { Box, Button, Container, Paper, TableBody, TableRow, Typography } from "@mui/material"; import { Box, Button, Container, Paper, TableBody, TableRow, Typography } from "@mui/material";
import { Augmentations } from "../../Augmentation/Augmentations";
import { AugmentationNames } from "../../Augmentation/data/AugmentationNames";
import { IPlayer } from "../../PersonObjects/IPlayer"; import { IPlayer } from "../../PersonObjects/IPlayer";
import { Table, TableCell } from "../../ui/React/Table"; import { Table, TableCell } from "../../ui/React/Table";
import { IRouter } from "../../ui/Router"; import { IRouter } from "../../ui/Router";

@ -59,9 +59,7 @@ export function Info(props: IProps): React.ReactElement {
</Typography> </Typography>
<MathJaxWrapper>{"\\(\\huge{r = \\text{total faction reputation}}\\)"}</MathJaxWrapper> <MathJaxWrapper>{"\\(\\huge{r = \\text{total faction reputation}}\\)"}</MathJaxWrapper>
<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> </MathJaxWrapper>
</> </>
} }
@ -86,7 +84,6 @@ export function Info(props: IProps): React.ReactElement {
<MathJaxWrapper>{"\\(\\huge{r = reputation}\\)"}</MathJaxWrapper> <MathJaxWrapper>{"\\(\\huge{r = reputation}\\)"}</MathJaxWrapper>
<MathJaxWrapper>{"\\(\\huge{\\Delta r = \\Delta r \\times \\frac{100+favor}{100}}\\)"}</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"; import { Reviver } from "../utils/JSONReviver";
interface GangTerritory { interface GangTerritory {

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

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

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

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

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

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

@ -32,7 +32,7 @@ export function GangRoot(): React.ReactElement {
return ( return (
<Context.Gang.Provider value={gang}> <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="Management" />
<Tab label="Equipment" /> <Tab label="Equipment" />
<Tab label="Territory" /> <Tab label="Territory" />

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

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

@ -13,9 +13,7 @@ export const TerritoryInfoModal = ({ open, onClose }: IProps): React.ReactElemen
return ( return (
<Modal open={open} onClose={onClose}> <Modal open={open} onClose={onClose}>
<> <>
<Typography variant='h4'> <Typography variant="h4">Clashing</Typography>
Clashing
</Typography>
<Typography> <Typography>
Every ~20 seconds, your gang has a chance to 'clash' with other gangs. Your chance to win a clash depends on 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 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. gang.
</Typography> </Typography>
<br /> <br />
<Typography variant='h4'> <Typography variant="h4">Territory</Typography>
Territory
</Typography>
<Typography> <Typography>
The amount of territory you have affects all aspects of your Gang members' production, including money, respect, The amount of territory you have affects all aspects of your Gang members' production, including money,
and wanted level. It is very beneficial to have high territory control. respect, and wanted level. It is very beneficial to have high territory control.
<br /> <br />
<br /> <br />
To increase your chances of winning territory, assign gang members to "Territory Warfare". This will build your To increase your chances of winning territory, assign gang members to "Territory Warfare". This will build
gang power. Then, enable "Engage in Territory Warfare" to start fighting over territory. your gang power. Then, enable "Engage in Territory Warfare" to start fighting over territory.
</Typography> </Typography>
<br /> <br />
<Typography variant='h4'> <Typography variant="h4">Territory Clash Chance</Typography>
Territory Clash Chance
</Typography>
<Typography> <Typography>
This percentage represents the chance you have of 'clashing' with another gang. If you do not wish to 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. 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> </Modal>
); );
} };

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

@ -49,7 +49,8 @@ export function calculatePercentMoneyHacked(server: Server, player: IPlayer): nu
const difficultyMult = (100 - server.hackDifficulty) / 100; const difficultyMult = (100 - server.hackDifficulty) / 100;
const skillMult = (player.hacking - (server.requiredHackingSkill - 1)) / player.hacking; 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) { if (percentMoneyHacked < 0) {
return 0; return 0;
} }

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

@ -34,7 +34,7 @@ export function CasinoLocation(props: IProps): React.ReactElement {
return ( return (
<> <>
{game === GameType.None && ( {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.Coin)}>Play coin flip</Button>
<Button onClick={() => updateGame(GameType.Slots)}>Play slots</Button> <Button onClick={() => updateGame(GameType.Slots)}>Play slots</Button>
<Button onClick={() => updateGame(GameType.Roulette)}>Play roulette</Button> <Button onClick={() => updateGame(GameType.Roulette)}>Play roulette</Button>

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

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

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

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

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

@ -114,7 +114,7 @@ export function SlumsLocation(): React.ReactElement {
const heistChance = Crimes.Heist.successRate(player); const heistChance = Crimes.Heist.successRate(player);
return ( 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</>}> <Tooltip title={<>Attempt to shoplift from a low-end retailer</>}>
<Button onClick={shoplift}> <Button onClick={shoplift}>
Shoplift ({numeralWrapper.formatPercentage(shopliftChance)} chance of success) Shoplift ({numeralWrapper.formatPercentage(shopliftChance)} chance of success)

@ -155,7 +155,11 @@ export function SpecialLocation(props: IProps): React.ReactElement {
if (!player.canAccessGrafting()) { if (!player.canAccessGrafting()) {
return <></>; 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 { function handleCotMG(): void {

@ -70,9 +70,7 @@ export function TechVendorLocation(props: IProps): React.ReactElement {
return ( return (
<> <>
<br /> <br />
<Box sx={{ display: 'grid', width: 'fit-content' }}> <Box sx={{ display: "grid", width: "fit-content" }}>{purchaseServerButtons}</Box>
{purchaseServerButtons}
</Box>
<br /> <br />
<Typography> <Typography>
<i>"You can order bigger servers via scripts. We don't take custom orders in person."</i> <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!`; const earnCharismaExpTooltip = `Gain charisma experience!`;
return ( return (
<Box sx={{ display: 'grid', width: 'fit-content' }}> <Box sx={{ display: "grid", width: "fit-content" }}>
<Tooltip title={earnHackingExpTooltip}> <Tooltip title={earnHackingExpTooltip}>
<Button onClick={study}>Study Computer Science (free)</Button> <Button onClick={study}>Study Computer Science (free)</Button>
</Tooltip> </Tooltip>

@ -1,4 +1,4 @@
import React from 'react'; import React from "react";
import { MathJax, MathJaxContext } from "better-react-mathjax"; import { MathJax, MathJaxContext } from "better-react-mathjax";
interface IProps { 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"}> <MathJaxContext version={3} src={"dist/ext/MathJax-3.2.0/es5/tex-chtml.js"}>
<MathJax>{children}</MathJax> <MathJax>{children}</MathJax>
</MathJaxContext> </MathJaxContext>
) );
} }

@ -226,7 +226,7 @@ export const RamCosts: IMap<any> = {
universityCourse: SF4Cost(RamCostConstants.ScriptSingularityFn1RamCost), universityCourse: SF4Cost(RamCostConstants.ScriptSingularityFn1RamCost),
gymWorkout: SF4Cost(RamCostConstants.ScriptSingularityFn1RamCost), gymWorkout: SF4Cost(RamCostConstants.ScriptSingularityFn1RamCost),
travelToCity: SF4Cost(RamCostConstants.ScriptSingularityFn1RamCost), travelToCity: SF4Cost(RamCostConstants.ScriptSingularityFn1RamCost),
goToLocation: SF4Cost(RamCostConstants.ScriptSingularityFn1RamCost), goToLocation: SF4Cost(RamCostConstants.ScriptSingularityFn3RamCost),
purchaseTor: SF4Cost(RamCostConstants.ScriptSingularityFn1RamCost), purchaseTor: SF4Cost(RamCostConstants.ScriptSingularityFn1RamCost),
purchaseProgram: SF4Cost(RamCostConstants.ScriptSingularityFn1RamCost), purchaseProgram: SF4Cost(RamCostConstants.ScriptSingularityFn1RamCost),
getCurrentServer: SF4Cost(RamCostConstants.ScriptSingularityFn1RamCost), getCurrentServer: SF4Cost(RamCostConstants.ScriptSingularityFn1RamCost),

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

@ -350,7 +350,8 @@ export function NetscriptBladeburner(
checkBladeburnerCity("switchCity", cityName); checkBladeburnerCity("switchCity", cityName);
const bladeburner = player.bladeburner; const bladeburner = player.bladeburner;
if (bladeburner === null) throw new Error("Should not be called without Bladeburner"); if (bladeburner === null) throw new Error("Should not be called without Bladeburner");
return bladeburner.city === cityName; bladeburner.city = cityName;
return true;
}, },
getStamina: function (): [number, number] { getStamina: function (): [number, number] {
updateRam("getStamina"); updateRam("getStamina");

@ -37,6 +37,7 @@ import {
calculateAscensionMult, calculateAscensionMult,
calculateAscensionPointsGain, calculateAscensionPointsGain,
} from "../Gang/formulas/formulas"; } from "../Gang/formulas/formulas";
import { favorToRep as calculateFavorToRep, repToFavor as calculateRepToFavor } from "../Faction/formulas/favor";
export function NetscriptFormulas(player: IPlayer, workerScript: WorkerScript, helper: INetscriptHelper): IFormulas { export function NetscriptFormulas(player: IPlayer, workerScript: WorkerScript, helper: INetscriptHelper): IFormulas {
const checkFormulasAccess = function (func: string): void { const checkFormulasAccess = function (func: string): void {
@ -45,6 +46,18 @@ export function NetscriptFormulas(player: IPlayer, workerScript: WorkerScript, h
} }
}; };
return { return {
reputation: {
calculateFavorToRep: function (_favor: unknown): number {
const favor = helper.number("calculateFavorToRep", "favor", _favor);
checkFormulasAccess("reputation.calculateFavorToRep");
return calculateFavorToRep(favor);
},
calculateRepToFavor: function (_rep: unknown): number {
const rep = helper.number("calculateRepToFavor", "rep", _rep);
checkFormulasAccess("reputation.calculateRepToFavor");
return calculateRepToFavor(rep);
},
},
skills: { skills: {
calculateSkill: function (_exp: unknown, _mult: unknown = 1): number { calculateSkill: function (_exp: unknown, _mult: unknown = 1): number {
const exp = helper.number("calculateSkill", "exp", _exp); const exp = helper.number("calculateSkill", "exp", _exp);

@ -3,7 +3,7 @@ import { BaseServer } from "../Server/BaseServer";
export interface INetscriptHelper { export interface INetscriptHelper {
updateDynamicRam(functionName: string, ram: number): void; updateDynamicRam(functionName: string, ram: number): void;
makeRuntimeErrorMsg(functionName: string, message: string): void; makeRuntimeErrorMsg(functionName: string, message: string): string;
string(funcName: string, argName: string, v: unknown): string; string(funcName: string, argName: string, v: unknown): string;
number(funcName: string, argName: string, v: unknown): number; number(funcName: string, argName: string, v: unknown): number;
city(funcName: string, argName: string, v: unknown): CityName; city(funcName: string, argName: string, v: unknown): CityName;

@ -1106,6 +1106,7 @@ export function NetscriptSingularity(
return true; return true;
default: default:
workerScript.log("workForFaction", () => `Invalid work type: '${type}`); workerScript.log("workForFaction", () => `Invalid work type: '${type}`);
return false;
} }
return true; return true;
}, },

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