merge dev

This commit is contained in:
phyzical 2022-04-07 16:27:23 +08:00
commit c70f59ac54
194 changed files with 4105 additions and 3294 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.

@ -6,18 +6,18 @@ async function enableAchievementsInterval(window) {
// If the Steam API could not be initialized on game start, we'll abort this.
if (global.greenworksError) return;
// This is backward but the game fills in an array called `document.achievements` and we retrieve it from
// This is backward but the game fills in an array called `document.achievements` and we retrieve it from
// here. Hey if it works it works.
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 {
const playerAchievements = await window.webContents.executeJavaScript("document.achievements");
for (const ach of playerAchievements) {
if (!steamAchievements.includes(ach)) continue; // Don't try activating achievements that don't exist Steam-side
if (playerAchieved.includes(ach)) continue; // Don't spam achievements that have already been recorded
if (playerAchieved.includes(ach)) continue; // Don't spam achievements that have already been recorded
log.info(`Granting Steam achievement ${ach}`);
greenworks.activateAchievement(ach, () => undefined);
playerAchieved.push(ach);
@ -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 => {
log.warn(`Failed to get Steam achievement ${name} status: ${err}`);
resolve("");
});
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({
success: false,
msg: 'Invalid authentication token'
}));
res.end(
JSON.stringify({
success: false,
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({
success: false,
msg: 'Invalid body data'
}));
res.end(
JSON.stringify({
success: false,
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({
success: false,
msg: 'Invalid script update request - No data'
}));
res.end(
JSON.stringify({
success: false,
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({
success: result.res,
msg: result.msg,
data: result.data
}));
res.end(
JSON.stringify({
success: result.res,
msg: result.msg,
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() {
next_process_func();
}, function(err) { error_process(err, error_callback); });
}, function(err) { error_process(err, error_callback); });
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);
},
);
}
// 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," +
"but steam_appid.txt is missing. Expected to find it in: " +
require('path').resolve('steam_appid.txt'));
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"),
);
}
if (!/^\d+ *\r?\n?$/.test(appId)) {
throw new Error("Steam initialization failed. " +
"steam_appid.txt appears to be invalid; " +
"it should contain a numeric ID: " + appId);
throw new Error(
"Steam initialization failed. " +
"steam_appid.txt appears to be invalid; " +
"it should contain a numeric ID: " +
appId,
);
}
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());
}
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(),
);
};
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);
@ -159,43 +160,51 @@ function setStopProcessHandler(app, window, enabled) {
const playtime = window.gameInfo.player.playtime;
log.silly(window.gameInfo);
if (playtime > minimumPlaytime) {
saveToCloud(save);
saveToCloud(save);
} else {
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 ...")
try {
const playerId = window.gameInfo.player.identifier;
await storage.pushGameSaveToSteamCloud(save, playerId);
log.silly("Saved Game to Steam Cloud");
} catch (error) {
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 });
const saveToCloud = debounce(
async (save) => {
log.debug("Saving to Steam Cloud ...");
try {
const playerId = window.gameInfo.player.identifier;
await storage.pushGameSaveToSteamCloud(save, playerId);
log.silly("Saved Game to Steam Cloud");
} catch (error) {
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 },
);
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('\\', '\\\\')}'`);
} 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 });
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("\\", "\\\\")}'`);
} 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 },
);
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}) => {
if (response === 1) {
utils.openExternal('https://github.com/bitburner-official/bitburner-vscode');
}
});
}
}
]
})
.then(({ response }) => {
if (response === 1) {
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,38 +1,36 @@
/* 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", {
send: (channel, data) => {
log.log("Send on channel " + channel)
// whitelist channels
let validChannels = [
"get-save-data-response",
"get-save-info-response",
"push-game-saved",
"push-game-ready",
"push-import-result",
"push-disable-restore",
];
if (validChannels.includes(channel)) {
ipcRenderer.send(channel, data);
}
},
receive: (channel, func) => {
log.log("Receive on channel " + channel)
let validChannels = [
"get-save-data-request",
"get-save-info-request",
"push-save-request",
"trigger-save",
"trigger-game-export",
"trigger-scripts-export",
];
if (validChannels.includes(channel)) {
// Deliberately strip event as it includes `sender`
ipcRenderer.on(channel, (event, ...args) => func(...args));
}
contextBridge.exposeInMainWorld("electronBridge", {
send: (channel, data) => {
log.log("Send on channel " + channel);
// whitelist channels
let validChannels = [
"get-save-data-response",
"get-save-info-response",
"push-game-saved",
"push-game-ready",
"push-import-result",
"push-disable-restore",
];
if (validChannels.includes(channel)) {
ipcRenderer.send(channel, data);
}
}
);
},
receive: (channel, func) => {
log.log("Receive on channel " + channel);
let validChannels = [
"get-save-data-request",
"get-save-info-request",
"push-save-request",
"trigger-save",
"trigger-game-export",
"trigger-scripts-export",
];
if (validChannels.includes(channel)) {
// 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,61 +9,61 @@ 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'],
defaultId: 0,
checkboxLabel: 'Kill all running scripts',
checkboxChecked: true,
noLink: true,
}).then(({response, checkboxChecked}) => {
if (response === 0) {
reloadAndKill(window, checkboxChecked);
} else {
attachUnresponsiveAppHandler(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"],
defaultId: 0,
checkboxLabel: "Kill all running scripts",
checkboxChecked: true,
noLink: true,
})
.then(({ response, checkboxChecked }) => {
if (response === 0) {
reloadAndKill(window, checkboxChecked);
} else {
attachUnresponsiveAppHandler(window);
}
});
}
function attachUnresponsiveAppHandler(window) {
window.unresponsiveHandler = () => promptForReload(window);
window.on('unresponsive', window.unresponsiveHandler);
window.on("unresponsive", window.unresponsiveHandler);
}
function detachUnresponsiveAppHandler(window) {
window.off('unresponsive', window.unresponsiveHandler);
window.off("unresponsive", window.unresponsiveHandler);
}
function showErrorBox(title, error) {
dialog.showErrorBox(
title,
`${error.name}\n\n${error.message}`
);
dialog.showErrorBox(title, `${error.name}\n\n${error.message}`);
}
function exportSaveFromIndexedDb() {
@ -71,15 +71,15 @@ function exportSaveFromIndexedDb() {
const dbRequest = indexedDB.open("bitburnerSave");
dbRequest.onsuccess = () => {
const db = dbRequest.result;
const transaction = db.transaction(['savestring'], "readonly");
const store = transaction.objectStore('savestring');
const request = store.get('save');
const transaction = db.transaction(["savestring"], "readonly");
const store = transaction.objectStore("savestring");
const request = store.get("save");
request.onsuccess = () => {
const file = new Blob([request.result], {type: 'text/plain'});
const file = new Blob([request.result], { type: "text/plain" });
const a = document.createElement("a");
const url = URL.createObjectURL(file);
a.href = url;
a.download = 'save.json';
a.download = "save.json";
document.body.appendChild(a);
a.click();
setTimeout(function () {
@ -87,24 +87,21 @@ function exportSaveFromIndexedDb() {
window.URL.revokeObjectURL(url);
resolve();
}, 0);
}
}
};
};
});
}
async function exportSave(window) {
await window.webContents
.executeJavaScript(`${exportSaveFromIndexedDb.toString()}; exportSaveFromIndexedDb();`, true);
await window.webContents.executeJavaScript(`${exportSaveFromIndexedDb.toString()}; exportSaveFromIndexedDb();`, true);
}
async function writeTerminal(window, message, type = null) {
await window.webContents
.executeJavaScript(`window.appNotifier.terminal("${message}", "${type}");`, true)
await window.webContents.executeJavaScript(`window.appNotifier.terminal("${message}", "${type}");`, true);
}
async function writeToast(window, message, type = "info", duration = 2000) {
await window.webContents
.executeJavaScript(`window.appNotifier.toast("${message}", "${type}", ${duration});`, true)
await window.webContents.executeJavaScript(`window.appNotifier.toast("${message}", "${type}", ${duration});`, true);
}
function openExternal(url) {
@ -113,7 +110,7 @@ function openExternal(url) {
}
function getZoomFactor() {
const configZoom = config.get('zoom', 1);
const configZoom = config.get("zoom", 1);
return configZoom;
}
@ -121,14 +118,20 @@ function setZoomFactor(window, zoom = null) {
if (zoom === null) {
zoom = 1;
} else {
config.set('zoom', zoom);
config.set("zoom", zoom);
}
window.webContents.setZoomFactor(zoom);
}
module.exports = {
reloadAndKill, showErrorBox, exportSave,
attachUnresponsiveAppHandler, detachUnresponsiveAppHandler,
openExternal, writeTerminal, writeToast,
getZoomFactor, setZoomFactor,
}
reloadAndKill,
showErrorBox,
exportSave,
attachUnresponsiveAppHandler,
detachUnresponsiveAppHandler,
openExternal,
writeTerminal,
writeToast,
getZoomFactor,
setZoomFactor,
};

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

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

@ -3,7 +3,7 @@ import React from "react";
import { Box, Typography } from "@mui/material";
import { Achievement } from "./Achievements";
import { Settings } from "../Settings/Settings"
import { Settings } from "../Settings/Settings";
import { AchievementIcon } from "./AchievementIcon";
interface IProps {
@ -13,43 +13,58 @@ interface IProps {
cssFiltersLocked: string;
}
export function AchievementEntry({ achievement, unlockedOn, cssFiltersUnlocked, cssFiltersLocked }: IProps): JSX.Element {
export function AchievementEntry({
achievement,
unlockedOn,
cssFiltersUnlocked,
cssFiltersLocked,
}: IProps): JSX.Element {
if (!achievement) return <></>;
const isUnlocked = !!unlockedOn;
const mainColor = isUnlocked ? Settings.theme.primary : Settings.theme.secondarylight;
let achievedOn = '';
let achievedOn = "";
if (unlockedOn) {
achievedOn = new Date(unlockedOn).toLocaleString();
}
return (
<Box sx={{
border: `1px solid ${Settings.theme.well}`, mb: 2
}}>
<Box sx={{
display: 'flex',
flexDirection: 'row',
flexWrap: 'wrap',
}}>
<Box
sx={{
border: `1px solid ${Settings.theme.well}`,
mb: 2,
}}
>
<Box
sx={{
display: "flex",
flexDirection: "row",
flexWrap: "wrap",
}}
>
<AchievementIcon
achievement={achievement} unlocked={isUnlocked} size="72px"
colorFilters={isUnlocked ? cssFiltersUnlocked: cssFiltersLocked} />
<Box sx={{
display: 'flex',
flexDirection: 'column',
justifyContent: 'center',
px: 1
}}>
<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 => ({
achievement,
unlockedOn: playerAchievements.find(playerAchievement => playerAchievement.ID === achievement.ID)?.unlockedOn,
})).sort((a, b) => (b.unlockedOn ?? 0) - (a.unlockedOn ?? 0));
const data = achievements
.map((achievement) => ({
achievement,
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>
@ -89,7 +100,7 @@ export function AchievementList({ achievements, playerAchievements }: IProps): J
</Typography>
</AccordionSummary>
<AccordionDetails>
<Typography sx={{ mt: 1 }}>
<Typography sx={{ mt: 1 }}>
{unavailable.length} additional achievements hidden behind content you don't have access to.
</Typography>
</AccordionDetails>
@ -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`

@ -112,7 +112,10 @@ export function updateAugmentationCosts(): void {
} else if (augmentationToUpdate.factions.includes(FactionNames.Infiltrators)) {
updateInfiltratorCosts(augmentationToUpdate);
} else {
augmentationToUpdate.baseCost = augmentationToUpdate.startingCost * getGenericAugmentationPriceMultiplier();
augmentationToUpdate.baseCost =
augmentationToUpdate.startingCost *
getGenericAugmentationPriceMultiplier() *
BitNodeMultipliers.AugmentationMoneyCost;
}
}
}

@ -134,9 +134,7 @@ export class Action implements IAction {
for (const decay of Object.keys(this.decays)) {
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`);
}
}
}

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

@ -26,8 +26,8 @@ export const ConsoleHelpText: {
"Usage: automate [var] [val] [hi/low]",
" ",
"A simple way to automate your Bladeburner actions. This console command can be used " +
"to automatically start an action when your stamina rises above a certain threshold, and " +
"automatically switch to another action when your stamina drops below another threshold.",
"to automatically start an action when your stamina rises above a certain threshold, and " +
"automatically switch to another action when your stamina drops below another threshold.",
" ",
" automate status - Check the current status of your automation and get a brief description of what it'll do",
" automate en - Enable the automation feature",
@ -41,9 +41,9 @@ export const ConsoleHelpText: {
" automate general 'Field Analysis' low",
" ",
"Using the four console commands above will set the automation to perform Tracking contracts " +
"if your stamina is 100 or higher, and then switch to Field Analysis if your stamina drops below " +
"50. Note that when setting the action, the name of the action is CASE-SENSITIVE. It must " +
"exactly match whatever the name is in the UI.",
"if your stamina is 100 or higher, and then switch to Field Analysis if your stamina drops below " +
"50. Note that when setting the action, the name of the action is CASE-SENSITIVE. It must " +
"exactly match whatever the name is in the UI.",
" ",
],
clear: ["Usage: clear", " ", "Clears the console", " "],
@ -52,8 +52,8 @@ export const ConsoleHelpText: {
"Usage: help [command]",
" ",
"Running 'help' with no arguments displays the general help text, which lists all console commands " +
"and a brief description of what they do. A command can be specified to get more specific help text " +
"about that particular command. For example:",
"and a brief description of what they do. A command can be specified to get more specific help text " +
"about that particular command. For example:",
" ",
" help automate",
" ",
@ -64,8 +64,8 @@ export const ConsoleHelpText: {
"Usage: log [en/dis] [type]",
" ",
"Enable or disable logging. By default, the results of completing actions such as contracts/operations are logged " +
"in the console. There are also random events that are logged in the console as well. The five categories of " +
"things that get logged are:",
"in the console. There are also random events that are logged in the console as well. The five categories of " +
"things that get logged are:",
" ",
"[general, contracts, ops, blackops, events]",
" ",
@ -91,8 +91,8 @@ export const ConsoleHelpText: {
" skill list",
" ",
"To display information about a specific skill, specify the name of the skill afterwards. " +
"Note that the name of the skill is case-sensitive. Enter it exactly as seen in the UI. If " +
"the name of the skill has whitespace, enclose the name of the skill in double quotation marks:",
"Note that the name of the skill is case-sensitive. Enter it exactly as seen in the UI. If " +
"the name of the skill has whitespace, enclose the name of the skill in double quotation marks:",
" ",
" skill list Reaper",
" skill list 'Digital Observer'",
@ -106,9 +106,9 @@ export const ConsoleHelpText: {
"Usage: start [type] [name]",
" ",
"Start an action. An action is specified by its type and its name. The " +
"name is case-sensitive. It must appear exactly as it does in the UI. If " +
"the name of the action has whitespace, enclose it in double quotation marks. " +
"Valid action types include:",
"name is case-sensitive. It must appear exactly as it does in the UI. If " +
"the name of the action has whitespace, enclose it in double quotation marks. " +
"Valid action types include:",
" ",
"[general, contract, op, blackop]",
" ",

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

@ -35,7 +35,7 @@ export function BladeburnerCinematic(): React.ReactElement {
router.toTerminal();
dialogBoxCreate(
`Visit the National Security Agency (NSA) to apply for their ${FactionNames.Bladeburners} ` +
"division! You will need 100 of each combat stat before doing this.",
"division! You will need 100 of each combat stat before doing this.",
);
}}
/>

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

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

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

@ -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)
@ -93,7 +95,7 @@ export function ExportModal(props: IProps): React.ReactElement {
<MenuItem key={division.name} value={division.name}>
{division.name}
</MenuItem>
))}
))}
</Select>
<Select onChange={onCityChange} value={city}>
{possibleCities.map((cityName: string) => {

@ -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,17 +149,17 @@ 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> : ""}
>
<Button
color={tutorial ? "error" : "primary"}
onClick={() => setPurchaseMaterialOpen(true)}
disabled={props.warehouse.smartSupplyEnabled && Object.keys(division.reqMats).includes(props.mat.name)}
>
{purchaseButtonText}
</Button>
<Button
color={tutorial ? "error" : "primary"}
onClick={() => setPurchaseMaterialOpen(true)}
disabled={props.warehouse.smartSupplyEnabled && Object.keys(division.reqMats).includes(props.mat.name)}
>
{purchaseButtonText}
</Button>
</Tooltip>
<PurchaseMaterialModal
mat={mat}

@ -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>
@ -200,8 +200,8 @@ function PublicButtons({ rerender }: IPublicButtonsProps): React.ReactElement {
const sellSharesTooltip = sellSharesOnCd
? "Cannot sell shares for " + corp.convertCooldownToString(corp.shareSaleCooldown)
: "Sell your shares in the company. The money earned from selling your " +
"shares goes into your personal account, not the Corporation's. " +
"This is one of the only ways to profit from your business venture.";
"shares goes into your personal account, not the Corporation's. " +
"This is one of the only ways to profit from your business venture.";
const issueNewSharesOnCd = corp.issueNewSharesCooldown > 0;
const issueNewSharesTooltip = issueNewSharesOnCd

@ -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;
@ -43,8 +43,8 @@ function Upgrade({ n, division }: INodeProps): React.ReactElement {
dialogBoxCreate(
`Researched ${n.text}. It may take a market cycle ` +
`(~${CorporationConstants.SecsPerMarketCycle} seconds) before the effects of ` +
`the Research apply.`,
`(~${CorporationConstants.SecsPerMarketCycle} seconds) before the effects of ` +
`the Research apply.`,
);
}
@ -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 {

@ -35,17 +35,17 @@ export function ThrowPartyModal(props: IProps): React.ReactElement {
if (cost === null || isNaN(cost) || cost < 0) {
dialogBoxCreate("Invalid value entered");
} else if (!canParty) {
dialogBoxCreate("You don't have enough company funds to throw a party!");
} else {
const mult = ThrowParty(corp, props.office, cost);
dialogBoxCreate(
"You threw a party for the office! The morale and happiness " +
"of each employee increased by " +
numeralWrapper.formatPercentage(mult - 1),
);
props.rerender();
props.onClose();
}
dialogBoxCreate("You don't have enough company funds to throw a party!");
} else {
const mult = ThrowParty(corp, props.office, cost);
dialogBoxCreate(
"You threw a party for the office! The morale and happiness " +
"of each employee increased by " +
numeralWrapper.formatPercentage(mult - 1),
);
props.rerender();
props.onClose();
}
}
function EffectText(): React.ReactElement {

@ -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,30 +75,40 @@ 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}>
<td>
{achieved ? (
<Tooltip title="Achieved">
<LockOpenIcon color="primary" />
return (
<tr key={"ach-" + i.ID}>
<td>
{achieved ? (
<Tooltip title="Achieved">
<LockOpenIcon color="primary" />
</Tooltip>
) : (
<Tooltip title="Locked">
<LockIcon color="secondary" />
</Tooltip>
)}
</td>
<td>
<Tooltip
title={
<>
{i.ID}
<br />
{i.Description}
</>
}
>
<Typography color={achieved ? "primary" : "secondary"}>{i.Name}:</Typography>
</Tooltip>
) : (
<Tooltip title="Locked">
<LockIcon color="secondary" />
</Tooltip>
)}
</td>
<td>
<Tooltip title={<>{i.ID}<br />{i.Description}</>}>
<Typography color={achieved ? 'primary': 'secondary'}>{i.Name}:</Typography>
</Tooltip>
</td>
<td>
<ButtonGroup>
<Button onClick={() => grantAchievement(i.ID)}>Grant</Button>
<Button onClick={() => removeAchievement(i.ID)}>Clear</Button>
</ButtonGroup>
</td>
</tr>
</td>
<td>
<ButtonGroup>
<Button onClick={() => grantAchievement(i.ID)}>Grant</Button>
<Button onClick={() => removeAchievement(i.ID)}>Clear</Button>
</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;
});
}

@ -33,8 +33,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];

@ -2,8 +2,6 @@ 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";

@ -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
checked={gang.territoryWarfareEngaged}
onChange={(event) => (gang.territoryWarfareEngaged = event.target.checked)}
/>}
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>Engage in Territory Warfare</Typography>
</Tooltip>} />
control={
<Switch
checked={gang.territoryWarfareEngaged}
onChange={(event) => (gang.territoryWarfareEngaged = event.target.checked)}
/>
}
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>Engage in Territory Warfare</Typography>
</Tooltip>
}
/>
<br />
<FormControlLabel
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>}>
<Typography>Notify about Gang Member Deaths</Typography>
</Tooltip>} />
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>
}
>
<Typography>Notify about Gang Member Deaths</Typography>
</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)} />
</>
);

@ -43,7 +43,7 @@ export function HacknetUpgradeElem(props: IProps): React.ReactElement {
if (!res) {
dialogBoxCreate(
"Failed to purchase upgrade. This may be because you do not have enough hashes, " +
"or because you do not have access to the feature upgrade affects.",
"or because you do not have access to the feature upgrade affects.",
);
}
props.rerender();

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

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

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

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

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

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

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

@ -114,7 +114,7 @@ export function SlumsLocation(): React.ReactElement {
const heistChance = Crimes.Heist.successRate(player);
return (
<Box sx={{ display: 'grid', width: 'fit-content' }}>
<Box sx={{ display: "grid", width: "fit-content" }}>
<Tooltip title={<>Attempt to shoplift from a low-end retailer</>}>
<Button onClick={shoplift}>
Shoplift ({numeralWrapper.formatPercentage(shopliftChance)} chance of success)

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

@ -70,9 +70,7 @@ export function TechVendorLocation(props: IProps): React.ReactElement {
return (
<>
<br />
<Box sx={{ display: 'grid', width: 'fit-content' }}>
{purchaseServerButtons}
</Box>
<Box sx={{ display: "grid", width: "fit-content" }}>{purchaseServerButtons}</Box>
<br />
<Typography>
<i>"You can order bigger servers via scripts. We don't take custom orders in person."</i>

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

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

@ -129,48 +129,48 @@ function initMessages(): void {
new Message(
MessageFilenames.Jumper0,
"I know you can sense it. I know you're searching for it. " +
"It's why you spend night after " +
"night at your computer. <br><br>It's real, I've seen it. And I can " +
"help you find it. But not right now. You're not ready yet.<br><br>" +
"Use this program to track your progress<br><br>" +
"The fl1ght.exe program was added to your home computer<br><br>" +
"-jump3R",
"It's why you spend night after " +
"night at your computer. <br><br>It's real, I've seen it. And I can " +
"help you find it. But not right now. You're not ready yet.<br><br>" +
"Use this program to track your progress<br><br>" +
"The fl1ght.exe program was added to your home computer<br><br>" +
"-jump3R",
),
);
AddToAllMessages(
new Message(
MessageFilenames.Jumper1,
`Soon you will be contacted by a hacking group known as ${FactionNames.NiteSec}. ` +
"They can help you with your search. <br><br>" +
"You should join them, garner their favor, and " +
"exploit them for their Augmentations. But do not trust them. " +
"They are not what they seem. No one is.<br><br>" +
"-jump3R",
"They can help you with your search. <br><br>" +
"You should join them, garner their favor, and " +
"exploit them for their Augmentations. But do not trust them. " +
"They are not what they seem. No one is.<br><br>" +
"-jump3R",
),
);
AddToAllMessages(
new Message(
MessageFilenames.Jumper2,
"Do not try to save the world. There is no world to save. If " +
"you want to find the truth, worry only about yourself. Ethics and " +
`morals will get you killed. <br><br>Watch out for a hacking group known as ${FactionNames.NiteSec}.` +
"<br><br>-jump3R",
"you want to find the truth, worry only about yourself. Ethics and " +
`morals will get you killed. <br><br>Watch out for a hacking group known as ${FactionNames.NiteSec}.` +
"<br><br>-jump3R",
),
);
AddToAllMessages(
new Message(
MessageFilenames.Jumper3,
"You must learn to walk before you can run. And you must " +
`run before you can fly. Look for ${FactionNames.TheBlackHand}. <br><br>` +
"I.I.I.I <br><br>-jump3R",
`run before you can fly. Look for ${FactionNames.TheBlackHand}. <br><br>` +
"I.I.I.I <br><br>-jump3R",
),
);
AddToAllMessages(
new Message(
MessageFilenames.Jumper4,
"To find what you are searching for, you must understand the bits. " +
"The bits are all around us. The runners will help you.<br><br>" +
"-jump3R",
"The bits are all around us. The runners will help you.<br><br>" +
"-jump3R",
),
);
@ -179,31 +179,31 @@ function initMessages(): void {
new Message(
MessageFilenames.CyberSecTest,
"We've been watching you. Your skills are very impressive. But you're wasting " +
"your talents. If you join us, you can put your skills to good use and change " +
"the world for the better. If you join us, we can unlock your full potential. <br><br>" +
"But first, you must pass our test. Find and install the backdoor on our server. <br><br>" +
`-${FactionNames.CyberSec}`,
"your talents. If you join us, you can put your skills to good use and change " +
"the world for the better. If you join us, we can unlock your full potential. <br><br>" +
"But first, you must pass our test. Find and install the backdoor on our server. <br><br>" +
`-${FactionNames.CyberSec}`,
),
);
AddToAllMessages(
new Message(
MessageFilenames.NiteSecTest,
"People say that the corrupted governments and corporations rule the world. " +
"Yes, maybe they do. But do you know who everyone really fears? People " +
"like us. Because they can't hide from us. Because they can't fight shadows " +
"and ideas with bullets. <br><br>" +
"Join us, and people will fear you, too. <br><br>" +
"Find and install the backdoor on our server. Then, we will contact you again." +
`<br><br>-${FactionNames.NiteSec}`,
"Yes, maybe they do. But do you know who everyone really fears? People " +
"like us. Because they can't hide from us. Because they can't fight shadows " +
"and ideas with bullets. <br><br>" +
"Join us, and people will fear you, too. <br><br>" +
"Find and install the backdoor on our server. Then, we will contact you again." +
`<br><br>-${FactionNames.NiteSec}`,
),
);
AddToAllMessages(
new Message(
MessageFilenames.BitRunnersTest,
"We know what you are doing. We know what drives you. We know " +
"what you are looking for. <br><br> " +
"We can help you find the answers.<br><br>" +
"run4theh111z",
"what you are looking for. <br><br> " +
"We can help you find the answers.<br><br>" +
"run4theh111z",
),
);
@ -211,9 +211,9 @@ function initMessages(): void {
new Message(
MessageFilenames.RedPill,
"@)(#V%*N)@(#*)*C)@#%*)*V)@#(*%V@)(#VN%*)@#(*%<br>" +
")@B(*#%)@)M#B*%V)____FIND___#$@)#%(B*)@#(*%B)<br>" +
"@_#(%_@#M(BDSPOMB__THE-CAVE_#)$(*@#$)@#BNBEGB<br>" +
"DFLSMFVMV)#@($*)@#*$MV)@#(*$V)M#(*$)M@(#*VM$)",
")@B(*#%)@)M#B*%V)____FIND___#$@)#%(B*)@#(*%B)<br>" +
"@_#(%_@#M(BDSPOMB__THE-CAVE_#)$(*@#$)@#BNBEGB<br>" +
"DFLSMFVMV)#@($*)@#*$MV)@#(*$V)M#(*$)M@(#*VM$)",
),
);
}

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

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