merge dev

This commit is contained in:
phyzical 2022-04-07 16:37:57 +08:00
commit d24a26a208
228 changed files with 7055 additions and 5966 deletions

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

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

@ -58,7 +58,7 @@ jobs:
- name: Get Comment Body
id: get-comment-body
if : steps.get-warnings.outputs.has_warnings == 'true'
if: steps.get-warnings.outputs.has_warnings == 'true'
run: |
cat warnings.txt > comment.txt
echo "" >> comment.txt
@ -73,12 +73,12 @@ jobs:
echo ::set-output name=body::$body
- name: Add github comment on problem
if : steps.get-warnings.outputs.has_warnings == 'true'
if: steps.get-warnings.outputs.has_warnings == 'true'
uses: peter-evans/commit-comment@v1
with:
body: ${{ steps.get-comment-body.outputs.body }}
- name: Flag as error
if : steps.get-warnings.outputs.has_warnings == 'true'
if: steps.get-warnings.outputs.has_warnings == 'true'
run: |
COMMIT_WARNINGS=$(cat warnings.txt)
echo "::warning:: $COMMIT_WARNINGS"

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

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 equation must have:
- several variables that can be upgraded, at least 1 variable must be strategic in it's upgrading (upgrading too much can cause drop in performance)
- Some sort of math twist that requires some thinking, like (-2)^c1 alters between positive and negative.

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

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

@ -3,6 +3,21 @@
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
----------------------------

@ -66,7 +66,7 @@ documentation_title = '{0} Documentation'.format(project)
# The short X.Y version.
version = '1.6'
# 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
# for a list of supported languages.

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

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

