mirror of
https://github.com/bitburner-official/bitburner-src.git
synced 2024-11-18 13:43:49 +01:00
commit
c94f31fc2a
3
.github/PULL_REQUEST_TEMPLATE
vendored
3
.github/PULL_REQUEST_TEMPLATE
vendored
@ -9,3 +9,6 @@
|
||||
|
||||
- Include how it was tested
|
||||
- Include screenshot / gif (if possible)
|
||||
|
||||
|
||||
Make sure you run `npm run format` and `npm run lint` before pushing.
|
||||
|
22
.github/workflows/bump-version.yml
vendored
22
.github/workflows/bump-version.yml
vendored
@ -4,28 +4,28 @@ on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
version:
|
||||
description: 'Version (format: x.y.z)'
|
||||
description: "Version (format: x.y.z)"
|
||||
required: true
|
||||
versionNumber:
|
||||
description: 'Version Number (for saves migration)'
|
||||
description: "Version Number (for saves migration)"
|
||||
required: true
|
||||
changelog:
|
||||
description: 'Changelog (url that points to RAW markdown)'
|
||||
default: ''
|
||||
description: "Changelog (url that points to RAW markdown)"
|
||||
default: ""
|
||||
buildApp:
|
||||
description: 'Include Application Build'
|
||||
description: "Include Application Build"
|
||||
type: boolean
|
||||
default: 'true'
|
||||
default: "true"
|
||||
required: true
|
||||
buildDoc:
|
||||
description: 'Include Documentation Build'
|
||||
description: "Include Documentation Build"
|
||||
type: boolean
|
||||
default: 'true'
|
||||
default: "true"
|
||||
required: true
|
||||
prepareRelease:
|
||||
description: 'Prepare Draft Release'
|
||||
description: "Prepare Draft Release"
|
||||
type: boolean
|
||||
default: 'true'
|
||||
default: "true"
|
||||
required: true
|
||||
|
||||
jobs:
|
||||
@ -46,7 +46,7 @@ jobs:
|
||||
uses: actions/setup-node@v2
|
||||
with:
|
||||
node-version: 16.13.1
|
||||
cache: 'npm'
|
||||
cache: "npm"
|
||||
- name: Install NPM dependencies for version updater
|
||||
working-directory: ./tools/bump-version
|
||||
run: npm ci
|
||||
|
6
.github/workflows/fetch-changes.yml
vendored
6
.github/workflows/fetch-changes.yml
vendored
@ -3,10 +3,10 @@ on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
fromCommit:
|
||||
description: 'From Commit SHA (full-length)'
|
||||
description: "From Commit SHA (full-length)"
|
||||
required: true
|
||||
toCommit:
|
||||
description: 'To Commit SHA (full-length, if omitted will use latest)'
|
||||
description: "To Commit SHA (full-length, if omitted will use latest)"
|
||||
|
||||
jobs:
|
||||
fetchChangelog:
|
||||
@ -17,7 +17,7 @@ jobs:
|
||||
uses: actions/setup-node@v2
|
||||
with:
|
||||
node-version: 16.13.1
|
||||
cache: 'npm'
|
||||
cache: "npm"
|
||||
- name: Install NPM dependencies
|
||||
working-directory: ./tools/fetch-changelog
|
||||
run: npm ci
|
||||
|
@ -25,8 +25,8 @@ I need equations that test many different aspect of "math culture", it can be ch
|
||||
|
||||
All variable purchasing will be scriptable.
|
||||
|
||||
|
||||
All equation must have:
|
||||
|
||||
- several variables that can be upgraded, at least 1 variable must be strategic in it's upgrading (upgrading too much can cause drop in performance)
|
||||
- Some sort of math twist that requires some thinking, like (-2)^c1 alters between positive and negative.
|
||||
|
||||
|
@ -115,7 +115,6 @@ Fork and clone the repo
|
||||
- Regularly rebase your branch against `dev` to make sure you have the latest updates pulled.
|
||||
- When merging, always merge your branch into `dev`. When releasing a new update, then merge `dev` into `master`
|
||||
|
||||
|
||||
## Running locally.
|
||||
|
||||
Install
|
||||
@ -131,7 +130,6 @@ Inside the root of the repo run
|
||||
After that you can open any browser and navigate to `localhost:8000` and play the game.
|
||||
Saving a file will reload the game automatically.
|
||||
|
||||
|
||||
### How to build the electron app
|
||||
|
||||
Tested on Node v16.13.1 (LTS) on Windows
|
||||
|
23
doc/FAQ.md
23
doc/FAQ.md
@ -23,10 +23,11 @@ Yes, just export the save file from the options menu & import it in the other pl
|
||||
## Game is stuck after running scripts!
|
||||
|
||||
You may have created an infinite loop with no sleep. You'll have to restart the game by killing all scripts.
|
||||
* On Browser: Stick `?noScript` at the end of the URL
|
||||
* On Steam:
|
||||
* In the menu, "Reloads" -> "Reload & Kill All Scripts".
|
||||
* If this does not work, when launching the game, use the kill all script options.
|
||||
|
||||
- On Browser: Stick `?noScript` at the end of the URL
|
||||
- On Steam:
|
||||
- In the menu, "Reloads" -> "Reload & Kill All Scripts".
|
||||
- If this does not work, when launching the game, use the kill all script options.
|
||||
|
||||
---
|
||||
|
||||
@ -51,11 +52,13 @@ You can navigate to the game files by right-clicking the game in your library an
|
||||
## Steam: Game won't launch
|
||||
|
||||
### **On Windows**
|
||||
|
||||
If the game is installed on a network drive, it will fail to start due to a [limitation in Chromium](https://github.com/electron/electron/issues/27356).
|
||||
|
||||
If you cannot move the game to another drive, you'll have to add the `--no-sandbox` launch option. In your Steam Library, Right click the game and hit "Properties". You'll see the launch option section in the "General" window.
|
||||
|
||||
### **On Linux**
|
||||
|
||||
The game is built natively, do not use Proton unless native does not work.
|
||||
|
||||
When launching the game, you will be prompted with three options. If the standard launch does not work, you may attempt the `--disable-seccomp-filter-sandbox` or `--no-sandbox` launch option. If this still does not work, the game should be able to start by launching it directly or through the terminal. See [How do I get to the game files?](#game-files).
|
||||
@ -68,17 +71,17 @@ When launching the game, you will be prompted with three options. If the standar
|
||||
|
||||
You may want access the logs to get information about crashes or such.
|
||||
|
||||
* on Linux: `~/.config/bitburner/logs/main.log`
|
||||
* on macOS: `~/Library/Logs/bitburner/main.log`
|
||||
* on Windows: `%USERPROFILE%\AppData\Roaming\bitburner\logs\main.log`
|
||||
- on Linux: `~/.config/bitburner/logs/main.log`
|
||||
- on macOS: `~/Library/Logs/bitburner/main.log`
|
||||
- on Windows: `%USERPROFILE%\AppData\Roaming\bitburner\logs\main.log`
|
||||
|
||||
### Config (using [electron-store](https://github.com/sindresorhus/electron-store#readme))
|
||||
|
||||
Configuration file will be written to disk in the application data directory.
|
||||
|
||||
* on Linux: `~/.config/bitburner/config.json`
|
||||
* on macOS: `~/Library/Application\ Support/bitburner/config.json`
|
||||
* on Windows: `%USERPROFILE%\AppData\Roaming\bitburner\config.json`
|
||||
- on Linux: `~/.config/bitburner/config.json`
|
||||
- on macOS: `~/Library/Application\ Support/bitburner/config.json`
|
||||
- on Windows: `%USERPROFILE%\AppData\Roaming\bitburner\config.json`
|
||||
|
||||
---
|
||||
|
||||
|
@ -10,7 +10,7 @@ async function enableAchievementsInterval(window) {
|
||||
// here. Hey if it works it works.
|
||||
const steamAchievements = greenworks.getAchievementNames();
|
||||
log.silly(`All Steam achievements ${JSON.stringify(steamAchievements)}`);
|
||||
const playerAchieved = (await Promise.all(steamAchievements.map(checkSteamAchievement))).filter(name => !!name);
|
||||
const playerAchieved = (await Promise.all(steamAchievements.map(checkSteamAchievement))).filter((name) => !!name);
|
||||
log.debug(`Player has Steam achievements ${JSON.stringify(playerAchieved)}`);
|
||||
const intervalID = setInterval(async () => {
|
||||
try {
|
||||
@ -26,7 +26,7 @@ async function enableAchievementsInterval(window) {
|
||||
log.error(error);
|
||||
|
||||
// The interval probably did not get cleared after a window kill
|
||||
log.warn('Clearing achievements timer');
|
||||
log.warn("Clearing achievements timer");
|
||||
clearInterval(intervalID);
|
||||
return;
|
||||
}
|
||||
@ -36,10 +36,14 @@ async function enableAchievementsInterval(window) {
|
||||
|
||||
function checkSteamAchievement(name) {
|
||||
return new Promise((resolve) => {
|
||||
greenworks.getAchievement(name, playerHas => resolve(playerHas ? name : ""), err => {
|
||||
greenworks.getAchievement(
|
||||
name,
|
||||
(playerHas) => resolve(playerHas ? name : ""),
|
||||
(err) => {
|
||||
log.warn(`Failed to get Steam achievement ${name} status: ${err}`);
|
||||
resolve("");
|
||||
});
|
||||
},
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
@ -50,5 +54,6 @@ function disableAchievementsInterval(window) {
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
enableAchievementsInterval, disableAchievementsInterval
|
||||
}
|
||||
enableAchievementsInterval,
|
||||
disableAchievementsInterval,
|
||||
};
|
||||
|
@ -12,25 +12,27 @@ async function initialize(win) {
|
||||
window = win;
|
||||
server = http.createServer(async function (req, res) {
|
||||
let body = "";
|
||||
res.setHeader('Content-Type', 'application/json');
|
||||
res.setHeader("Content-Type", "application/json");
|
||||
|
||||
req.on("data", (chunk) => {
|
||||
body += chunk.toString(); // convert Buffer to string
|
||||
});
|
||||
|
||||
req.on("end", async () => {
|
||||
const providedToken = req.headers?.authorization?.replace('Bearer ', '') ?? '';
|
||||
const providedToken = req.headers?.authorization?.replace("Bearer ", "") ?? "";
|
||||
const isValid = providedToken === getAuthenticationToken();
|
||||
if (isValid) {
|
||||
log.debug('Valid authentication token');
|
||||
log.debug("Valid authentication token");
|
||||
} else {
|
||||
log.log('Invalid authentication token');
|
||||
log.log("Invalid authentication token");
|
||||
res.writeHead(401);
|
||||
|
||||
res.end(JSON.stringify({
|
||||
res.end(
|
||||
JSON.stringify({
|
||||
success: false,
|
||||
msg: 'Invalid authentication token'
|
||||
}));
|
||||
msg: "Invalid authentication token",
|
||||
}),
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
@ -40,10 +42,12 @@ async function initialize(win) {
|
||||
} catch (error) {
|
||||
log.warn(`Invalid body data`);
|
||||
res.writeHead(400);
|
||||
res.end(JSON.stringify({
|
||||
res.end(
|
||||
JSON.stringify({
|
||||
success: false,
|
||||
msg: 'Invalid body data'
|
||||
}));
|
||||
msg: "Invalid body data",
|
||||
}),
|
||||
);
|
||||
|
||||
return;
|
||||
}
|
||||
@ -62,10 +66,12 @@ async function initialize(win) {
|
||||
if (!data) {
|
||||
log.warn(`Invalid script update request - No data`);
|
||||
res.writeHead(400);
|
||||
res.end(JSON.stringify({
|
||||
res.end(
|
||||
JSON.stringify({
|
||||
success: false,
|
||||
msg: 'Invalid script update request - No data'
|
||||
}));
|
||||
msg: "Invalid script update request - No data",
|
||||
}),
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
@ -84,19 +90,20 @@ async function initialize(win) {
|
||||
log.warn(`Api Server Error`, result.msg);
|
||||
}
|
||||
|
||||
res.end(JSON.stringify({
|
||||
res.end(
|
||||
JSON.stringify({
|
||||
success: result.res,
|
||||
msg: result.msg,
|
||||
data: result.data
|
||||
}));
|
||||
|
||||
data: result.data,
|
||||
}),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
const autostart = config.get('autostart', false);
|
||||
const autostart = config.get("autostart", false);
|
||||
if (autostart) {
|
||||
try {
|
||||
await enable()
|
||||
await enable();
|
||||
} catch (error) {
|
||||
return Promise.reject(error);
|
||||
}
|
||||
@ -105,15 +112,14 @@ async function initialize(win) {
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
|
||||
function enable() {
|
||||
if (isListening()) {
|
||||
log.warn('API server already listening');
|
||||
log.warn("API server already listening");
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
const port = config.get('port', 9990);
|
||||
const host = config.get('host', '127.0.0.1');
|
||||
const port = config.get("port", 9990);
|
||||
const host = config.get("host", "127.0.0.1");
|
||||
log.log(`Starting http server on port ${port} - listening on ${host}`);
|
||||
|
||||
// https://stackoverflow.com/a/62289870
|
||||
@ -125,13 +131,10 @@ function enable() {
|
||||
resolve();
|
||||
}
|
||||
});
|
||||
server.once('error', (err) => {
|
||||
server.once("error", (err) => {
|
||||
if (!startFinished) {
|
||||
startFinished = true;
|
||||
console.log(
|
||||
'There was an error starting the server in the error listener:',
|
||||
err
|
||||
);
|
||||
console.log("There was an error starting the server in the error listener:", err);
|
||||
reject(err);
|
||||
}
|
||||
});
|
||||
@ -140,11 +143,11 @@ function enable() {
|
||||
|
||||
function disable() {
|
||||
if (!isListening()) {
|
||||
log.warn('API server not listening');
|
||||
log.warn("API server not listening");
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
log.log('Stopping http server');
|
||||
log.log("Stopping http server");
|
||||
return server.close();
|
||||
}
|
||||
|
||||
@ -162,31 +165,35 @@ function isListening() {
|
||||
|
||||
function toggleAutostart() {
|
||||
const newValue = !isAutostart();
|
||||
config.set('autostart', newValue);
|
||||
config.set("autostart", newValue);
|
||||
log.log(`New autostart value is '${newValue}'`);
|
||||
}
|
||||
|
||||
function isAutostart() {
|
||||
return config.get('autostart');
|
||||
return config.get("autostart");
|
||||
}
|
||||
|
||||
function getAuthenticationToken() {
|
||||
const token = config.get('token');
|
||||
const token = config.get("token");
|
||||
if (token) return token;
|
||||
|
||||
const newToken = generateToken();
|
||||
config.set('token', newToken);
|
||||
config.set("token", newToken);
|
||||
return newToken;
|
||||
}
|
||||
|
||||
function generateToken() {
|
||||
const buffer = crypto.randomBytes(48);
|
||||
return buffer.toString('base64')
|
||||
return buffer.toString("base64");
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
initialize,
|
||||
enable, disable, toggleServer,
|
||||
toggleAutostart, isAutostart,
|
||||
getAuthenticationToken, isListening,
|
||||
}
|
||||
enable,
|
||||
disable,
|
||||
toggleServer,
|
||||
toggleAutostart,
|
||||
isAutostart,
|
||||
getAuthenticationToken,
|
||||
isListening,
|
||||
};
|
||||
|
@ -15,20 +15,20 @@ const debug = process.argv.includes("--debug");
|
||||
|
||||
async function createWindow(killall) {
|
||||
const setStopProcessHandler = global.app_handlers.stopProcess;
|
||||
app.setAppUserModelId("Bitburner")
|
||||
app.setAppUserModelId("Bitburner");
|
||||
|
||||
let icon;
|
||||
if (process.platform == 'linux') {
|
||||
icon = path.join(__dirname, 'icon.png');
|
||||
if (process.platform == "linux") {
|
||||
icon = path.join(__dirname, "icon.png");
|
||||
}
|
||||
|
||||
const tracker = windowTracker('main');
|
||||
const tracker = windowTracker("main");
|
||||
const window = new BrowserWindow({
|
||||
icon,
|
||||
show: false,
|
||||
backgroundThrottling: false,
|
||||
backgroundColor: "#000000",
|
||||
title: 'Bitburner',
|
||||
title: "Bitburner",
|
||||
x: tracker.state.x,
|
||||
y: tracker.state.y,
|
||||
width: tracker.state.width,
|
||||
@ -37,7 +37,7 @@ async function createWindow(killall) {
|
||||
minHeight: 400,
|
||||
webPreferences: {
|
||||
nativeWindowOpen: true,
|
||||
preload: path.join(__dirname, 'preload.js'),
|
||||
preload: path.join(__dirname, "preload.js"),
|
||||
},
|
||||
});
|
||||
|
||||
@ -52,12 +52,12 @@ async function createWindow(killall) {
|
||||
|
||||
window.webContents.on("new-window", async function (e, url) {
|
||||
// Let's make sure sure we have a proper url
|
||||
let parsedUrl
|
||||
let parsedUrl;
|
||||
try {
|
||||
parsedUrl = new URL(url);
|
||||
} catch (_) {
|
||||
// This is an invalid url, let's just do nothing
|
||||
log.warn(`Invalid url found: ${url}`)
|
||||
log.warn(`Invalid url found: ${url}`);
|
||||
e.preventDefault();
|
||||
return;
|
||||
}
|
||||
@ -74,11 +74,13 @@ async function createWindow(killall) {
|
||||
|
||||
if (!isChild) {
|
||||
// If we're not relative to our app's path let's abort
|
||||
log.warn(`Requested path ${filePath.dir}${path.sep}${filePath.base} is not relative to the app: ${appPath.dir}${path.sep}${appPath.base}`)
|
||||
log.warn(
|
||||
`Requested path ${filePath.dir}${path.sep}${filePath.base} is not relative to the app: ${appPath.dir}${path.sep}${appPath.base}`,
|
||||
);
|
||||
e.preventDefault();
|
||||
} else if (!fileExists) {
|
||||
// If the file does not exist let's abort
|
||||
log.warn(`Requested path ${filePath.dir}${path.sep}${filePath.base} does not exist`)
|
||||
log.warn(`Requested path ${filePath.dir}${path.sep}${filePath.base} does not exist`);
|
||||
e.preventDefault();
|
||||
}
|
||||
|
||||
@ -90,7 +92,7 @@ async function createWindow(killall) {
|
||||
let urlToOpen = parsedUrl.toString();
|
||||
if (parsedUrl.search) {
|
||||
log.log(`Cannot open a path with parameters: ${parsedUrl.search}`);
|
||||
urlToOpen = urlToOpen.replace(parsedUrl.search, '');
|
||||
urlToOpen = urlToOpen.replace(parsedUrl.search, "");
|
||||
// It would be possible to launch an URL with parameter using this, but it would mess up the process again...
|
||||
// const escapedUri = parsedUrl.href.replace('&', '^&');
|
||||
// cp.spawn("cmd.exe", ["/c", "start", escapedUri], { detached: true, stdio: "ignore" });
|
||||
|
@ -3,83 +3,91 @@
|
||||
// found in the LICENSE file.
|
||||
|
||||
// The source code can be found in https://github.com/greenheartgames/greenworks
|
||||
var fs = require('fs');
|
||||
var fs = require("fs");
|
||||
|
||||
var greenworks;
|
||||
|
||||
if (process.platform == 'darwin') {
|
||||
if (process.arch == 'x64')
|
||||
greenworks = require('./lib/greenworks-osx64');
|
||||
else if (process.arch == 'ia32')
|
||||
greenworks = require('./lib/greenworks-osx32');
|
||||
} else if (process.platform == 'win32') {
|
||||
if (process.arch == 'x64')
|
||||
greenworks = require('./lib/greenworks-win64');
|
||||
else if (process.arch == 'ia32')
|
||||
greenworks = require('./lib/greenworks-win32');
|
||||
} else if (process.platform == 'linux') {
|
||||
if (process.arch == 'x64')
|
||||
greenworks = require('./lib/greenworks-linux64');
|
||||
else if (process.arch == 'ia32')
|
||||
greenworks = require('./lib/greenworks-linux32');
|
||||
if (process.platform == "darwin") {
|
||||
if (process.arch == "x64") greenworks = require("./lib/greenworks-osx64");
|
||||
else if (process.arch == "ia32") greenworks = require("./lib/greenworks-osx32");
|
||||
} else if (process.platform == "win32") {
|
||||
if (process.arch == "x64") greenworks = require("./lib/greenworks-win64");
|
||||
else if (process.arch == "ia32") greenworks = require("./lib/greenworks-win32");
|
||||
} else if (process.platform == "linux") {
|
||||
if (process.arch == "x64") greenworks = require("./lib/greenworks-linux64");
|
||||
else if (process.arch == "ia32") greenworks = require("./lib/greenworks-linux32");
|
||||
}
|
||||
|
||||
function error_process(err, error_callback) {
|
||||
if (err && error_callback)
|
||||
error_callback(err);
|
||||
if (err && error_callback) error_callback(err);
|
||||
}
|
||||
|
||||
greenworks.ugcGetItems = function(options, ugc_matching_type, ugc_query_type,
|
||||
success_callback, error_callback) {
|
||||
if (typeof options !== 'object') {
|
||||
greenworks.ugcGetItems = function (options, ugc_matching_type, ugc_query_type, success_callback, error_callback) {
|
||||
if (typeof options !== "object") {
|
||||
error_callback = success_callback;
|
||||
success_callback = ugc_query_type;
|
||||
ugc_query_type = ugc_matching_type;
|
||||
ugc_matching_type = options;
|
||||
options = {
|
||||
'app_id': greenworks.getAppId(),
|
||||
'page_num': 1
|
||||
}
|
||||
}
|
||||
greenworks._ugcGetItems(options, ugc_matching_type, ugc_query_type,
|
||||
success_callback, error_callback);
|
||||
app_id: greenworks.getAppId(),
|
||||
page_num: 1,
|
||||
};
|
||||
}
|
||||
greenworks._ugcGetItems(options, ugc_matching_type, ugc_query_type, success_callback, error_callback);
|
||||
};
|
||||
|
||||
greenworks.ugcGetUserItems = function(options, ugc_matching_type,
|
||||
ugc_list_sort_order, ugc_list, success_callback, error_callback) {
|
||||
if (typeof options !== 'object') {
|
||||
greenworks.ugcGetUserItems = function (
|
||||
options,
|
||||
ugc_matching_type,
|
||||
ugc_list_sort_order,
|
||||
ugc_list,
|
||||
success_callback,
|
||||
error_callback,
|
||||
) {
|
||||
if (typeof options !== "object") {
|
||||
error_callback = success_callback;
|
||||
success_callback = ugc_list;
|
||||
ugc_list = ugc_list_sort_order;
|
||||
ugc_list_sort_order = ugc_matching_type;
|
||||
ugc_matching_type = options;
|
||||
options = {
|
||||
'app_id': greenworks.getAppId(),
|
||||
'page_num': 1
|
||||
}
|
||||
}
|
||||
greenworks._ugcGetUserItems(options, ugc_matching_type, ugc_list_sort_order,
|
||||
ugc_list, success_callback, error_callback);
|
||||
app_id: greenworks.getAppId(),
|
||||
page_num: 1,
|
||||
};
|
||||
}
|
||||
greenworks._ugcGetUserItems(
|
||||
options,
|
||||
ugc_matching_type,
|
||||
ugc_list_sort_order,
|
||||
ugc_list,
|
||||
success_callback,
|
||||
error_callback,
|
||||
);
|
||||
};
|
||||
|
||||
greenworks.ugcSynchronizeItems = function (options, sync_dir, success_callback,
|
||||
error_callback) {
|
||||
if (typeof options !== 'object') {
|
||||
greenworks.ugcSynchronizeItems = function (options, sync_dir, success_callback, error_callback) {
|
||||
if (typeof options !== "object") {
|
||||
error_callback = success_callback;
|
||||
success_callback = sync_dir;
|
||||
sync_dir = options;
|
||||
options = {
|
||||
'app_id': greenworks.getAppId(),
|
||||
'page_num': 1
|
||||
}
|
||||
}
|
||||
greenworks._ugcSynchronizeItems(options, sync_dir, success_callback,
|
||||
error_callback);
|
||||
app_id: greenworks.getAppId(),
|
||||
page_num: 1,
|
||||
};
|
||||
}
|
||||
greenworks._ugcSynchronizeItems(options, sync_dir, success_callback, error_callback);
|
||||
};
|
||||
|
||||
greenworks.publishWorkshopFile = function(options, file_path, image_path, title,
|
||||
description, success_callback, error_callback) {
|
||||
if (typeof options !== 'object') {
|
||||
greenworks.publishWorkshopFile = function (
|
||||
options,
|
||||
file_path,
|
||||
image_path,
|
||||
title,
|
||||
description,
|
||||
success_callback,
|
||||
error_callback,
|
||||
) {
|
||||
if (typeof options !== "object") {
|
||||
error_callback = success_callback;
|
||||
success_callback = description;
|
||||
description = title;
|
||||
@ -87,18 +95,24 @@ greenworks.publishWorkshopFile = function(options, file_path, image_path, title,
|
||||
image_path = file_path;
|
||||
file_path = options;
|
||||
options = {
|
||||
'app_id': greenworks.getAppId(),
|
||||
'tags': []
|
||||
}
|
||||
}
|
||||
greenworks._publishWorkshopFile(options, file_path, image_path, title,
|
||||
description, success_callback, error_callback);
|
||||
app_id: greenworks.getAppId(),
|
||||
tags: [],
|
||||
};
|
||||
}
|
||||
greenworks._publishWorkshopFile(options, file_path, image_path, title, description, success_callback, error_callback);
|
||||
};
|
||||
|
||||
greenworks.updatePublishedWorkshopFile = function(options,
|
||||
published_file_handle, file_path, image_path, title, description,
|
||||
success_callback, error_callback) {
|
||||
if (typeof options !== 'object') {
|
||||
greenworks.updatePublishedWorkshopFile = function (
|
||||
options,
|
||||
published_file_handle,
|
||||
file_path,
|
||||
image_path,
|
||||
title,
|
||||
description,
|
||||
success_callback,
|
||||
error_callback,
|
||||
) {
|
||||
if (typeof options !== "object") {
|
||||
error_callback = success_callback;
|
||||
success_callback = description;
|
||||
description = title;
|
||||
@ -107,104 +121,166 @@ greenworks.updatePublishedWorkshopFile = function(options,
|
||||
file_path = published_file_handle;
|
||||
published_file_handle = options;
|
||||
options = {
|
||||
'tags': [] // No tags are set
|
||||
}
|
||||
}
|
||||
greenworks._updatePublishedWorkshopFile(options, published_file_handle,
|
||||
file_path, image_path, title, description, success_callback,
|
||||
error_callback);
|
||||
tags: [], // No tags are set
|
||||
};
|
||||
}
|
||||
greenworks._updatePublishedWorkshopFile(
|
||||
options,
|
||||
published_file_handle,
|
||||
file_path,
|
||||
image_path,
|
||||
title,
|
||||
description,
|
||||
success_callback,
|
||||
error_callback,
|
||||
);
|
||||
};
|
||||
|
||||
// An utility function for publish related APIs.
|
||||
// It processes remains steps after saving files to Steam Cloud.
|
||||
function file_share_process(file_name, image_name, next_process_func,
|
||||
error_callback, progress_callback) {
|
||||
if (progress_callback)
|
||||
progress_callback("Completed on saving files on Steam Cloud.");
|
||||
greenworks.fileShare(file_name, function() {
|
||||
greenworks.fileShare(image_name, function() {
|
||||
function file_share_process(file_name, image_name, next_process_func, error_callback, progress_callback) {
|
||||
if (progress_callback) progress_callback("Completed on saving files on Steam Cloud.");
|
||||
greenworks.fileShare(
|
||||
file_name,
|
||||
function () {
|
||||
greenworks.fileShare(
|
||||
image_name,
|
||||
function () {
|
||||
next_process_func();
|
||||
}, function(err) { error_process(err, error_callback); });
|
||||
}, function(err) { error_process(err, error_callback); });
|
||||
},
|
||||
function (err) {
|
||||
error_process(err, error_callback);
|
||||
},
|
||||
);
|
||||
},
|
||||
function (err) {
|
||||
error_process(err, error_callback);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
// Publishing user generated content(ugc) to Steam contains following steps:
|
||||
// 1. Save file and image to Steam Cloud.
|
||||
// 2. Share the file and image.
|
||||
// 3. publish the file to workshop.
|
||||
greenworks.ugcPublish = function(file_name, title, description, image_name,
|
||||
success_callback, error_callback, progress_callback) {
|
||||
greenworks.ugcPublish = function (
|
||||
file_name,
|
||||
title,
|
||||
description,
|
||||
image_name,
|
||||
success_callback,
|
||||
error_callback,
|
||||
progress_callback,
|
||||
) {
|
||||
var publish_file_process = function () {
|
||||
if (progress_callback)
|
||||
progress_callback("Completed on sharing files.");
|
||||
greenworks.publishWorkshopFile(file_name, image_name, title, description,
|
||||
function(publish_file_id) { success_callback(publish_file_id); },
|
||||
function(err) { error_process(err, error_callback); });
|
||||
if (progress_callback) progress_callback("Completed on sharing files.");
|
||||
greenworks.publishWorkshopFile(
|
||||
file_name,
|
||||
image_name,
|
||||
title,
|
||||
description,
|
||||
function (publish_file_id) {
|
||||
success_callback(publish_file_id);
|
||||
},
|
||||
function (err) {
|
||||
error_process(err, error_callback);
|
||||
},
|
||||
);
|
||||
};
|
||||
greenworks.saveFilesToCloud(
|
||||
[file_name, image_name],
|
||||
function () {
|
||||
file_share_process(file_name, image_name, publish_file_process, error_callback, progress_callback);
|
||||
},
|
||||
function (err) {
|
||||
error_process(err, error_callback);
|
||||
},
|
||||
);
|
||||
};
|
||||
greenworks.saveFilesToCloud([file_name, image_name], function() {
|
||||
file_share_process(file_name, image_name, publish_file_process,
|
||||
error_callback, progress_callback);
|
||||
}, function(err) { error_process(err, error_callback); });
|
||||
}
|
||||
|
||||
// Update publish ugc steps:
|
||||
// 1. Save new file and image to Steam Cloud.
|
||||
// 2. Share file and images.
|
||||
// 3. Update published file.
|
||||
greenworks.ugcPublishUpdate = function(published_file_id, file_name, title,
|
||||
description, image_name, success_callback, error_callback,
|
||||
progress_callback) {
|
||||
greenworks.ugcPublishUpdate = function (
|
||||
published_file_id,
|
||||
file_name,
|
||||
title,
|
||||
description,
|
||||
image_name,
|
||||
success_callback,
|
||||
error_callback,
|
||||
progress_callback,
|
||||
) {
|
||||
var update_published_file_process = function () {
|
||||
if (progress_callback)
|
||||
progress_callback("Completed on sharing files.");
|
||||
greenworks.updatePublishedWorkshopFile(published_file_id,
|
||||
file_name, image_name, title, description,
|
||||
function() { success_callback(); },
|
||||
function(err) { error_process(err, error_callback); });
|
||||
if (progress_callback) progress_callback("Completed on sharing files.");
|
||||
greenworks.updatePublishedWorkshopFile(
|
||||
published_file_id,
|
||||
file_name,
|
||||
image_name,
|
||||
title,
|
||||
description,
|
||||
function () {
|
||||
success_callback();
|
||||
},
|
||||
function (err) {
|
||||
error_process(err, error_callback);
|
||||
},
|
||||
);
|
||||
};
|
||||
|
||||
greenworks.saveFilesToCloud([file_name, image_name], function() {
|
||||
file_share_process(file_name, image_name, update_published_file_process,
|
||||
error_callback, progress_callback);
|
||||
}, function(err) { error_process(err, error_callback); });
|
||||
}
|
||||
greenworks.saveFilesToCloud(
|
||||
[file_name, image_name],
|
||||
function () {
|
||||
file_share_process(file_name, image_name, update_published_file_process, error_callback, progress_callback);
|
||||
},
|
||||
function (err) {
|
||||
error_process(err, error_callback);
|
||||
},
|
||||
);
|
||||
};
|
||||
|
||||
// Greenworks Utils APIs implmentation.
|
||||
greenworks.Utils.move = function(source_dir, target_dir, success_callback,
|
||||
error_callback) {
|
||||
greenworks.Utils.move = function (source_dir, target_dir, success_callback, error_callback) {
|
||||
fs.rename(source_dir, target_dir, function (err) {
|
||||
if (err) {
|
||||
if (error_callback) error_callback(err);
|
||||
return;
|
||||
}
|
||||
if (success_callback)
|
||||
success_callback();
|
||||
if (success_callback) success_callback();
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
greenworks.init = function () {
|
||||
if (this.initAPI()) return true;
|
||||
if (!this.isSteamRunning())
|
||||
throw new Error("Steam initialization failed. Steam is not running.");
|
||||
if (!this.isSteamRunning()) throw new Error("Steam initialization failed. Steam is not running.");
|
||||
var appId;
|
||||
try {
|
||||
appId = fs.readFileSync('steam_appid.txt', 'utf8');
|
||||
appId = fs.readFileSync("steam_appid.txt", "utf8");
|
||||
} catch (e) {
|
||||
throw new Error("Steam initialization failed. Steam is running," +
|
||||
throw new Error(
|
||||
"Steam initialization failed. Steam is running," +
|
||||
"but steam_appid.txt is missing. Expected to find it in: " +
|
||||
require('path').resolve('steam_appid.txt'));
|
||||
require("path").resolve("steam_appid.txt"),
|
||||
);
|
||||
}
|
||||
if (!/^\d+ *\r?\n?$/.test(appId)) {
|
||||
throw new Error("Steam initialization failed. " +
|
||||
throw new Error(
|
||||
"Steam initialization failed. " +
|
||||
"steam_appid.txt appears to be invalid; " +
|
||||
"it should contain a numeric ID: " + appId);
|
||||
"it should contain a numeric ID: " +
|
||||
appId,
|
||||
);
|
||||
}
|
||||
throw new Error("Steam initialization failed, but Steam is running, " +
|
||||
throw new Error(
|
||||
"Steam initialization failed, but Steam is running, " +
|
||||
"and steam_appid.txt is present and valid." +
|
||||
"Maybe that's not really YOUR app ID? " + appId.trim());
|
||||
}
|
||||
"Maybe that's not really YOUR app ID? " +
|
||||
appId.trim(),
|
||||
);
|
||||
};
|
||||
|
||||
var EventEmitter = require('events').EventEmitter;
|
||||
var EventEmitter = require("events").EventEmitter;
|
||||
greenworks.__proto__ = EventEmitter.prototype;
|
||||
EventEmitter.call(greenworks);
|
||||
|
||||
@ -212,6 +288,6 @@ greenworks._steam_events.on = function () {
|
||||
greenworks.emit.apply(greenworks, arguments);
|
||||
};
|
||||
|
||||
process.versions['greenworks'] = greenworks._version;
|
||||
process.versions["greenworks"] = greenworks._version;
|
||||
|
||||
module.exports = greenworks;
|
||||
|
@ -18,7 +18,7 @@ log.transports.console.level = config.get("console-log-level", "debug");
|
||||
log.catchErrors();
|
||||
log.info(`Started app: ${JSON.stringify(process.argv)}`);
|
||||
|
||||
process.on('uncaughtException', function () {
|
||||
process.on("uncaughtException", function () {
|
||||
// The exception will already have been logged by electron-log
|
||||
process.exit(1);
|
||||
});
|
||||
@ -67,42 +67,43 @@ function setStopProcessHandler(app, window, enabled) {
|
||||
// So we'll alert the player to close their browser.
|
||||
if (global.app_playerOpenedExternalLink) {
|
||||
await dialog.showMessageBox({
|
||||
title: 'Bitburner',
|
||||
message: 'You may have to close your browser to properly exit the game.',
|
||||
detail: 'Steam will keep tracking Bitburner as "Running" if any process started within the game is still running.' +
|
||||
' This includes launching an external link, which opens up your browser.',
|
||||
type: 'warning', buttons: ['OK']
|
||||
title: "Bitburner",
|
||||
message: "You may have to close your browser to properly exit the game.",
|
||||
detail:
|
||||
'Steam will keep tracking Bitburner as "Running" if any process started within the game is still running.' +
|
||||
" This includes launching an external link, which opens up your browser.",
|
||||
type: "warning",
|
||||
buttons: ["OK"],
|
||||
});
|
||||
}
|
||||
// We'll try to execute javascript on the page to see if we're stuck
|
||||
let canRunJS = false;
|
||||
window.webContents.executeJavaScript('window.stop(); document.close()', true)
|
||||
.then(() => canRunJS = true);
|
||||
window.webContents.executeJavaScript("window.stop(); document.close()", true).then(() => (canRunJS = true));
|
||||
setTimeout(() => {
|
||||
// Wait a few milliseconds to prevent a race condition before loading the exit screen
|
||||
window.webContents.stop();
|
||||
window.loadFile("exit.html")
|
||||
window.loadFile("exit.html");
|
||||
}, 20);
|
||||
|
||||
// Wait 200ms, if the promise has not yet resolved, let's crash the process since we're possibly in a stuck scenario
|
||||
setTimeout(() => {
|
||||
if (!canRunJS) {
|
||||
// We're stuck, let's crash the process
|
||||
log.warn('Forcefully crashing the renderer process');
|
||||
log.warn("Forcefully crashing the renderer process");
|
||||
window.webContents.forcefullyCrashRenderer();
|
||||
}
|
||||
|
||||
log.debug('Destroying the window');
|
||||
log.debug("Destroying the window");
|
||||
window.destroy();
|
||||
}, 200);
|
||||
}
|
||||
};
|
||||
|
||||
const clearWindowHandler = () => {
|
||||
window = null;
|
||||
};
|
||||
|
||||
const stopProcessHandler = () => {
|
||||
log.info('Quitting the app...');
|
||||
log.info("Quitting the app...");
|
||||
app.isQuiting = true;
|
||||
app.quit();
|
||||
process.exit(0);
|
||||
@ -121,12 +122,12 @@ function setStopProcessHandler(app, window, enabled) {
|
||||
const restoreNewest = config.get("onload-restore-newest", true);
|
||||
if (restoreNewest && !isRestoreDisabled) {
|
||||
try {
|
||||
await storage.restoreIfNewerExists(window)
|
||||
await storage.restoreIfNewerExists(window);
|
||||
} catch (error) {
|
||||
log.error("Could not restore newer file", error);
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const receivedDisableRestoreHandler = async (event, arg) => {
|
||||
if (!window) {
|
||||
@ -140,7 +141,7 @@ function setStopProcessHandler(app, window, enabled) {
|
||||
isRestoreDisabled = false;
|
||||
log.debug("Re-enabling auto-restore");
|
||||
}, arg.duration);
|
||||
}
|
||||
};
|
||||
|
||||
const receivedGameSavedHandler = async (event, arg) => {
|
||||
if (!window) {
|
||||
@ -164,10 +165,11 @@ function setStopProcessHandler(app, window, enabled) {
|
||||
log.debug(`Auto-save to cloud disabled for save game under ${minimumPlaytime}ms (${playtime}ms)`);
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const saveToCloud = debounce(async (save) => {
|
||||
log.debug("Saving to Steam Cloud ...")
|
||||
const saveToCloud = debounce(
|
||||
async (save) => {
|
||||
log.debug("Saving to Steam Cloud ...");
|
||||
try {
|
||||
const playerId = window.gameInfo.player.identifier;
|
||||
await storage.pushGameSaveToSteamCloud(save, playerId);
|
||||
@ -176,26 +178,33 @@ function setStopProcessHandler(app, window, enabled) {
|
||||
log.error(error);
|
||||
utils.writeToast(window, "Could not save to Steam Cloud.", "error", 5000);
|
||||
}
|
||||
}, config.get("cloud-save-min-time", 1000 * 60 * 15), { leading: true });
|
||||
},
|
||||
config.get("cloud-save-min-time", 1000 * 60 * 15),
|
||||
{ leading: true },
|
||||
);
|
||||
|
||||
const saveToDisk = debounce(async (save, fileName) => {
|
||||
log.debug("Saving to Disk ...")
|
||||
const saveToDisk = debounce(
|
||||
async (save, fileName) => {
|
||||
log.debug("Saving to Disk ...");
|
||||
try {
|
||||
const file = await storage.saveGameToDisk(window, { save, fileName });
|
||||
log.silly(`Saved Game to '${file.replaceAll('\\', '\\\\')}'`);
|
||||
log.silly(`Saved Game to '${file.replaceAll("\\", "\\\\")}'`);
|
||||
} catch (error) {
|
||||
log.error(error);
|
||||
utils.writeToast(window, "Could not save to disk", "error", 5000);
|
||||
}
|
||||
}, config.get("disk-save-min-time", 1000 * 60 * 5), { leading: true });
|
||||
},
|
||||
config.get("disk-save-min-time", 1000 * 60 * 5),
|
||||
{ leading: true },
|
||||
);
|
||||
|
||||
if (enabled) {
|
||||
log.debug("Adding closing handlers");
|
||||
ipcMain.on("push-game-ready", receivedGameReadyHandler);
|
||||
ipcMain.on("push-game-saved", receivedGameSavedHandler);
|
||||
ipcMain.on("push-disable-restore", receivedDisableRestoreHandler)
|
||||
ipcMain.on("push-disable-restore", receivedDisableRestoreHandler);
|
||||
window.on("closed", clearWindowHandler);
|
||||
window.on("close", closingWindowHandler)
|
||||
window.on("close", closingWindowHandler);
|
||||
app.on("window-all-closed", stopProcessHandler);
|
||||
} else {
|
||||
log.debug("Removing closing handlers");
|
||||
@ -213,7 +222,7 @@ async function startWindow(noScript) {
|
||||
global.app_handlers = {
|
||||
stopProcess: setStopProcessHandler,
|
||||
createWindow: startWindow,
|
||||
}
|
||||
};
|
||||
|
||||
app.whenReady().then(async () => {
|
||||
log.info("Application is ready!");
|
||||
@ -231,7 +240,8 @@ app.whenReady().then(async () => {
|
||||
title: "Bitburner",
|
||||
message: "Could not connect to Steam",
|
||||
detail: `${global.greenworksError}\n\nYou won't be able to receive achievements until this is resolved and you restart the game.`,
|
||||
type: 'warning', buttons: ['OK']
|
||||
type: "warning",
|
||||
buttons: ["OK"],
|
||||
});
|
||||
}
|
||||
}
|
||||
|
106
electron/menu.js
106
electron/menu.js
@ -37,7 +37,7 @@ function getMenu(window) {
|
||||
log.error(error);
|
||||
utils.writeToast(window, "Could not load last save from disk", "error", 5000);
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
label: "Load From File",
|
||||
@ -51,9 +51,7 @@ function getMenu(window) {
|
||||
{ name: "Game Saves", extensions: ["json", "json.gz", "txt"] },
|
||||
{ name: "All", extensions: ["*"] },
|
||||
],
|
||||
properties: [
|
||||
"openFile", "dontAddToRecent",
|
||||
]
|
||||
properties: ["openFile", "dontAddToRecent"],
|
||||
});
|
||||
if (result.canceled) return;
|
||||
const file = result.filePaths[0];
|
||||
@ -65,7 +63,7 @@ function getMenu(window) {
|
||||
log.error(error);
|
||||
utils.writeToast(window, "Could not load save from disk", "error", 5000);
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
label: "Load From Steam Cloud",
|
||||
@ -78,7 +76,7 @@ function getMenu(window) {
|
||||
log.error(error);
|
||||
utils.writeToast(window, "Could not load from Steam Cloud", "error", 5000);
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
type: "separator",
|
||||
@ -89,8 +87,7 @@ function getMenu(window) {
|
||||
checked: storage.isSaveCompressionEnabled(),
|
||||
click: (menuItem) => {
|
||||
storage.setSaveCompressionConfig(menuItem.checked);
|
||||
utils.writeToast(window,
|
||||
`${menuItem.checked ? "Enabled" : "Disabled"} Save Compression`, "info", 5000);
|
||||
utils.writeToast(window, `${menuItem.checked ? "Enabled" : "Disabled"} Save Compression`, "info", 5000);
|
||||
refreshMenu(window);
|
||||
},
|
||||
},
|
||||
@ -100,8 +97,7 @@ function getMenu(window) {
|
||||
checked: storage.isAutosaveEnabled(),
|
||||
click: (menuItem) => {
|
||||
storage.setAutosaveConfig(menuItem.checked);
|
||||
utils.writeToast(window,
|
||||
`${menuItem.checked ? "Enabled" : "Disabled"} Auto-Save to Disk`, "info", 5000);
|
||||
utils.writeToast(window, `${menuItem.checked ? "Enabled" : "Disabled"} Auto-Save to Disk`, "info", 5000);
|
||||
refreshMenu(window);
|
||||
},
|
||||
},
|
||||
@ -112,8 +108,12 @@ function getMenu(window) {
|
||||
checked: storage.isCloudEnabled(),
|
||||
click: (menuItem) => {
|
||||
storage.setCloudEnabledConfig(menuItem.checked);
|
||||
utils.writeToast(window,
|
||||
`${menuItem.checked ? "Enabled" : "Disabled"} Auto-Save to Steam Cloud`, "info", 5000);
|
||||
utils.writeToast(
|
||||
window,
|
||||
`${menuItem.checked ? "Enabled" : "Disabled"} Auto-Save to Steam Cloud`,
|
||||
"info",
|
||||
5000,
|
||||
);
|
||||
refreshMenu(window);
|
||||
},
|
||||
},
|
||||
@ -123,8 +123,12 @@ function getMenu(window) {
|
||||
checked: config.get("onload-restore-newest", true),
|
||||
click: (menuItem) => {
|
||||
config.set("onload-restore-newest", menuItem.checked);
|
||||
utils.writeToast(window,
|
||||
`${menuItem.checked ? "Enabled" : "Disabled"} Restore Newest on Load`, "info", 5000);
|
||||
utils.writeToast(
|
||||
window,
|
||||
`${menuItem.checked ? "Enabled" : "Disabled"} Restore Newest on Load`,
|
||||
"info",
|
||||
5000,
|
||||
);
|
||||
refreshMenu(window);
|
||||
},
|
||||
},
|
||||
@ -153,7 +157,7 @@ function getMenu(window) {
|
||||
label: "Open Data Directory",
|
||||
click: () => shell.openPath(app.getPath("userData")),
|
||||
},
|
||||
]
|
||||
],
|
||||
},
|
||||
{
|
||||
type: "separator",
|
||||
@ -162,7 +166,7 @@ function getMenu(window) {
|
||||
label: "Quit",
|
||||
click: () => app.quit(),
|
||||
},
|
||||
]
|
||||
],
|
||||
},
|
||||
{
|
||||
label: "Edit",
|
||||
@ -210,29 +214,29 @@ function getMenu(window) {
|
||||
label: "API Server",
|
||||
submenu: [
|
||||
{
|
||||
label: api.isListening() ? 'Disable Server' : 'Enable Server',
|
||||
click: (async () => {
|
||||
label: api.isListening() ? "Disable Server" : "Enable Server",
|
||||
click: async () => {
|
||||
let success = false;
|
||||
try {
|
||||
await api.toggleServer();
|
||||
success = true;
|
||||
} catch (error) {
|
||||
log.error(error);
|
||||
utils.showErrorBox('Error Toggling Server', error);
|
||||
utils.showErrorBox("Error Toggling Server", error);
|
||||
}
|
||||
if (success && api.isListening()) {
|
||||
utils.writeToast(window, "Started API Server", "success");
|
||||
} else if (success && !api.isListening()) {
|
||||
utils.writeToast(window, "Stopped API Server", "success");
|
||||
} else {
|
||||
utils.writeToast(window, 'Error Toggling Server', "error");
|
||||
utils.writeToast(window, "Error Toggling Server", "error");
|
||||
}
|
||||
refreshMenu(window);
|
||||
})
|
||||
},
|
||||
},
|
||||
{
|
||||
label: api.isAutostart() ? 'Disable Autostart' : 'Enable Autostart',
|
||||
click: (async () => {
|
||||
label: api.isAutostart() ? "Disable Autostart" : "Enable Autostart",
|
||||
click: async () => {
|
||||
api.toggleAutostart();
|
||||
if (api.isAutostart()) {
|
||||
utils.writeToast(window, "Enabled API Server Autostart", "success");
|
||||
@ -240,42 +244,45 @@ function getMenu(window) {
|
||||
utils.writeToast(window, "Disabled API Server Autostart", "success");
|
||||
}
|
||||
refreshMenu(window);
|
||||
})
|
||||
},
|
||||
},
|
||||
{
|
||||
label: 'Copy Auth Token',
|
||||
click: (async () => {
|
||||
label: "Copy Auth Token",
|
||||
click: async () => {
|
||||
const token = api.getAuthenticationToken();
|
||||
log.log('Wrote authentication token to clipboard');
|
||||
log.log("Wrote authentication token to clipboard");
|
||||
clipboard.writeText(token);
|
||||
utils.writeToast(window, "Copied Authentication Token to Clipboard", "info");
|
||||
})
|
||||
},
|
||||
},
|
||||
{
|
||||
type: 'separator',
|
||||
type: "separator",
|
||||
},
|
||||
{
|
||||
label: 'Information',
|
||||
label: "Information",
|
||||
click: () => {
|
||||
dialog.showMessageBox({
|
||||
type: 'info',
|
||||
title: 'Bitburner > API Server Information',
|
||||
message: 'The API Server is used to write script files to your in-game home.',
|
||||
detail: 'There is an official Visual Studio Code extension that makes use of that feature.\n\n' +
|
||||
'It allows you to write your script file in an external IDE and have them pushed over to the game automatically.\n' +
|
||||
'If you want more information, head over to: https://github.com/bitburner-official/bitburner-vscode.',
|
||||
buttons: ['Dismiss', 'Open Extension Link (GitHub)'],
|
||||
dialog
|
||||
.showMessageBox({
|
||||
type: "info",
|
||||
title: "Bitburner > API Server Information",
|
||||
message: "The API Server is used to write script files to your in-game home.",
|
||||
detail:
|
||||
"There is an official Visual Studio Code extension that makes use of that feature.\n\n" +
|
||||
"It allows you to write your script file in an external IDE and have them pushed over to the game automatically.\n" +
|
||||
"If you want more information, head over to: https://github.com/bitburner-official/bitburner-vscode.",
|
||||
buttons: ["Dismiss", "Open Extension Link (GitHub)"],
|
||||
defaultId: 0,
|
||||
cancelId: 0,
|
||||
noLink: true,
|
||||
}).then(({response}) => {
|
||||
})
|
||||
.then(({ response }) => {
|
||||
if (response === 1) {
|
||||
utils.openExternal('https://github.com/bitburner-official/bitburner-vscode');
|
||||
utils.openExternal("https://github.com/bitburner-official/bitburner-vscode");
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
label: "Zoom",
|
||||
@ -291,7 +298,7 @@ function getMenu(window) {
|
||||
utils.setZoomFactor(window, newZoom);
|
||||
refreshMenu(window);
|
||||
} else {
|
||||
log.log('Max zoom out')
|
||||
log.log("Max zoom out");
|
||||
utils.writeToast(window, "Cannot zoom in anymore", "warning");
|
||||
}
|
||||
},
|
||||
@ -307,7 +314,7 @@ function getMenu(window) {
|
||||
utils.setZoomFactor(window, newZoom);
|
||||
refreshMenu(window);
|
||||
} else {
|
||||
log.log('Max zoom in')
|
||||
log.log("Max zoom in");
|
||||
utils.writeToast(window, "Cannot zoom out anymore", "warning");
|
||||
}
|
||||
},
|
||||
@ -340,8 +347,8 @@ function getMenu(window) {
|
||||
} catch (error) {
|
||||
log.error(error);
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
]);
|
||||
@ -352,5 +359,6 @@ function refreshMenu(window) {
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
getMenu, refreshMenu,
|
||||
}
|
||||
getMenu,
|
||||
refreshMenu,
|
||||
};
|
||||
|
@ -1,11 +1,10 @@
|
||||
/* eslint-disable @typescript-eslint/no-var-requires */
|
||||
const { ipcRenderer, contextBridge } = require('electron')
|
||||
const { ipcRenderer, contextBridge } = require("electron");
|
||||
const log = require("electron-log");
|
||||
|
||||
contextBridge.exposeInMainWorld(
|
||||
"electronBridge", {
|
||||
contextBridge.exposeInMainWorld("electronBridge", {
|
||||
send: (channel, data) => {
|
||||
log.log("Send on channel " + channel)
|
||||
log.log("Send on channel " + channel);
|
||||
// whitelist channels
|
||||
let validChannels = [
|
||||
"get-save-data-response",
|
||||
@ -20,7 +19,7 @@ contextBridge.exposeInMainWorld(
|
||||
}
|
||||
},
|
||||
receive: (channel, func) => {
|
||||
log.log("Receive on channel " + channel)
|
||||
log.log("Receive on channel " + channel);
|
||||
let validChannels = [
|
||||
"get-save-data-request",
|
||||
"get-save-info-request",
|
||||
@ -33,6 +32,5 @@ contextBridge.exposeInMainWorld(
|
||||
// Deliberately strip event as it includes `sender`
|
||||
ipcRenderer.on(channel, (event, ...args) => func(...args));
|
||||
}
|
||||
}
|
||||
}
|
||||
);
|
||||
},
|
||||
});
|
||||
|
@ -16,9 +16,9 @@ const config = new Config();
|
||||
// https://stackoverflow.com/a/69418940
|
||||
const dirSize = async (directory) => {
|
||||
const files = await fs.readdir(directory);
|
||||
const stats = files.map(file => fs.stat(path.join(directory, file)));
|
||||
const stats = files.map((file) => fs.stat(path.join(directory, file)));
|
||||
return (await Promise.all(stats)).reduce((accumulator, { size }) => accumulator + size, 0);
|
||||
}
|
||||
};
|
||||
|
||||
const getDirFileStats = async (directory) => {
|
||||
const files = await fs.readdir(directory);
|
||||
@ -26,30 +26,31 @@ const getDirFileStats = async (directory) => {
|
||||
const file = path.join(directory, f);
|
||||
return fs.stat(file).then((stat) => ({ file, stat }));
|
||||
});
|
||||
const data = (await Promise.all(stats));
|
||||
const data = await Promise.all(stats);
|
||||
return data;
|
||||
};
|
||||
|
||||
const getNewestFile = async (directory) => {
|
||||
const data = await getDirFileStats(directory)
|
||||
const data = await getDirFileStats(directory);
|
||||
return data.sort((a, b) => b.stat.mtime.getTime() - a.stat.mtime.getTime())[0];
|
||||
};
|
||||
|
||||
const getAllSaves = async (window) => {
|
||||
const rootDirectory = await getSaveFolder(window, true);
|
||||
const data = await fs.readdir(rootDirectory, { withFileTypes: true });
|
||||
const savesPromises = data.filter((e) => e.isDirectory()).
|
||||
map((dir) => path.join(rootDirectory, dir.name)).
|
||||
map((dir) => getDirFileStats(dir));
|
||||
const savesPromises = data
|
||||
.filter((e) => e.isDirectory())
|
||||
.map((dir) => path.join(rootDirectory, dir.name))
|
||||
.map((dir) => getDirFileStats(dir));
|
||||
const saves = await Promise.all(savesPromises);
|
||||
const flat = flatten(saves);
|
||||
return flat;
|
||||
}
|
||||
};
|
||||
|
||||
async function prepareSaveFolders(window) {
|
||||
const rootFolder = await getSaveFolder(window, true);
|
||||
const currentFolder = await getSaveFolder(window);
|
||||
const backupsFolder = path.join(rootFolder, "/_backups")
|
||||
const backupsFolder = path.join(rootFolder, "/_backups");
|
||||
await prepareFolders(rootFolder, currentFolder, backupsFolder);
|
||||
}
|
||||
|
||||
@ -60,7 +61,7 @@ async function prepareFolders(...folders) {
|
||||
// eslint-disable-next-line no-await-in-loop
|
||||
await fs.stat(folder);
|
||||
} catch (error) {
|
||||
if (error.code === 'ENOENT') {
|
||||
if (error.code === "ENOENT") {
|
||||
log.warn(`'${folder}' not found, creating it...`);
|
||||
// eslint-disable-next-line no-await-in-loop
|
||||
await fs.mkdir(folder);
|
||||
@ -125,14 +126,14 @@ function isCloudEnabled() {
|
||||
function saveCloudFile(name, content) {
|
||||
return new Promise((resolve, reject) => {
|
||||
greenworks.saveTextToFile(name, content, resolve, reject);
|
||||
})
|
||||
});
|
||||
}
|
||||
|
||||
function getFirstCloudFile() {
|
||||
const nbFiles = greenworks.getFileCount();
|
||||
if (nbFiles === 0) throw new Error('No files in cloud');
|
||||
if (nbFiles === 0) throw new Error("No files in cloud");
|
||||
const file = greenworks.getFileNameAndSize(0);
|
||||
log.silly(`Found ${nbFiles} files.`)
|
||||
log.silly(`Found ${nbFiles} files.`);
|
||||
log.silly(`First File: ${file.name} (${file.size} bytes)`);
|
||||
return file.name;
|
||||
}
|
||||
@ -153,7 +154,7 @@ function deleteCloudFile() {
|
||||
|
||||
async function getSteamCloudQuota() {
|
||||
return new Promise((resolve, reject) => {
|
||||
greenworks.getCloudQuota(resolve, reject)
|
||||
greenworks.getCloudQuota(resolve, reject);
|
||||
});
|
||||
}
|
||||
|
||||
@ -166,9 +167,9 @@ async function backupSteamDataToDisk(currentPlayerId) {
|
||||
if (previousPlayerId !== currentPlayerId) {
|
||||
const backupSave = await getSteamCloudSaveString();
|
||||
const backupFile = path.join(app.getPath("userData"), "/saves/_backups", `${previousPlayerId}.json.gz`);
|
||||
const buffer = Buffer.from(backupSave, 'base64').toString('utf8');
|
||||
const buffer = Buffer.from(backupSave, "base64").toString("utf8");
|
||||
saveContent = await gzip(buffer);
|
||||
await fs.writeFile(backupFile, saveContent, 'utf8');
|
||||
await fs.writeFile(backupFile, saveContent, "utf8");
|
||||
log.debug(`Saved backup game to '${backupFile}`);
|
||||
}
|
||||
}
|
||||
@ -219,7 +220,9 @@ async function saveGameToDisk(window, saveData) {
|
||||
const remainingSpaceBytes = maxFolderSizeBytes - saveFolderSizeBytes;
|
||||
log.debug(`Folder Usage: ${saveFolderSizeBytes} bytes`);
|
||||
log.debug(`Folder Capacity: ${maxFolderSizeBytes} bytes`);
|
||||
log.debug(`Remaining: ${remainingSpaceBytes} bytes (${(saveFolderSizeBytes / maxFolderSizeBytes * 100).toFixed(2)}% used)`)
|
||||
log.debug(
|
||||
`Remaining: ${remainingSpaceBytes} bytes (${((saveFolderSizeBytes / maxFolderSizeBytes) * 100).toFixed(2)}% used)`,
|
||||
);
|
||||
const shouldCompress = isSaveCompressionEnabled();
|
||||
const fileName = saveData.fileName;
|
||||
const file = path.join(currentFolder, fileName + (shouldCompress ? ".gz" : ""));
|
||||
@ -227,10 +230,10 @@ async function saveGameToDisk(window, saveData) {
|
||||
let saveContent = saveData.save;
|
||||
if (shouldCompress) {
|
||||
// Let's decode the base64 string so GZIP is more efficient.
|
||||
const buffer = Buffer.from(saveContent, 'base64').toString('utf8');
|
||||
const buffer = Buffer.from(saveContent, "base64").toString("utf8");
|
||||
saveContent = await gzip(buffer);
|
||||
}
|
||||
await fs.writeFile(file, saveContent, 'utf8');
|
||||
await fs.writeFile(file, saveContent, "utf8");
|
||||
log.debug(`Saved Game to '${file}'`);
|
||||
log.debug(`Save Size: ${saveContent.length} bytes`);
|
||||
} catch (error) {
|
||||
@ -240,7 +243,8 @@ async function saveGameToDisk(window, saveData) {
|
||||
const fileStats = await getDirFileStats(currentFolder);
|
||||
const oldestFiles = fileStats
|
||||
.sort((a, b) => a.stat.mtime.getTime() - b.stat.mtime.getTime())
|
||||
.map(f => f.file).filter(f => f !== file);
|
||||
.map((f) => f.file)
|
||||
.filter((f) => f !== file);
|
||||
|
||||
while (saveFolderSizeBytes > maxFolderSizeBytes && oldestFiles.length > 0) {
|
||||
const fileToRemove = oldestFiles.shift();
|
||||
@ -255,7 +259,12 @@ async function saveGameToDisk(window, saveData) {
|
||||
// eslint-disable-next-line no-await-in-loop
|
||||
saveFolderSizeBytes = await getFolderSizeInBytes(currentFolder);
|
||||
log.debug(`Save Folder: ${saveFolderSizeBytes} bytes`);
|
||||
log.debug(`Remaining: ${maxFolderSizeBytes - saveFolderSizeBytes} bytes (${(saveFolderSizeBytes / maxFolderSizeBytes * 100).toFixed(2)}% used)`)
|
||||
log.debug(
|
||||
`Remaining: ${maxFolderSizeBytes - saveFolderSizeBytes} bytes (${(
|
||||
(saveFolderSizeBytes / maxFolderSizeBytes) *
|
||||
100
|
||||
).toFixed(2)}% used)`,
|
||||
);
|
||||
}
|
||||
|
||||
return file;
|
||||
@ -271,13 +280,13 @@ async function loadLastFromDisk(window) {
|
||||
async function loadFileFromDisk(path) {
|
||||
const buffer = await fs.readFile(path);
|
||||
let content;
|
||||
if (path.endsWith('.gz')) {
|
||||
if (path.endsWith(".gz")) {
|
||||
const uncompressedBuffer = await gunzip(buffer);
|
||||
content = uncompressedBuffer.toString('base64');
|
||||
content = uncompressedBuffer.toString("base64");
|
||||
log.debug(`Uncompressed file content (new size: ${content.length} bytes)`);
|
||||
} else {
|
||||
content = buffer.toString('utf8');
|
||||
log.debug(`Loaded file with ${content.length} bytes`)
|
||||
content = buffer.toString("utf8");
|
||||
log.debug(`Loaded file with ${content.length} bytes`);
|
||||
}
|
||||
return content;
|
||||
}
|
||||
@ -293,10 +302,10 @@ function getSaveInformation(window, save) {
|
||||
|
||||
function getCurrentSave(window) {
|
||||
return new Promise((resolve) => {
|
||||
ipcMain.once('get-save-data-response', (event, data) => {
|
||||
ipcMain.once("get-save-data-response", (event, data) => {
|
||||
resolve(data);
|
||||
});
|
||||
window.webContents.send('get-save-data-request');
|
||||
window.webContents.send("get-save-data-request");
|
||||
});
|
||||
}
|
||||
|
||||
@ -322,8 +331,7 @@ async function restoreIfNewerExists(window) {
|
||||
}
|
||||
|
||||
try {
|
||||
const saves = (await getAllSaves()).
|
||||
sort((a, b) => b.stat.mtime.getTime() - a.stat.mtime.getTime());
|
||||
const saves = (await getAllSaves()).sort((a, b) => b.stat.mtime.getTime() - a.stat.mtime.getTime());
|
||||
if (saves.length > 0) {
|
||||
disk.save = await loadFileFromDisk(saves[0].file);
|
||||
disk.data = await getSaveInformation(window, disk.save);
|
||||
@ -339,18 +347,17 @@ async function restoreIfNewerExists(window) {
|
||||
log.info("No data to import");
|
||||
} else if (!steam.data) {
|
||||
// We'll just compare using the lastSave field for now.
|
||||
log.debug('Best potential save match: Disk');
|
||||
log.debug("Best potential save match: Disk");
|
||||
bestMatch = disk;
|
||||
} else if (!disk.data) {
|
||||
log.debug('Best potential save match: Steam Cloud');
|
||||
log.debug("Best potential save match: Steam Cloud");
|
||||
bestMatch = steam;
|
||||
} else if ((steam.data.lastSave >= disk.data.lastSave)
|
||||
|| (steam.data.playtime + lowPlaytime > disk.data.playtime)) {
|
||||
} else if (steam.data.lastSave >= disk.data.lastSave || steam.data.playtime + lowPlaytime > disk.data.playtime) {
|
||||
// We want to prioritze steam data if the playtime is very close
|
||||
log.debug('Best potential save match: Steam Cloud');
|
||||
log.debug("Best potential save match: Steam Cloud");
|
||||
bestMatch = steam;
|
||||
} else {
|
||||
log.debug('Best potential save match: disk');
|
||||
log.debug("Best potential save match: disk");
|
||||
bestMatch = disk;
|
||||
}
|
||||
if (bestMatch) {
|
||||
@ -373,12 +380,24 @@ async function restoreIfNewerExists(window) {
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
getCurrentSave, getSaveInformation,
|
||||
restoreIfNewerExists, pushSaveGameForImport,
|
||||
pushGameSaveToSteamCloud, getSteamCloudSaveString, getSteamCloudQuota, deleteCloudFile,
|
||||
saveGameToDisk, loadLastFromDisk, loadFileFromDisk,
|
||||
getSaveFolder, prepareSaveFolders, getAllSaves,
|
||||
isCloudEnabled, setCloudEnabledConfig,
|
||||
isAutosaveEnabled, setAutosaveConfig,
|
||||
isSaveCompressionEnabled, setSaveCompressionConfig,
|
||||
getCurrentSave,
|
||||
getSaveInformation,
|
||||
restoreIfNewerExists,
|
||||
pushSaveGameForImport,
|
||||
pushGameSaveToSteamCloud,
|
||||
getSteamCloudSaveString,
|
||||
getSteamCloudQuota,
|
||||
deleteCloudFile,
|
||||
saveGameToDisk,
|
||||
loadLastFromDisk,
|
||||
loadFileFromDisk,
|
||||
getSaveFolder,
|
||||
prepareSaveFolders,
|
||||
getAllSaves,
|
||||
isCloudEnabled,
|
||||
setCloudEnabledConfig,
|
||||
isAutosaveEnabled,
|
||||
setAutosaveConfig,
|
||||
isSaveCompressionEnabled,
|
||||
setSaveCompressionConfig,
|
||||
};
|
||||
|
@ -9,39 +9,42 @@ const Config = require("electron-config");
|
||||
const config = new Config();
|
||||
|
||||
function reloadAndKill(window, killScripts) {
|
||||
const setStopProcessHandler = global.app_handlers.stopProcess
|
||||
const setStopProcessHandler = global.app_handlers.stopProcess;
|
||||
const createWindowHandler = global.app_handlers.createWindow;
|
||||
|
||||
log.info('Reloading & Killing all scripts...');
|
||||
log.info("Reloading & Killing all scripts...");
|
||||
setStopProcessHandler(app, window, false);
|
||||
|
||||
achievements.disableAchievementsInterval(window);
|
||||
api.disable();
|
||||
|
||||
window.webContents.forcefullyCrashRenderer();
|
||||
window.on('closed', () => {
|
||||
window.on("closed", () => {
|
||||
// Wait for window to be closed before opening the new one to prevent race conditions
|
||||
log.debug('Opening new window');
|
||||
log.debug("Opening new window");
|
||||
createWindowHandler(killScripts);
|
||||
})
|
||||
});
|
||||
|
||||
window.close();
|
||||
}
|
||||
|
||||
function promptForReload(window) {
|
||||
detachUnresponsiveAppHandler(window);
|
||||
dialog.showMessageBox({
|
||||
type: 'error',
|
||||
title: 'Bitburner > Application Unresponsive',
|
||||
message: 'The application is unresponsive, possibly due to an infinite loop in your scripts.',
|
||||
detail:' Did you forget a ns.sleep(x)?\n\n' +
|
||||
'The application will be restarted for you, do you want to kill all running scripts?',
|
||||
buttons: ['Restart', 'Cancel'],
|
||||
dialog
|
||||
.showMessageBox({
|
||||
type: "error",
|
||||
title: "Bitburner > Application Unresponsive",
|
||||
message: "The application is unresponsive, possibly due to an infinite loop in your scripts.",
|
||||
detail:
|
||||
" Did you forget a ns.sleep(x)?\n\n" +
|
||||
"The application will be restarted for you, do you want to kill all running scripts?",
|
||||
buttons: ["Restart", "Cancel"],
|
||||
defaultId: 0,
|
||||
checkboxLabel: 'Kill all running scripts',
|
||||
checkboxLabel: "Kill all running scripts",
|
||||
checkboxChecked: true,
|
||||
noLink: true,
|
||||
}).then(({response, checkboxChecked}) => {
|
||||
})
|
||||
.then(({ response, checkboxChecked }) => {
|
||||
if (response === 0) {
|
||||
reloadAndKill(window, checkboxChecked);
|
||||
} else {
|
||||
@ -52,18 +55,15 @@ function promptForReload(window) {
|
||||
|
||||
function attachUnresponsiveAppHandler(window) {
|
||||
window.unresponsiveHandler = () => promptForReload(window);
|
||||
window.on('unresponsive', window.unresponsiveHandler);
|
||||
window.on("unresponsive", window.unresponsiveHandler);
|
||||
}
|
||||
|
||||
function detachUnresponsiveAppHandler(window) {
|
||||
window.off('unresponsive', window.unresponsiveHandler);
|
||||
window.off("unresponsive", window.unresponsiveHandler);
|
||||
}
|
||||
|
||||
function showErrorBox(title, error) {
|
||||
dialog.showErrorBox(
|
||||
title,
|
||||
`${error.name}\n\n${error.message}`
|
||||
);
|
||||
dialog.showErrorBox(title, `${error.name}\n\n${error.message}`);
|
||||
}
|
||||
|
||||
function exportSaveFromIndexedDb() {
|
||||
@ -71,15 +71,15 @@ function exportSaveFromIndexedDb() {
|
||||
const dbRequest = indexedDB.open("bitburnerSave");
|
||||
dbRequest.onsuccess = () => {
|
||||
const db = dbRequest.result;
|
||||
const transaction = db.transaction(['savestring'], "readonly");
|
||||
const store = transaction.objectStore('savestring');
|
||||
const request = store.get('save');
|
||||
const transaction = db.transaction(["savestring"], "readonly");
|
||||
const store = transaction.objectStore("savestring");
|
||||
const request = store.get("save");
|
||||
request.onsuccess = () => {
|
||||
const file = new Blob([request.result], {type: 'text/plain'});
|
||||
const file = new Blob([request.result], { type: "text/plain" });
|
||||
const a = document.createElement("a");
|
||||
const url = URL.createObjectURL(file);
|
||||
a.href = url;
|
||||
a.download = 'save.json';
|
||||
a.download = "save.json";
|
||||
document.body.appendChild(a);
|
||||
a.click();
|
||||
setTimeout(function () {
|
||||
@ -87,24 +87,21 @@ function exportSaveFromIndexedDb() {
|
||||
window.URL.revokeObjectURL(url);
|
||||
resolve();
|
||||
}, 0);
|
||||
}
|
||||
}
|
||||
};
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
async function exportSave(window) {
|
||||
await window.webContents
|
||||
.executeJavaScript(`${exportSaveFromIndexedDb.toString()}; exportSaveFromIndexedDb();`, true);
|
||||
await window.webContents.executeJavaScript(`${exportSaveFromIndexedDb.toString()}; exportSaveFromIndexedDb();`, true);
|
||||
}
|
||||
|
||||
async function writeTerminal(window, message, type = null) {
|
||||
await window.webContents
|
||||
.executeJavaScript(`window.appNotifier.terminal("${message}", "${type}");`, true)
|
||||
await window.webContents.executeJavaScript(`window.appNotifier.terminal("${message}", "${type}");`, true);
|
||||
}
|
||||
|
||||
async function writeToast(window, message, type = "info", duration = 2000) {
|
||||
await window.webContents
|
||||
.executeJavaScript(`window.appNotifier.toast("${message}", "${type}", ${duration});`, true)
|
||||
await window.webContents.executeJavaScript(`window.appNotifier.toast("${message}", "${type}", ${duration});`, true);
|
||||
}
|
||||
|
||||
function openExternal(url) {
|
||||
@ -113,7 +110,7 @@ function openExternal(url) {
|
||||
}
|
||||
|
||||
function getZoomFactor() {
|
||||
const configZoom = config.get('zoom', 1);
|
||||
const configZoom = config.get("zoom", 1);
|
||||
return configZoom;
|
||||
}
|
||||
|
||||
@ -121,14 +118,20 @@ function setZoomFactor(window, zoom = null) {
|
||||
if (zoom === null) {
|
||||
zoom = 1;
|
||||
} else {
|
||||
config.set('zoom', zoom);
|
||||
config.set("zoom", zoom);
|
||||
}
|
||||
window.webContents.setZoomFactor(zoom);
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
reloadAndKill, showErrorBox, exportSave,
|
||||
attachUnresponsiveAppHandler, detachUnresponsiveAppHandler,
|
||||
openExternal, writeTerminal, writeToast,
|
||||
getZoomFactor, setZoomFactor,
|
||||
}
|
||||
reloadAndKill,
|
||||
showErrorBox,
|
||||
exportSave,
|
||||
attachUnresponsiveAppHandler,
|
||||
detachUnresponsiveAppHandler,
|
||||
openExternal,
|
||||
writeTerminal,
|
||||
writeToast,
|
||||
getZoomFactor,
|
||||
setZoomFactor,
|
||||
};
|
||||
|
@ -4,12 +4,11 @@ module.exports = {
|
||||
transform: {
|
||||
"^.+\\.(js|jsx|ts|tsx)$": "babel-jest",
|
||||
},
|
||||
testPathIgnorePatterns: [
|
||||
'.cypress', 'node_modules', 'dist',
|
||||
],
|
||||
testPathIgnorePatterns: [".cypress", "node_modules", "dist"],
|
||||
testEnvironment: "jsdom",
|
||||
moduleNameMapper: {
|
||||
"\\.(jpg|jpeg|png|gif|eot|otf|webp|svg|ttf|woff|woff2|mp4|webm|wav|mp3|m4a|aac|oga)$": "<rootDir>/test/__mocks__/fileMock.js",
|
||||
"\\.(css|less)$": "<rootDir>/test/__mocks__/styleMock.js"
|
||||
}
|
||||
"\\.(jpg|jpeg|png|gif|eot|otf|webp|svg|ttf|woff|woff2|mp4|webm|wav|mp3|m4a|aac|oga)$":
|
||||
"<rootDir>/test/__mocks__/fileMock.js",
|
||||
"\\.(css|less)$": "<rootDir>/test/__mocks__/styleMock.js",
|
||||
},
|
||||
};
|
||||
|
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@ -3,7 +3,7 @@ import React from "react";
|
||||
import { Box, Typography } from "@mui/material";
|
||||
|
||||
import { Achievement } from "./Achievements";
|
||||
import { Settings } from "../Settings/Settings"
|
||||
import { Settings } from "../Settings/Settings";
|
||||
import { AchievementIcon } from "./AchievementIcon";
|
||||
|
||||
interface IProps {
|
||||
@ -13,43 +13,58 @@ interface IProps {
|
||||
cssFiltersLocked: string;
|
||||
}
|
||||
|
||||
export function AchievementEntry({ achievement, unlockedOn, cssFiltersUnlocked, cssFiltersLocked }: IProps): JSX.Element {
|
||||
export function AchievementEntry({
|
||||
achievement,
|
||||
unlockedOn,
|
||||
cssFiltersUnlocked,
|
||||
cssFiltersLocked,
|
||||
}: IProps): JSX.Element {
|
||||
if (!achievement) return <></>;
|
||||
const isUnlocked = !!unlockedOn;
|
||||
|
||||
const mainColor = isUnlocked ? Settings.theme.primary : Settings.theme.secondarylight;
|
||||
|
||||
let achievedOn = '';
|
||||
let achievedOn = "";
|
||||
if (unlockedOn) {
|
||||
achievedOn = new Date(unlockedOn).toLocaleString();
|
||||
}
|
||||
|
||||
return (
|
||||
<Box sx={{
|
||||
border: `1px solid ${Settings.theme.well}`, mb: 2
|
||||
}}>
|
||||
<Box sx={{
|
||||
display: 'flex',
|
||||
flexDirection: 'row',
|
||||
flexWrap: 'wrap',
|
||||
}}>
|
||||
<Box
|
||||
sx={{
|
||||
border: `1px solid ${Settings.theme.well}`,
|
||||
mb: 2,
|
||||
}}
|
||||
>
|
||||
<Box
|
||||
sx={{
|
||||
display: "flex",
|
||||
flexDirection: "row",
|
||||
flexWrap: "wrap",
|
||||
}}
|
||||
>
|
||||
<AchievementIcon
|
||||
achievement={achievement} unlocked={isUnlocked} size="72px"
|
||||
colorFilters={isUnlocked ? cssFiltersUnlocked: cssFiltersLocked} />
|
||||
<Box sx={{
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
justifyContent: 'center',
|
||||
px: 1
|
||||
}}>
|
||||
achievement={achievement}
|
||||
unlocked={isUnlocked}
|
||||
size="72px"
|
||||
colorFilters={isUnlocked ? cssFiltersUnlocked : cssFiltersLocked}
|
||||
/>
|
||||
<Box
|
||||
sx={{
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
justifyContent: "center",
|
||||
px: 1,
|
||||
}}
|
||||
>
|
||||
<Typography variant="h6" sx={{ color: mainColor }}>
|
||||
{achievement.Name}
|
||||
</Typography>
|
||||
<Typography variant="body2" sx={{ maxWidth: '500px', color: mainColor}}>
|
||||
<Typography variant="body2" sx={{ maxWidth: "500px", color: mainColor }}>
|
||||
{achievement.Description}
|
||||
</Typography>
|
||||
{isUnlocked && (
|
||||
<Typography variant="caption" sx={{ fontSize: '12px', color: Settings.theme.primarydark }}>
|
||||
<Typography variant="caption" sx={{ fontSize: "12px", color: Settings.theme.primarydark }}>
|
||||
Acquired on {achievedOn}
|
||||
</Typography>
|
||||
)}
|
||||
|
@ -3,7 +3,7 @@ import React, { useState } from "react";
|
||||
import { Box } from "@mui/material";
|
||||
|
||||
import { Achievement } from "./Achievements";
|
||||
import { Settings } from "../Settings/Settings"
|
||||
import { Settings } from "../Settings/Settings";
|
||||
|
||||
interface IProps {
|
||||
achievement: Achievement;
|
||||
@ -16,20 +16,23 @@ export function AchievementIcon({ achievement, unlocked, colorFilters, size }: I
|
||||
const [imgLoaded, setImgLoaded] = useState(false);
|
||||
const mainColor = unlocked ? Settings.theme.primarydark : Settings.theme.secondarydark;
|
||||
|
||||
if (!achievement.Icon) return (<></>);
|
||||
if (!achievement.Icon) return <></>;
|
||||
return (
|
||||
<Box
|
||||
sx={{
|
||||
border: `1px solid ${mainColor}`,
|
||||
width: size, height: size,
|
||||
width: size,
|
||||
height: size,
|
||||
m: 1,
|
||||
visibility: imgLoaded ? 'visible' : 'hidden'
|
||||
visibility: imgLoaded ? "visible" : "hidden",
|
||||
}}
|
||||
>
|
||||
<img src={`dist/icons/achievements/${encodeURI(achievement.Icon)}.svg`}
|
||||
<img
|
||||
src={`dist/icons/achievements/${encodeURI(achievement.Icon)}.svg`}
|
||||
style={{ filter: colorFilters, width: size, height: size }}
|
||||
onLoad={() => setImgLoaded(true)}
|
||||
alt={achievement.Name} />
|
||||
alt={achievement.Name}
|
||||
/>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
@ -4,7 +4,7 @@ import { Accordion, AccordionSummary, AccordionDetails, Box, Typography } from "
|
||||
|
||||
import { AchievementEntry } from "./AchievementEntry";
|
||||
import { Achievement, PlayerAchievement } from "./Achievements";
|
||||
import { Settings } from "../Settings/Settings"
|
||||
import { Settings } from "../Settings/Settings";
|
||||
import { getFiltersFromHex } from "../ThirdParty/colorUtils";
|
||||
import { CorruptableText } from "../ui/React/CorruptableText";
|
||||
|
||||
@ -18,32 +18,39 @@ export function AchievementList({ achievements, playerAchievements }: IProps): J
|
||||
const cssPrimary = getFiltersFromHex(Settings.theme.primary);
|
||||
const cssSecondary = getFiltersFromHex(Settings.theme.secondary);
|
||||
|
||||
const data = achievements.map(achievement => ({
|
||||
const data = achievements
|
||||
.map((achievement) => ({
|
||||
achievement,
|
||||
unlockedOn: playerAchievements.find(playerAchievement => playerAchievement.ID === achievement.ID)?.unlockedOn,
|
||||
})).sort((a, b) => (b.unlockedOn ?? 0) - (a.unlockedOn ?? 0));
|
||||
unlockedOn: playerAchievements.find((playerAchievement) => playerAchievement.ID === achievement.ID)?.unlockedOn,
|
||||
}))
|
||||
.sort((a, b) => (b.unlockedOn ?? 0) - (a.unlockedOn ?? 0));
|
||||
|
||||
const unlocked = data.filter(entry => entry.unlockedOn);
|
||||
const unlocked = data.filter((entry) => entry.unlockedOn);
|
||||
|
||||
// Hidden achievements
|
||||
const secret = data.filter(entry => !entry.unlockedOn && entry.achievement.Secret)
|
||||
const secret = data.filter((entry) => !entry.unlockedOn && entry.achievement.Secret);
|
||||
|
||||
// Locked behind locked content (bitnode x)
|
||||
const unavailable = data.filter(entry => !entry.unlockedOn && !entry.achievement.Secret && entry.achievement.Visible && !entry.achievement.Visible());
|
||||
const unavailable = data.filter(
|
||||
(entry) =>
|
||||
!entry.unlockedOn && !entry.achievement.Secret && entry.achievement.Visible && !entry.achievement.Visible(),
|
||||
);
|
||||
|
||||
// Remaining achievements
|
||||
const locked = data
|
||||
.filter(entry => !unlocked.map(u => u.achievement.ID).includes(entry.achievement.ID))
|
||||
.filter(entry => !secret.map(u => u.achievement.ID).includes(entry.achievement.ID))
|
||||
.filter(entry => !unavailable.map(u => u.achievement.ID).includes(entry.achievement.ID));
|
||||
.filter((entry) => !unlocked.map((u) => u.achievement.ID).includes(entry.achievement.ID))
|
||||
.filter((entry) => !secret.map((u) => u.achievement.ID).includes(entry.achievement.ID))
|
||||
.filter((entry) => !unavailable.map((u) => u.achievement.ID).includes(entry.achievement.ID));
|
||||
|
||||
return (
|
||||
<Box sx={{ pr: 18, my: 2 }}>
|
||||
<Box sx={{
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
flexWrap: 'wrap',
|
||||
}}>
|
||||
<Box
|
||||
sx={{
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
flexWrap: "wrap",
|
||||
}}
|
||||
>
|
||||
{unlocked.length > 0 && (
|
||||
<Accordion defaultExpanded disableGutters square>
|
||||
<AccordionSummary>
|
||||
@ -52,12 +59,14 @@ export function AchievementList({ achievements, playerAchievements }: IProps): J
|
||||
</Typography>
|
||||
</AccordionSummary>
|
||||
<AccordionDetails sx={{ pt: 2 }}>
|
||||
{unlocked.map(item => (
|
||||
<AchievementEntry key={`unlocked_${item.achievement.ID}`}
|
||||
{unlocked.map((item) => (
|
||||
<AchievementEntry
|
||||
key={`unlocked_${item.achievement.ID}`}
|
||||
achievement={item.achievement}
|
||||
unlockedOn={item.unlockedOn}
|
||||
cssFiltersUnlocked={cssPrimary}
|
||||
cssFiltersLocked={cssSecondary} />
|
||||
cssFiltersLocked={cssSecondary}
|
||||
/>
|
||||
))}
|
||||
</AccordionDetails>
|
||||
</Accordion>
|
||||
@ -71,11 +80,13 @@ export function AchievementList({ achievements, playerAchievements }: IProps): J
|
||||
</Typography>
|
||||
</AccordionSummary>
|
||||
<AccordionDetails sx={{ pt: 2 }}>
|
||||
{locked.map(item => (
|
||||
<AchievementEntry key={`locked_${item.achievement.ID}`}
|
||||
{locked.map((item) => (
|
||||
<AchievementEntry
|
||||
key={`locked_${item.achievement.ID}`}
|
||||
achievement={item.achievement}
|
||||
cssFiltersUnlocked={cssPrimary}
|
||||
cssFiltersLocked={cssSecondary} />
|
||||
cssFiltersLocked={cssSecondary}
|
||||
/>
|
||||
))}
|
||||
</AccordionDetails>
|
||||
</Accordion>
|
||||
@ -105,7 +116,7 @@ export function AchievementList({ achievements, playerAchievements }: IProps): J
|
||||
</AccordionSummary>
|
||||
<AccordionDetails>
|
||||
<Typography color="secondary" sx={{ mt: 1 }}>
|
||||
{secret.map(item => (
|
||||
{secret.map((item) => (
|
||||
<span key={`secret_${item.achievement.ID}`}>
|
||||
<CorruptableText content={item.achievement.ID}></CorruptableText>
|
||||
<br />
|
||||
|
@ -1,9 +1,9 @@
|
||||
# Adding Achievements
|
||||
|
||||
* Add a .svg in `./assets/Steam/achievements/real`
|
||||
* Create the achievement in Steam Dev Portal
|
||||
* Run `sh ./assets/Steam/achievements/pack-for-web.sh`
|
||||
* Run `node ./tools/fetch-steam-achievements-data DEVKEYHERE`
|
||||
* Get your key here: https://steamcommunity.com/dev/apikey
|
||||
* Add an entry in `./src/Achievements/Achievements.ts` -> achievements
|
||||
* Commit `./dist/icons/achievements` & `./src/Achievements/AchievementData.json`
|
||||
- Add a .svg in `./assets/Steam/achievements/real`
|
||||
- Create the achievement in Steam Dev Portal
|
||||
- Run `sh ./assets/Steam/achievements/pack-for-web.sh`
|
||||
- Run `node ./tools/fetch-steam-achievements-data DEVKEYHERE`
|
||||
- Get your key here: https://steamcommunity.com/dev/apikey
|
||||
- Add an entry in `./src/Achievements/Achievements.ts` -> achievements
|
||||
- Commit `./dist/icons/achievements` & `./src/Achievements/AchievementData.json`
|
||||
|
@ -134,9 +134,7 @@ export class Action implements IAction {
|
||||
for (const decay of Object.keys(this.decays)) {
|
||||
if (this.decays.hasOwnProperty(decay)) {
|
||||
if (this.decays[decay] > 1) {
|
||||
throw new Error(
|
||||
`Invalid decays when constructing Action ${this.name}. Decay value cannot be greater than 1`,
|
||||
);
|
||||
throw new Error(`Invalid decays when constructing Action ${this.name}. Decay value cannot be greater than 1`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -9,7 +9,7 @@ const actionNames : Array<string> = [
|
||||
"Recruitment",
|
||||
"Diplomacy",
|
||||
"Hyperbolic Regeneration Chamber",
|
||||
"Incite Violence"
|
||||
"Incite Violence",
|
||||
];
|
||||
|
||||
for (const actionName of actionNames) {
|
||||
|
@ -7,8 +7,7 @@ export const Skills: IMap<Skill> = {};
|
||||
(function () {
|
||||
Skills[SkillNames.BladesIntuition] = new Skill({
|
||||
name: SkillNames.BladesIntuition,
|
||||
desc:
|
||||
"Each level of this skill increases your success chance for all Contracts, Operations, and BlackOps by 3%",
|
||||
desc: "Each level of this skill increases your success chance for all Contracts, Operations, and BlackOps by 3%",
|
||||
baseCost: 3,
|
||||
costInc: 2.1,
|
||||
successChanceAll: 3,
|
||||
|
@ -17,8 +17,8 @@ export const BlackOperations: {
|
||||
Zenyatta, along with the rest of the PMC, is a Synthoid.
|
||||
<br />
|
||||
<br />
|
||||
The goal of {BlackOperationNames.OperationTyphoon} is to find and eliminate Zenyatta and RedWater by any means necessary. After the
|
||||
task is completed, the actions must be covered up from the general public.
|
||||
The goal of {BlackOperationNames.OperationTyphoon} is to find and eliminate Zenyatta and RedWater by any means
|
||||
necessary. After the task is completed, the actions must be covered up from the general public.
|
||||
</>
|
||||
),
|
||||
},
|
||||
@ -30,10 +30,10 @@ export const BlackOperations: {
|
||||
supporter of Synthoid rights. He must be removed.
|
||||
<br />
|
||||
<br />
|
||||
The goal of {BlackOperationNames.OperationZero} is to covertly infiltrate AeroCorp and uncover any incriminating evidence or
|
||||
information against Watataki that will cause him to be removed from his position at AeroCorp. Incriminating
|
||||
evidence can be fabricated as a last resort. Be warned that AeroCorp has some of the most advanced security
|
||||
measures in the world.
|
||||
The goal of {BlackOperationNames.OperationZero} is to covertly infiltrate AeroCorp and uncover any incriminating
|
||||
evidence or information against Watataki that will cause him to be removed from his position at AeroCorp.
|
||||
Incriminating evidence can be fabricated as a last resort. Be warned that AeroCorp has some of the most advanced
|
||||
security measures in the world.
|
||||
</>
|
||||
),
|
||||
},
|
||||
@ -47,8 +47,8 @@ export const BlackOperations: {
|
||||
<br />
|
||||
<br />
|
||||
Samizdat has done a good job of keeping hidden and anonymous. However, we've just received intelligence that
|
||||
their base of operations is in {CityName.Ishima}'s underground sewer systems. Your task is to investigate the sewer
|
||||
systems, and eliminate Samizdat. They must never publish anything again.
|
||||
their base of operations is in {CityName.Ishima}'s underground sewer systems. Your task is to investigate the
|
||||
sewer systems, and eliminate Samizdat. They must never publish anything again.
|
||||
</>
|
||||
),
|
||||
},
|
||||
@ -61,9 +61,9 @@ export const BlackOperations: {
|
||||
dangerous.
|
||||
<br />
|
||||
<br />
|
||||
Your goal is to enter and destroy the Bioengineering department's facility in {CityName.Aevum}. The task is not just to
|
||||
retire the Synthoids there, but also to destroy any information or research at the facility that is relevant to
|
||||
the Synthoids and their goals.
|
||||
Your goal is to enter and destroy the Bioengineering department's facility in {CityName.Aevum}. The task is not
|
||||
just to retire the Synthoids there, but also to destroy any information or research at the facility that is
|
||||
relevant to the Synthoids and their goals.
|
||||
</>
|
||||
),
|
||||
},
|
||||
@ -96,10 +96,10 @@ export const BlackOperations: {
|
||||
desc: (
|
||||
<>
|
||||
The CIA has just encountered a new security threat. A new criminal group, lead by a shadowy operative who calls
|
||||
himself Juggernaut, has been smuggling drugs and weapons (including suspected bioweapons) into {CityName.Sector12}. We
|
||||
also have reason to believe they tried to break into one of Universal Energy's facilities in order to cause a
|
||||
city-wide blackout. The CIA suspects that Juggernaut is a heavily-augmented Synthoid, and have thus enlisted our
|
||||
help.
|
||||
himself Juggernaut, has been smuggling drugs and weapons (including suspected bioweapons) into{" "}
|
||||
{CityName.Sector12}. We also have reason to believe they tried to break into one of Universal Energy's
|
||||
facilities in order to cause a city-wide blackout. The CIA suspects that Juggernaut is a heavily-augmented
|
||||
Synthoid, and have thus enlisted our help.
|
||||
<br />
|
||||
<br />
|
||||
Your mission is to eradicate Juggernaut and his followers.
|
||||
@ -109,13 +109,13 @@ export const BlackOperations: {
|
||||
[BlackOperationNames.OperationRedDragon]: {
|
||||
desc: (
|
||||
<>
|
||||
The {FactionNames.Tetrads} criminal organization is suspected of reverse-engineering the MK-VI Synthoid design. We believe they
|
||||
altered and possibly improved the design and began manufacturing their own Synthoid models in order to bolster
|
||||
their criminal activities.
|
||||
The {FactionNames.Tetrads} criminal organization is suspected of reverse-engineering the MK-VI Synthoid design.
|
||||
We believe they altered and possibly improved the design and began manufacturing their own Synthoid models in
|
||||
order to bolster their criminal activities.
|
||||
<br />
|
||||
<br />
|
||||
Your task is to infiltrate and destroy the {FactionNames.Tetrads}' base of operations in Los Angeles. Intelligence tells us
|
||||
that their base houses one of their Synthoid manufacturing units.
|
||||
Your task is to infiltrate and destroy the {FactionNames.Tetrads}' base of operations in Los Angeles.
|
||||
Intelligence tells us that their base houses one of their Synthoid manufacturing units.
|
||||
</>
|
||||
),
|
||||
},
|
||||
@ -138,23 +138,24 @@ export const BlackOperations: {
|
||||
[BlackOperationNames.OperationDeckard]: {
|
||||
desc: (
|
||||
<>
|
||||
Despite your success in eliminating VitaLife's new android-replicating technology in {BlackOperationNames.OperationK}, we've
|
||||
discovered that a small group of MK-VI Synthoids were able to make off with the schematics and design of the
|
||||
technology before the Operation. It is almost a certainty that these Synthoids are some of the rogue MK-VI ones
|
||||
from the Synthoid Uprising.
|
||||
Despite your success in eliminating VitaLife's new android-replicating technology in{" "}
|
||||
{BlackOperationNames.OperationK}, we've discovered that a small group of MK-VI Synthoids were able to make off
|
||||
with the schematics and design of the technology before the Operation. It is almost a certainty that these
|
||||
Synthoids are some of the rogue MK-VI ones from the Synthoid Uprising.
|
||||
<br />
|
||||
<br />
|
||||
The goal of {BlackOperationNames.OperationDeckard} is to hunt down these Synthoids and retire them. I don't need to tell you how
|
||||
critical this mission is.
|
||||
The goal of {BlackOperationNames.OperationDeckard} is to hunt down these Synthoids and retire them. I don't need
|
||||
to tell you how critical this mission is.
|
||||
</>
|
||||
),
|
||||
},
|
||||
[BlackOperationNames.OperationTyrell]: {
|
||||
desc: (
|
||||
<>
|
||||
A week ago {FactionNames.BladeIndustries} reported a small break-in at one of their {CityName.Aevum} Augmentation storage facilities. We
|
||||
figured out that {FactionNames.TheDarkArmy} was behind the heist, and didn't think any more of it. However, we've just
|
||||
discovered that several known MK-VI Synthoids were part of that break-in group.
|
||||
A week ago {FactionNames.BladeIndustries} reported a small break-in at one of their {CityName.Aevum}{" "}
|
||||
Augmentation storage facilities. We figured out that {FactionNames.TheDarkArmy} was behind the heist, and didn't
|
||||
think any more of it. However, we've just discovered that several known MK-VI Synthoids were part of that
|
||||
break-in group.
|
||||
<br />
|
||||
<br />
|
||||
We cannot have Synthoids upgrading their already-enhanced abilities with Augmentations. Your task is to hunt
|
||||
@ -165,15 +166,17 @@ export const BlackOperations: {
|
||||
[BlackOperationNames.OperationWallace]: {
|
||||
desc: (
|
||||
<>
|
||||
Based on information gathered from {BlackOperationNames.OperationTyrell}, we've discovered that {FactionNames.TheDarkArmy} was well aware that
|
||||
there were Synthoids amongst their ranks. Even worse, we believe that {FactionNames.TheDarkArmy} is working together with
|
||||
other criminal organizations such as {FactionNames.TheSyndicate} and that they are planning some sort of large-scale takeover
|
||||
of multiple major cities, most notably {CityName.Aevum}. We suspect that Synthoids have infiltrated the ranks of these
|
||||
criminal factions and are trying to stage another Synthoid uprising.
|
||||
Based on information gathered from {BlackOperationNames.OperationTyrell}, we've discovered that{" "}
|
||||
{FactionNames.TheDarkArmy} was well aware that there were Synthoids amongst their ranks. Even worse, we believe
|
||||
that {FactionNames.TheDarkArmy} is working together with other criminal organizations such as{" "}
|
||||
{FactionNames.TheSyndicate} and that they are planning some sort of large-scale takeover of multiple major
|
||||
cities, most notably {CityName.Aevum}. We suspect that Synthoids have infiltrated the ranks of these criminal
|
||||
factions and are trying to stage another Synthoid uprising.
|
||||
<br />
|
||||
<br />
|
||||
The best way to deal with this is to prevent it before it even happens. The goal of {BlackOperationNames.OperationWallace} is to
|
||||
destroy {FactionNames.TheDarkArmy} and Syndicate factions in {CityName.Aevum} immediately. Leave no survivors.
|
||||
The best way to deal with this is to prevent it before it even happens. The goal of{" "}
|
||||
{BlackOperationNames.OperationWallace} is to destroy {FactionNames.TheDarkArmy} and Syndicate factions in{" "}
|
||||
{CityName.Aevum} immediately. Leave no survivors.
|
||||
</>
|
||||
),
|
||||
},
|
||||
@ -193,18 +196,18 @@ export const BlackOperations: {
|
||||
[BlackOperationNames.OperationHyron]: {
|
||||
desc: (
|
||||
<>
|
||||
Our intelligence tells us that {FactionNames.FulcrumSecretTechnologies} is developing a quantum supercomputer using human brains as
|
||||
core processors. This supercomputer is rumored to be able to store vast amounts of data and perform computations
|
||||
unmatched by any other supercomputer on the planet. But more importantly, the use of organic human brains means
|
||||
that the supercomputer may be able to reason abstractly and become self-aware.
|
||||
Our intelligence tells us that {FactionNames.FulcrumSecretTechnologies} is developing a quantum supercomputer
|
||||
using human brains as core processors. This supercomputer is rumored to be able to store vast amounts of data
|
||||
and perform computations unmatched by any other supercomputer on the planet. But more importantly, the use of
|
||||
organic human brains means that the supercomputer may be able to reason abstractly and become self-aware.
|
||||
<br />
|
||||
<br />
|
||||
I do not need to remind you why sentient-level AIs pose a serious threat to all of mankind.
|
||||
<br />
|
||||
<br />
|
||||
The research for this project is being conducted at one of {FactionNames.FulcrumSecretTechnologies} secret facilities in {CityName.Aevum},
|
||||
codenamed 'Alpha Ranch'. Infiltrate the compound, delete and destroy the work, and then find and kill the
|
||||
project lead.
|
||||
The research for this project is being conducted at one of {FactionNames.FulcrumSecretTechnologies} secret
|
||||
facilities in {CityName.Aevum}, codenamed 'Alpha Ranch'. Infiltrate the compound, delete and destroy the work,
|
||||
and then find and kill the project lead.
|
||||
</>
|
||||
),
|
||||
},
|
||||
@ -213,8 +216,8 @@ export const BlackOperations: {
|
||||
<>
|
||||
DreamSense Technologies is an advertising company that uses special technology to transmit their ads into the
|
||||
people's dreams and subconcious. They do this using broadcast transmitter towers. Based on information from our
|
||||
agents and informants in {CityName.Chongqing}, we have reason to believe that one of the broadcast towers there has been
|
||||
compromised by Synthoids and is being used to spread pro-Synthoid propaganda.
|
||||
agents and informants in {CityName.Chongqing}, we have reason to believe that one of the broadcast towers there
|
||||
has been compromised by Synthoids and is being used to spread pro-Synthoid propaganda.
|
||||
<br />
|
||||
<br />
|
||||
The mission is to destroy this broadcast tower. Speed and stealth are of the utmost importance for this.
|
||||
@ -224,36 +227,38 @@ export const BlackOperations: {
|
||||
[BlackOperationNames.OperationIonStorm]: {
|
||||
desc: (
|
||||
<>
|
||||
Our analysts have uncovered a gathering of MK-VI Synthoids that have taken up residence in the {CityName.Sector12} Slums.
|
||||
We don't know if they are rogue Synthoids from the Uprising, but we do know that they have been stockpiling
|
||||
weapons, money, and other resources. This makes them dangerous.
|
||||
Our analysts have uncovered a gathering of MK-VI Synthoids that have taken up residence in the{" "}
|
||||
{CityName.Sector12} Slums. We don't know if they are rogue Synthoids from the Uprising, but we do know that they
|
||||
have been stockpiling weapons, money, and other resources. This makes them dangerous.
|
||||
<br />
|
||||
<br />
|
||||
This is a full-scale assault operation to find and retire all of these Synthoids in the {CityName.Sector12} Slums.
|
||||
This is a full-scale assault operation to find and retire all of these Synthoids in the {CityName.Sector12}{" "}
|
||||
Slums.
|
||||
</>
|
||||
),
|
||||
},
|
||||
[BlackOperationNames.OperationAnnihilus]: {
|
||||
desc: (
|
||||
<>
|
||||
Our superiors have ordered us to eradicate everything and everyone in an underground facility located in {CityName.Aevum}.
|
||||
They tell us that the facility houses many dangerous Synthoids and belongs to a terrorist organization called
|
||||
'{FactionNames.TheCovenant}'. We have no prior intelligence about this organization, so you are going in blind.
|
||||
Our superiors have ordered us to eradicate everything and everyone in an underground facility located in{" "}
|
||||
{CityName.Aevum}. They tell us that the facility houses many dangerous Synthoids and belongs to a terrorist
|
||||
organization called '{FactionNames.TheCovenant}'. We have no prior intelligence about this organization, so you
|
||||
are going in blind.
|
||||
</>
|
||||
),
|
||||
},
|
||||
[BlackOperationNames.OperationUltron]: {
|
||||
desc: (
|
||||
<>
|
||||
{FactionNames.OmniTekIncorporated}, the original designer and manufacturer of Synthoids, has notified us of a malfunction in
|
||||
their AI design. This malfunction, when triggered, causes MK-VI Synthoids to become radicalized and seek out the
|
||||
destruction of humanity. They say that this bug affects all MK-VI Synthoids, not just the rogue ones from the
|
||||
Uprising.
|
||||
{FactionNames.OmniTekIncorporated}, the original designer and manufacturer of Synthoids, has notified us of a
|
||||
malfunction in their AI design. This malfunction, when triggered, causes MK-VI Synthoids to become radicalized
|
||||
and seek out the destruction of humanity. They say that this bug affects all MK-VI Synthoids, not just the rogue
|
||||
ones from the Uprising.
|
||||
<br />
|
||||
<br />
|
||||
{FactionNames.OmniTekIncorporated} has also told us they they believe someone has triggered this malfunction in a large group of MK-VI
|
||||
Synthoids, and that these newly-radicalized Synthoids are now amassing in {CityName.Volhaven} to form a terrorist group
|
||||
called Ultron.
|
||||
{FactionNames.OmniTekIncorporated} has also told us they they believe someone has triggered this malfunction in
|
||||
a large group of MK-VI Synthoids, and that these newly-radicalized Synthoids are now amassing in{" "}
|
||||
{CityName.Volhaven} to form a terrorist group called Ultron.
|
||||
<br />
|
||||
<br />
|
||||
Intelligence suggests Ultron is heavily armed and that their members are augmented. We believe Ultron is making
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { CityName } from './../../Locations/data/CityNames';
|
||||
import { CityName } from "./../../Locations/data/CityNames";
|
||||
export const BladeburnerConstants: {
|
||||
CityNames: string[];
|
||||
CyclesPerSecond: number;
|
||||
@ -28,7 +28,14 @@ export const BladeburnerConstants: {
|
||||
HrcHpGain: number;
|
||||
HrcStaminaGain: number;
|
||||
} = {
|
||||
CityNames: [CityName.Aevum, CityName.Chongqing, CityName.Sector12, CityName.NewTokyo, CityName.Ishima, CityName.Volhaven],
|
||||
CityNames: [
|
||||
CityName.Aevum,
|
||||
CityName.Chongqing,
|
||||
CityName.Sector12,
|
||||
CityName.NewTokyo,
|
||||
CityName.Ishima,
|
||||
CityName.Volhaven,
|
||||
],
|
||||
CyclesPerSecond: 5, // Game cycle is 200 ms
|
||||
|
||||
StaminaGainPerSecond: 0.0085,
|
||||
|
@ -18,7 +18,10 @@ export function BlackOpPage(props: IProps): React.ReactElement {
|
||||
successively by completing the one before it.
|
||||
<br />
|
||||
<br />
|
||||
<b>Your ultimate goal to climb through the ranks of {FactionNames.Bladeburners} is to complete all of the Black Ops.</b>
|
||||
<b>
|
||||
Your ultimate goal to climb through the ranks of {FactionNames.Bladeburners} is to complete all of the Black
|
||||
Ops.
|
||||
</b>
|
||||
<br />
|
||||
<br />
|
||||
Like normal operations, you may use a team for Black Ops. Failing a black op will incur heavy HP and rank
|
||||
|
@ -44,13 +44,15 @@ export function Stats(props: IProps): React.ReactElement {
|
||||
}
|
||||
|
||||
return (
|
||||
<Paper sx={{ p: 1, overflowY: 'auto', overflowX: 'hidden', wordBreak: 'break-all' }}>
|
||||
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 1, maxHeight: '60vh' }}>
|
||||
<Box sx={{ alignSelf: 'flex-start', width: '100%' }}>
|
||||
<Button onClick={() => setTravelOpen(true)} sx={{ width: '50%' }}>Travel</Button>
|
||||
<Paper sx={{ p: 1, overflowY: "auto", overflowX: "hidden", wordBreak: "break-all" }}>
|
||||
<Box sx={{ display: "flex", flexDirection: "column", gap: 1, maxHeight: "60vh" }}>
|
||||
<Box sx={{ alignSelf: "flex-start", width: "100%" }}>
|
||||
<Button onClick={() => setTravelOpen(true)} sx={{ width: "50%" }}>
|
||||
Travel
|
||||
</Button>
|
||||
<Tooltip title={!inFaction ? <Typography>Rank 25 required.</Typography> : ""}>
|
||||
<span>
|
||||
<Button disabled={!inFaction} onClick={openFaction} sx={{ width: '50%' }}>
|
||||
<Button disabled={!inFaction} onClick={openFaction} sx={{ width: "50%" }}>
|
||||
Faction
|
||||
</Button>
|
||||
</span>
|
||||
@ -110,8 +112,8 @@ export function Stats(props: IProps): React.ReactElement {
|
||||
<Tooltip
|
||||
title={
|
||||
<Typography>
|
||||
This is your Bladeburner division's estimate of how many Synthoids exist in your current city. An accurate
|
||||
population count increases success rate estimates.
|
||||
This is your Bladeburner division's estimate of how many Synthoids exist in your current city. An
|
||||
accurate population count increases success rate estimates.
|
||||
</Typography>
|
||||
}
|
||||
>
|
||||
|
@ -159,7 +159,7 @@ export function SlotMachine(props: IProps): React.ReactElement {
|
||||
const copy = index.slice();
|
||||
for (let i = 0; i < copy.length; i++) {
|
||||
if (copy[i] === locks[i] && !stoppedOne) continue;
|
||||
copy[i] = (copy[i] - 1 >= 0) ? copy[i] - 1 : symbols.length - 1;
|
||||
copy[i] = copy[i] - 1 >= 0 ? copy[i] - 1 : symbols.length - 1;
|
||||
stoppedOne = true;
|
||||
}
|
||||
|
||||
|
@ -435,10 +435,10 @@ export class Industry implements IIndustry {
|
||||
const popularityGain = corporation.getDreamSenseGain(),
|
||||
awarenessGain = popularityGain * 4;
|
||||
if (popularityGain > 0) {
|
||||
const awareness = this.awareness + (awarenessGain * marketCycles);
|
||||
const awareness = this.awareness + awarenessGain * marketCycles;
|
||||
this.awareness = Math.min(awareness, Number.MAX_VALUE);
|
||||
|
||||
const popularity = this.popularity + (popularityGain * marketCycles);
|
||||
const popularity = this.popularity + popularityGain * marketCycles;
|
||||
this.popularity = Math.min(popularity, Number.MAX_VALUE);
|
||||
}
|
||||
|
||||
@ -1188,7 +1188,8 @@ export class Industry implements IIndustry {
|
||||
}
|
||||
}
|
||||
|
||||
product.maxsll = 0.5 *
|
||||
product.maxsll =
|
||||
0.5 *
|
||||
Math.pow(product.rat, 0.65) *
|
||||
marketFactor *
|
||||
corporation.getSalesMultiplier() *
|
||||
@ -1281,10 +1282,10 @@ export class Industry implements IIndustry {
|
||||
case 1: {
|
||||
//AdVert.Inc,
|
||||
const advMult = corporation.getAdvertisingMultiplier() * this.getAdvertisingMultiplier();
|
||||
const awareness = (this.awareness + (3 * advMult)) * (1.01 * advMult);
|
||||
const awareness = (this.awareness + 3 * advMult) * (1.01 * advMult);
|
||||
this.awareness = Math.min(awareness, Number.MAX_VALUE);
|
||||
|
||||
const popularity = (this.popularity + (1 * advMult)) * ((1 + getRandomInt(1, 3) / 100) * advMult);
|
||||
const popularity = (this.popularity + 1 * advMult) * ((1 + getRandomInt(1, 3) / 100) * advMult);
|
||||
this.popularity = Math.min(popularity, Number.MAX_VALUE);
|
||||
break;
|
||||
}
|
||||
|
@ -177,7 +177,7 @@ export class OfficeSpace {
|
||||
let jobCount = this.employees.reduce((acc, employee) => (employee.pos === job ? acc + 1 : acc), 0);
|
||||
|
||||
for (const employee of this.employees) {
|
||||
if (jobCount == amount) return true
|
||||
if (jobCount == amount) return true;
|
||||
if (employee.pos === EmployeePositions.Unassigned && jobCount <= amount) {
|
||||
employee.pos = job;
|
||||
jobCount++;
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { CityName } from './../../Locations/data/CityNames';
|
||||
import { CityName } from "./../../Locations/data/CityNames";
|
||||
const CyclesPerMarketCycle = 50;
|
||||
const AllCorporationStates = ["START", "PURCHASE", "PRODUCTION", "SALE", "EXPORT"];
|
||||
export const CorporationConstants: {
|
||||
@ -38,7 +38,14 @@ export const CorporationConstants: {
|
||||
CyclesPerIndustryStateCycle: CyclesPerMarketCycle / AllCorporationStates.length,
|
||||
SecsPerMarketCycle: CyclesPerMarketCycle / 5,
|
||||
|
||||
Cities: [CityName.Aevum, CityName.Chongqing, CityName.Sector12, CityName.NewTokyo, CityName.Ishima, CityName.Volhaven],
|
||||
Cities: [
|
||||
CityName.Aevum,
|
||||
CityName.Chongqing,
|
||||
CityName.Sector12,
|
||||
CityName.NewTokyo,
|
||||
CityName.Ishima,
|
||||
CityName.Volhaven,
|
||||
],
|
||||
|
||||
WarehouseInitialCost: 5e9, //Initial purchase cost of warehouse
|
||||
WarehouseInitialSize: 100,
|
||||
@ -74,16 +81,6 @@ export const CorporationConstants: {
|
||||
"AI Cores",
|
||||
"Real Estate",
|
||||
],
|
||||
FundingRoundShares: [
|
||||
0.1,
|
||||
0.35,
|
||||
0.25,
|
||||
0.2
|
||||
],
|
||||
FundingRoundMultiplier: [
|
||||
4,
|
||||
3,
|
||||
3,
|
||||
2.5
|
||||
],
|
||||
FundingRoundShares: [0.1, 0.35, 0.25, 0.2],
|
||||
FundingRoundMultiplier: [4, 3, 3, 2.5],
|
||||
};
|
||||
|
@ -6,8 +6,8 @@ import { useCorporation } from "./Context";
|
||||
import Typography from "@mui/material/Typography";
|
||||
import Button from "@mui/material/Button";
|
||||
import TextField from "@mui/material/TextField";
|
||||
import { BuyBackShares } from '../Actions';
|
||||
import { dialogBoxCreate } from '../../ui/React/DialogBox';
|
||||
import { BuyBackShares } from "../Actions";
|
||||
import { dialogBoxCreate } from "../../ui/React/DialogBox";
|
||||
import { KEY } from "../../utils/helpers/keyCodes";
|
||||
|
||||
interface IProps {
|
||||
@ -40,9 +40,8 @@ export function BuybackSharesModal(props: IProps): React.ReactElement {
|
||||
function buy(): void {
|
||||
if (disabled) return;
|
||||
try {
|
||||
BuyBackShares(corp, player, shares)
|
||||
}
|
||||
catch (err) {
|
||||
BuyBackShares(corp, player, shares);
|
||||
} catch (err) {
|
||||
dialogBoxCreate(err + "");
|
||||
}
|
||||
props.onClose();
|
||||
|
@ -30,7 +30,7 @@ export function CityTabs(props: IProps): React.ReactElement {
|
||||
}
|
||||
return (
|
||||
<>
|
||||
<Tabs variant="fullWidth" value={city} onChange={handleChange} sx={{ maxWidth: '65%' }}>
|
||||
<Tabs variant="fullWidth" value={city} onChange={handleChange} sx={{ maxWidth: "65%" }}>
|
||||
{Object.values(division.offices).map(
|
||||
(office: OfficeSpace | 0) => office !== 0 && <Tab key={office.loc} label={office.loc} value={office.loc} />,
|
||||
)}
|
||||
|
@ -38,7 +38,7 @@ export function CorporationRoot(): React.ReactElement {
|
||||
|
||||
return (
|
||||
<Context.Corporation.Provider value={corporation}>
|
||||
<Tabs variant="scrollable" value={divisionName} onChange={handleChange} sx={{ maxWidth: '65%' }} scrollButtons>
|
||||
<Tabs variant="scrollable" value={divisionName} onChange={handleChange} sx={{ maxWidth: "65%" }} scrollButtons>
|
||||
<Tab label={corporation.name} value={"Overview"} />
|
||||
{corporation.divisions.map((div) => (
|
||||
<Tab key={div.name} label={div.name} value={div.name} />
|
||||
|
@ -56,9 +56,11 @@ export function CreateCorporationModal(props: IProps): React.ReactElement {
|
||||
return (
|
||||
<Modal open={props.open} onClose={props.onClose}>
|
||||
<Typography>
|
||||
Would you like to start a corporation? This will require $150b for registration and initial funding. {player.bitNodeN === 3 && (`This $150b
|
||||
Would you like to start a corporation? This will require $150b for registration and initial funding.{" "}
|
||||
{player.bitNodeN === 3 &&
|
||||
`This $150b
|
||||
can either be self-funded, or you can obtain the seed money from the government in exchange for 500 million
|
||||
shares`)}
|
||||
shares`}
|
||||
<br />
|
||||
<br />
|
||||
If you would like to start one, please enter a name for your corporation below:
|
||||
|
@ -23,7 +23,9 @@ interface IProps {
|
||||
// Create a popup that lets the player manage exports
|
||||
export function ExportModal(props: IProps): React.ReactElement {
|
||||
const corp = useCorporation();
|
||||
const possibleDivisions = corp.divisions.filter((division: IIndustry) => isRelevantMaterial(props.mat.name, division));
|
||||
const possibleDivisions = corp.divisions.filter((division: IIndustry) =>
|
||||
isRelevantMaterial(props.mat.name, division),
|
||||
);
|
||||
if (possibleDivisions.length === 0) throw new Error("Export popup created with no divisions.");
|
||||
const defaultDivision = possibleDivisions[0];
|
||||
if (Object.keys(defaultDivision.warehouses).length === 0)
|
||||
|
@ -17,7 +17,11 @@ interface IProps {
|
||||
export function FindInvestorsModal(props: IProps): React.ReactElement {
|
||||
const corporation = useCorporation();
|
||||
const val = corporation.determineValuation();
|
||||
if (corporation.fundingRound >= CorporationConstants.FundingRoundShares.length || corporation.fundingRound >= CorporationConstants.FundingRoundMultiplier.length) return <></>;
|
||||
if (
|
||||
corporation.fundingRound >= CorporationConstants.FundingRoundShares.length ||
|
||||
corporation.fundingRound >= CorporationConstants.FundingRoundMultiplier.length
|
||||
)
|
||||
return <></>;
|
||||
const percShares = CorporationConstants.FundingRoundShares[corporation.fundingRound];
|
||||
const roundMultiplier = CorporationConstants.FundingRoundMultiplier[corporation.fundingRound];
|
||||
const funding = val * percShares * roundMultiplier;
|
||||
|
@ -18,7 +18,5 @@ export function IndustryProductEquation(props: IProps): React.ReactElement {
|
||||
prod.push("Products");
|
||||
}
|
||||
|
||||
return (
|
||||
<MathJaxWrapper>{"\\(" + reqs.join("+") + `\\Rightarrow ` + prod.join("+") + "\\)"}</MathJaxWrapper>
|
||||
);
|
||||
return <MathJaxWrapper>{"\\(" + reqs.join("+") + `\\Rightarrow ` + prod.join("+") + "\\)"}</MathJaxWrapper>;
|
||||
}
|
||||
|
@ -42,9 +42,9 @@ interface IProps {
|
||||
const useStyles = makeStyles(() =>
|
||||
createStyles({
|
||||
retainHeight: {
|
||||
minHeight: '3em',
|
||||
minHeight: "3em",
|
||||
},
|
||||
})
|
||||
}),
|
||||
);
|
||||
|
||||
function WarehouseRoot(props: IProps): React.ReactElement {
|
||||
@ -127,27 +127,53 @@ function WarehouseRoot(props: IProps): React.ReactElement {
|
||||
const mat = props.warehouse.materials[matName];
|
||||
if (!MaterialSizes.hasOwnProperty(matName)) continue;
|
||||
if (mat.qty === 0) continue;
|
||||
breakdownItems.push(<>{matName}: {numeralWrapper.format(mat.qty * MaterialSizes[matName], "0,0.0")}</>);
|
||||
breakdownItems.push(
|
||||
<>
|
||||
{matName}: {numeralWrapper.format(mat.qty * MaterialSizes[matName], "0,0.0")}
|
||||
</>,
|
||||
);
|
||||
}
|
||||
|
||||
for (const prodName of Object.keys(division.products)) {
|
||||
const prod = division.products[prodName];
|
||||
if (prod === undefined) continue;
|
||||
breakdownItems.push(<>{prodName}: {numeralWrapper.format(prod.data[props.warehouse.loc][0] * prod.siz, "0,0.0")}</>);
|
||||
breakdownItems.push(
|
||||
<>
|
||||
{prodName}: {numeralWrapper.format(prod.data[props.warehouse.loc][0] * prod.siz, "0,0.0")}
|
||||
</>,
|
||||
);
|
||||
}
|
||||
|
||||
let breakdown;
|
||||
if (breakdownItems && breakdownItems.length > 0) {
|
||||
breakdown = breakdownItems.reduce(
|
||||
(previous: JSX.Element, current: JSX.Element): JSX.Element => previous && <>{previous}<br />{current}</> || <>{current}</>);
|
||||
(previous: JSX.Element, current: JSX.Element): JSX.Element =>
|
||||
(previous && (
|
||||
<>
|
||||
{previous}
|
||||
<br />
|
||||
{current}
|
||||
</>
|
||||
)) || <>{current}</>,
|
||||
);
|
||||
} else {
|
||||
breakdown = <>No items in storage.</>
|
||||
breakdown = <>No items in storage.</>;
|
||||
}
|
||||
|
||||
return (
|
||||
<Paper>
|
||||
<Box display="flex" alignItems="center">
|
||||
<Tooltip title={props.warehouse.sizeUsed !== 0 ? <Typography><>{breakdown}</></Typography> : ""}>
|
||||
<Tooltip
|
||||
title={
|
||||
props.warehouse.sizeUsed !== 0 ? (
|
||||
<Typography>
|
||||
<>{breakdown}</>
|
||||
</Typography>
|
||||
) : (
|
||||
""
|
||||
)
|
||||
}
|
||||
>
|
||||
<Typography color={props.warehouse.sizeUsed >= props.warehouse.size ? "error" : "primary"}>
|
||||
Storage: {numeralWrapper.formatBigNumber(props.warehouse.sizeUsed)} /{" "}
|
||||
{numeralWrapper.formatBigNumber(props.warehouse.size)}
|
||||
|
@ -112,7 +112,7 @@ export function MaterialElem(props: IMaterialProps): React.ReactElement {
|
||||
|
||||
return (
|
||||
<Paper>
|
||||
<Box sx={{ display: 'grid', gridTemplateColumns: '2fr 1fr', m: '5px' }}>
|
||||
<Box sx={{ display: "grid", gridTemplateColumns: "2fr 1fr", m: "5px" }}>
|
||||
<Box>
|
||||
<Tooltip
|
||||
title={
|
||||
@ -149,7 +149,7 @@ export function MaterialElem(props: IMaterialProps): React.ReactElement {
|
||||
</Tooltip>
|
||||
</Box>
|
||||
|
||||
<Box sx={{ "& button": { width: '100%' } }}>
|
||||
<Box sx={{ "& button": { width: "100%" } }}>
|
||||
<Tooltip
|
||||
title={tutorial ? <Typography>Purchase your required materials to get production started!</Typography> : ""}
|
||||
>
|
||||
|
@ -89,7 +89,7 @@ export function Overview({ rerender }: IProps): React.ReactElement {
|
||||
<StatsTable rows={multRows} />
|
||||
<br />
|
||||
<BonusTime />
|
||||
<Box sx={{ display: 'grid', gridTemplateColumns: 'repeat(3, 1fr)', width: 'fit-content' }}>
|
||||
<Box sx={{ display: "grid", gridTemplateColumns: "repeat(3, 1fr)", width: "fit-content" }}>
|
||||
<Tooltip
|
||||
title={
|
||||
<Typography>
|
||||
|
@ -16,7 +16,7 @@ import Box from "@mui/material/Box";
|
||||
import Collapse from "@mui/material/Collapse";
|
||||
import ExpandMore from "@mui/icons-material/ExpandMore";
|
||||
import ExpandLess from "@mui/icons-material/ExpandLess";
|
||||
import CheckIcon from '@mui/icons-material/Check';
|
||||
import CheckIcon from "@mui/icons-material/Check";
|
||||
|
||||
interface INodeProps {
|
||||
n: Node | null;
|
||||
@ -66,19 +66,23 @@ function Upgrade({ n, division }: INodeProps): React.ReactElement {
|
||||
>
|
||||
{ele}
|
||||
</Tooltip>
|
||||
)
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
const but = (
|
||||
<Box>
|
||||
{wrapInTooltip(
|
||||
<span>
|
||||
<Button color={color} disabled={disabled && !n.researched} onClick={research}
|
||||
style={{ width: '100%', textAlign: 'left', justifyContent: 'unset' }}
|
||||
<Button
|
||||
color={color}
|
||||
disabled={disabled && !n.researched}
|
||||
onClick={research}
|
||||
style={{ width: "100%", textAlign: "left", justifyContent: "unset" }}
|
||||
>
|
||||
{n.researched && (<CheckIcon sx={{ mr: 1 }} />)}{n.text}
|
||||
{n.researched && <CheckIcon sx={{ mr: 1 }} />}
|
||||
{n.text}
|
||||
</Button>
|
||||
</span>
|
||||
</span>,
|
||||
)}
|
||||
</Box>
|
||||
);
|
||||
@ -87,20 +91,29 @@ function Upgrade({ n, division }: INodeProps): React.ReactElement {
|
||||
|
||||
return (
|
||||
<Box>
|
||||
<Box display="flex" sx={{ border: '1px solid ' + Settings.theme.well }}>
|
||||
<Box display="flex" sx={{ border: "1px solid " + Settings.theme.well }}>
|
||||
{wrapInTooltip(
|
||||
<span style={{ width: '100%' }}>
|
||||
<Button color={color} disabled={disabled && !n.researched} onClick={research} sx={{
|
||||
width: '100%',
|
||||
textAlign: 'left',
|
||||
justifyContent: 'unset',
|
||||
borderColor: Settings.theme.button
|
||||
}}>
|
||||
{n.researched && (<CheckIcon sx={{ mr: 1 }} />)}{n.text}
|
||||
<span style={{ width: "100%" }}>
|
||||
<Button
|
||||
color={color}
|
||||
disabled={disabled && !n.researched}
|
||||
onClick={research}
|
||||
sx={{
|
||||
width: "100%",
|
||||
textAlign: "left",
|
||||
justifyContent: "unset",
|
||||
borderColor: Settings.theme.button,
|
||||
}}
|
||||
>
|
||||
{n.researched && <CheckIcon sx={{ mr: 1 }} />}
|
||||
{n.text}
|
||||
</Button>
|
||||
</span>
|
||||
</span>,
|
||||
)}
|
||||
<Button onClick={() => setOpen((old) => !old)} sx={{ borderColor: Settings.theme.button, minWidth: 'fit-content' }}>
|
||||
<Button
|
||||
onClick={() => setOpen((old) => !old)}
|
||||
sx={{ borderColor: Settings.theme.button, minWidth: "fit-content" }}
|
||||
>
|
||||
{open ? <ExpandLess color="primary" /> : <ExpandMore color="primary" />}
|
||||
</Button>
|
||||
</Box>
|
||||
|
@ -51,7 +51,7 @@ export function SellSharesModal(props: IProps): React.ReactElement {
|
||||
function sell(): void {
|
||||
if (disabled) return;
|
||||
try {
|
||||
const profit = SellShares(corp, player, shares)
|
||||
const profit = SellShares(corp, player, shares);
|
||||
props.onClose();
|
||||
dialogBoxCreate(
|
||||
<>
|
||||
@ -65,7 +65,6 @@ export function SellSharesModal(props: IProps): React.ReactElement {
|
||||
} catch (err) {
|
||||
dialogBoxCreate(err + "");
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
function onKeyDown(event: React.KeyboardEvent<HTMLInputElement>): void {
|
||||
|
@ -5,5 +5,5 @@ export const StanekConstants: {
|
||||
} = {
|
||||
RAMBonus: 0.1,
|
||||
BaseSize: 9,
|
||||
MaxSize: 25
|
||||
MaxSize: 25,
|
||||
};
|
||||
|
@ -107,7 +107,9 @@ export function buyAllDarkwebItems(): void {
|
||||
}
|
||||
|
||||
if (cost > Player.money) {
|
||||
Terminal.error("Not enough money to purchase remaining programs, " + numeralWrapper.formatMoney(cost) + " required");
|
||||
Terminal.error(
|
||||
"Not enough money to purchase remaining programs, " + numeralWrapper.formatMoney(cost) + " required",
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
|
@ -8,8 +8,8 @@ import Typography from "@mui/material/Typography";
|
||||
import Button from "@mui/material/Button";
|
||||
import ButtonGroup from "@mui/material/ButtonGroup";
|
||||
import { Tooltip } from "@mui/material";
|
||||
import LockIcon from '@mui/icons-material/Lock';
|
||||
import LockOpenIcon from '@mui/icons-material/LockOpen';
|
||||
import LockIcon from "@mui/icons-material/Lock";
|
||||
import LockOpenIcon from "@mui/icons-material/LockOpen";
|
||||
|
||||
import { IPlayer } from "../../PersonObjects/IPlayer";
|
||||
import { achievements } from "../../Achievements/Achievements";
|
||||
@ -21,26 +21,26 @@ interface IProps {
|
||||
}
|
||||
|
||||
export function Achievements(props: IProps): React.ReactElement {
|
||||
const [playerAchievement, setPlayerAchievements] = useState(props.player.achievements.map(m => m.ID));
|
||||
const [playerAchievement, setPlayerAchievements] = useState(props.player.achievements.map((m) => m.ID));
|
||||
|
||||
function grantAchievement(id: string): void {
|
||||
props.player.giveAchievement(id);
|
||||
setPlayerAchievements(props.player.achievements.map(m => m.ID));
|
||||
setPlayerAchievements(props.player.achievements.map((m) => m.ID));
|
||||
}
|
||||
|
||||
function grantAllAchievements(): void {
|
||||
Object.values(achievements).forEach(a => props.player.giveAchievement(a.ID));
|
||||
setPlayerAchievements(props.player.achievements.map(m => m.ID));
|
||||
Object.values(achievements).forEach((a) => props.player.giveAchievement(a.ID));
|
||||
setPlayerAchievements(props.player.achievements.map((m) => m.ID));
|
||||
}
|
||||
|
||||
function removeAchievement(id: string): void {
|
||||
props.player.achievements = props.player.achievements.filter(a => a.ID !== id);
|
||||
setPlayerAchievements(props.player.achievements.map(m => m.ID));
|
||||
props.player.achievements = props.player.achievements.filter((a) => a.ID !== id);
|
||||
setPlayerAchievements(props.player.achievements.map((m) => m.ID));
|
||||
}
|
||||
|
||||
function clearAchievements(): void {
|
||||
props.player.achievements = [];
|
||||
setPlayerAchievements(props.player.achievements.map(m => m.ID));
|
||||
setPlayerAchievements(props.player.achievements.map((m) => m.ID));
|
||||
}
|
||||
|
||||
function disableEngine(): void {
|
||||
@ -48,7 +48,7 @@ export function Achievements(props: IProps): React.ReactElement {
|
||||
}
|
||||
|
||||
function enableEngine(): void {
|
||||
props.engine.Counters.achievementsCounter = 0
|
||||
props.engine.Counters.achievementsCounter = 0;
|
||||
}
|
||||
|
||||
return (
|
||||
@ -60,8 +60,7 @@ export function Achievements(props: IProps): React.ReactElement {
|
||||
<table>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>
|
||||
</td>
|
||||
<td></td>
|
||||
<td>
|
||||
<Typography>Achievements:</Typography>
|
||||
</td>
|
||||
@ -76,7 +75,8 @@ export function Achievements(props: IProps): React.ReactElement {
|
||||
</tr>
|
||||
{Object.values(achievements).map((i) => {
|
||||
const achieved = playerAchievement.includes(i.ID);
|
||||
return <tr key={"ach-" + i.ID}>
|
||||
return (
|
||||
<tr key={"ach-" + i.ID}>
|
||||
<td>
|
||||
{achieved ? (
|
||||
<Tooltip title="Achieved">
|
||||
@ -89,8 +89,16 @@ export function Achievements(props: IProps): React.ReactElement {
|
||||
)}
|
||||
</td>
|
||||
<td>
|
||||
<Tooltip title={<>{i.ID}<br />{i.Description}</>}>
|
||||
<Typography color={achieved ? 'primary': 'secondary'}>{i.Name}:</Typography>
|
||||
<Tooltip
|
||||
title={
|
||||
<>
|
||||
{i.ID}
|
||||
<br />
|
||||
{i.Description}
|
||||
</>
|
||||
}
|
||||
>
|
||||
<Typography color={achieved ? "primary" : "secondary"}>{i.Name}:</Typography>
|
||||
</Tooltip>
|
||||
</td>
|
||||
<td>
|
||||
@ -100,6 +108,7 @@ export function Achievements(props: IProps): React.ReactElement {
|
||||
</ButtonGroup>
|
||||
</td>
|
||||
</tr>
|
||||
);
|
||||
})}
|
||||
</tbody>
|
||||
</table>
|
||||
|
@ -37,7 +37,7 @@ export function Factions(props: IProps): React.ReactElement {
|
||||
}
|
||||
|
||||
function receiveAllInvites(): void {
|
||||
Object.values(FactionNames).forEach(faction => props.player.receiveInvite(faction))
|
||||
Object.values(FactionNames).forEach((faction) => props.player.receiveInvite(faction));
|
||||
}
|
||||
|
||||
function modifyFactionRep(modifier: number): (x: number) => void {
|
||||
|
@ -46,7 +46,7 @@ export function General(props: IProps): React.ReactElement {
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
if (error) throw new ReferenceError('Manually thrown error');
|
||||
if (error) throw new ReferenceError("Manually thrown error");
|
||||
}, [error]);
|
||||
|
||||
return (
|
||||
|
@ -85,8 +85,6 @@ export class FactionInfo {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* A map of all factions and associated info to them.
|
||||
*/
|
||||
@ -143,8 +141,9 @@ export const FactionInfos: IMap<FactionInfo> = {
|
||||
[FactionNames.ECorp]: new FactionInfo(
|
||||
(
|
||||
<>
|
||||
{FactionNames.ECorp}'s mission is simple: to connect the world of today with the technology of tomorrow. With our wide range of
|
||||
Internet-related software and commercial hardware, {FactionNames.ECorp} makes the world's information universally accessible.
|
||||
{FactionNames.ECorp}'s mission is simple: to connect the world of today with the technology of tomorrow. With
|
||||
our wide range of Internet-related software and commercial hardware, {FactionNames.ECorp} makes the world's
|
||||
information universally accessible.
|
||||
</>
|
||||
),
|
||||
[],
|
||||
@ -159,12 +158,13 @@ export const FactionInfos: IMap<FactionInfo> = {
|
||||
[FactionNames.MegaCorp]: new FactionInfo(
|
||||
(
|
||||
<>
|
||||
{FactionNames.MegaCorp} does what no other dares to do. We imagine. We create. We invent. We create what others have never even
|
||||
dreamed of. Our work fills the world's needs for food, water, power, and transportation on an unprecedented
|
||||
scale, in ways that no other company can.
|
||||
{FactionNames.MegaCorp} does what no other dares to do. We imagine. We create. We invent. We create what others
|
||||
have never even dreamed of. Our work fills the world's needs for food, water, power, and transportation on an
|
||||
unprecedented scale, in ways that no other company can.
|
||||
<br />
|
||||
<br />
|
||||
In our labs and factories and on the ground with customers, {FactionNames.MegaCorp} is ushering in a new era for the world.
|
||||
In our labs and factories and on the ground with customers, {FactionNames.MegaCorp} is ushering in a new era for
|
||||
the world.
|
||||
</>
|
||||
),
|
||||
[],
|
||||
@ -194,7 +194,16 @@ export const FactionInfos: IMap<FactionInfo> = {
|
||||
true,
|
||||
),
|
||||
|
||||
[FactionNames.BladeIndustries]: new FactionInfo(<>Augmentation is Salvation.</>, [], true, true, true, true, false, true),
|
||||
[FactionNames.BladeIndustries]: new FactionInfo(
|
||||
<>Augmentation is Salvation.</>,
|
||||
[],
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
false,
|
||||
true,
|
||||
),
|
||||
|
||||
[FactionNames.NWO]: new FactionInfo(
|
||||
(
|
||||
@ -486,12 +495,39 @@ export const FactionInfos: IMap<FactionInfo> = {
|
||||
false,
|
||||
),
|
||||
|
||||
[FactionNames.SlumSnakes]: new FactionInfo(<>{FactionNames.SlumSnakes} rule!</>, [], false, false, true, true, false, false),
|
||||
[FactionNames.SlumSnakes]: new FactionInfo(
|
||||
<>{FactionNames.SlumSnakes} rule!</>,
|
||||
[],
|
||||
false,
|
||||
false,
|
||||
true,
|
||||
true,
|
||||
false,
|
||||
false,
|
||||
),
|
||||
|
||||
// Earlygame factions - factions the player will prestige with early on that don't belong in other categories.
|
||||
[FactionNames.Netburners]: new FactionInfo(<>{"~~//*>H4CK||3T 8URN3R5**>?>\\~~"}</>, [], true, true, false, false, false, false),
|
||||
[FactionNames.Netburners]: new FactionInfo(
|
||||
<>{"~~//*>H4CK||3T 8URN3R5**>?>\\~~"}</>,
|
||||
[],
|
||||
true,
|
||||
true,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
),
|
||||
|
||||
[FactionNames.TianDiHui]: new FactionInfo(<>Obey Heaven and work righteously.</>, [], true, true, false, true, false, false),
|
||||
[FactionNames.TianDiHui]: new FactionInfo(
|
||||
<>Obey Heaven and work righteously.</>,
|
||||
[],
|
||||
true,
|
||||
true,
|
||||
false,
|
||||
true,
|
||||
false,
|
||||
false,
|
||||
),
|
||||
|
||||
[FactionNames.CyberSec]: new FactionInfo(
|
||||
(
|
||||
@ -517,7 +553,8 @@ export const FactionInfos: IMap<FactionInfo> = {
|
||||
It's too bad they won't live. But then again, who does?
|
||||
<br />
|
||||
<br />
|
||||
Note that for this faction, reputation can only be gained through {FactionNames.Bladeburners} actions. Completing {FactionNames.Bladeburners}
|
||||
Note that for this faction, reputation can only be gained through {FactionNames.Bladeburners} actions.
|
||||
Completing {FactionNames.Bladeburners}
|
||||
contracts/operations will increase your reputation.
|
||||
</>
|
||||
),
|
||||
|
@ -59,9 +59,7 @@ export function Info(props: IProps): React.ReactElement {
|
||||
</Typography>
|
||||
<MathJaxWrapper>{"\\(\\huge{r = \\text{total faction reputation}}\\)"}</MathJaxWrapper>
|
||||
<MathJaxWrapper>
|
||||
{
|
||||
"\\(\\huge{favor=1+\\left\\lfloor\\log_{1.02}\\left(\\frac{r+25000}{25500}\\right)\\right\\rfloor}\\)"
|
||||
}
|
||||
{"\\(\\huge{favor=1+\\left\\lfloor\\log_{1.02}\\left(\\frac{r+25000}{25500}\\right)\\right\\rfloor}\\)"}
|
||||
</MathJaxWrapper>
|
||||
</>
|
||||
}
|
||||
@ -86,7 +84,6 @@ export function Info(props: IProps): React.ReactElement {
|
||||
|
||||
<MathJaxWrapper>{"\\(\\huge{r = reputation}\\)"}</MathJaxWrapper>
|
||||
<MathJaxWrapper>{"\\(\\huge{\\Delta r = \\Delta r \\times \\frac{100+favor}{100}}\\)"}</MathJaxWrapper>
|
||||
|
||||
</>
|
||||
}
|
||||
>
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { FactionNames } from '../Faction/data/FactionNames';
|
||||
import { FactionNames } from "../Faction/data/FactionNames";
|
||||
import { Reviver } from "../utils/JSONReviver";
|
||||
|
||||
interface GangTerritory {
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { FactionNames } from '../../Faction/data/FactionNames';
|
||||
import { FactionNames } from "../../Faction/data/FactionNames";
|
||||
export const PowerMultiplier: {
|
||||
[key: string]: number | undefined;
|
||||
} = {
|
||||
|
@ -51,7 +51,7 @@ export function AscensionModal(props: IProps): React.ReactElement {
|
||||
<br />
|
||||
Charisma: x{numeralWrapper.format(res.cha, "0.000")}
|
||||
<br />
|
||||
</>
|
||||
</>,
|
||||
);
|
||||
props.onClose();
|
||||
}
|
||||
|
@ -76,8 +76,8 @@ function UpgradeButton(props: IUpgradeButtonProps): React.ReactElement {
|
||||
return (
|
||||
<Tooltip title={<Typography dangerouslySetInnerHTML={{ __html: props.upg.desc }} />}>
|
||||
<span>
|
||||
<Button onClick={onClick} sx={{ display: 'flex', flexDirection: 'column', width: '100%', height: '100%' }}>
|
||||
<Typography sx={{ display: 'block' }}>{props.upg.name}</Typography>
|
||||
<Button onClick={onClick} sx={{ display: "flex", flexDirection: "column", width: "100%", height: "100%" }}>
|
||||
<Typography sx={{ display: "block" }}>{props.upg.name}</Typography>
|
||||
<Money money={gang.getUpgradeCost(props.upg)} />
|
||||
</Button>
|
||||
</span>
|
||||
@ -113,8 +113,8 @@ function GangMemberUpgradePanel(props: IPanelProps): React.ReactElement {
|
||||
|
||||
const onChange = (event: SelectChangeEvent<string>): void => {
|
||||
setCurrentCategory(event.target.value);
|
||||
rerender()
|
||||
}
|
||||
rerender();
|
||||
};
|
||||
|
||||
const weaponUpgrades = filterUpgrades(props.member.upgrades, UpgradeType.Weapon);
|
||||
const armorUpgrades = filterUpgrades(props.member.upgrades, UpgradeType.Armor);
|
||||
@ -123,11 +123,11 @@ function GangMemberUpgradePanel(props: IPanelProps): React.ReactElement {
|
||||
const augUpgrades = filterUpgrades(props.member.augmentations, UpgradeType.Augmentation);
|
||||
|
||||
const categories: { [key: string]: (GangMemberUpgrade[] | UpgradeType)[] } = {
|
||||
'Weapons': [weaponUpgrades, UpgradeType.Weapon],
|
||||
'Armor': [armorUpgrades, UpgradeType.Armor],
|
||||
'Vehicles': [vehicleUpgrades, UpgradeType.Vehicle],
|
||||
'Rootkits': [rootkitUpgrades, UpgradeType.Rootkit],
|
||||
'Augmentations': [augUpgrades, UpgradeType.Augmentation]
|
||||
Weapons: [weaponUpgrades, UpgradeType.Weapon],
|
||||
Armor: [armorUpgrades, UpgradeType.Armor],
|
||||
Vehicles: [vehicleUpgrades, UpgradeType.Vehicle],
|
||||
Rootkits: [rootkitUpgrades, UpgradeType.Rootkit],
|
||||
Augmentations: [augUpgrades, UpgradeType.Augmentation],
|
||||
};
|
||||
|
||||
const asc = {
|
||||
@ -140,7 +140,7 @@ function GangMemberUpgradePanel(props: IPanelProps): React.ReactElement {
|
||||
};
|
||||
return (
|
||||
<Paper>
|
||||
<Box display="grid" sx={{ gridTemplateColumns: '1fr 1fr', m: 1, gap: 1 }}>
|
||||
<Box display="grid" sx={{ gridTemplateColumns: "1fr 1fr", m: 1, gap: 1 }}>
|
||||
<span>
|
||||
<Typography variant="h5" color="primary">
|
||||
{props.member.name} ({props.member.task})
|
||||
@ -149,47 +149,70 @@ function GangMemberUpgradePanel(props: IPanelProps): React.ReactElement {
|
||||
title={
|
||||
<Typography>
|
||||
Hk: x{numeralWrapper.formatMultiplier(props.member.hack_mult * asc.hack)}(x
|
||||
{numeralWrapper.formatMultiplier(props.member.hack_mult)} Eq, x{numeralWrapper.formatMultiplier(asc.hack)}{" "}
|
||||
Asc)
|
||||
{numeralWrapper.formatMultiplier(props.member.hack_mult)} Eq, x
|
||||
{numeralWrapper.formatMultiplier(asc.hack)} Asc)
|
||||
<br />
|
||||
St: x{numeralWrapper.formatMultiplier(props.member.str_mult * asc.str)}
|
||||
(x{numeralWrapper.formatMultiplier(props.member.str_mult)} Eq, x{numeralWrapper.formatMultiplier(asc.str)}{" "}
|
||||
Asc)
|
||||
(x{numeralWrapper.formatMultiplier(props.member.str_mult)} Eq, x
|
||||
{numeralWrapper.formatMultiplier(asc.str)} Asc)
|
||||
<br />
|
||||
Df: x{numeralWrapper.formatMultiplier(props.member.def_mult * asc.def)}
|
||||
(x{numeralWrapper.formatMultiplier(props.member.def_mult)} Eq, x{numeralWrapper.formatMultiplier(asc.def)}{" "}
|
||||
Asc)
|
||||
(x{numeralWrapper.formatMultiplier(props.member.def_mult)} Eq, x
|
||||
{numeralWrapper.formatMultiplier(asc.def)} Asc)
|
||||
<br />
|
||||
Dx: x{numeralWrapper.formatMultiplier(props.member.dex_mult * asc.dex)}
|
||||
(x{numeralWrapper.formatMultiplier(props.member.dex_mult)} Eq, x{numeralWrapper.formatMultiplier(asc.dex)}{" "}
|
||||
Asc)
|
||||
(x{numeralWrapper.formatMultiplier(props.member.dex_mult)} Eq, x
|
||||
{numeralWrapper.formatMultiplier(asc.dex)} Asc)
|
||||
<br />
|
||||
Ag: x{numeralWrapper.formatMultiplier(props.member.agi_mult * asc.agi)}
|
||||
(x{numeralWrapper.formatMultiplier(props.member.agi_mult)} Eq, x{numeralWrapper.formatMultiplier(asc.agi)}{" "}
|
||||
Asc)
|
||||
(x{numeralWrapper.formatMultiplier(props.member.agi_mult)} Eq, x
|
||||
{numeralWrapper.formatMultiplier(asc.agi)} Asc)
|
||||
<br />
|
||||
Ch: x{numeralWrapper.formatMultiplier(props.member.cha_mult * asc.cha)}
|
||||
(x{numeralWrapper.formatMultiplier(props.member.cha_mult)} Eq, x{numeralWrapper.formatMultiplier(asc.cha)}{" "}
|
||||
Asc)
|
||||
(x{numeralWrapper.formatMultiplier(props.member.cha_mult)} Eq, x
|
||||
{numeralWrapper.formatMultiplier(asc.cha)} Asc)
|
||||
</Typography>
|
||||
}
|
||||
>
|
||||
<Table>
|
||||
<TableBody>
|
||||
<StatsRow name="Hacking" color={Settings.theme.hack} data={{ level: props.member.hack, exp: props.member.hack_exp }} />
|
||||
<StatsRow name="Strength" color={Settings.theme.combat} data={{ level: props.member.str, exp: props.member.str_exp }} />
|
||||
<StatsRow name="Defense" color={Settings.theme.combat} data={{ level: props.member.def, exp: props.member.def_exp }} />
|
||||
<StatsRow name="Dexterity" color={Settings.theme.combat} data={{ level: props.member.dex, exp: props.member.dex_exp }} />
|
||||
<StatsRow name="Agility" color={Settings.theme.combat} data={{ level: props.member.agi, exp: props.member.agi_exp }} />
|
||||
<StatsRow name="Charisma" color={Settings.theme.cha} data={{ level: props.member.cha, exp: props.member.cha_exp }} />
|
||||
<StatsRow
|
||||
name="Hacking"
|
||||
color={Settings.theme.hack}
|
||||
data={{ level: props.member.hack, exp: props.member.hack_exp }}
|
||||
/>
|
||||
<StatsRow
|
||||
name="Strength"
|
||||
color={Settings.theme.combat}
|
||||
data={{ level: props.member.str, exp: props.member.str_exp }}
|
||||
/>
|
||||
<StatsRow
|
||||
name="Defense"
|
||||
color={Settings.theme.combat}
|
||||
data={{ level: props.member.def, exp: props.member.def_exp }}
|
||||
/>
|
||||
<StatsRow
|
||||
name="Dexterity"
|
||||
color={Settings.theme.combat}
|
||||
data={{ level: props.member.dex, exp: props.member.dex_exp }}
|
||||
/>
|
||||
<StatsRow
|
||||
name="Agility"
|
||||
color={Settings.theme.combat}
|
||||
data={{ level: props.member.agi, exp: props.member.agi_exp }}
|
||||
/>
|
||||
<StatsRow
|
||||
name="Charisma"
|
||||
color={Settings.theme.cha}
|
||||
data={{ level: props.member.cha, exp: props.member.cha_exp }}
|
||||
/>
|
||||
</TableBody>
|
||||
</Table>
|
||||
</Tooltip>
|
||||
|
||||
</span>
|
||||
|
||||
<span>
|
||||
<Select onChange={onChange} value={currentCategory} sx={{ width: '100%', mb: 1 }}>
|
||||
<Select onChange={onChange} value={currentCategory} sx={{ width: "100%", mb: 1 }}>
|
||||
{Object.keys(categories).map((k, i) => (
|
||||
<MenuItem key={i + 1} value={k}>
|
||||
<Typography variant="h6">{k}</Typography>
|
||||
@ -197,32 +220,22 @@ function GangMemberUpgradePanel(props: IPanelProps): React.ReactElement {
|
||||
))}
|
||||
</Select>
|
||||
|
||||
<Box sx={{ width: '100%' }}>
|
||||
<Box sx={{ width: "100%" }}>
|
||||
{(categories[currentCategory][0] as GangMemberUpgrade[]).length === 0 && (
|
||||
<Typography>
|
||||
All upgrades owned!
|
||||
</Typography>
|
||||
<Typography>All upgrades owned!</Typography>
|
||||
)}
|
||||
<Box display="grid" sx={{ gridTemplateColumns: '1fr 1fr' }}>
|
||||
<Box display="grid" sx={{ gridTemplateColumns: "1fr 1fr" }}>
|
||||
{(categories[currentCategory][0] as GangMemberUpgrade[]).map((upg) => (
|
||||
<UpgradeButton
|
||||
key={upg.name}
|
||||
rerender={rerender}
|
||||
member={props.member}
|
||||
upg={upg}
|
||||
/>
|
||||
<UpgradeButton key={upg.name} rerender={rerender} member={props.member} upg={upg} />
|
||||
))}
|
||||
</Box>
|
||||
<NextReveal
|
||||
type={categories[currentCategory][1] as UpgradeType}
|
||||
upgrades={props.member.upgrades}
|
||||
/>
|
||||
<NextReveal type={categories[currentCategory][1] as UpgradeType} upgrades={props.member.upgrades} />
|
||||
</Box>
|
||||
</span>
|
||||
</Box>
|
||||
|
||||
<Typography sx={{ mx: 1 }}>Purchased Upgrades: </Typography>
|
||||
<Box display="grid" sx={{ gridTemplateColumns: 'repeat(4, 1fr)', m: 1 }}>
|
||||
<Box display="grid" sx={{ gridTemplateColumns: "repeat(4, 1fr)", m: 1 }}>
|
||||
{props.member.upgrades.map((upg: string) => (
|
||||
<PurchasedUpgrade key={upg} upgName={upg} />
|
||||
))}
|
||||
@ -238,13 +251,11 @@ export function EquipmentsSubpage(): React.ReactElement {
|
||||
const gang = useGang();
|
||||
const [filter, setFilter] = useState("");
|
||||
|
||||
|
||||
const handleFilterChange = (event: React.ChangeEvent<HTMLInputElement>): void => {
|
||||
setFilter(event.target.value.toLowerCase());
|
||||
}
|
||||
};
|
||||
|
||||
const members = gang.members
|
||||
.filter((member) => member && member.name.toLowerCase().includes(filter));
|
||||
const members = gang.members.filter((member) => member && member.name.toLowerCase().includes(filter));
|
||||
|
||||
return (
|
||||
<>
|
||||
@ -265,13 +276,13 @@ export function EquipmentsSubpage(): React.ReactElement {
|
||||
autoFocus
|
||||
InputProps={{
|
||||
startAdornment: <SearchIcon />,
|
||||
spellCheck: false
|
||||
spellCheck: false,
|
||||
}}
|
||||
placeholder="Filter by member name"
|
||||
sx={{ m: 1, width: '15%' }}
|
||||
sx={{ m: 1, width: "15%" }}
|
||||
/>
|
||||
|
||||
<Box display="grid" sx={{ gridTemplateColumns: '1fr 1fr', width: '100%' }}>
|
||||
<Box display="grid" sx={{ gridTemplateColumns: "1fr 1fr", width: "100%" }}>
|
||||
{members.map((member: GangMember) => (
|
||||
<GangMemberUpgradePanel key={member.name} member={member} />
|
||||
))}
|
||||
|
@ -16,7 +16,7 @@ interface IProps {
|
||||
|
||||
export function GangMemberCard(props: IProps): React.ReactElement {
|
||||
return (
|
||||
<Box component={Paper} sx={{ width: 'auto' }}>
|
||||
<Box component={Paper} sx={{ width: "auto" }}>
|
||||
<Box sx={{ m: 1 }}>
|
||||
<ListItemText primary={<b>{props.member.name}</b>} />
|
||||
<GangMemberCardContent member={props.member} />
|
||||
|
@ -26,15 +26,17 @@ export function GangMemberCardContent(props: IProps): React.ReactElement {
|
||||
return (
|
||||
<>
|
||||
{props.member.canAscend() && (
|
||||
<Box sx={{ display: 'flex', justifyContent: 'space-between', my: 1 }}>
|
||||
<Button onClick={() => setAscendOpen(true)} style={{ flexGrow: 1, borderRightWidth: 0 }}>Ascend</Button>
|
||||
<Box sx={{ display: "flex", justifyContent: "space-between", my: 1 }}>
|
||||
<Button onClick={() => setAscendOpen(true)} style={{ flexGrow: 1, borderRightWidth: 0 }}>
|
||||
Ascend
|
||||
</Button>
|
||||
<AscensionModal
|
||||
open={ascendOpen}
|
||||
onClose={() => setAscendOpen(false)}
|
||||
member={props.member}
|
||||
onAscend={() => setRerender((old) => !old)}
|
||||
/>
|
||||
<Button onClick={() => setHelpOpen(true)} style={{ width: 'fit-content', borderLeftWidth: 0 }}>
|
||||
<Button onClick={() => setHelpOpen(true)} style={{ width: "fit-content", borderLeftWidth: 0 }}>
|
||||
<HelpIcon />
|
||||
</Button>
|
||||
<StaticModal open={helpOpen} onClose={() => setHelpOpen(false)}>
|
||||
@ -53,7 +55,7 @@ export function GangMemberCardContent(props: IProps): React.ReactElement {
|
||||
</StaticModal>
|
||||
</Box>
|
||||
)}
|
||||
<Box display="grid" sx={{ gridTemplateColumns: '1fr 1fr', width: '100%', gap: 1 }}>
|
||||
<Box display="grid" sx={{ gridTemplateColumns: "1fr 1fr", width: "100%", gap: 1 }}>
|
||||
<GangMemberStats member={props.member} />
|
||||
<TaskSelector onTaskChange={() => setRerender((old) => !old)} member={props.member} />
|
||||
</Box>
|
||||
|
@ -20,7 +20,7 @@ export function GangMemberList(): React.ReactElement {
|
||||
|
||||
const handleFilterChange = (event: React.ChangeEvent<HTMLInputElement>): void => {
|
||||
setFilter(event.target.value.toLowerCase());
|
||||
}
|
||||
};
|
||||
|
||||
const members = gang.members
|
||||
.filter((member) => member && member.name.toLowerCase().includes(filter))
|
||||
@ -38,23 +38,18 @@ export function GangMemberList(): React.ReactElement {
|
||||
autoFocus
|
||||
InputProps={{
|
||||
startAdornment: <SearchIcon />,
|
||||
spellCheck: false
|
||||
spellCheck: false,
|
||||
}}
|
||||
placeholder="Filter by member name"
|
||||
sx={{ m: 1, width: '15%' }}
|
||||
sx={{ m: 1, width: "15%" }}
|
||||
/>
|
||||
<OptionSwitch
|
||||
checked={ascendOnly}
|
||||
onChange={(newValue) => (setAscendOnly(newValue))}
|
||||
onChange={(newValue) => setAscendOnly(newValue)}
|
||||
text="Show only ascendable"
|
||||
tooltip={
|
||||
<>
|
||||
Filter the members list by whether or not the member
|
||||
can be ascended.
|
||||
</>
|
||||
}
|
||||
tooltip={<>Filter the members list by whether or not the member can be ascended.</>}
|
||||
/>
|
||||
<Box display="grid" sx={{ gridTemplateColumns: 'repeat(2, 1fr)' }}>
|
||||
<Box display="grid" sx={{ gridTemplateColumns: "repeat(2, 1fr)" }}>
|
||||
{members.map((member: GangMember) => (
|
||||
<GangMemberCard key={member.name} member={member} />
|
||||
))}
|
||||
|
@ -32,7 +32,7 @@ export function GangRoot(): React.ReactElement {
|
||||
|
||||
return (
|
||||
<Context.Gang.Provider value={gang}>
|
||||
<Tabs variant="fullWidth" value={value} onChange={handleChange} sx={{ minWidth: 'fit-content', maxWidth: '45%' }}>
|
||||
<Tabs variant="fullWidth" value={value} onChange={handleChange} sx={{ minWidth: "fit-content", maxWidth: "45%" }}>
|
||||
<Tab label="Management" />
|
||||
<Tab label="Equipment" />
|
||||
<Tab label="Territory" />
|
||||
|
@ -25,10 +25,10 @@ export function RecruitButton(props: IProps): React.ReactElement {
|
||||
const respect = gang.getRespectNeededToRecruitMember();
|
||||
return (
|
||||
<Box display="flex" alignItems="center" sx={{ mx: 1 }}>
|
||||
<Button disabled>
|
||||
Recruit Gang Member
|
||||
</Button>
|
||||
<Typography sx={{ ml: 1 }}>{numeralWrapper.formatRespect(respect)} respect needed to recruit next member</Typography>
|
||||
<Button disabled>Recruit Gang Member</Button>
|
||||
<Typography sx={{ ml: 1 }}>
|
||||
{numeralWrapper.formatRespect(respect)} respect needed to recruit next member
|
||||
</Typography>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
@ -21,11 +21,9 @@ export function TaskSelector(props: IProps): React.ReactElement {
|
||||
const gang = useGang();
|
||||
const [currentTask, setCurrentTask] = useState(props.member.task);
|
||||
|
||||
const contextMember = gang.members.find(member => member.name == props.member.name)
|
||||
if (contextMember &&
|
||||
contextMember.task != currentTask
|
||||
) {
|
||||
setCurrentTask(contextMember.task)
|
||||
const contextMember = gang.members.find((member) => member.name == props.member.name);
|
||||
if (contextMember && contextMember.task != currentTask) {
|
||||
setCurrentTask(contextMember.task);
|
||||
}
|
||||
|
||||
function onChange(event: SelectChangeEvent<string>): void {
|
||||
@ -39,7 +37,7 @@ export function TaskSelector(props: IProps): React.ReactElement {
|
||||
|
||||
return (
|
||||
<Box>
|
||||
<Select onChange={onChange} value={currentTask} sx={{ width: '100%' }}>
|
||||
<Select onChange={onChange} value={currentTask} sx={{ width: "100%" }}>
|
||||
<MenuItem key={0} value={"Unassigned"}>
|
||||
Unassigned
|
||||
</MenuItem>
|
||||
|
@ -13,9 +13,7 @@ export const TerritoryInfoModal = ({ open, onClose }: IProps): React.ReactElemen
|
||||
return (
|
||||
<Modal open={open} onClose={onClose}>
|
||||
<>
|
||||
<Typography variant='h4'>
|
||||
Clashing
|
||||
</Typography>
|
||||
<Typography variant="h4">Clashing</Typography>
|
||||
<Typography>
|
||||
Every ~20 seconds, your gang has a chance to 'clash' with other gangs. Your chance to win a clash depends on
|
||||
your gang's power, which is listed in the display below. Your gang's power slowly accumulates over time. The
|
||||
@ -29,21 +27,17 @@ export const TerritoryInfoModal = ({ open, onClose }: IProps): React.ReactElemen
|
||||
gang.
|
||||
</Typography>
|
||||
<br />
|
||||
<Typography variant='h4'>
|
||||
Territory
|
||||
</Typography>
|
||||
<Typography variant="h4">Territory</Typography>
|
||||
<Typography>
|
||||
The amount of territory you have affects all aspects of your Gang members' production, including money, respect,
|
||||
and wanted level. It is very beneficial to have high territory control.
|
||||
The amount of territory you have affects all aspects of your Gang members' production, including money,
|
||||
respect, and wanted level. It is very beneficial to have high territory control.
|
||||
<br />
|
||||
<br />
|
||||
To increase your chances of winning territory, assign gang members to "Territory Warfare". This will build your
|
||||
gang power. Then, enable "Engage in Territory Warfare" to start fighting over territory.
|
||||
To increase your chances of winning territory, assign gang members to "Territory Warfare". This will build
|
||||
your gang power. Then, enable "Engage in Territory Warfare" to start fighting over territory.
|
||||
</Typography>
|
||||
<br />
|
||||
<Typography variant='h4'>
|
||||
Territory Clash Chance
|
||||
</Typography>
|
||||
<Typography variant="h4">Territory Clash Chance</Typography>
|
||||
<Typography>
|
||||
This percentage represents the chance you have of 'clashing' with another gang. If you do not wish to
|
||||
gain/lose territory, then keep this percentage at 0% by not engaging in territory warfare.
|
||||
@ -51,4 +45,4 @@ export const TerritoryInfoModal = ({ open, onClose }: IProps): React.ReactElemen
|
||||
</>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
@ -3,16 +3,7 @@
|
||||
*/
|
||||
import React, { useState } from "react";
|
||||
|
||||
import {
|
||||
Container,
|
||||
Button,
|
||||
Paper,
|
||||
Box,
|
||||
Tooltip,
|
||||
Switch,
|
||||
FormControlLabel,
|
||||
Typography
|
||||
} from "@mui/material";
|
||||
import { Container, Button, Paper, Box, Tooltip, Switch, FormControlLabel, Typography } from "@mui/material";
|
||||
import { Help } from "@mui/icons-material";
|
||||
|
||||
import { numeralWrapper } from "../../ui/numeralFormat";
|
||||
@ -41,35 +32,51 @@ export function TerritorySubpage(): React.ReactElement {
|
||||
</Button>
|
||||
|
||||
<Box component={Paper} sx={{ p: 1, mb: 1 }}>
|
||||
<Typography variant="h6" sx={{ display: 'flex', alignItems: 'center', flexWrap: 'wrap' }}>
|
||||
<Typography variant="h6" sx={{ display: "flex", alignItems: "center", flexWrap: "wrap" }}>
|
||||
{gang.facName} (Your gang)
|
||||
</Typography>
|
||||
|
||||
<FormControlLabel
|
||||
control={<Switch
|
||||
control={
|
||||
<Switch
|
||||
checked={gang.territoryWarfareEngaged}
|
||||
onChange={(event) => (gang.territoryWarfareEngaged = event.target.checked)}
|
||||
/>}
|
||||
label={<Tooltip
|
||||
title={<Typography>
|
||||
/>
|
||||
}
|
||||
label={
|
||||
<Tooltip
|
||||
title={
|
||||
<Typography>
|
||||
Engaging in Territory Warfare sets your clash chance to 100%. Disengaging will cause your clash chance
|
||||
to gradually decrease until it reaches 0%.
|
||||
</Typography>}>
|
||||
</Typography>
|
||||
}
|
||||
>
|
||||
<Typography>Engage in Territory Warfare</Typography>
|
||||
</Tooltip>} />
|
||||
</Tooltip>
|
||||
}
|
||||
/>
|
||||
<br />
|
||||
<FormControlLabel
|
||||
control={<Switch
|
||||
control={
|
||||
<Switch
|
||||
checked={gang.notifyMemberDeath}
|
||||
onChange={(event) => (gang.notifyMemberDeath = event.target.checked)}
|
||||
/>}
|
||||
label={<Tooltip
|
||||
title={<Typography>
|
||||
If this is enabled, then you will receive a pop-up notifying you whenever one of your Gang Members dies
|
||||
in a territory clash.
|
||||
</Typography>}>
|
||||
/>
|
||||
}
|
||||
label={
|
||||
<Tooltip
|
||||
title={
|
||||
<Typography>
|
||||
If this is enabled, then you will receive a pop-up notifying you whenever one of your Gang Members
|
||||
dies in a territory clash.
|
||||
</Typography>
|
||||
}
|
||||
>
|
||||
<Typography>Notify about Gang Member Deaths</Typography>
|
||||
</Tooltip>} />
|
||||
</Tooltip>
|
||||
}
|
||||
/>
|
||||
|
||||
<Typography>
|
||||
<b>Territory Clash Chance:</b> {numeralWrapper.formatPercentage(gang.territoryClashChance, 3)} <br />
|
||||
@ -77,7 +84,7 @@ export function TerritorySubpage(): React.ReactElement {
|
||||
<b>Territory:</b> {formatTerritory(AllGangs[gang.facName].territory)}% <br />
|
||||
</Typography>
|
||||
</Box>
|
||||
<Box sx={{ display: 'grid', gridTemplateColumns: 'repeat(3, 1fr)' }}>
|
||||
<Box sx={{ display: "grid", gridTemplateColumns: "repeat(3, 1fr)" }}>
|
||||
{gangNames.map((name) => (
|
||||
<OtherGangTerritory key={name} name={name} />
|
||||
))}
|
||||
@ -109,7 +116,7 @@ function OtherGangTerritory(props: ITerritoryProps): React.ReactElement {
|
||||
const clashVictoryChance = playerPower / (power + playerPower);
|
||||
return (
|
||||
<Box component={Paper} sx={{ p: 1 }}>
|
||||
<Typography variant="h6" sx={{ display: 'flex', alignItems: 'center', flexWrap: 'wrap' }}>
|
||||
<Typography variant="h6" sx={{ display: "flex", alignItems: "center", flexWrap: "wrap" }}>
|
||||
{props.name}
|
||||
</Typography>
|
||||
<Typography>
|
||||
|
@ -49,7 +49,8 @@ export function calculatePercentMoneyHacked(server: Server, player: IPlayer): nu
|
||||
|
||||
const difficultyMult = (100 - server.hackDifficulty) / 100;
|
||||
const skillMult = (player.hacking - (server.requiredHackingSkill - 1)) / player.hacking;
|
||||
const percentMoneyHacked = (difficultyMult * skillMult * player.hacking_money_mult * BitNodeMultipliers.ScriptHackMoney) / balanceFactor;
|
||||
const percentMoneyHacked =
|
||||
(difficultyMult * skillMult * player.hacking_money_mult * BitNodeMultipliers.ScriptHackMoney) / balanceFactor;
|
||||
if (percentMoneyHacked < 0) {
|
||||
return 0;
|
||||
}
|
||||
|
@ -21,14 +21,13 @@ import { Generic_fromJSON, Generic_toJSON, Reviver } from "../utils/JSONReviver"
|
||||
import { ObjectValidator, minMax } from "../utils/Validator";
|
||||
|
||||
export class HacknetNode implements IHacknetNode {
|
||||
|
||||
static validationData: ObjectValidator<HacknetNode> = {
|
||||
cores: minMax(1, 1, HacknetNodeConstants.MaxCores),
|
||||
level: minMax(1, 1, HacknetNodeConstants.MaxLevel),
|
||||
ram: minMax(1, 1, HacknetNodeConstants.MaxRam),
|
||||
onlineTimeSeconds: minMax(0, 0, Infinity),
|
||||
totalMoneyGenerated: minMax(0, 0, Infinity)
|
||||
}
|
||||
totalMoneyGenerated: minMax(0, 0, Infinity),
|
||||
};
|
||||
|
||||
// Node's number of cores
|
||||
cores = 1;
|
||||
|
@ -137,7 +137,7 @@ export function HacknetRoot(props: IProps): React.ReactElement {
|
||||
|
||||
{hasHacknetServers(props.player) && <Button onClick={() => setOpen(true)}>Spend Hashes on Upgrades</Button>}
|
||||
|
||||
<Box sx={{ display: 'grid', width: 'fit-content', gridTemplateColumns: 'repeat(3, 1fr)' }}>{nodes}</Box>
|
||||
<Box sx={{ display: "grid", width: "fit-content", gridTemplateColumns: "repeat(3, 1fr)" }}>{nodes}</Box>
|
||||
<HashUpgradeModal open={open} onClose={() => setOpen(false)} />
|
||||
</>
|
||||
);
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { CityName } from './../Locations/data/CityNames';
|
||||
import { CityName } from "./../Locations/data/CityNames";
|
||||
import { Literature } from "./Literature";
|
||||
import { LiteratureNames } from "./data/LiteratureNames";
|
||||
import { IMap } from "../types";
|
||||
|
@ -34,7 +34,7 @@ export function CasinoLocation(props: IProps): React.ReactElement {
|
||||
return (
|
||||
<>
|
||||
{game === GameType.None && (
|
||||
<Box sx={{ display: 'grid', width: 'fit-content' }}>
|
||||
<Box sx={{ display: "grid", width: "fit-content" }}>
|
||||
<Button onClick={() => updateGame(GameType.Coin)}>Play coin flip</Button>
|
||||
<Button onClick={() => updateGame(GameType.Slots)}>Play slots</Button>
|
||||
<Button onClick={() => updateGame(GameType.Roulette)}>Play roulette</Button>
|
||||
|
@ -228,9 +228,9 @@ export function CompanyLocation(props: IProps): React.ReactElement {
|
||||
<br />
|
||||
</>
|
||||
)}
|
||||
<Box sx={{ display: 'grid', width: 'fit-content' }}>
|
||||
<Box sx={{ display: "grid", width: "fit-content" }}>
|
||||
{isEmployedHere && (
|
||||
<Box sx={{ display: 'grid', gridTemplateColumns: '1fr 1fr' }}>
|
||||
<Box sx={{ display: "grid", gridTemplateColumns: "1fr 1fr" }}>
|
||||
<Button onClick={work}>Work</Button>
|
||||
<Button onClick={() => setQuitOpen(true)}>Quit</Button>
|
||||
<QuitJobModal
|
||||
@ -241,9 +241,7 @@ export function CompanyLocation(props: IProps): React.ReactElement {
|
||||
onClose={() => setQuitOpen(false)}
|
||||
/>
|
||||
</Box>
|
||||
)
|
||||
|
||||
}
|
||||
)}
|
||||
{company.hasAgentPositions() && (
|
||||
<ApplyToJobButton
|
||||
company={company}
|
||||
|
@ -31,11 +31,7 @@ export function CoresButton(props: IProps): React.ReactElement {
|
||||
}
|
||||
|
||||
return (
|
||||
<Tooltip
|
||||
title={
|
||||
<MathJaxWrapper>{`\\(\\large{cost = 10^9 \\cdot 7.5 ^{\\text{cores}}}\\)`}</MathJaxWrapper>
|
||||
}
|
||||
>
|
||||
<Tooltip title={<MathJaxWrapper>{`\\(\\large{cost = 10^9 \\cdot 7.5 ^{\\text{cores}}}\\)`}</MathJaxWrapper>}>
|
||||
<span>
|
||||
<br />
|
||||
<Typography>
|
||||
|
@ -96,8 +96,13 @@ export function GenericLocation({ loc }: IProps): React.ReactElement {
|
||||
<Typography variant="h4" sx={{ mt: 1 }}>
|
||||
{backdoorInstalled && !Settings.DisableTextEffects ? (
|
||||
<Tooltip title={`Backdoor installed on ${loc.name}.`}>
|
||||
<span><CorruptableText content={loc.name} /></span>
|
||||
</Tooltip>) : loc.name}
|
||||
<span>
|
||||
<CorruptableText content={loc.name} />
|
||||
</span>
|
||||
</Tooltip>
|
||||
) : (
|
||||
loc.name
|
||||
)}
|
||||
</Typography>
|
||||
{locContent}
|
||||
</>
|
||||
|
@ -59,7 +59,7 @@ export function GymLocation(props: IProps): React.ReactElement {
|
||||
const cost = CONSTANTS.ClassGymBaseCost * calculateCost();
|
||||
|
||||
return (
|
||||
<Box sx={{ display: 'grid', width: 'fit-content' }}>
|
||||
<Box sx={{ display: "grid", width: "fit-content" }}>
|
||||
<Button onClick={trainStrength}>
|
||||
Train Strength (<Money money={cost} player={props.p} /> / sec)
|
||||
</Button>
|
||||
|
@ -32,9 +32,7 @@ export function RamButton(props: IProps): React.ReactElement {
|
||||
|
||||
return (
|
||||
<Tooltip
|
||||
title={
|
||||
<MathJaxWrapper>{`\\(\\large{cost = 3.2 \\cdot 10^3 \\cdot 1.58^{log_2{(ram)}}}\\)`}</MathJaxWrapper>
|
||||
}
|
||||
title={<MathJaxWrapper>{`\\(\\large{cost = 3.2 \\cdot 10^3 \\cdot 1.58^{log_2{(ram)}}}\\)`}</MathJaxWrapper>}
|
||||
>
|
||||
<span>
|
||||
<br />
|
||||
|
@ -114,7 +114,7 @@ export function SlumsLocation(): React.ReactElement {
|
||||
const heistChance = Crimes.Heist.successRate(player);
|
||||
|
||||
return (
|
||||
<Box sx={{ display: 'grid', width: 'fit-content' }}>
|
||||
<Box sx={{ display: "grid", width: "fit-content" }}>
|
||||
<Tooltip title={<>Attempt to shoplift from a low-end retailer</>}>
|
||||
<Button onClick={shoplift}>
|
||||
Shoplift ({numeralWrapper.formatPercentage(shopliftChance)} chance of success)
|
||||
|
@ -155,7 +155,11 @@ export function SpecialLocation(props: IProps): React.ReactElement {
|
||||
if (!player.canAccessGrafting()) {
|
||||
return <></>;
|
||||
}
|
||||
return <Button onClick={handleGrafting} sx={{ my: 5 }}>Enter the secret lab</Button>;
|
||||
return (
|
||||
<Button onClick={handleGrafting} sx={{ my: 5 }}>
|
||||
Enter the secret lab
|
||||
</Button>
|
||||
);
|
||||
}
|
||||
|
||||
function handleCotMG(): void {
|
||||
|
@ -70,9 +70,7 @@ export function TechVendorLocation(props: IProps): React.ReactElement {
|
||||
return (
|
||||
<>
|
||||
<br />
|
||||
<Box sx={{ display: 'grid', width: 'fit-content' }}>
|
||||
{purchaseServerButtons}
|
||||
</Box>
|
||||
<Box sx={{ display: "grid", width: "fit-content" }}>{purchaseServerButtons}</Box>
|
||||
<br />
|
||||
<Typography>
|
||||
<i>"You can order bigger servers via scripts. We don't take custom orders in person."</i>
|
||||
|
@ -75,7 +75,7 @@ export function UniversityLocation(props: IProps): React.ReactElement {
|
||||
const earnCharismaExpTooltip = `Gain charisma experience!`;
|
||||
|
||||
return (
|
||||
<Box sx={{ display: 'grid', width: 'fit-content' }}>
|
||||
<Box sx={{ display: "grid", width: "fit-content" }}>
|
||||
<Tooltip title={earnHackingExpTooltip}>
|
||||
<Button onClick={study}>Study Computer Science (free)</Button>
|
||||
</Tooltip>
|
||||
|
@ -1,4 +1,4 @@
|
||||
import React from 'react';
|
||||
import React from "react";
|
||||
import { MathJax, MathJaxContext } from "better-react-mathjax";
|
||||
|
||||
interface IProps {
|
||||
@ -10,5 +10,5 @@ export function MathJaxWrapper({ children }: IProps): React.ReactElement {
|
||||
<MathJaxContext version={3} src={"dist/ext/MathJax-3.2.0/es5/tex-chtml.js"}>
|
||||
<MathJax>{children}</MathJax>
|
||||
</MathJaxContext>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
@ -601,7 +601,7 @@ export function NetscriptFunctions(workerScript: WorkerScript): NS {
|
||||
|
||||
return calculateHackingChance(server, Player);
|
||||
},
|
||||
sleep: async function (_time: unknown = 0): Promise<void> {
|
||||
sleep: async function (_time: unknown = 0): Promise<true> {
|
||||
const time = helper.number("sleep", "time", _time);
|
||||
updateDynamicRam("sleep", getRamCost(Player, "sleep"));
|
||||
if (time === undefined) {
|
||||
@ -609,17 +609,17 @@ export function NetscriptFunctions(workerScript: WorkerScript): NS {
|
||||
}
|
||||
workerScript.log("sleep", () => `Sleeping for ${time} milliseconds`);
|
||||
return netscriptDelay(time, workerScript).then(function () {
|
||||
return Promise.resolve();
|
||||
return Promise.resolve(true);
|
||||
});
|
||||
},
|
||||
asleep: function (_time: unknown = 0): Promise<void> {
|
||||
asleep: function (_time: unknown = 0): Promise<true> {
|
||||
const time = helper.number("asleep", "time", _time);
|
||||
updateDynamicRam("asleep", getRamCost(Player, "asleep"));
|
||||
if (time === undefined) {
|
||||
throw makeRuntimeErrorMsg("asleep", "Takes 1 argument.");
|
||||
}
|
||||
workerScript.log("asleep", () => `Sleeping for ${time} milliseconds`);
|
||||
return new Promise((resolve) => setTimeout(resolve, time));
|
||||
return new Promise((resolve) => setTimeout(() => resolve(true), time));
|
||||
},
|
||||
grow: async function (
|
||||
_hostname: unknown,
|
||||
|
@ -2,7 +2,12 @@ import { INetscriptHelper } from "./INetscriptHelper";
|
||||
import { WorkerScript } from "../Netscript/WorkerScript";
|
||||
import { IPlayer } from "../PersonObjects/IPlayer";
|
||||
import { getRamCost } from "../Netscript/RamCostGenerator";
|
||||
import { GameInfo, IStyleSettings, UserInterface as IUserInterface, UserInterfaceTheme } from "../ScriptEditor/NetscriptDefinitions";
|
||||
import {
|
||||
GameInfo,
|
||||
IStyleSettings,
|
||||
UserInterface as IUserInterface,
|
||||
UserInterfaceTheme,
|
||||
} from "../ScriptEditor/NetscriptDefinitions";
|
||||
import { Settings } from "../Settings/Settings";
|
||||
import { ThemeEvents } from "../Themes/ui/Theme";
|
||||
import { defaultTheme } from "../Themes/Themes";
|
||||
@ -29,13 +34,13 @@ export function NetscriptUserInterface(
|
||||
setTheme: function (newTheme: UserInterfaceTheme): void {
|
||||
helper.updateDynamicRam("setTheme", getRamCost(player, "ui", "setTheme"));
|
||||
const hex = /^(#)((?:[A-Fa-f0-9]{2}){3,4}|(?:[A-Fa-f0-9]{3}))$/;
|
||||
const currentTheme = {...Settings.theme}
|
||||
const currentTheme = { ...Settings.theme };
|
||||
const errors: string[] = [];
|
||||
for (const key of Object.keys(newTheme)) {
|
||||
if (!currentTheme[key]) {
|
||||
// Invalid key
|
||||
errors.push(`Invalid key "${key}"`);
|
||||
} else if (!hex.test(newTheme[key] ?? '')) {
|
||||
} else if (!hex.test(newTheme[key] ?? "")) {
|
||||
errors.push(`Invalid color "${key}": ${newTheme[key]}`);
|
||||
} else {
|
||||
currentTheme[key] = newTheme[key];
|
||||
@ -47,17 +52,17 @@ export function NetscriptUserInterface(
|
||||
ThemeEvents.emit();
|
||||
workerScript.log("ui.setTheme", () => `Successfully set theme`);
|
||||
} else {
|
||||
workerScript.log("ui.setTheme", () => `Failed to set theme. Errors: ${errors.join(', ')}`);
|
||||
workerScript.log("ui.setTheme", () => `Failed to set theme. Errors: ${errors.join(", ")}`);
|
||||
}
|
||||
},
|
||||
|
||||
setStyles: function (newStyles: IStyleSettings): void {
|
||||
helper.updateDynamicRam("setStyles", getRamCost(player, "ui", "setStyles"));
|
||||
|
||||
const currentStyles = {...Settings.styles}
|
||||
const currentStyles = { ...Settings.styles };
|
||||
const errors: string[] = [];
|
||||
for (const key of Object.keys(newStyles)) {
|
||||
if (!((currentStyles as any)[key])) {
|
||||
if (!(currentStyles as any)[key]) {
|
||||
// Invalid key
|
||||
errors.push(`Invalid key "${key}"`);
|
||||
} else {
|
||||
@ -70,7 +75,7 @@ export function NetscriptUserInterface(
|
||||
ThemeEvents.emit();
|
||||
workerScript.log("ui.setStyles", () => `Successfully set styles`);
|
||||
} else {
|
||||
workerScript.log("ui.setStyles", () => `Failed to set styles. Errors: ${errors.join(', ')}`);
|
||||
workerScript.log("ui.setStyles", () => `Failed to set styles. Errors: ${errors.join(", ")}`);
|
||||
}
|
||||
},
|
||||
|
||||
@ -92,13 +97,15 @@ export function NetscriptUserInterface(
|
||||
helper.updateDynamicRam("getGameInfo", getRamCost(player, "ui", "getGameInfo"));
|
||||
const version = CONSTANTS.VersionString;
|
||||
const commit = hash();
|
||||
const platform = (navigator.userAgent.toLowerCase().indexOf(" electron/") > -1) ? 'Steam' : 'Browser';
|
||||
const platform = navigator.userAgent.toLowerCase().indexOf(" electron/") > -1 ? "Steam" : "Browser";
|
||||
|
||||
const gameInfo = {
|
||||
version, commit, platform,
|
||||
}
|
||||
version,
|
||||
commit,
|
||||
platform,
|
||||
};
|
||||
|
||||
return gameInfo;
|
||||
}
|
||||
}
|
||||
},
|
||||
};
|
||||
}
|
||||
|
@ -178,7 +178,9 @@ function startNetscript1Script(workerScript: WorkerScript): Promise<WorkerScript
|
||||
const entry = ns[name];
|
||||
if (typeof entry === "function") {
|
||||
//Async functions need to be wrapped. See JS-Interpreter documentation
|
||||
if (["hack", "grow", "weaken", "sleep", "prompt", "manualHack", "scp", "write", "share", "wget"].includes(name)) {
|
||||
if (
|
||||
["hack", "grow", "weaken", "sleep", "prompt", "manualHack", "scp", "write", "share", "wget"].includes(name)
|
||||
) {
|
||||
const tempWrapper = function (...args: any[]): void {
|
||||
const fnArgs = [];
|
||||
|
||||
|
@ -859,7 +859,7 @@ export class Sleeve extends Person {
|
||||
* Returns boolean indicating success
|
||||
*/
|
||||
workForFaction(p: IPlayer, factionName: string, workType: string): boolean {
|
||||
const faction = Factions[factionName]
|
||||
const faction = Factions[factionName];
|
||||
if (factionName === "" || !faction || !(faction instanceof Faction) || !p.factions.includes(factionName)) {
|
||||
return false;
|
||||
}
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { FactionNames } from '../../Faction/data/FactionNames';
|
||||
import { FactionNames } from "../../Faction/data/FactionNames";
|
||||
import { Sleeve } from "./Sleeve";
|
||||
|
||||
import { IPlayer } from "../IPlayer";
|
||||
|
@ -91,7 +91,8 @@ export function FAQModal({ open, onClose }: IProps): React.ReactElement {
|
||||
<Typography variant="h4">Why can't I buy the X Augmentation for my sleeve?</Typography>
|
||||
<br />
|
||||
<Typography>
|
||||
Certain Augmentations, like {FactionNames.Bladeburners}-specific ones and NeuroFlux Governor, are not available for sleeves.
|
||||
Certain Augmentations, like {FactionNames.Bladeburners}-specific ones and NeuroFlux Governor, are not
|
||||
available for sleeves.
|
||||
</Typography>
|
||||
<br />
|
||||
<br />
|
||||
@ -110,8 +111,8 @@ export function FAQModal({ open, onClose }: IProps): React.ReactElement {
|
||||
<br />
|
||||
<br />
|
||||
<Typography>
|
||||
Memory can only be increased by purchasing upgrades from {FactionNames.TheCovenant}. It is a persistent stat, meaning it
|
||||
never gets resets back to 1. The maximum possible value for a sleeve's memory is 100.
|
||||
Memory can only be increased by purchasing upgrades from {FactionNames.TheCovenant}. It is a persistent stat,
|
||||
meaning it never gets resets back to 1. The maximum possible value for a sleeve's memory is 100.
|
||||
</Typography>
|
||||
</>
|
||||
</Modal>
|
||||
|
@ -91,11 +91,18 @@ export function SleeveAugmentationsModal(props: IProps): React.ReactElement {
|
||||
{ownedAugNames.length > 0 && (
|
||||
<>
|
||||
<Typography sx={{ mx: 1 }}>Owned Augmentations:</Typography>
|
||||
<Box display="grid" sx={{ gridTemplateColumns: 'repeat(5, 1fr)', m: 1 }}>
|
||||
<Box display="grid" sx={{ gridTemplateColumns: "repeat(5, 1fr)", m: 1 }}>
|
||||
{ownedAugNames.map((augName) => {
|
||||
const aug = Augmentations[augName];
|
||||
const info = typeof aug.info === "string" ? <span>{aug.info}</span> : aug.info
|
||||
const tooltip = (<>{info}<br /><br />{aug.stats}</>);
|
||||
const info = typeof aug.info === "string" ? <span>{aug.info}</span> : aug.info;
|
||||
const tooltip = (
|
||||
<>
|
||||
{info}
|
||||
<br />
|
||||
<br />
|
||||
{aug.stats}
|
||||
</>
|
||||
);
|
||||
|
||||
return (
|
||||
<Tooltip key={augName} title={<Typography>{tooltip}</Typography>}>
|
||||
|
@ -1,12 +1,6 @@
|
||||
import React, { useState } from "react";
|
||||
|
||||
import {
|
||||
Box,
|
||||
Paper,
|
||||
Typography,
|
||||
Button,
|
||||
Tooltip
|
||||
} from "@mui/material";
|
||||
import { Box, Paper, Typography, Button, Tooltip } from "@mui/material";
|
||||
|
||||
import { CONSTANTS } from "../../../Constants";
|
||||
import { Crimes } from "../../../Crime/Crimes";
|
||||
@ -137,12 +131,12 @@ export function SleeveElem(props: IProps): React.ReactElement {
|
||||
}
|
||||
|
||||
return (
|
||||
<Box component={Paper} sx={{ width: 'auto' }}>
|
||||
<Box component={Paper} sx={{ width: "auto" }}>
|
||||
<Box sx={{ m: 1 }}>
|
||||
<Box display="grid" sx={{ gridTemplateColumns: '1fr 1fr', width: '100%', gap: 1 }}>
|
||||
<Box display="grid" sx={{ gridTemplateColumns: "1fr 1fr", width: "100%", gap: 1 }}>
|
||||
<Box>
|
||||
<StatsElement sleeve={props.sleeve} />
|
||||
<Box display="grid" sx={{ gridTemplateColumns: '1fr 1fr', width: '100%' }}>
|
||||
<Box display="grid" sx={{ gridTemplateColumns: "1fr 1fr", width: "100%" }}>
|
||||
<Button onClick={() => setStatsOpen(true)}>More Stats</Button>
|
||||
<Button onClick={() => setEarningsOpen(true)}>More Earnings Info</Button>
|
||||
<Tooltip title={player.money < CONSTANTS.TravelCost ? <Typography>Insufficient funds</Typography> : ""}>
|
||||
@ -150,20 +144,22 @@ export function SleeveElem(props: IProps): React.ReactElement {
|
||||
<Button
|
||||
onClick={() => setTravelOpen(true)}
|
||||
disabled={player.money < CONSTANTS.TravelCost}
|
||||
sx={{ width: '100%', height: '100%' }}
|
||||
sx={{ width: "100%", height: "100%" }}
|
||||
>
|
||||
Travel
|
||||
</Button>
|
||||
</span>
|
||||
</Tooltip>
|
||||
<Tooltip
|
||||
title={props.sleeve.shock < 100 ? <Typography>Unlocked when sleeve has fully recovered</Typography> : ""}
|
||||
title={
|
||||
props.sleeve.shock < 100 ? <Typography>Unlocked when sleeve has fully recovered</Typography> : ""
|
||||
}
|
||||
>
|
||||
<span>
|
||||
<Button
|
||||
onClick={() => setAugmentationsOpen(true)}
|
||||
disabled={props.sleeve.shock < 100}
|
||||
sx={{ width: '100%', height: '100%' }}
|
||||
sx={{ width: "100%", height: "100%" }}
|
||||
>
|
||||
Manage Augmentations
|
||||
</Button>
|
||||
@ -175,7 +171,9 @@ export function SleeveElem(props: IProps): React.ReactElement {
|
||||
<EarningsElement sleeve={props.sleeve} />
|
||||
<Box>
|
||||
<TaskSelector player={player} sleeve={props.sleeve} setABC={setABC} />
|
||||
<Button onClick={setTask} sx={{ width: '100%' }}>Set Task</Button>
|
||||
<Button onClick={setTask} sx={{ width: "100%" }}>
|
||||
Set Task
|
||||
</Button>
|
||||
<Typography>{desc}</Typography>
|
||||
<Typography>
|
||||
{props.sleeve.currentTask === SleeveTaskType.Crime &&
|
||||
|
@ -1,11 +1,6 @@
|
||||
import React, { useState, useEffect } from "react";
|
||||
|
||||
import {
|
||||
Box,
|
||||
Typography,
|
||||
Button,
|
||||
Container
|
||||
} from "@mui/material";
|
||||
import { Box, Typography, Button, Container } from "@mui/material";
|
||||
|
||||
import { use } from "../../../ui/Context";
|
||||
|
||||
@ -38,14 +33,16 @@ export function SleeveRoot(): React.ReactElement {
|
||||
<br />
|
||||
<br />
|
||||
</Typography>
|
||||
|
||||
</Container>
|
||||
|
||||
<Button onClick={() => setFAQOpen(true)}>FAQ</Button>
|
||||
<Button href="https://bitburner.readthedocs.io/en/latest/advancedgameplay/sleeves.html#duplicate-sleeves" target="_blank">
|
||||
<Button
|
||||
href="https://bitburner.readthedocs.io/en/latest/advancedgameplay/sleeves.html#duplicate-sleeves"
|
||||
target="_blank"
|
||||
>
|
||||
Wiki Documentation
|
||||
</Button>
|
||||
<Box display="grid" sx={{ gridTemplateColumns: 'repeat(2, 1fr)', mt: 1 }}>
|
||||
<Box display="grid" sx={{ gridTemplateColumns: "repeat(2, 1fr)", mt: 1 }}>
|
||||
{player.sleeves.map((sleeve, i) => (
|
||||
<SleeveElem key={i} rerender={rerender} sleeve={sleeve} />
|
||||
))}
|
||||
|
@ -1,12 +1,6 @@
|
||||
import React from "react";
|
||||
|
||||
import {
|
||||
Typography,
|
||||
Table,
|
||||
TableBody,
|
||||
TableCell,
|
||||
TableRow,
|
||||
} from "@mui/material";
|
||||
import { Typography, Table, TableBody, TableCell, TableRow } from "@mui/material";
|
||||
|
||||
import { numeralWrapper } from "../../../ui/numeralFormat";
|
||||
import { Settings } from "../../../Settings/Settings";
|
||||
@ -28,29 +22,69 @@ export function StatsElement(props: IProps): React.ReactElement {
|
||||
const classes = useStyles();
|
||||
|
||||
return (
|
||||
<Table sx={{ display: 'table', mb: 1, width: '100%' }}>
|
||||
<Table sx={{ display: "table", mb: 1, width: "100%" }}>
|
||||
<TableBody>
|
||||
<StatsRow name="City" color={Settings.theme.primary} data={{ content: props.sleeve.city }} />
|
||||
<StatsRow name="HP" color={Settings.theme.hp}
|
||||
data={{ content: `${numeralWrapper.formatHp(props.sleeve.hp)} / ${numeralWrapper.formatHp(props.sleeve.max_hp)}` }}
|
||||
<StatsRow
|
||||
name="HP"
|
||||
color={Settings.theme.hp}
|
||||
data={{
|
||||
content: `${numeralWrapper.formatHp(props.sleeve.hp)} / ${numeralWrapper.formatHp(props.sleeve.max_hp)}`,
|
||||
}}
|
||||
/>
|
||||
<StatsRow
|
||||
name="Hacking"
|
||||
color={Settings.theme.hack}
|
||||
data={{ level: props.sleeve.hacking, exp: props.sleeve.hacking_exp }}
|
||||
/>
|
||||
<StatsRow
|
||||
name="Strength"
|
||||
color={Settings.theme.combat}
|
||||
data={{ level: props.sleeve.strength, exp: props.sleeve.strength_exp }}
|
||||
/>
|
||||
<StatsRow
|
||||
name="Defense"
|
||||
color={Settings.theme.combat}
|
||||
data={{ level: props.sleeve.defense, exp: props.sleeve.defense_exp }}
|
||||
/>
|
||||
<StatsRow
|
||||
name="Dexterity"
|
||||
color={Settings.theme.combat}
|
||||
data={{ level: props.sleeve.dexterity, exp: props.sleeve.dexterity_exp }}
|
||||
/>
|
||||
<StatsRow
|
||||
name="Agility"
|
||||
color={Settings.theme.combat}
|
||||
data={{ level: props.sleeve.agility, exp: props.sleeve.agility_exp }}
|
||||
/>
|
||||
<StatsRow
|
||||
name="Charisma"
|
||||
color={Settings.theme.cha}
|
||||
data={{ level: props.sleeve.charisma, exp: props.sleeve.charisma_exp }}
|
||||
/>
|
||||
<StatsRow name="Hacking" color={Settings.theme.hack} data={{ level: props.sleeve.hacking, exp: props.sleeve.hacking_exp }} />
|
||||
<StatsRow name="Strength" color={Settings.theme.combat} data={{ level: props.sleeve.strength, exp: props.sleeve.strength_exp }} />
|
||||
<StatsRow name="Defense" color={Settings.theme.combat} data={{ level: props.sleeve.defense, exp: props.sleeve.defense_exp }} />
|
||||
<StatsRow name="Dexterity" color={Settings.theme.combat} data={{ level: props.sleeve.dexterity, exp: props.sleeve.dexterity_exp }} />
|
||||
<StatsRow name="Agility" color={Settings.theme.combat} data={{ level: props.sleeve.agility, exp: props.sleeve.agility_exp }} />
|
||||
<StatsRow name="Charisma" color={Settings.theme.cha} data={{ level: props.sleeve.charisma, exp: props.sleeve.charisma_exp }} />
|
||||
<TableRow>
|
||||
<TableCell classes={{ root: classes.cellNone }}>
|
||||
<br />
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
<StatsRow name="Shock" color={Settings.theme.primary} data={{ content: numeralWrapper.formatSleeveShock(100 - props.sleeve.shock) }} />
|
||||
<StatsRow name="Sync" color={Settings.theme.primary} data={{ content: numeralWrapper.formatSleeveSynchro(props.sleeve.sync) }} />
|
||||
<StatsRow name="Memory" color={Settings.theme.primary} data={{ content: numeralWrapper.formatSleeveMemory(props.sleeve.memory) }} />
|
||||
<StatsRow
|
||||
name="Shock"
|
||||
color={Settings.theme.primary}
|
||||
data={{ content: numeralWrapper.formatSleeveShock(100 - props.sleeve.shock) }}
|
||||
/>
|
||||
<StatsRow
|
||||
name="Sync"
|
||||
color={Settings.theme.primary}
|
||||
data={{ content: numeralWrapper.formatSleeveSynchro(props.sleeve.sync) }}
|
||||
/>
|
||||
<StatsRow
|
||||
name="Memory"
|
||||
color={Settings.theme.primary}
|
||||
data={{ content: numeralWrapper.formatSleeveMemory(props.sleeve.memory) }}
|
||||
/>
|
||||
</TableBody>
|
||||
</Table>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
export function EarningsElement(props: IProps): React.ReactElement {
|
||||
@ -60,7 +94,12 @@ export function EarningsElement(props: IProps): React.ReactElement {
|
||||
let data: any[][] = [];
|
||||
if (props.sleeve.currentTask === SleeveTaskType.Crime) {
|
||||
data = [
|
||||
[`Money`, <><Money money={parseFloat(props.sleeve.currentTaskLocation)} /> (on success)</>],
|
||||
[
|
||||
`Money`,
|
||||
<>
|
||||
<Money money={parseFloat(props.sleeve.currentTaskLocation)} /> (on success)
|
||||
</>,
|
||||
],
|
||||
[`Hacking Exp`, `${numeralWrapper.formatExp(props.sleeve.gainRatesForTask.hack)} (2x on success)`],
|
||||
[`Strength Exp`, `${numeralWrapper.formatExp(props.sleeve.gainRatesForTask.str)} (2x on success)`],
|
||||
[`Defense Exp`, `${numeralWrapper.formatExp(props.sleeve.gainRatesForTask.def)} (2x on success)`],
|
||||
@ -85,13 +124,11 @@ export function EarningsElement(props: IProps): React.ReactElement {
|
||||
}
|
||||
|
||||
return (
|
||||
<Table sx={{ display: 'table', mb: 1, width: '100%', lineHeight: 0 }}>
|
||||
<Table sx={{ display: "table", mb: 1, width: "100%", lineHeight: 0 }}>
|
||||
<TableBody>
|
||||
<TableRow>
|
||||
<TableCell classes={{ root: classes.cellNone }}>
|
||||
<Typography variant='h6'>
|
||||
Earnings
|
||||
</Typography>
|
||||
<Typography variant="h6">Earnings</Typography>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
{data.map(([a, b]) => (
|
||||
@ -106,5 +143,5 @@ export function EarningsElement(props: IProps): React.ReactElement {
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
@ -76,9 +76,9 @@ function possibleFactions(player: IPlayer, sleeve: Sleeve): string[] {
|
||||
}
|
||||
}
|
||||
|
||||
return factions.filter(faction => {
|
||||
return factions.filter((faction) => {
|
||||
const facInfo = Factions[faction].getInfo();
|
||||
return facInfo.offerHackingWork || facInfo.offerFieldWork || facInfo.offerSecurityWork
|
||||
return facInfo.offerHackingWork || facInfo.offerFieldWork || facInfo.offerSecurityWork;
|
||||
});
|
||||
}
|
||||
|
||||
@ -279,7 +279,7 @@ export function TaskSelector(props: IProps): React.ReactElement {
|
||||
|
||||
return (
|
||||
<>
|
||||
<Select onChange={onS0Change} value={s0} sx={{ width: '100%' }}>
|
||||
<Select onChange={onS0Change} value={s0} sx={{ width: "100%" }}>
|
||||
{validActions.map((task) => (
|
||||
<MenuItem key={task} value={task}>
|
||||
{task}
|
||||
@ -288,7 +288,7 @@ export function TaskSelector(props: IProps): React.ReactElement {
|
||||
</Select>
|
||||
{!(details.first.length === 1 && details.first[0] === "------") && (
|
||||
<>
|
||||
<Select onChange={onS1Change} value={s1} sx={{ width: '100%' }}>
|
||||
<Select onChange={onS1Change} value={s1} sx={{ width: "100%" }}>
|
||||
{details.first.map((detail) => (
|
||||
<MenuItem key={detail} value={detail}>
|
||||
{detail}
|
||||
@ -299,7 +299,7 @@ export function TaskSelector(props: IProps): React.ReactElement {
|
||||
)}
|
||||
{!(details2.length === 1 && details2[0] === "------") && (
|
||||
<>
|
||||
<Select onChange={onS2Change} value={s2} sx={{ width: '100%' }}>
|
||||
<Select onChange={onS2Change} value={s2} sx={{ width: "100%" }}>
|
||||
{details2.map((detail) => (
|
||||
<MenuItem key={detail} value={detail}>
|
||||
{detail}
|
||||
|
@ -24,20 +24,22 @@ export function getHackingWorkRepGain(p: IPlayer, f: Faction): number {
|
||||
|
||||
export function getFactionSecurityWorkRepGain(p: IPlayer, f: Faction): number {
|
||||
const t =
|
||||
(0.9 *
|
||||
(p.strength + p.defense + p.dexterity + p.agility +
|
||||
(p.hacking + p.intelligence) * CalculateShareMult()
|
||||
)
|
||||
) / CONSTANTS.MaxSkillLevel / 4.5;
|
||||
(0.9 * (p.strength + p.defense + p.dexterity + p.agility + (p.hacking + p.intelligence) * CalculateShareMult())) /
|
||||
CONSTANTS.MaxSkillLevel /
|
||||
4.5;
|
||||
return t * p.faction_rep_mult * mult(f) * p.getIntelligenceBonus(1);
|
||||
}
|
||||
|
||||
export function getFactionFieldWorkRepGain(p: IPlayer, f: Faction): number {
|
||||
const t =
|
||||
(0.9 *
|
||||
(p.strength + p.defense + p.dexterity + p.agility + p.charisma +
|
||||
(p.hacking + p.intelligence) * CalculateShareMult()
|
||||
)
|
||||
) / CONSTANTS.MaxSkillLevel / 5.5;
|
||||
(p.strength +
|
||||
p.defense +
|
||||
p.dexterity +
|
||||
p.agility +
|
||||
p.charisma +
|
||||
(p.hacking + p.intelligence) * CalculateShareMult())) /
|
||||
CONSTANTS.MaxSkillLevel /
|
||||
5.5;
|
||||
return t * p.faction_rep_mult * mult(f) * p.getIntelligenceBonus(1);
|
||||
}
|
||||
|
@ -13,14 +13,14 @@ export function calculateSkillProgress(exp: number, mult = 1): ISkillProgress {
|
||||
let baseExperience = calculateExp(currentSkill, mult);
|
||||
if (baseExperience < 0) baseExperience = 0;
|
||||
|
||||
let nextExperience = calculateExp(nextSkill, mult)
|
||||
let nextExperience = calculateExp(nextSkill, mult);
|
||||
if (nextExperience < 0) nextExperience = 0;
|
||||
|
||||
const normalize = (value: number): number => ((value - baseExperience) * 100) / (nextExperience - baseExperience);
|
||||
let progress = (nextExperience - baseExperience !== 0) ? normalize(exp) : 99.99;
|
||||
let progress = nextExperience - baseExperience !== 0 ? normalize(exp) : 99.99;
|
||||
|
||||
// Clamp progress: When sleeves are working out, the player gets way too much progress
|
||||
if (progress < 0) progress = 0
|
||||
if (progress < 0) progress = 0;
|
||||
if (progress > 100) progress = 100;
|
||||
|
||||
// Clamp floating point imprecisions
|
||||
@ -37,8 +37,8 @@ export function calculateSkillProgress(exp: number, mult = 1): ISkillProgress {
|
||||
nextExperience,
|
||||
currentExperience,
|
||||
remainingExperience,
|
||||
progress
|
||||
}
|
||||
progress,
|
||||
};
|
||||
}
|
||||
|
||||
export interface ISkillProgress {
|
||||
@ -54,9 +54,13 @@ export interface ISkillProgress {
|
||||
|
||||
export function getEmptySkillProgress(): ISkillProgress {
|
||||
return {
|
||||
currentSkill: 0, nextSkill: 0,
|
||||
baseExperience: 0, experience: 0, nextExperience: 0,
|
||||
currentExperience: 0, remainingExperience: 0,
|
||||
currentSkill: 0,
|
||||
nextSkill: 0,
|
||||
baseExperience: 0,
|
||||
experience: 0,
|
||||
nextExperience: 0,
|
||||
currentExperience: 0,
|
||||
remainingExperience: 0,
|
||||
progress: 0,
|
||||
};
|
||||
}
|
||||
|
@ -1,13 +1,7 @@
|
||||
import React, { useState, useEffect } from "react";
|
||||
import { find } from "lodash";
|
||||
|
||||
import {
|
||||
Box,
|
||||
Typography,
|
||||
Button,
|
||||
Container,
|
||||
Paper
|
||||
} from "@mui/material";
|
||||
import { Box, Typography, Button, Container, Paper } from "@mui/material";
|
||||
import { Check, Lock, Create } from "@mui/icons-material";
|
||||
|
||||
import { use } from "../../ui/Context";
|
||||
@ -26,7 +20,7 @@ export function ProgramsRoot(): React.ReactElement {
|
||||
}
|
||||
|
||||
const programs = [...Object.values(Programs)]
|
||||
.filter(prog => {
|
||||
.filter((prog) => {
|
||||
const create = prog.create;
|
||||
if (create === null) return false;
|
||||
if (prog.name === "b1t_flum3.exe") {
|
||||
@ -38,7 +32,7 @@ export function ProgramsRoot(): React.ReactElement {
|
||||
if (player.hasProgram(a.name)) return 1;
|
||||
if (player.hasProgram(b.name)) return -1;
|
||||
return (a.create?.level ?? 0) - (b.create?.level ?? 0);
|
||||
})
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
programs.forEach((p) => {
|
||||
@ -54,11 +48,11 @@ export function ProgramsRoot(): React.ReactElement {
|
||||
|
||||
const getHackingLevelRemaining = (lvl: number): number => {
|
||||
return Math.ceil(Math.max(lvl - (player.hacking + player.intelligence / 2), 0));
|
||||
}
|
||||
};
|
||||
|
||||
const getProgCompletion = (name: string): number => {
|
||||
const programFile = find(player.getHomeComputer().programs, p => {
|
||||
return (p.startsWith(name) && p.endsWith("%-INC"));
|
||||
const programFile = find(player.getHomeComputer().programs, (p) => {
|
||||
return p.startsWith(name) && p.endsWith("%-INC");
|
||||
});
|
||||
if (!programFile) return -1;
|
||||
|
||||
@ -69,7 +63,7 @@ export function ProgramsRoot(): React.ReactElement {
|
||||
return -1;
|
||||
}
|
||||
return percComplete;
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Container disableGutters maxWidth="lg" sx={{ mx: 0, mb: 10 }}>
|
||||
@ -80,21 +74,26 @@ export function ProgramsRoot(): React.ReactElement {
|
||||
time. Your progress will be saved and you can continue later.
|
||||
</Typography>
|
||||
|
||||
<Box sx={{ display: 'grid', gridTemplateColumns: "repeat(3, 1fr)", my: 1 }}>
|
||||
<Box sx={{ display: "grid", gridTemplateColumns: "repeat(3, 1fr)", my: 1 }}>
|
||||
{programs.map((program) => {
|
||||
const create = program.create;
|
||||
if (create === null) return <></>;
|
||||
const curCompletion = getProgCompletion(program.name);
|
||||
|
||||
return (
|
||||
<Box component={Paper} sx={{ p: 1, opacity: player.hasProgram(program.name) ? 0.75 : 1 }} key={program.name}>
|
||||
<Typography variant="h6" sx={{ display: 'flex', alignItems: 'center', flexWrap: 'wrap' }}>
|
||||
{player.hasProgram(program.name) && <Check sx={{ mr: 1 }} /> ||
|
||||
(create.req(player) && <Create sx={{ mr: 1 }} /> || <Lock sx={{ mr: 1 }} />)}
|
||||
<Box
|
||||
component={Paper}
|
||||
sx={{ p: 1, opacity: player.hasProgram(program.name) ? 0.75 : 1 }}
|
||||
key={program.name}
|
||||
>
|
||||
<Typography variant="h6" sx={{ display: "flex", alignItems: "center", flexWrap: "wrap" }}>
|
||||
{(player.hasProgram(program.name) && <Check sx={{ mr: 1 }} />) ||
|
||||
(create.req(player) && <Create sx={{ mr: 1 }} />) || <Lock sx={{ mr: 1 }} />}
|
||||
{program.name}
|
||||
</Typography>
|
||||
{(!player.hasProgram(program.name) && create.req(player)) && <Button
|
||||
sx={{ my: 1, width: '100%' }}
|
||||
{!player.hasProgram(program.name) && create.req(player) && (
|
||||
<Button
|
||||
sx={{ my: 1, width: "100%" }}
|
||||
onClick={(event) => {
|
||||
if (!event.isTrusted) return;
|
||||
player.startCreateProgramWork(program.name, create.time, create.level);
|
||||
@ -103,18 +102,19 @@ export function ProgramsRoot(): React.ReactElement {
|
||||
}}
|
||||
>
|
||||
Create program
|
||||
</Button>}
|
||||
{(player.hasProgram(program.name) || getHackingLevelRemaining(create.level) === 0) ||
|
||||
</Button>
|
||||
)}
|
||||
{player.hasProgram(program.name) || getHackingLevelRemaining(create.level) === 0 || (
|
||||
<Typography color={Settings.theme.hack}>
|
||||
<b>Unlocks in:</b> {getHackingLevelRemaining(create.level)} hacking levels
|
||||
</Typography>}
|
||||
{(curCompletion !== -1) &&
|
||||
</Typography>
|
||||
)}
|
||||
{curCompletion !== -1 && (
|
||||
<Typography color={Settings.theme.infolight}>
|
||||
<b>Current completion:</b> {curCompletion}%
|
||||
</Typography>}
|
||||
<Typography>
|
||||
{create.tooltip}
|
||||
</Typography>
|
||||
)}
|
||||
<Typography>{create.tooltip}</Typography>
|
||||
</Box>
|
||||
);
|
||||
})}
|
||||
|
@ -17,7 +17,7 @@ import { areImportsEquals } from "../Terminal/DirectoryHelpers";
|
||||
import { IPlayer } from "../PersonObjects/IPlayer";
|
||||
|
||||
export interface RamUsageEntry {
|
||||
type: 'ns' | 'dom' | 'fn' | 'misc';
|
||||
type: "ns" | "dom" | "fn" | "misc";
|
||||
name: string;
|
||||
cost: number;
|
||||
}
|
||||
@ -139,7 +139,9 @@ async function parseOnlyRamCalculate(
|
||||
// Finally, walk the reference map and generate a ram cost. The initial set of keys to scan
|
||||
// are those that start with __SPECIAL_INITIAL_MODULE__.
|
||||
let ram = RamCostConstants.ScriptBaseRamCost;
|
||||
const detailedCosts: RamUsageEntry[] = [{ type: 'misc', name: 'baseCost', cost: RamCostConstants.ScriptBaseRamCost}];
|
||||
const detailedCosts: RamUsageEntry[] = [
|
||||
{ type: "misc", name: "baseCost", cost: RamCostConstants.ScriptBaseRamCost },
|
||||
];
|
||||
const unresolvedRefs = Object.keys(dependencyMap).filter((s) => s.startsWith(initialModule));
|
||||
const resolvedRefs = new Set();
|
||||
while (unresolvedRefs.length > 0) {
|
||||
@ -149,19 +151,19 @@ async function parseOnlyRamCalculate(
|
||||
// Check if this is one of the special keys, and add the appropriate ram cost if so.
|
||||
if (ref === "hacknet" && !resolvedRefs.has("hacknet")) {
|
||||
ram += RamCostConstants.ScriptHacknetNodesRamCost;
|
||||
detailedCosts.push({ type: 'ns', name: 'hacknet', cost: RamCostConstants.ScriptHacknetNodesRamCost});
|
||||
detailedCosts.push({ type: "ns", name: "hacknet", cost: RamCostConstants.ScriptHacknetNodesRamCost });
|
||||
}
|
||||
if (ref === "document" && !resolvedRefs.has("document")) {
|
||||
ram += RamCostConstants.ScriptDomRamCost;
|
||||
detailedCosts.push({ type: 'dom', name: 'document', cost: RamCostConstants.ScriptDomRamCost});
|
||||
detailedCosts.push({ type: "dom", name: "document", cost: RamCostConstants.ScriptDomRamCost });
|
||||
}
|
||||
if (ref === "window" && !resolvedRefs.has("window")) {
|
||||
ram += RamCostConstants.ScriptDomRamCost;
|
||||
detailedCosts.push({ type: 'dom', name: 'window', cost: RamCostConstants.ScriptDomRamCost});
|
||||
detailedCosts.push({ type: "dom", name: "window", cost: RamCostConstants.ScriptDomRamCost });
|
||||
}
|
||||
if (ref === "corporation" && !resolvedRefs.has("corporation")) {
|
||||
ram += RamCostConstants.ScriptCorporationRamCost;
|
||||
detailedCosts.push({ type: 'ns', name: 'corporation', cost: RamCostConstants.ScriptCorporationRamCost});
|
||||
detailedCosts.push({ type: "ns", name: "corporation", cost: RamCostConstants.ScriptCorporationRamCost });
|
||||
}
|
||||
|
||||
resolvedRefs.add(ref);
|
||||
@ -203,7 +205,7 @@ async function parseOnlyRamCalculate(
|
||||
|
||||
// This accounts for namespaces (Bladeburner, CodingCpntract, etc.)
|
||||
let func;
|
||||
let refDetail = 'n/a';
|
||||
let refDetail = "n/a";
|
||||
if (ref in workerScript.env.vars.bladeburner) {
|
||||
func = workerScript.env.vars.bladeburner[ref];
|
||||
refDetail = `bladeburner.${ref}`;
|
||||
@ -234,12 +236,12 @@ async function parseOnlyRamCalculate(
|
||||
}
|
||||
const fnRam = applyFuncRam(func);
|
||||
ram += fnRam;
|
||||
detailedCosts.push({ type: 'fn', name: refDetail, cost: fnRam});
|
||||
detailedCosts.push({ type: "fn", name: refDetail, cost: fnRam });
|
||||
} catch (error) {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
return { cost: ram, entries: detailedCosts.filter(e => e.cost > 0) };
|
||||
return { cost: ram, entries: detailedCosts.filter((e) => e.cost > 0) };
|
||||
} catch (error) {
|
||||
// console.info("parse or eval error: ", error);
|
||||
// This is not unexpected. The user may be editing a script, and it may be in
|
||||
|
4
src/ScriptEditor/NetscriptDefinitions.d.ts
vendored
4
src/ScriptEditor/NetscriptDefinitions.d.ts
vendored
@ -4642,7 +4642,7 @@ export interface NS extends Singularity {
|
||||
* ```
|
||||
* @returns
|
||||
*/
|
||||
sleep(millis: number): Promise<void>;
|
||||
sleep(millis: number): Promise<true>;
|
||||
|
||||
/**
|
||||
* Suspends the script for n milliseconds. Doesn't block with concurrent calls.
|
||||
@ -4652,7 +4652,7 @@ export interface NS extends Singularity {
|
||||
* @param millis - Number of milliseconds to sleep.
|
||||
* @returns
|
||||
*/
|
||||
asleep(millis: number): Promise<void>;
|
||||
asleep(millis: number): Promise<true>;
|
||||
|
||||
/**
|
||||
* Prints one or move values or variables to the script’s logs.
|
||||
|
@ -1,4 +1,4 @@
|
||||
export type WordWrapOptions = 'on' | 'off' | 'bounded' | 'wordWrapColumn';
|
||||
export type WordWrapOptions = "on" | "off" | "bounded" | "wordWrapColumn";
|
||||
export interface Options {
|
||||
theme: string;
|
||||
insertSpaces: boolean;
|
||||
|
@ -794,11 +794,11 @@ export function Root(props: IProps): React.ReactElement {
|
||||
setFilter(event.target.value);
|
||||
}
|
||||
function handleExpandSearch(): void {
|
||||
setFilter("")
|
||||
setSearchExpanded(!searchExpanded)
|
||||
setFilter("");
|
||||
setSearchExpanded(!searchExpanded);
|
||||
}
|
||||
const filteredOpenScripts = Object.values(openScripts).filter(
|
||||
(script) => (script.hostname.includes(filter) || script.fileName.includes(filter))
|
||||
(script) => script.hostname.includes(filter) || script.fileName.includes(filter),
|
||||
);
|
||||
|
||||
// Toolbars are roughly 112px:
|
||||
@ -835,7 +835,7 @@ export function Root(props: IProps): React.ReactElement {
|
||||
}}
|
||||
>
|
||||
<Tooltip title={"Search Open Scripts"}>
|
||||
{searchExpanded ?
|
||||
{searchExpanded ? (
|
||||
<TextField
|
||||
value={filter}
|
||||
onChange={handleFilterChange}
|
||||
@ -843,9 +843,14 @@ export function Root(props: IProps): React.ReactElement {
|
||||
InputProps={{
|
||||
startAdornment: <SearchIcon />,
|
||||
spellCheck: false,
|
||||
endAdornment: <CloseIcon onClick={handleExpandSearch} />
|
||||
endAdornment: <CloseIcon onClick={handleExpandSearch} />,
|
||||
}}
|
||||
/> : <Button onClick={handleExpandSearch} ><SearchIcon /></Button>}
|
||||
/>
|
||||
) : (
|
||||
<Button onClick={handleExpandSearch}>
|
||||
<SearchIcon />
|
||||
</Button>
|
||||
)}
|
||||
</Tooltip>
|
||||
{filteredOpenScripts.map(({ fileName, hostname }, index) => {
|
||||
const iconButtonStyle = {
|
||||
@ -896,7 +901,7 @@ export function Root(props: IProps): React.ReactElement {
|
||||
}}
|
||||
style={{
|
||||
maxWidth: `${tabTextWidth}px`,
|
||||
minHeight: '38.5px',
|
||||
minHeight: "38.5px",
|
||||
overflow: "hidden",
|
||||
...(currentScript?.fileName === filteredOpenScripts[index].fileName
|
||||
? {
|
||||
|
@ -260,7 +260,6 @@ export async function loadThemes(monaco: { editor: any }): Promise<void> {
|
||||
token: "ns",
|
||||
foreground: "FFB86C",
|
||||
fontStyle: "italic",
|
||||
|
||||
},
|
||||
{
|
||||
token: "netscriptfunction",
|
||||
@ -273,7 +272,7 @@ export async function loadThemes(monaco: { editor: any }): Promise<void> {
|
||||
{
|
||||
token: "type.identifier.js",
|
||||
foreground: "7EE9FD",
|
||||
fontStyle: "italic"
|
||||
fontStyle: "italic",
|
||||
},
|
||||
{
|
||||
token: "delimiter.square.js",
|
||||
@ -281,7 +280,7 @@ export async function loadThemes(monaco: { editor: any }): Promise<void> {
|
||||
},
|
||||
{
|
||||
token: "delimiter.parenthesis.js",
|
||||
foreground: "FFD709"
|
||||
foreground: "FFD709",
|
||||
},
|
||||
{
|
||||
token: "delimiter.bracket.js",
|
||||
@ -293,7 +292,7 @@ export async function loadThemes(monaco: { editor: any }): Promise<void> {
|
||||
fontStyle: "italic",
|
||||
},
|
||||
],
|
||||
"colors": {
|
||||
colors: {
|
||||
"editor.foreground": "#F8F8F2",
|
||||
"editor.background": "#282A36",
|
||||
"editorLineNumber.foreground": "#6272A4",
|
||||
@ -402,6 +401,6 @@ export async function loadThemes(monaco: { editor: any }): Promise<void> {
|
||||
"scrollbarSlider.background": "#4E566680",
|
||||
"scrollbarSlider.activeBackground": "#747D9180",
|
||||
"scrollbarSlider.hoverBackground": "#5A637580",
|
||||
}
|
||||
},
|
||||
});
|
||||
}
|
||||
|
@ -105,19 +105,32 @@ export function numCycleForGrowthTransition(server: Server, growth: number, p: I
|
||||
* @param p - Reference to Player object
|
||||
* @returns Number of "growth cycles" needed
|
||||
*/
|
||||
export function numCycleForGrowthCorrected(server: Server, targetMoney: number, startMoney: number, p: IPlayer, cores = 1): number {
|
||||
if (startMoney < 0) { startMoney = 0; } // servers "can't" have less than 0 dollars on them
|
||||
if (targetMoney > server.moneyMax) { targetMoney = server.moneyMax; } // can't grow a server to more than its moneyMax
|
||||
if (targetMoney <= startMoney) { return 0; } // no growth --> no threads
|
||||
export function numCycleForGrowthCorrected(
|
||||
server: Server,
|
||||
targetMoney: number,
|
||||
startMoney: number,
|
||||
p: IPlayer,
|
||||
cores = 1,
|
||||
): number {
|
||||
if (startMoney < 0) {
|
||||
startMoney = 0;
|
||||
} // servers "can't" have less than 0 dollars on them
|
||||
if (targetMoney > server.moneyMax) {
|
||||
targetMoney = server.moneyMax;
|
||||
} // can't grow a server to more than its moneyMax
|
||||
if (targetMoney <= startMoney) {
|
||||
return 0;
|
||||
} // no growth --> no threads
|
||||
|
||||
// exponential base adjusted by security
|
||||
const adjGrowthRate = (1 + (CONSTANTS.ServerBaseGrowthRate - 1) / server.hackDifficulty);
|
||||
const adjGrowthRate = 1 + (CONSTANTS.ServerBaseGrowthRate - 1) / server.hackDifficulty;
|
||||
const exponentialBase = Math.min(adjGrowthRate, CONSTANTS.ServerMaxGrowthRate); // cap growth rate
|
||||
|
||||
// total of all grow thread multipliers
|
||||
const serverGrowthPercentage = server.serverGrowth / 100.0;
|
||||
const coreMultiplier = 1 + ((cores - 1) / 16);
|
||||
const threadMultiplier = serverGrowthPercentage * p.hacking_grow_mult * coreMultiplier * BitNodeMultipliers.ServerGrowthRate;
|
||||
const coreMultiplier = 1 + (cores - 1) / 16;
|
||||
const threadMultiplier =
|
||||
serverGrowthPercentage * p.hacking_grow_mult * coreMultiplier * BitNodeMultipliers.ServerGrowthRate;
|
||||
|
||||
/* To understand what is done below we need to do some math. I hope the explanation is clear enough.
|
||||
* First of, the names will be shortened for ease of manipulation:
|
||||
@ -169,7 +182,7 @@ export function numCycleForGrowthCorrected(server: Server, targetMoney: number,
|
||||
* using Padé rational fraction approximation [2](5)
|
||||
*/
|
||||
const ey = Math.exp(y);
|
||||
w = (ey + 4/3 * ey*ey) / (1 + 7/3 * ey + 5/6 * ey*ey);
|
||||
w = (ey + (4 / 3) * ey * ey) / (1 + (7 / 3) * ey + (5 / 6) * ey * ey);
|
||||
} else {
|
||||
/* obtain initial approximation from rough asymptotic [1](4.18)
|
||||
* w = y [- log y when 0 <= y]
|
||||
@ -207,9 +220,9 @@ export function numCycleForGrowthCorrected(server: Server, targetMoney: number,
|
||||
const bct = bt ** cycles;
|
||||
const opc = startMoney + cycles;
|
||||
const diff = opc * bct - targetMoney;
|
||||
corr = diff / (opc * x + 1.0) / bct
|
||||
corr = diff / (opc * x + 1.0) / bct;
|
||||
cycles -= corr;
|
||||
} while (Math.abs(corr) >= 1)
|
||||
} while (Math.abs(corr) >= 1);
|
||||
/* c is now within +/- 1 of the exact result.
|
||||
* We want the ceiling of the exact result, so the floor if the approximation is above,
|
||||
* the ceiling if the approximation is in the same unit as the exact result,
|
||||
@ -236,9 +249,15 @@ export function numCycleForGrowthCorrected(server: Server, targetMoney: number,
|
||||
* @param p - Reference to Player object
|
||||
* @returns Number of "growth cycles" needed to reverse the described hack
|
||||
*/
|
||||
export function numCycleForGrowthByHackAmt(server: Server, hackProp: number, prehackMoney: number, p: IPlayer, cores = 1): number{
|
||||
export function numCycleForGrowthByHackAmt(
|
||||
server: Server,
|
||||
hackProp: number,
|
||||
prehackMoney: number,
|
||||
p: IPlayer,
|
||||
cores = 1,
|
||||
): number {
|
||||
if (prehackMoney > server.moneyMax) prehackMoney = server.moneyMax;
|
||||
const posthackMoney = Math.floor(prehackMoney * Math.min(1, Math.max(0, (1 - hackProp))));
|
||||
const posthackMoney = Math.floor(prehackMoney * Math.min(1, Math.max(0, 1 - hackProp)));
|
||||
return numCycleForGrowthCorrected(server, prehackMoney, posthackMoney, p, cores);
|
||||
}
|
||||
|
||||
@ -258,7 +277,6 @@ export function numCycleForGrowthByMultiplier(server: Server, growth: number, p:
|
||||
return numCycleForGrowthCorrected(server, targetMoney, startingMoney, p, cores);
|
||||
}
|
||||
|
||||
|
||||
//Applied server growth for a single server. Returns the percentage growth
|
||||
export function processSingleServerGrowth(server: Server, threads: number, p: IPlayer, cores = 1): number {
|
||||
let serverGrowth = calculateServerGrowth(server, threads, p, cores);
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { FactionNames } from '../../Faction/data/FactionNames';
|
||||
import { FactionNames } from "../../Faction/data/FactionNames";
|
||||
// tslint:disable:max-file-line-count
|
||||
|
||||
// This could actually be a JSON file as it should be constant metadata to be imported...
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user