Merge pull request #3369 from danielyxie/dev

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

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

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

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

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

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

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

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

@ -6,18 +6,18 @@ async function enableAchievementsInterval(window) {
// If the Steam API could not be initialized on game start, we'll abort this. // If the Steam API could not be initialized on game start, we'll abort this.
if (global.greenworksError) return; 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. // here. Hey if it works it works.
const steamAchievements = greenworks.getAchievementNames(); const steamAchievements = greenworks.getAchievementNames();
log.silly(`All Steam achievements ${JSON.stringify(steamAchievements)}`); log.silly(`All Steam achievements ${JSON.stringify(steamAchievements)}`);
const playerAchieved = (await Promise.all(steamAchievements.map(checkSteamAchievement))).filter(name => !!name); const playerAchieved = (await Promise.all(steamAchievements.map(checkSteamAchievement))).filter((name) => !!name);
log.debug(`Player has Steam achievements ${JSON.stringify(playerAchieved)}`); log.debug(`Player has Steam achievements ${JSON.stringify(playerAchieved)}`);
const intervalID = setInterval(async () => { const intervalID = setInterval(async () => {
try { try {
const playerAchievements = await window.webContents.executeJavaScript("document.achievements"); const playerAchievements = await window.webContents.executeJavaScript("document.achievements");
for (const ach of playerAchievements) { for (const ach of playerAchievements) {
if (!steamAchievements.includes(ach)) continue; // Don't try activating achievements that don't exist Steam-side 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}`); log.info(`Granting Steam achievement ${ach}`);
greenworks.activateAchievement(ach, () => undefined); greenworks.activateAchievement(ach, () => undefined);
playerAchieved.push(ach); playerAchieved.push(ach);
@ -26,7 +26,7 @@ async function enableAchievementsInterval(window) {
log.error(error); log.error(error);
// The interval probably did not get cleared after a window kill // The interval probably did not get cleared after a window kill
log.warn('Clearing achievements timer'); log.warn("Clearing achievements timer");
clearInterval(intervalID); clearInterval(intervalID);
return; return;
} }
@ -36,10 +36,14 @@ async function enableAchievementsInterval(window) {
function checkSteamAchievement(name) { function checkSteamAchievement(name) {
return new Promise((resolve) => { return new Promise((resolve) => {
greenworks.getAchievement(name, playerHas => resolve(playerHas ? name : ""), err => { greenworks.getAchievement(
log.warn(`Failed to get Steam achievement ${name} status: ${err}`); name,
resolve(""); (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 = { module.exports = {
enableAchievementsInterval, disableAchievementsInterval enableAchievementsInterval,
} disableAchievementsInterval,
};

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

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

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

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

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

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

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

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

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

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

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

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

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

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

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

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

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

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

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

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

@ -26,8 +26,8 @@ export const ConsoleHelpText: {
"Usage: automate [var] [val] [hi/low]", "Usage: automate [var] [val] [hi/low]",
" ", " ",
"A simple way to automate your Bladeburner actions. This console command can be used " + "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 " + "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.", "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 status - Check the current status of your automation and get a brief description of what it'll do",
" automate en - Enable the automation feature", " automate en - Enable the automation feature",
@ -41,9 +41,9 @@ export const ConsoleHelpText: {
" automate general 'Field Analysis' low", " automate general 'Field Analysis' low",
" ", " ",
"Using the four console commands above will set the automation to perform Tracking contracts " + "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 " + "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 " + "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.", "exactly match whatever the name is in the UI.",
" ", " ",
], ],
clear: ["Usage: clear", " ", "Clears the console", " "], clear: ["Usage: clear", " ", "Clears the console", " "],
@ -52,8 +52,8 @@ export const ConsoleHelpText: {
"Usage: help [command]", "Usage: help [command]",
" ", " ",
"Running 'help' with no arguments displays the general help text, which lists all console commands " + "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 " + "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:", "about that particular command. For example:",
" ", " ",
" help automate", " help automate",
" ", " ",
@ -64,8 +64,8 @@ export const ConsoleHelpText: {
"Usage: log [en/dis] [type]", "Usage: log [en/dis] [type]",
" ", " ",
"Enable or disable logging. By default, the results of completing actions such as contracts/operations are logged " + "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 " + "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:", "things that get logged are:",
" ", " ",
"[general, contracts, ops, blackops, events]", "[general, contracts, ops, blackops, events]",
" ", " ",
@ -91,8 +91,8 @@ export const ConsoleHelpText: {
" skill list", " skill list",
" ", " ",
"To display information about a specific skill, specify the name of the skill afterwards. " + "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 " + "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:", "the name of the skill has whitespace, enclose the name of the skill in double quotation marks:",
" ", " ",
" skill list Reaper", " skill list Reaper",
" skill list 'Digital Observer'", " skill list 'Digital Observer'",
@ -106,9 +106,9 @@ export const ConsoleHelpText: {
"Usage: start [type] [name]", "Usage: start [type] [name]",
" ", " ",
"Start an action. An action is specified by its type and its name. The " + "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 " + "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. " + "the name of the action has whitespace, enclose it in double quotation marks. " +
"Valid action types include:", "Valid action types include:",
" ", " ",
"[general, contract, op, blackop]", "[general, contract, op, blackop]",
" ", " ",

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

@ -35,7 +35,7 @@ export function BladeburnerCinematic(): React.ReactElement {
router.toTerminal(); router.toTerminal();
dialogBoxCreate( dialogBoxCreate(
`Visit the National Security Agency (NSA) to apply for their ${FactionNames.Bladeburners} ` + `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 ( return (
<Paper sx={{ p: 1, overflowY: 'auto', overflowX: 'hidden', wordBreak: 'break-all' }}> <Paper sx={{ p: 1, overflowY: "auto", overflowX: "hidden", wordBreak: "break-all" }}>
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 1, maxHeight: '60vh' }}> <Box sx={{ display: "flex", flexDirection: "column", gap: 1, maxHeight: "60vh" }}>
<Box sx={{ alignSelf: 'flex-start', width: '100%' }}> <Box sx={{ alignSelf: "flex-start", width: "100%" }}>
<Button onClick={() => setTravelOpen(true)} sx={{ width: '50%' }}>Travel</Button> <Button onClick={() => setTravelOpen(true)} sx={{ width: "50%" }}>
Travel
</Button>
<Tooltip title={!inFaction ? <Typography>Rank 25 required.</Typography> : ""}> <Tooltip title={!inFaction ? <Typography>Rank 25 required.</Typography> : ""}>
<span> <span>
<Button disabled={!inFaction} onClick={openFaction} sx={{ width: '50%' }}> <Button disabled={!inFaction} onClick={openFaction} sx={{ width: "50%" }}>
Faction Faction
</Button> </Button>
</span> </span>
@ -110,8 +112,8 @@ export function Stats(props: IProps): React.ReactElement {
<Tooltip <Tooltip
title={ title={
<Typography> <Typography>
This is your Bladeburner division's estimate of how many Synthoids exist in your current city. An accurate This is your Bladeburner division's estimate of how many Synthoids exist in your current city. An
population count increases success rate estimates. accurate population count increases success rate estimates.
</Typography> </Typography>
} }
> >

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

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

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

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

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

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

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

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

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

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

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

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

@ -112,7 +112,7 @@ export function MaterialElem(props: IMaterialProps): React.ReactElement {
return ( return (
<Paper> <Paper>
<Box sx={{ display: 'grid', gridTemplateColumns: '2fr 1fr', m: '5px' }}> <Box sx={{ display: "grid", gridTemplateColumns: "2fr 1fr", m: "5px" }}>
<Box> <Box>
<Tooltip <Tooltip
title={ title={
@ -149,17 +149,17 @@ export function MaterialElem(props: IMaterialProps): React.ReactElement {
</Tooltip> </Tooltip>
</Box> </Box>
<Box sx={{ "& button": { width: '100%' } }}> <Box sx={{ "& button": { width: "100%" } }}>
<Tooltip <Tooltip
title={tutorial ? <Typography>Purchase your required materials to get production started!</Typography> : ""} title={tutorial ? <Typography>Purchase your required materials to get production started!</Typography> : ""}
> >
<Button <Button
color={tutorial ? "error" : "primary"} color={tutorial ? "error" : "primary"}
onClick={() => setPurchaseMaterialOpen(true)} onClick={() => setPurchaseMaterialOpen(true)}
disabled={props.warehouse.smartSupplyEnabled && Object.keys(division.reqMats).includes(props.mat.name)} disabled={props.warehouse.smartSupplyEnabled && Object.keys(division.reqMats).includes(props.mat.name)}
> >
{purchaseButtonText} {purchaseButtonText}
</Button> </Button>
</Tooltip> </Tooltip>
<PurchaseMaterialModal <PurchaseMaterialModal
mat={mat} mat={mat}

@ -89,7 +89,7 @@ export function Overview({ rerender }: IProps): React.ReactElement {
<StatsTable rows={multRows} /> <StatsTable rows={multRows} />
<br /> <br />
<BonusTime /> <BonusTime />
<Box sx={{ display: 'grid', gridTemplateColumns: 'repeat(3, 1fr)', width: 'fit-content' }}> <Box sx={{ display: "grid", gridTemplateColumns: "repeat(3, 1fr)", width: "fit-content" }}>
<Tooltip <Tooltip
title={ title={
<Typography> <Typography>
@ -200,8 +200,8 @@ function PublicButtons({ rerender }: IPublicButtonsProps): React.ReactElement {
const sellSharesTooltip = sellSharesOnCd const sellSharesTooltip = sellSharesOnCd
? "Cannot sell shares for " + corp.convertCooldownToString(corp.shareSaleCooldown) ? "Cannot sell shares for " + corp.convertCooldownToString(corp.shareSaleCooldown)
: "Sell your shares in the company. The money earned from selling your " + : "Sell your shares in the company. The money earned from selling your " +
"shares goes into your personal account, not the Corporation's. " + "shares goes into your personal account, not the Corporation's. " +
"This is one of the only ways to profit from your business venture."; "This is one of the only ways to profit from your business venture.";
const issueNewSharesOnCd = corp.issueNewSharesCooldown > 0; const issueNewSharesOnCd = corp.issueNewSharesCooldown > 0;
const issueNewSharesTooltip = issueNewSharesOnCd const issueNewSharesTooltip = issueNewSharesOnCd

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

@ -43,7 +43,7 @@ export function HacknetUpgradeElem(props: IProps): React.ReactElement {
if (!res) { if (!res) {
dialogBoxCreate( dialogBoxCreate(
"Failed to purchase upgrade. This may be because you do not have enough hashes, " + "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(); props.rerender();

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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