@ -1,7 +1,7 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8"/>
<meta charset="utf-8" />
<title>Bitburner</title>
<style>
body {

@ -1,7 +1,7 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8"/>
<meta charset="utf-8" />
<title>Bitburner</title>
<style>
body {

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

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

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

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

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

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

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

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

1
letter_to_fandom.com Normal file

@ -0,0 +1 @@
I want the wiki here https://bitburner.fandom.com/wiki/Bitburner_Wiki taken down please.

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

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

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

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

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

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

@ -1,234 +1,121 @@
export const AugmentationNames: {
Targeting1: string;
Targeting2: string;
Targeting3: string;
SyntheticHeart: string;
SynfibrilMuscle: string;
CombatRib1: string;
CombatRib2: string;
CombatRib3: string;
NanofiberWeave: string;
SubdermalArmor: string;
WiredReflexes: string;
GrapheneBoneLacings: string;
BionicSpine: string;
GrapheneBionicSpine: string;
BionicLegs: string;
GrapheneBionicLegs: string;
SpeechProcessor: string;
TITN41Injection: string;
EnhancedSocialInteractionImplant: string;
BitWire: string;
ArtificialBioNeuralNetwork: string;
ArtificialSynapticPotentiation: string;
EnhancedMyelinSheathing: string;
SynapticEnhancement: string;
NeuralRetentionEnhancement: string;
DataJack: string;
ENM: string;
ENMCore: string;
ENMCoreV2: string;
ENMCoreV3: string;
ENMAnalyzeEngine: string;
ENMDMA: string;
Neuralstimulator: string;
NeuralAccelerator: string;
CranialSignalProcessorsG1: string;
CranialSignalProcessorsG2: string;
CranialSignalProcessorsG3: string;
CranialSignalProcessorsG4: string;
CranialSignalProcessorsG5: string;
NeuronalDensification: string;
NeuroreceptorManager: string;
NuoptimalInjectorImplant: string;
SpeechEnhancement: string;
FocusWire: string;
PCDNI: string;
PCDNIOptimizer: string;
PCDNINeuralNetwork: string;
PCMatrix: string;
ADRPheromone1: string;
ADRPheromone2: string;
ShadowsSimulacrum: string;
HacknetNodeCPUUpload: string;
HacknetNodeCacheUpload: string;
HacknetNodeNICUpload: string;
HacknetNodeKernelDNI: string;
HacknetNodeCoreDNI: string;
NeuroFluxGovernor: string;
Neurotrainer1: string;
Neurotrainer2: string;
Neurotrainer3: string;
Hypersight: string;
LuminCloaking1: string;
LuminCloaking2: string;
HemoRecirculator: string;
SmartSonar: string;
PowerRecirculator: string;
QLink: string;
TheRedPill: string;
SPTN97: string;
HiveMind: string;
CordiARCReactor: string;
SmartJaw: string;
Neotra: string;
Xanipher: string;
nextSENS: string;
OmniTekInfoLoad: string;
PhotosyntheticCells: string;
Neurolink: string;
TheBlackHand: string;
UnstableCircadianModulator: string;
CRTX42AA: string;
Neuregen: string;
CashRoot: string;
NutriGen: string;
INFRARet: string;
DermaForce: string;
GrapheneBrachiBlades: string;
GrapheneBionicArms: string;
BrachiBlades: string;
BionicArms: string;
SNA: string;
HydroflameLeftArm: string;
EsperEyewear: string;
EMS4Recombination: string;
OrionShoulder: string;
HyperionV1: string;
HyperionV2: string;
GolemSerum: string;
VangelisVirus: string;
VangelisVirus3: string;
INTERLINKED: string;
BladeRunner: string;
BladeArmor: string;
BladeArmorPowerCells: string;
BladeArmorEnergyShielding: string;
BladeArmorUnibeam: string;
BladeArmorOmnibeam: string;
BladeArmorIPU: string;
BladesSimulacrum: string;
StaneksGift1: string;
StaneksGift2: string;
StaneksGift3: string;
} = {
Targeting1: "Augmented Targeting I",
Targeting2: "Augmented Targeting II",
Targeting3: "Augmented Targeting III",
SyntheticHeart: "Synthetic Heart",
SynfibrilMuscle: "Synfibril Muscle",
CombatRib1: "Combat Rib I",
CombatRib2: "Combat Rib II",
CombatRib3: "Combat Rib III",
NanofiberWeave: "Nanofiber Weave",
SubdermalArmor: "NEMEAN Subdermal Weave",
WiredReflexes: "Wired Reflexes",
GrapheneBoneLacings: "Graphene Bone Lacings",
BionicSpine: "Bionic Spine",
GrapheneBionicSpine: "Graphene Bionic Spine Upgrade",
BionicLegs: "Bionic Legs",
GrapheneBionicLegs: "Graphene Bionic Legs Upgrade",
SpeechProcessor: "Speech Processor Implant",
TITN41Injection: "TITN-41 Gene-Modification Injection",
EnhancedSocialInteractionImplant: "Enhanced Social Interaction Implant",
BitWire: "BitWire",
ArtificialBioNeuralNetwork: "Artificial Bio-neural Network Implant",
ArtificialSynapticPotentiation: "Artificial Synaptic Potentiation",
EnhancedMyelinSheathing: "Enhanced Myelin Sheathing",
SynapticEnhancement: "Synaptic Enhancement Implant",
NeuralRetentionEnhancement: "Neural-Retention Enhancement",
DataJack: "DataJack",
ENM: "Embedded Netburner Module",
ENMCore: "Embedded Netburner Module Core Implant",
ENMCoreV2: "Embedded Netburner Module Core V2 Upgrade",
ENMCoreV3: "Embedded Netburner Module Core V3 Upgrade",
ENMAnalyzeEngine: "Embedded Netburner Module Analyze Engine",
ENMDMA: "Embedded Netburner Module Direct Memory Access Upgrade",
Neuralstimulator: "Neuralstimulator",
NeuralAccelerator: "Neural Accelerator",
CranialSignalProcessorsG1: "Cranial Signal Processors - Gen I",
CranialSignalProcessorsG2: "Cranial Signal Processors - Gen II",
CranialSignalProcessorsG3: "Cranial Signal Processors - Gen III",
CranialSignalProcessorsG4: "Cranial Signal Processors - Gen IV",
CranialSignalProcessorsG5: "Cranial Signal Processors - Gen V",
NeuronalDensification: "Neuronal Densification",
NeuroreceptorManager: "Neuroreceptor Management Implant",
NuoptimalInjectorImplant: "Nuoptimal Nootropic Injector Implant",
SpeechEnhancement: "Speech Enhancement",
FocusWire: "FocusWire",
PCDNI: "PC Direct-Neural Interface",
PCDNIOptimizer: "PC Direct-Neural Interface Optimization Submodule",
PCDNINeuralNetwork: "PC Direct-Neural Interface NeuroNet Injector",
PCMatrix: "PCMatrix",
ADRPheromone1: "ADR-V1 Pheromone Gene",
ADRPheromone2: "ADR-V2 Pheromone Gene",
ShadowsSimulacrum: "The Shadow's Simulacrum",
HacknetNodeCPUUpload: "Hacknet Node CPU Architecture Neural-Upload",
HacknetNodeCacheUpload: "Hacknet Node Cache Architecture Neural-Upload",
HacknetNodeNICUpload: "Hacknet Node NIC Architecture Neural-Upload",
HacknetNodeKernelDNI: "Hacknet Node Kernel Direct-Neural Interface",
HacknetNodeCoreDNI: "Hacknet Node Core Direct-Neural Interface",
NeuroFluxGovernor: "NeuroFlux Governor",
Neurotrainer1: "Neurotrainer I",
Neurotrainer2: "Neurotrainer II",
Neurotrainer3: "Neurotrainer III",
Hypersight: "HyperSight Corneal Implant",
LuminCloaking1: "LuminCloaking-V1 Skin Implant",
LuminCloaking2: "LuminCloaking-V2 Skin Implant",
HemoRecirculator: "HemoRecirculator",
SmartSonar: "SmartSonar Implant",
PowerRecirculator: "Power Recirculation Core",
QLink: "QLink",
TheRedPill: "The Red Pill",
SPTN97: "SPTN-97 Gene Modification",
HiveMind: "ECorp HVMind Implant",
CordiARCReactor: "CordiARC Fusion Reactor",
SmartJaw: "SmartJaw",
Neotra: "Neotra",
Xanipher: "Xanipher",
nextSENS: "nextSENS Gene Modification",
OmniTekInfoLoad: "OmniTek InfoLoad",
PhotosyntheticCells: "Photosynthetic Cells",
Neurolink: "BitRunners Neurolink",
TheBlackHand: "The Black Hand",
UnstableCircadianModulator: "Unstable Circadian Modulator",
CRTX42AA: "CRTX42-AA Gene Modification",
Neuregen: "Neuregen Gene Modification",
CashRoot: "CashRoot Starter Kit",
NutriGen: "NutriGen Implant",
INFRARet: "INFRARET Enhancement",
DermaForce: "DermaForce Particle Barrier",
GrapheneBrachiBlades: "Graphene BrachiBlades Upgrade",
GrapheneBionicArms: "Graphene Bionic Arms Upgrade",
BrachiBlades: "BrachiBlades",
BionicArms: "Bionic Arms",
SNA: "Social Negotiation Assistant (S.N.A)",
HydroflameLeftArm: "Hydroflame Left Arm",
EsperEyewear: "EsperTech Bladeburner Eyewear",
EMS4Recombination: "EMS-4 Recombination",
OrionShoulder: "ORION-MKIV Shoulder",
HyperionV1: "Hyperion Plasma Cannon V1",
HyperionV2: "Hyperion Plasma Cannon V2",
GolemSerum: "GOLEM Serum",
VangelisVirus: "Vangelis Virus",
VangelisVirus3: "Vangelis Virus 3.0",
INTERLINKED: "I.N.T.E.R.L.I.N.K.E.D",
BladeRunner: "Blade's Runners",
BladeArmor: "BLADE-51b Tesla Armor",
BladeArmorPowerCells: "BLADE-51b Tesla Armor: Power Cells Upgrade",
BladeArmorEnergyShielding: "BLADE-51b Tesla Armor: Energy Shielding Upgrade",
BladeArmorUnibeam: "BLADE-51b Tesla Armor: Unibeam Upgrade",
BladeArmorOmnibeam: "BLADE-51b Tesla Armor: Omnibeam Upgrade",
BladeArmorIPU: "BLADE-51b Tesla Armor: IPU Upgrade",
BladesSimulacrum: "The Blade's Simulacrum",
export enum AugmentationNames {
Targeting1 = "Augmented Targeting I",
Targeting2 = "Augmented Targeting II",
Targeting3 = "Augmented Targeting III",
SyntheticHeart = "Synthetic Heart",
SynfibrilMuscle = "Synfibril Muscle",
CombatRib1 = "Combat Rib I",
CombatRib2 = "Combat Rib II",
CombatRib3 = "Combat Rib III",
NanofiberWeave = "Nanofiber Weave",
SubdermalArmor = "NEMEAN Subdermal Weave",
WiredReflexes = "Wired Reflexes",
GrapheneBoneLacings = "Graphene Bone Lacings",
BionicSpine = "Bionic Spine",
GrapheneBionicSpine = "Graphene Bionic Spine Upgrade",
BionicLegs = "Bionic Legs",
GrapheneBionicLegs = "Graphene Bionic Legs Upgrade",
SpeechProcessor = "Speech Processor Implant",
TITN41Injection = "TITN-41 Gene-Modification Injection",
EnhancedSocialInteractionImplant = "Enhanced Social Interaction Implant",
BitWire = "BitWire",
ArtificialBioNeuralNetwork = "Artificial Bio-neural Network Implant",
ArtificialSynapticPotentiation = "Artificial Synaptic Potentiation",
EnhancedMyelinSheathing = "Enhanced Myelin Sheathing",
SynapticEnhancement = "Synaptic Enhancement Implant",
NeuralRetentionEnhancement = "Neural-Retention Enhancement",
DataJack = "DataJack",
ENM = "Embedded Netburner Module",
ENMCore = "Embedded Netburner Module Core Implant",
ENMCoreV2 = "Embedded Netburner Module Core V2 Upgrade",
ENMCoreV3 = "Embedded Netburner Module Core V3 Upgrade",
ENMAnalyzeEngine = "Embedded Netburner Module Analyze Engine",
ENMDMA = "Embedded Netburner Module Direct Memory Access Upgrade",
Neuralstimulator = "Neuralstimulator",
NeuralAccelerator = "Neural Accelerator",
CranialSignalProcessorsG1 = "Cranial Signal Processors - Gen I",
CranialSignalProcessorsG2 = "Cranial Signal Processors - Gen II",
CranialSignalProcessorsG3 = "Cranial Signal Processors - Gen III",
CranialSignalProcessorsG4 = "Cranial Signal Processors - Gen IV",
CranialSignalProcessorsG5 = "Cranial Signal Processors - Gen V",
NeuronalDensification = "Neuronal Densification",
NeuroreceptorManager = "Neuroreceptor Management Implant",
NuoptimalInjectorImplant = "Nuoptimal Nootropic Injector Implant",
SpeechEnhancement = "Speech Enhancement",
FocusWire = "FocusWire",
PCDNI = "PC Direct-Neural Interface",
PCDNIOptimizer = "PC Direct-Neural Interface Optimization Submodule",
PCDNINeuralNetwork = "PC Direct-Neural Interface NeuroNet Injector",
PCMatrix = "PCMatrix",
ADRPheromone1 = "ADR-V1 Pheromone Gene",
ADRPheromone2 = "ADR-V2 Pheromone Gene",
ShadowsSimulacrum = "The Shadow's Simulacrum",
HacknetNodeCPUUpload = "Hacknet Node CPU Architecture Neural-Upload",
HacknetNodeCacheUpload = "Hacknet Node Cache Architecture Neural-Upload",
HacknetNodeNICUpload = "Hacknet Node NIC Architecture Neural-Upload",
HacknetNodeKernelDNI = "Hacknet Node Kernel Direct-Neural Interface",
HacknetNodeCoreDNI = "Hacknet Node Core Direct-Neural Interface",
NeuroFluxGovernor = "NeuroFlux Governor",
Neurotrainer1 = "Neurotrainer I",
Neurotrainer2 = "Neurotrainer II",
Neurotrainer3 = "Neurotrainer III",
Hypersight = "HyperSight Corneal Implant",
LuminCloaking1 = "LuminCloaking-V1 Skin Implant",
LuminCloaking2 = "LuminCloaking-V2 Skin Implant",
HemoRecirculator = "HemoRecirculator",
SmartSonar = "SmartSonar Implant",
PowerRecirculator = "Power Recirculation Core",
QLink = "QLink",
TheRedPill = "The Red Pill",
SPTN97 = "SPTN-97 Gene Modification",
HiveMind = "ECorp HVMind Implant",
CordiARCReactor = "CordiARC Fusion Reactor",
SmartJaw = "SmartJaw",
Neotra = "Neotra",
Xanipher = "Xanipher",
nextSENS = "nextSENS Gene Modification",
OmniTekInfoLoad = "OmniTek InfoLoad",
PhotosyntheticCells = "Photosynthetic Cells",
Neurolink = "BitRunners Neurolink",
TheBlackHand = "The Black Hand",
UnstableCircadianModulator = "Unstable Circadian Modulator",
CRTX42AA = "CRTX42-AA Gene Modification",
Neuregen = "Neuregen Gene Modification",
CashRoot = "CashRoot Starter Kit",
NutriGen = "NutriGen Implant",
INFRARet = "INFRARET Enhancement",
DermaForce = "DermaForce Particle Barrier",
GrapheneBrachiBlades = "Graphene BrachiBlades Upgrade",
GrapheneBionicArms = "Graphene Bionic Arms Upgrade",
BrachiBlades = "BrachiBlades",
BionicArms = "Bionic Arms",
SNA = "Social Negotiation Assistant (S.N.A)",
HydroflameLeftArm = "Hydroflame Left Arm",
EsperEyewear = "EsperTech Bladeburner Eyewear",
EMS4Recombination = "EMS-4 Recombination",
OrionShoulder = "ORION-MKIV Shoulder",
HyperionV1 = "Hyperion Plasma Cannon V1",
HyperionV2 = "Hyperion Plasma Cannon V2",
GolemSerum = "GOLEM Serum",
VangelisVirus = "Vangelis Virus",
VangelisVirus3 = "Vangelis Virus 3.0",
INTERLINKED = "I.N.T.E.R.L.I.N.K.E.D",
BladeRunner = "Blade's Runners",
BladeArmor = "BLADE-51b Tesla Armor",
BladeArmorPowerCells = "BLADE-51b Tesla Armor: Power Cells Upgrade",
BladeArmorEnergyShielding = "BLADE-51b Tesla Armor: Energy Shielding Upgrade",
BladeArmorUnibeam = "BLADE-51b Tesla Armor: Unibeam Upgrade",
BladeArmorOmnibeam = "BLADE-51b Tesla Armor: Omnibeam Upgrade",
BladeArmorIPU = "BLADE-51b Tesla Armor: IPU Upgrade",
BladesSimulacrum = "The Blade's Simulacrum",
StaneksGift1: "Stanek's Gift - Genesis",
StaneksGift2: "Stanek's Gift - Awakening",
StaneksGift3: "Stanek's Gift - Serenity",
StaneksGift1 = "Stanek's Gift - Genesis",
StaneksGift2 = "Stanek's Gift - Awakening",
StaneksGift3 = "Stanek's Gift - Serenity",
//Wasteland Augs
//PepBoy: "P.E.P-Boy", Plasma Energy Projection System
//PepBoyForceField Generates plasma force fields
//PepBoyBlasts Generate high density plasma concussive blasts
//PepBoyDataStorage STore more data on pep boy,
};
}

@ -82,7 +82,9 @@ BitNodes["BitNode2"] = new BitNode(
<br />
The amount of money gained from crimes and Infiltration is tripled
<br />
Certain Factions ({FactionNames.SlumSnakes}, {FactionNames.Tetrads}, {FactionNames.TheSyndicate}, {FactionNames.TheDarkArmy}, {FactionNames.SpeakersForTheDead}, {FactionNames.NiteSec}, {FactionNames.TheBlackHand}) give the player the ability to form and manage their own gangs. These gangs will earn the player money and
Certain Factions ({FactionNames.SlumSnakes}, {FactionNames.Tetrads}, {FactionNames.TheSyndicate},{" "}
{FactionNames.TheDarkArmy}, {FactionNames.SpeakersForTheDead}, {FactionNames.NiteSec}, {FactionNames.TheBlackHand}
) give the player the ability to form and manage their own gangs. These gangs will earn the player money and
reputation with the corresponding Faction
<br />
Every Augmentation in the game will be available through the Factions listed above
@ -225,15 +227,15 @@ BitNodes["BitNode6"] = new BitNode(
"Like Tears in Rain",
(
<>
In the middle of the 21st century, {FactionNames.OmniTekIncorporated} began designing and manufacturing advanced synthetic
androids, or Synthoids for short. They achieved a major technological breakthrough in the sixth generation of
their Synthoid design, called MK-VI, by developing a hyperintelligent AI. Many argue that this was the first
sentient AI ever created. This resulted in Synthoid models that were stronger, faster, and more intelligent than
the humans that had created them.
In the middle of the 21st century, {FactionNames.OmniTekIncorporated} began designing and manufacturing advanced
synthetic androids, or Synthoids for short. They achieved a major technological breakthrough in the sixth
generation of their Synthoid design, called MK-VI, by developing a hyperintelligent AI. Many argue that this was
the first sentient AI ever created. This resulted in Synthoid models that were stronger, faster, and more
intelligent than the humans that had created them.
<br />
<br />
In this BitNode you will be able to access the {FactionNames.Bladeburners} Division at the NSA, which provides a new mechanic for
progression. Furthermore:
In this BitNode you will be able to access the {FactionNames.Bladeburners} Division at the NSA, which provides a
new mechanic for progression. Furthermore:
<br />
<br />
Hacking and Hacknet Nodes will be less profitable
@ -250,9 +252,9 @@ BitNodes["BitNode6"] = new BitNode(
<br />
<br />
Destroying this BitNode will give you Source-File 6, or if you already have this Source-File it will upgrade its
level up to a maximum of 3. This Source-File allows you to access the NSA's {FactionNames.Bladeburners} Division in other
BitNodes. In addition, this Source-File will raise both the level and experience gain rate of all your combat
stats by:
level up to a maximum of 3. This Source-File allows you to access the NSA's {FactionNames.Bladeburners} Division
in other BitNodes. In addition, this Source-File will raise both the level and experience gain rate of all your
combat stats by:
<br />
<br />
Level 1: 8%
@ -270,14 +272,15 @@ BitNodes["BitNode7"] = new BitNode(
"More human than humans",
(
<>
In the middle of the 21st century, you were doing cutting-edge work at {FactionNames.OmniTekIncorporated} as part of the AI
design team for advanced synthetic androids, or Synthoids for short. You helped achieve a major technological
breakthrough in the sixth generation of the company's Synthoid design, called MK-VI, by developing a
In the middle of the 21st century, you were doing cutting-edge work at {FactionNames.OmniTekIncorporated} as part
of the AI design team for advanced synthetic androids, or Synthoids for short. You helped achieve a major
technological breakthrough in the sixth generation of the company's Synthoid design, called MK-VI, by developing a
hyperintelligent AI. Many argue that this was the first sentient AI ever created. This resulted in Synthoid models
that were stronger, faster, and more intelligent than the humans that had created them.
<br />
<br />
In this BitNode you will be able to access the {FactionNames.Bladeburners} API, which allows you to access {FactionNames.Bladeburners}
In this BitNode you will be able to access the {FactionNames.Bladeburners} API, which allows you to access{" "}
{FactionNames.Bladeburners}
functionality through Netscript. Furthermore: <br />
<br />
The rank you gain from {FactionNames.Bladeburners} contracts/operations is reduced by 40%
@ -300,8 +303,9 @@ BitNodes["BitNode7"] = new BitNode(
<br />
<br />
Destroying this BitNode will give you Source-File 7, or if you already have this Source-File it will upgrade its
level up to a maximum of 3. This Source-File allows you to access the {FactionNames.Bladeburners} Netscript API in other BitNodes.
In addition, this Source-File will increase all of your {FactionNames.Bladeburners} multipliers by:
level up to a maximum of 3. This Source-File allows you to access the {FactionNames.Bladeburners} Netscript API in
other BitNodes. In addition, this Source-File will increase all of your {FactionNames.Bladeburners} multipliers
by:
<br />
<br />
Level 1: 8%
@ -364,10 +368,10 @@ BitNodes["BitNode9"] = new BitNode(
"Hacknet Unleashed",
(
<>
When {FactionNames.FulcrumSecretTechnologies} released their open-source Linux distro Chapeau, it quickly became the OS of choice for
the underground hacking community. Chapeau became especially notorious for powering the Hacknet, a global,
decentralized network used for nefarious purposes. {FactionNames.FulcrumSecretTechnologies} quickly abandoned the project and dissociated
themselves from it.
When {FactionNames.FulcrumSecretTechnologies} released their open-source Linux distro Chapeau, it quickly became
the OS of choice for the underground hacking community. Chapeau became especially notorious for powering the
Hacknet, a global, decentralized network used for nefarious purposes. {FactionNames.FulcrumSecretTechnologies}{" "}
quickly abandoned the project and dissociated themselves from it.
<br />
<br />
This BitNode unlocks the Hacknet Server, an upgraded version of the Hacknet Node. Hacknet Servers generate hashes,
@ -538,8 +542,8 @@ BitNodes["BitNode13"] = new BitNode(
"1 step back, 2 steps forward",
(
<>
With the invention of Augmentations in the 2040s a religious group known as the {FactionNames.ChurchOfTheMachineGod} has
rallied far more support than anyone would have hoped.
With the invention of Augmentations in the 2040s a religious group known as the{" "}
{FactionNames.ChurchOfTheMachineGod} has rallied far more support than anyone would have hoped.
<br />
<br />
Their leader, Allison "Mother" Stanek is said to have created her own Augmentation whose power goes beyond any
@ -555,7 +559,8 @@ BitNodes["BitNode13"] = new BitNode(
<br />
<br />
Destroying this BitNode will give you Source-File 13, or if you already have this Source-File it will upgrade its
level up to a maximum of 3. This Source-File lets the {FactionNames.ChurchOfTheMachineGod} appear in other BitNodes.
level up to a maximum of 3. This Source-File lets the {FactionNames.ChurchOfTheMachineGod} appear in other
BitNodes.
<br />
<br />
Each level of this Source-File increases the size of Stanek's Gift.
@ -624,6 +629,7 @@ export function initBitNodeMultipliers(p: IPlayer): void {
BitNodeMultipliers.PurchasedServerSoftcap = 1.3;
BitNodeMultipliers.GangSoftcap = 0.9;
BitNodeMultipliers.WorldDaemonDifficulty = 2;
BitNodeMultipliers.GangUniqueAugs = 0.5;
break;
case 4: // The Singularity
BitNodeMultipliers.ServerMaxMoney = 0.15;
@ -642,6 +648,7 @@ export function initBitNodeMultipliers(p: IPlayer): void {
BitNodeMultipliers.StaneksGiftExtraSize = 0;
BitNodeMultipliers.PurchasedServerSoftcap = 1.2;
BitNodeMultipliers.WorldDaemonDifficulty = 3;
BitNodeMultipliers.GangUniqueAugs = 0.5;
break;
case 5: // Artificial intelligence
BitNodeMultipliers.ServerMaxMoney = 2;
@ -659,6 +666,7 @@ export function initBitNodeMultipliers(p: IPlayer): void {
BitNodeMultipliers.StaneksGiftExtraSize = 0;
BitNodeMultipliers.PurchasedServerSoftcap = 1.2;
BitNodeMultipliers.WorldDaemonDifficulty = 1.5;
BitNodeMultipliers.GangUniqueAugs = 0.5;
break;
case 6: // Bladeburner
BitNodeMultipliers.HackingLevelMultiplier = 0.35;
@ -679,6 +687,7 @@ export function initBitNodeMultipliers(p: IPlayer): void {
BitNodeMultipliers.GangSoftcap = 0.7;
BitNodeMultipliers.CorporationSoftCap = 0.9;
BitNodeMultipliers.WorldDaemonDifficulty = 2;
BitNodeMultipliers.GangUniqueAugs = 0.2;
break;
case 7: // Bladeburner 2079
BitNodeMultipliers.BladeburnerRank = 0.6;
@ -704,6 +713,7 @@ export function initBitNodeMultipliers(p: IPlayer): void {
BitNodeMultipliers.GangSoftcap = 0.7;
BitNodeMultipliers.CorporationSoftCap = 0.9;
BitNodeMultipliers.WorldDaemonDifficulty = 2;
BitNodeMultipliers.GangUniqueAugs = 0.2;
break;
case 8: // Ghost of Wall Street
BitNodeMultipliers.ScriptHackMoney = 0.3;
@ -720,6 +730,7 @@ export function initBitNodeMultipliers(p: IPlayer): void {
BitNodeMultipliers.PurchasedServerSoftcap = 4;
BitNodeMultipliers.GangSoftcap = 0;
BitNodeMultipliers.CorporationSoftCap = 0;
BitNodeMultipliers.GangUniqueAugs = 0;
break;
case 9: // Hacktocracy
BitNodeMultipliers.HackingLevelMultiplier = 0.5;
@ -746,6 +757,7 @@ export function initBitNodeMultipliers(p: IPlayer): void {
BitNodeMultipliers.GangSoftcap = 0.8;
BitNodeMultipliers.CorporationSoftCap = 0.7;
BitNodeMultipliers.WorldDaemonDifficulty = 2;
BitNodeMultipliers.GangUniqueAugs = 0.25;
break;
case 10: // Digital Carbon
BitNodeMultipliers.HackingLevelMultiplier = 0.35;
@ -775,6 +787,7 @@ export function initBitNodeMultipliers(p: IPlayer): void {
BitNodeMultipliers.GangSoftcap = 0.9;
BitNodeMultipliers.CorporationSoftCap = 0.9;
BitNodeMultipliers.WorldDaemonDifficulty = 2;
BitNodeMultipliers.GangUniqueAugs = 0.25;
break;
case 11: //The Big Crash
BitNodeMultipliers.HackingLevelMultiplier = 0.6;
@ -796,6 +809,7 @@ export function initBitNodeMultipliers(p: IPlayer): void {
BitNodeMultipliers.PurchasedServerSoftcap = 2;
BitNodeMultipliers.CorporationSoftCap = 0.9;
BitNodeMultipliers.WorldDaemonDifficulty = 1.5;
BitNodeMultipliers.GangUniqueAugs = 0.75;
break;
case 12: {
//The Recursion
@ -870,6 +884,8 @@ export function initBitNodeMultipliers(p: IPlayer): void {
BitNodeMultipliers.GangSoftcap = 0.8;
BitNodeMultipliers.CorporationSoftCap = 0.8;
BitNodeMultipliers.WorldDaemonDifficulty = inc;
BitNodeMultipliers.GangUniqueAugs = dec;
break;
}
case 13: {
@ -912,6 +928,7 @@ export function initBitNodeMultipliers(p: IPlayer): void {
BitNodeMultipliers.GangSoftcap = 0.3;
BitNodeMultipliers.CorporationSoftCap = 0.3;
BitNodeMultipliers.WorldDaemonDifficulty = 3;
BitNodeMultipliers.GangUniqueAugs = 0.1;
break;
}
default:

@ -114,6 +114,11 @@ interface IBitNodeMultipliers {
*/
GangSoftcap: number;
/**
* Percentage of unique augs that the gang has.
*/
GangUniqueAugs: number;
/**
* Influences the experienced gained when hacking a server.
*/
@ -232,6 +237,11 @@ interface IBitNodeMultipliers {
*/
WorldDaemonDifficulty: number;
/**
* Influences corporation dividends.
*/
CorporationSoftCap: number;
// Index signature
[key: string]: number;
}
@ -295,6 +305,7 @@ export const BitNodeMultipliers: IBitNodeMultipliers = {
BladeburnerSkillCost: 1,
GangSoftcap: 1,
GangUniqueAugs: 1,
DaedalusAugsRequirement: 1,

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

@ -35,6 +35,7 @@ import { joinFaction } from "../Faction/FactionHelpers";
import { WorkerScript } from "../Netscript/WorkerScript";
import { FactionNames } from "../Faction/data/FactionNames";
import { BlackOperationNames } from "./data/BlackOperationNames";
import { KEY } from "../utils/helpers/keyCodes";
interface BlackOpsAttempt {
error?: string;
@ -793,7 +794,7 @@ export class Bladeburner implements IBladeburner {
if (c === '"') {
// Double quotes
const endQuote = command.indexOf('"', i + 1);
if (endQuote !== -1 && (endQuote === command.length - 1 || command.charAt(endQuote + 1) === " ")) {
if (endQuote !== -1 && (endQuote === command.length - 1 || command.charAt(endQuote + 1) === KEY.SPACE)) {
args.push(command.substr(i + 1, endQuote - i - 1));
if (endQuote === command.length - 1) {
start = i = endQuote + 1;
@ -805,7 +806,7 @@ export class Bladeburner implements IBladeburner {
} else if (c === "'") {
// Single quotes, same thing as above
const endQuote = command.indexOf("'", i + 1);
if (endQuote !== -1 && (endQuote === command.length - 1 || command.charAt(endQuote + 1) === " ")) {
if (endQuote !== -1 && (endQuote === command.length - 1 || command.charAt(endQuote + 1) === KEY.SPACE)) {
args.push(command.substr(i + 1, endQuote - i - 1));
if (endQuote === command.length - 1) {
start = i = endQuote + 1;
@ -814,7 +815,7 @@ export class Bladeburner implements IBladeburner {
}
continue;
}
} else if (c === " ") {
} else if (c === KEY.SPACE) {
args.push(command.substr(start, i - start));
start = i + 1;
}

@ -3,16 +3,16 @@ import { IMap } from "../types";
export const GeneralActions: IMap<Action> = {};
const actionNames : Array<string> = [
const actionNames: Array<string> = [
"Training",
"Field Analysis",
"Recruitment",
"Diplomacy",
"Hyperbolic Regeneration Chamber",
"Incite Violence"
"Incite Violence",
];
for (const actionName of actionNames){
for (const actionName of actionNames) {
GeneralActions[actionName] = new Action({
name: actionName,
});

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

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

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

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

@ -89,7 +89,7 @@ export function Console(props: IProps): React.ReactElement {
const consoleHistory = props.bladeburner.consoleHistory;
if (event.key === KEY.UPARROW) {
if (event.key === KEY.UP_ARROW) {
// up
let i = consoleHistoryIndex;
const len = consoleHistory.length;
@ -109,7 +109,7 @@ export function Console(props: IProps): React.ReactElement {
setCommand(prevCommand);
}
if (event.key === KEY.DOWNARROW) {
if (event.key === KEY.DOWN_ARROW) {
const i = consoleHistoryIndex;
const len = consoleHistory.length;
@ -140,14 +140,16 @@ export function Console(props: IProps): React.ReactElement {
return (
<Paper sx={{ p: 1 }}>
<Box sx={{
height: '60vh',
paddingBottom: '8px',
display: 'flex',
alignItems: 'stretch',
whiteSpace: 'pre-wrap',
<Box
sx={{
height: "60vh",
paddingBottom: "8px",
display: "flex",
alignItems: "stretch",
whiteSpace: "pre-wrap",
}}
onClick={handleClick}>
onClick={handleClick}
>
<Box>
<Logs entries={[...props.bladeburner.consoleLogs]} />
</Box>
@ -195,9 +197,7 @@ function Logs({ entries }: ILogProps): React.ReactElement {
return (
<List sx={{ height: "100%", overflow: "auto", p: 1 }} ref={scrollHook}>
{entries && entries.map((log: any, i: number) => (
<Line key={i} content={log} />
))}
{entries && entries.map((log: any, i: number) => <Line key={i} content={log} />)}
</List>
);
}

@ -44,13 +44,15 @@ export function Stats(props: IProps): React.ReactElement {
}
return (
<Paper sx={{ p: 1, overflowY: 'auto', overflowX: 'hidden', wordBreak: 'break-all' }}>
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 1, maxHeight: '60vh' }}>
<Box sx={{ alignSelf: 'flex-start', width: '100%' }}>
<Button onClick={() => setTravelOpen(true)} sx={{ width: '50%' }}>Travel</Button>
<Paper sx={{ p: 1, overflowY: "auto", overflowX: "hidden", wordBreak: "break-all" }}>
<Box sx={{ display: "flex", flexDirection: "column", gap: 1, maxHeight: "60vh" }}>
<Box sx={{ alignSelf: "flex-start", width: "100%" }}>
<Button onClick={() => setTravelOpen(true)} sx={{ width: "50%" }}>
Travel
</Button>
<Tooltip title={!inFaction ? <Typography>Rank 25 required.</Typography> : ""}>
<span>
<Button disabled={!inFaction} onClick={openFaction} sx={{ width: '50%' }}>
<Button disabled={!inFaction} onClick={openFaction} sx={{ width: "50%" }}>
Faction
</Button>
</span>
@ -110,8 +112,8 @@ export function Stats(props: IProps): React.ReactElement {
<Tooltip
title={
<Typography>
This is your Bladeburner division's estimate of how many Synthoids exist in your current city. An accurate
population count increases success rate estimates.
This is your Bladeburner division's estimate of how many Synthoids exist in your current city. An
accurate population count increases success rate estimates.
</Typography>
}
>

@ -61,3 +61,35 @@ export class WHRNG implements RNG {
return (this.s1 / 30269.0 + this.s2 / 30307.0 + this.s3 / 30323.0) % 1.0;
}
}
export function SFC32RNG(seed: string): () => number {
let h = 1779033703 ^ seed.length;
for (let i = 0; i < seed.length; i++) {
h = Math.imul(h ^ seed.charCodeAt(i), 3432918353);
h = (h << 13) | (h >>> 19);
}
const genSeed = (): number => {
h = Math.imul(h ^ (h >>> 16), 2246822507);
h = Math.imul(h ^ (h >>> 13), 3266489909);
return (h ^= h >>> 16) >>> 0;
};
let a = genSeed();
let b = genSeed();
let c = genSeed();
let d = genSeed();
return (): number => {
a >>>= 0;
b >>>= 0;
c >>>= 0;
d >>>= 0;
let t = (a + b) | 0;
a = b ^ (b >>> 9);
b = (c + (c << 3)) | 0;
c = (c << 21) | (c >>> 11);
d = (d + 1) | 0;
t = (t + d) | 0;
c = (c + t) | 0;
return (t >>> 0) / 4294967296;
};
}

@ -159,7 +159,7 @@ export function SlotMachine(props: IProps): React.ReactElement {
const copy = index.slice();
for (let i = 0; i < copy.length; i++) {
if (copy[i] === locks[i] && !stoppedOne) continue;
copy[i] = (copy[i] - 1 >= 0) ? copy[i] - 1 : symbols.length - 1;
copy[i] = copy[i] - 1 >= 0 ? copy[i] - 1 : symbols.length - 1;
stoppedOne = true;
}
@ -170,7 +170,7 @@ export function SlotMachine(props: IProps): React.ReactElement {
}
}
function getTable(index:number[], symbols:string[]): string[][] {
function getTable(index: number[], symbols: string[]): string[][] {
return [
[
symbols[(index[0] + symbols.length - 1) % symbols.length],
@ -209,7 +209,7 @@ export function SlotMachine(props: IProps): React.ReactElement {
]);
}
function checkWinnings(t:string[][]): void {
function checkWinnings(t: string[][]): void {
const getPaylineData = function (payline: number[][]): string[] {
const data = [];
for (const point of payline) {

@ -116,8 +116,8 @@ export const CONSTANTS: {
TotalNumBitNodes: number;
LatestUpdate: string;
} = {
VersionString: "1.6.0",
VersionNumber: 12,
VersionString: "1.6.3",
VersionNumber: 13,
// Speed (in ms) at which the main loop is updated
_idleSpeed: 200,
@ -287,105 +287,19 @@ export const CONSTANTS: {
TotalNumBitNodes: 24,
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.
* Credit to @nickofolas for his incredible work.
* 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.
** 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.
`,
};

@ -1,5 +1,5 @@
import { IPlayer } from 'src/PersonObjects/IPlayer';
import { MaterialSizes } from './MaterialSizes';
import { IPlayer } from "src/PersonObjects/IPlayer";
import { MaterialSizes } from "./MaterialSizes";
import { ICorporation } from "./ICorporation";
import { IIndustry } from "./IIndustry";
import { IndustryStartingCosts, IndustryResearchTrees } from "./IndustryData";
@ -64,7 +64,7 @@ export function UnlockUpgrade(corporation: ICorporation, upgrade: CorporationUnl
if (corporation.funds < upgrade[1]) {
throw new Error("Insufficient funds");
}
if(corporation.unlockUpgrades[upgrade[0]] === 1){
if (corporation.unlockUpgrades[upgrade[0]] === 1) {
throw new Error(`You have already unlocked the ${upgrade[2]} upgrade!`);
}
corporation.unlock(upgrade);
@ -257,7 +257,7 @@ export function BulkPurchase(corp: ICorporation, warehouse: Warehouse, material:
if (isNaN(amt) || amt < 0) {
throw new Error(`Invalid input amount`);
}
if (amt * matSize > maxAmount) {
if (amt * matSize <= maxAmount) {
throw new Error(`You do not have enough warehouse size to fit this purchase`);
}
const cost = amt * material.bCost;
@ -295,7 +295,7 @@ export function BuyBackShares(corporation: ICorporation, player: IPlayer, numSha
if (numShares > corporation.issuedShares) throw new Error("You don't have that many shares to buy!");
if (!corporation.public) throw new Error("You haven't gone public!");
const buybackPrice = corporation.sharePrice * 1.1;
if (player.money < (numShares * buybackPrice)) throw new Error("You cant afford that many shares!");
if (player.money < numShares * buybackPrice) throw new Error("You cant afford that many shares!");
corporation.numShares += numShares;
corporation.issuedShares -= numShares;
player.loseMoney(numShares * buybackPrice, "corporation");
@ -404,13 +404,13 @@ export function MakeProduct(
if (corp.funds < designInvest + marketingInvest) {
throw new Error("You don't have enough company funds to make this large of an investment");
}
let maxProducts = 3
let maxProducts = 3;
if (division.hasResearch("uPgrade: Capacity.II")) {
maxProducts = 5
maxProducts = 5;
} else if (division.hasResearch("uPgrade: Capacity.I")) {
maxProducts = 4
maxProducts = 4;
}
const products = division.products
const products = division.products;
if (Object.keys(products).length >= maxProducts) {
throw new Error(`You are already at the max products (${maxProducts}) for division: ${division.name}!`);
}
@ -445,7 +445,13 @@ export function Research(division: IIndustry, researchName: string): void {
division.researched[researchName] = true;
}
export function ExportMaterial(divisionName: string, cityName: string, material: Material, amt: string, division?: Industry): void {
export function ExportMaterial(
divisionName: string,
cityName: string,
material: Material,
amt: string,
division?: Industry,
): void {
// Sanitize amt
let sanitizedAmt = amt.replace(/\s+/g, "").toUpperCase();
sanitizedAmt = sanitizedAmt.replace(/[^-()\d/*+.MAX]/g, "");

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

@ -10,7 +10,7 @@ export interface IActiveFragmentParams {
export class ActiveFragment {
id: number;
avgCharge: number;
highestCharge: number;
numCharge: number;
rotation: number;
x: number;
@ -21,16 +21,16 @@ export class ActiveFragment {
this.id = params.fragment.id;
this.x = params.x;
this.y = params.y;
this.avgCharge = 0;
this.highestCharge = 0;
this.numCharge = 0;
this.rotation = params.rotation;
} else {
this.id = -1;
this.x = -1;
this.y = -1;
this.avgCharge = -1;
this.numCharge = -1;
this.rotation = -1;
this.highestCharge = 0;
this.numCharge = 0;
this.rotation = 0;
}
}
@ -71,7 +71,7 @@ export class ActiveFragment {
const fragment = FragmentById(this.id);
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 });
c.avgCharge = this.avgCharge;
c.highestCharge = this.highestCharge;
c.numCharge = this.numCharge;
return c;
}

@ -61,7 +61,7 @@ export function Effect(tpe: FragmentType): string {
return "+x% hacknet production";
}
case FragmentType.HacknetCost: {
return "x% cheaper hacknet cost";
return "-x% cheaper hacknet cost";
}
case FragmentType.Rep: {
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 { ActiveFragment } from "./ActiveFragment";
import { FragmentType } from "./FragmentType";
@ -19,19 +19,23 @@ export class StaneksGift implements IStaneksGift {
fragments: ActiveFragment[] = [];
baseSize(): number {
return StanekConstants.BaseSize + BitNodeMultipliers.StaneksGiftExtraSize + Player.sourceFileLvl(13)
return StanekConstants.BaseSize + BitNodeMultipliers.StaneksGiftExtraSize + Player.sourceFileLvl(13);
}
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 {
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 {
af.avgCharge = (af.numCharge * af.avgCharge + threads) / (af.numCharge + 1);
af.numCharge++;
if (threads > af.highestCharge) {
af.numCharge = (af.highestCharge * af.numCharge) / threads + 1;
af.highestCharge = threads;
} else {
af.numCharge += threads / af.highestCharge;
}
const cotmg = Factions[FactionNames.ChurchOfTheMachineGod];
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) {
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 {
@ -126,7 +130,7 @@ export class StaneksGift implements IStaneksGift {
clearCharge(): void {
this.fragments.forEach((f) => {
f.avgCharge = 0;
f.highestCharge = 0;
f.numCharge = 0;
});
}

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

@ -48,16 +48,18 @@ export function FragmentInspector(props: IProps): React.ReactElement {
}
const f = props.fragment.fragment();
let charge = `${numeralWrapper.formatStaneksGiftCharge(props.fragment.avgCharge)} avg. * ${
props.fragment.numCharge
} times`;
let charge = numeralWrapper.formatStaneksGiftCharge(props.fragment.highestCharge * props.fragment.numCharge);
let effect = "N/A";
// Boosters and cooling don't deal with heat.
if ([FragmentType.Booster, FragmentType.None, FragmentType.Delete].includes(f.type)) {
charge = "N/A";
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));
} 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 (

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

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

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

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

@ -27,20 +27,20 @@ export function Stanek(): React.ReactElement {
function addCharge(): void {
staneksGift.fragments.forEach((f) => {
f.avgCharge = 1e21;
f.highestCharge = 1e21;
f.numCharge = 1e21;
});
}
function modCharge(modify: number): (x: 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 {
staneksGift.fragments.forEach((f) => {
f.avgCharge = 0;
f.highestCharge = 0;
f.numCharge = 0;
});
}

@ -8,6 +8,7 @@ import { CONSTANTS } from "../Constants";
import { Faction } from "./Faction";
import { Factions } from "./Factions";
import { Player } from "../Player";
import { IPlayer } from "../PersonObjects/IPlayer";
import { Settings } from "../Settings/Settings";
import {
getHackingWorkRepGain,
@ -19,6 +20,7 @@ import { SourceFileFlags } from "../SourceFile/SourceFileFlags";
import { dialogBoxCreate } from "../ui/React/DialogBox";
import { InvitationEvent } from "./ui/InvitationModal";
import { FactionNames } from "./data/FactionNames";
import { SFC32RNG } from "../Casino/RNG";
export function inviteToFaction(faction: Faction): void {
Player.receiveInvite(faction.name);
@ -32,9 +34,8 @@ export function joinFaction(faction: Faction): void {
if (faction.isMember) return;
faction.isMember = true;
Player.factions.push(faction.name);
const allFactions = Object.values(FactionNames).map(faction => faction as string)
Player.factions.sort((a, b) =>
allFactions.indexOf(a) - allFactions.indexOf(b));
const allFactions = Object.values(FactionNames).map((faction) => faction as string);
Player.factions.sort((a, b) => allFactions.indexOf(a) - allFactions.indexOf(b));
const factionInfo = faction.getInfo();
//Determine what factions you are banned from now that you have joined this faction
@ -193,3 +194,43 @@ export function processPassiveFactionRepGain(numCycles: number): void {
faction.playerReputation += rate * numCycles * Player.faction_rep_mult * BitNodeMultipliers.FactionPassiveRepGain;
}
}
export const getFactionAugmentationsFiltered = (player: IPlayer, faction: Faction): string[] => {
// If player has a gang with this faction, return (almost) all augmentations
if (player.hasGangWith(faction.name)) {
let augs = Object.values(Augmentations);
// Remove special augs
augs = augs.filter((a) => !a.isSpecial);
const blacklist: string[] = [AugmentationNames.NeuroFluxGovernor];
if (player.bitNodeN !== 2) {
// TRP is not available outside of BN2 for Gangs
blacklist.push(AugmentationNames.TheRedPill);
}
const rng = SFC32RNG(`BN${player.bitNodeN}.${player.sourceFileLvl(player.bitNodeN)}`);
// Remove faction-unique augs that don't belong to this faction
const uniqueFilter = (a: Augmentation): boolean => {
// Keep all the non-unique one
if (a.factions.length > 1) {
return true;
}
// Keep all the ones that this faction has anyway.
if (faction.augmentations.includes(a.name)) {
return true;
}
return rng() >= 1 - BitNodeMultipliers.GangUniqueAugs;
};
augs = augs.filter(uniqueFilter);
// Remove blacklisted augs
augs = augs.filter((a) => !blacklist.includes(a.name));
return augs.map((a) => a.name);
}
return faction.augmentations.slice();
};

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

@ -10,7 +10,7 @@ import { AugmentationNames } from "../../Augmentation/data/AugmentationNames";
import { Faction } from "../Faction";
import { PurchaseAugmentationsOrderSetting } from "../../Settings/SettingEnums";
import { Settings } from "../../Settings/Settings";
import { hasAugmentationPrereqs } from "../FactionHelpers";
import { hasAugmentationPrereqs, getFactionAugmentationsFiltered } from "../FactionHelpers";
import { use } from "../../ui/Context";
import { Reputation } from "../../ui/React/Reputation";
@ -23,7 +23,7 @@ import Typography from "@mui/material/Typography";
import Tooltip from "@mui/material/Tooltip";
import TableBody from "@mui/material/TableBody";
import Table from "@mui/material/Table";
import { CONSTANTS } from "../../Constants";
import { getGenericAugmentationPriceMultiplier } from "../../Augmentation/AugmentationHelpers";
type IProps = {
faction: Faction;
@ -32,8 +32,6 @@ type IProps = {
export function AugmentationsPage(props: IProps): React.ReactElement {
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];
@ -42,25 +40,7 @@ export function AugmentationsPage(props: IProps): React.ReactElement {
}
function getAugs(): string[] {
if (isPlayersGang) {
let augs = Object.values(Augmentations);
// Remove special augs.
augs = augs.filter((a) => !a.isSpecial);
if (player.bitNodeN !== 2) {
// Remove faction-unique augs outside BN2. (But keep the one for this faction.)
augs = augs.filter((a) => a.factions.length > 1 || props.faction.augmentations.includes(a.name));
// Remove blacklisted augs.
const blacklist = [AugmentationNames.NeuroFluxGovernor, AugmentationNames.TheRedPill];
augs = augs.filter((a) => !blacklist.includes(a.name));
}
return augs.map((a) => a.name);
} else {
return props.faction.augmentations.slice();
}
return getFactionAugmentationsFiltered(player, props.faction);
}
function getAugsSorted(): string[] {
@ -184,10 +164,7 @@ export function AugmentationsPage(props: IProps): React.ReactElement {
</>
);
}
const mult = Math.pow(
CONSTANTS.MultipleAugMultiplier * [1, 0.96, 0.94, 0.93][player.sourceFileLvl(11)],
player.queuedAugmentations.length,
);
return (
<>
<Button onClick={props.routeToMainPage}>Back</Button>
@ -208,7 +185,9 @@ export function AugmentationsPage(props: IProps): React.ReactElement {
</Typography>
}
>
<Typography>Price multiplier: x {numeralWrapper.formatMultiplier(mult)}</Typography>
<Typography>
Price multiplier: x {numeralWrapper.formatMultiplier(getGenericAugmentationPriceMultiplier())}
</Typography>
</Tooltip>
</Box>
<Button onClick={() => switchSortOrder(PurchaseAugmentationsOrderSetting.Cost)}>Sort by Cost</Button>

@ -2,14 +2,12 @@ import React, { useEffect, useState } from "react";
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 { Table, TableCell } from "../../ui/React/Table";
import { IRouter } from "../../ui/Router";
import { Faction } from "../Faction";
import { joinFaction } from "../FactionHelpers";
import { joinFaction, getFactionAugmentationsFiltered } from "../FactionHelpers";
import { Factions } from "../Factions";
import { FactionNames } from "../data/FactionNames";
@ -52,26 +50,7 @@ export function FactionsRoot(props: IProps): React.ReactElement {
}
const getAugsLeft = (faction: Faction, player: IPlayer): number => {
const isPlayersGang = player.inGang() && player.getGangName() === faction.name;
let augs: string[] = [];
if (isPlayersGang) {
for (const augName of Object.keys(Augmentations)) {
const aug = Augmentations[augName];
if (
augName === AugmentationNames.NeuroFluxGovernor ||
(augName === AugmentationNames.TheRedPill && player.bitNodeN !== 2) ||
// Special augs (i.e. Bladeburner augs)
aug.isSpecial ||
// Exclusive augs (i.e. QLink)
(aug.factions.length <= 1 && !faction.augmentations.includes(augName) && player.bitNodeN !== 2)
)
continue;
augs.push(augName);
}
} else {
augs = faction.augmentations.slice();
}
const augs = getFactionAugmentationsFiltered(player, faction);
return augs.filter((augmentation: string) => !player.hasAugmentation(augmentation)).length;
};

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

@ -7,6 +7,7 @@ import { random } from "../utils";
import { interpolate } from "./Difficulty";
import { BlinkingCursor } from "./BlinkingCursor";
import Typography from "@mui/material/Typography";
import { KEY } from "../../utils/helpers/keyCodes";
interface Difficulty {
[key: string]: number;
@ -36,7 +37,7 @@ export function BackwardGame(props: IMinigameProps): React.ReactElement {
function press(this: Document, event: KeyboardEvent): void {
event.preventDefault();
if (event.key === "Backspace") return;
if (event.key === KEY.BACKSPACE) return;
const nextGuess = guess + event.key.toUpperCase();
if (!answer.startsWith(nextGuess)) props.onFailure();
else if (answer === nextGuess) props.onSuccess();

@ -7,6 +7,7 @@ import { random } from "../utils";
import { interpolate } from "./Difficulty";
import { BlinkingCursor } from "./BlinkingCursor";
import Typography from "@mui/material/Typography";
import { KEY } from "../../utils/helpers/keyCodes";
interface Difficulty {
[key: string]: number;
@ -29,28 +30,29 @@ const difficulties: {
function generateLeftSide(difficulty: Difficulty): string {
let str = "";
const options = [KEY.OPEN_BRACKET, KEY.LESS_THAN, KEY.OPEN_PARENTHESIS, KEY.OPEN_BRACE];
const length = random(difficulty.min, difficulty.max);
for (let i = 0; i < length; i++) {
str += ["[", "<", "(", "{"][Math.floor(Math.random() * 4)];
str += options[Math.floor(Math.random() * 4)];
}
return str;
}
function getChar(event: KeyboardEvent): string {
if (event.key === ")") return ")";
if (event.key === "]") return "]";
if (event.key === "}") return "}";
if (event.key === ">") return ">";
if (event.key === KEY.CLOSE_PARENTHESIS) return KEY.CLOSE_PARENTHESIS;
if (event.key === KEY.CLOSE_BRACKET) return KEY.CLOSE_BRACKET;
if (event.key === KEY.CLOSE_BRACE) return KEY.CLOSE_BRACE;
if (event.key === KEY.GREATER_THAN) return KEY.GREATER_THAN;
return "";
}
function match(left: string, right: string): boolean {
return (
(left === "[" && right === "]") ||
(left === "<" && right === ">") ||
(left === "(" && right === ")") ||
(left === "{" && right === "}")
(left === KEY.OPEN_BRACKET && right === KEY.CLOSE_BRACKET) ||
(left === KEY.LESS_THAN && right === KEY.GREATER_THAN) ||
(left === KEY.OPEN_PARENTHESIS && right === KEY.CLOSE_PARENTHESIS) ||
(left === KEY.OPEN_BRACE && right === KEY.CLOSE_BRACE)
);
}

@ -5,6 +5,8 @@ import { KeyHandler } from "./KeyHandler";
import { GameTimer } from "./GameTimer";
import { interpolate } from "./Difficulty";
import Typography from "@mui/material/Typography";
import { KEY } from "../../utils/helpers/keyCodes";
import { downArrowSymbol, upArrowSymbol } from "../utils";
interface Difficulty {
[key: string]: number;
@ -34,15 +36,15 @@ export function BribeGame(props: IMinigameProps): React.ReactElement {
function press(this: Document, event: KeyboardEvent): void {
event.preventDefault();
const k = event.key;
if (k === " ") {
if (k === KEY.SPACE) {
if (positive.includes(choices[index])) props.onSuccess();
else props.onFailure();
return;
}
let newIndex = index;
if (["ArrowUp", "w", "ArrowRight", "d"].includes(k)) newIndex++;
if (["ArrowDown", "s", "ArrowLeft", "a"].includes(k)) newIndex--;
if ([KEY.UP_ARROW, KEY.W, KEY.RIGHT_ARROW, KEY.D].map((key) => key as string).includes(k)) newIndex++;
if ([KEY.DOWN_ARROW, KEY.S, KEY.LEFT_ARROW, KEY.A].map((key) => key as string).includes(k)) newIndex--;
while (newIndex < 0) newIndex += choices.length;
while (newIndex > choices.length - 1) newIndex -= choices.length;
setIndex(newIndex);
@ -57,13 +59,13 @@ export function BribeGame(props: IMinigameProps): React.ReactElement {
</Grid>
<Grid item xs={6}>
<Typography variant="h5" color="primary">
{upArrowSymbol}
</Typography>
<Typography variant="h5" color="primary">
{choices[index]}
</Typography>
<Typography variant="h5" color="primary">
{downArrowSymbol}
</Typography>
</Grid>
</Grid>

@ -3,7 +3,7 @@ import Grid from "@mui/material/Grid";
import { IMinigameProps } from "./IMinigameProps";
import { KeyHandler } from "./KeyHandler";
import { GameTimer } from "./GameTimer";
import { random, getArrow } from "../utils";
import { random, getArrow, rightArrowSymbol, leftArrowSymbol, upArrowSymbol, downArrowSymbol } from "../utils";
import { interpolate } from "./Difficulty";
import Typography from "@mui/material/Typography";
@ -56,7 +56,7 @@ export function CheatCodeGame(props: IMinigameProps): React.ReactElement {
}
function generateCode(difficulty: Difficulty): string {
const arrows = ["←", "→", "↑", "↓"];
const arrows = [leftArrowSymbol, rightArrowSymbol, upArrowSymbol, downArrowSymbol];
let code = "";
for (let i = 0; i < random(difficulty.min, difficulty.max); i++) {
let arrow = arrows[Math.floor(4 * Math.random())];

@ -4,8 +4,9 @@ import { IMinigameProps } from "./IMinigameProps";
import { KeyHandler } from "./KeyHandler";
import { GameTimer } from "./GameTimer";
import { interpolate } from "./Difficulty";
import { getArrow } from "../utils";
import { downArrowSymbol, getArrow, leftArrowSymbol, rightArrowSymbol, upArrowSymbol } from "../utils";
import Typography from "@mui/material/Typography";
import { KEY } from "../../utils/helpers/keyCodes";
interface Difficulty {
[key: string]: number;
@ -41,16 +42,16 @@ export function Cyberpunk2077Game(props: IMinigameProps): React.ReactElement {
const move = [0, 0];
const arrow = getArrow(event);
switch (arrow) {
case "↑":
case upArrowSymbol:
move[1]--;
break;
case "←":
case leftArrowSymbol:
move[0]--;
break;
case "↓":
case downArrowSymbol:
move[1]++;
break;
case "→":
case rightArrowSymbol:
move[0]++;
break;
}
@ -59,7 +60,7 @@ export function Cyberpunk2077Game(props: IMinigameProps): React.ReactElement {
next[1] = (next[1] + grid.length) % grid.length;
setPos(next);
if (event.key === " ") {
if (event.key === KEY.SPACE) {
const selected = grid[pos[1]][pos[0]];
const expected = answer[index];
if (selected !== expected) {

@ -4,8 +4,9 @@ import { IMinigameProps } from "./IMinigameProps";
import { KeyHandler } from "./KeyHandler";
import { GameTimer } from "./GameTimer";
import { interpolate } from "./Difficulty";
import { getArrow } from "../utils";
import { downArrowSymbol, getArrow, leftArrowSymbol, rightArrowSymbol, upArrowSymbol } from "../utils";
import Typography from "@mui/material/Typography";
import { KEY } from "../../utils/helpers/keyCodes";
interface Difficulty {
[key: string]: number;
@ -42,16 +43,16 @@ export function MinesweeperGame(props: IMinigameProps): React.ReactElement {
const move = [0, 0];
const arrow = getArrow(event);
switch (arrow) {
case "↑":
case upArrowSymbol:
move[1]--;
break;
case "←":
case leftArrowSymbol:
move[0]--;
break;
case "↓":
case downArrowSymbol:
move[1]++;
break;
case "→":
case rightArrowSymbol:
move[0]++;
break;
}
@ -60,7 +61,7 @@ export function MinesweeperGame(props: IMinigameProps): React.ReactElement {
next[1] = (next[1] + minefield.length) % minefield.length;
setPos(next);
if (event.key == " ") {
if (event.key == KEY.SPACE) {
if (!minefield[pos[1]][pos[0]]) {
props.onFailure();
return;

@ -5,6 +5,7 @@ import { KeyHandler } from "./KeyHandler";
import { GameTimer } from "./GameTimer";
import { interpolate } from "./Difficulty";
import Typography from "@mui/material/Typography";
import { KEY } from "../../utils/helpers/keyCodes";
interface Difficulty {
[key: string]: number;
@ -30,7 +31,7 @@ export function SlashGame(props: IMinigameProps): React.ReactElement {
function press(this: Document, event: KeyboardEvent): void {
event.preventDefault();
if (event.key !== " ") return;
if (event.key !== KEY.SPACE) return;
if (phase !== 2) {
props.onFailure();
} else {

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