mirror of
https://github.com/bitburner-official/bitburner-src.git
synced 2024-11-23 16:13:49 +01:00
Merge branch 'dev' into add-ns-getRecentScripts
This commit is contained in:
commit
2add4e9112
@ -1,27 +1,26 @@
|
||||
node_modules/
|
||||
dist/
|
||||
input/
|
||||
|
||||
.dist
|
||||
.tmp
|
||||
.package
|
||||
|
||||
assets/
|
||||
css/
|
||||
.build
|
||||
.cypress/
|
||||
cypress/
|
||||
|
||||
dist/
|
||||
input/
|
||||
assets/
|
||||
doc/
|
||||
markdown/
|
||||
netscript_tests/
|
||||
scripts/
|
||||
|
||||
|
||||
test/netscript/
|
||||
|
||||
electron/lib
|
||||
electron/greenworks.js
|
||||
src/ThirdParty/*
|
||||
src/JSInterpreter.js
|
||||
tools/engines-check/
|
||||
|
||||
test/*.bundle.*
|
||||
editor.main.js
|
||||
main.bundle.js
|
||||
webpack.config.js
|
||||
webpack.config-test.js
|
||||
|
100
.github/workflows/bump-version.yml
vendored
Normal file
100
.github/workflows/bump-version.yml
vendored
Normal file
@ -0,0 +1,100 @@
|
||||
name: Bump BitBurner Version
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
version:
|
||||
description: 'Version (format: x.y.z)'
|
||||
required: true
|
||||
versionNumber:
|
||||
description: 'Version Number (for saves migration)'
|
||||
required: true
|
||||
changelog:
|
||||
description: 'Changelog (url that points to RAW markdown)'
|
||||
default: ''
|
||||
buildApp:
|
||||
description: 'Include Application Build'
|
||||
type: boolean
|
||||
default: 'true'
|
||||
required: true
|
||||
buildDoc:
|
||||
description: 'Include Documentation Build'
|
||||
type: boolean
|
||||
default: 'true'
|
||||
required: true
|
||||
prepareRelease:
|
||||
description: 'Prepare Draft Release'
|
||||
type: boolean
|
||||
default: 'true'
|
||||
required: true
|
||||
|
||||
jobs:
|
||||
bumpVersion:
|
||||
name: Bump Version
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: write
|
||||
pull-requests: write
|
||||
steps:
|
||||
- name: Install pandoc dependency
|
||||
run: sudo apt-get install -y pandoc
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v2
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- name: Use Node.js 16.13.1
|
||||
uses: actions/setup-node@v2
|
||||
with:
|
||||
node-version: 16.13.1
|
||||
cache: 'npm'
|
||||
- name: Install NPM dependencies for version updater
|
||||
working-directory: ./tools/bump-version
|
||||
run: npm ci
|
||||
- name: Bump version & update changelogs
|
||||
working-directory: ./tools/bump-version
|
||||
run: |
|
||||
curl ${{ github.event.inputs.changelog }} > changes.md
|
||||
node index.js --version=${{ github.event.inputs.version }} --versionNumber=${{ github.event.inputs.versionNumber }} < changes.md
|
||||
- name: Install NPM dependencies for app
|
||||
if: ${{ github.event.inputs.buildApp == 'true' || github.event.inputs.buildDoc == 'true' }}
|
||||
run: npm ci
|
||||
- name: Build Production App
|
||||
if: ${{ github.event.inputs.buildApp == 'true' }}
|
||||
run: npm run build
|
||||
- name: Build Documentation
|
||||
if: ${{ github.event.inputs.buildDoc == 'true' }}
|
||||
run: npm run doc
|
||||
- name: Commit Files
|
||||
run: |
|
||||
git config --global user.name "GitHub"
|
||||
git config --global user.email "noreply@github.com"
|
||||
git checkout -b bump/v${{ github.event.inputs.version }}
|
||||
git add -A
|
||||
echo "Bump version to v${{ github.event.inputs.version }}" > commitmessage.txt
|
||||
echo "" >> commitmessage.txt
|
||||
cat ./tools/bump-version/changes.md >> commitmessage.txt
|
||||
git commit -F commitmessage.txt
|
||||
git push -u origin bump/v${{ github.event.inputs.version }}
|
||||
- name: Create Pull Request
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
run: |
|
||||
gh pr create \
|
||||
--base "${{ github.ref_name }}" \
|
||||
--head "bump/v${{ github.event.inputs.version }}" \
|
||||
--title "Bump version to v${{ github.event.inputs.version }}" \
|
||||
--body-file ./tools/bump-version/changes.md
|
||||
- name: Prepare release
|
||||
if: ${{ github.event.inputs.prepareRelease == 'true' }}
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
run: |
|
||||
RELEASE_TITLE="$(head -n 1 ./tools/bump-version/changes.md | sed 's/## //')"
|
||||
RELEASE_TITLE="${RELEASE_TITLE:-v${{ github.event.inputs.version }}}"
|
||||
gh release create \
|
||||
v${{ github.event.inputs.version }} \
|
||||
--target dev \
|
||||
--title "$RELEASE_TITLE" \
|
||||
--notes-file ./tools/bump-version/changes.md \
|
||||
--generate-notes \
|
||||
--draft
|
44
.github/workflows/fetch-changes.yml
vendored
Normal file
44
.github/workflows/fetch-changes.yml
vendored
Normal file
@ -0,0 +1,44 @@
|
||||
name: Fetch Merged Changes
|
||||
on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
fromCommit:
|
||||
description: 'From Commit SHA (full-length)'
|
||||
required: true
|
||||
toCommit:
|
||||
description: 'To Commit SHA (full-length, if omitted will use latest)'
|
||||
|
||||
jobs:
|
||||
fetchChangelog:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- name: Use Node v16.13.1
|
||||
uses: actions/setup-node@v2
|
||||
with:
|
||||
node-version: 16.13.1
|
||||
cache: 'npm'
|
||||
- name: Install NPM dependencies
|
||||
working-directory: ./tools/fetch-changelog
|
||||
run: npm ci
|
||||
- name: Fetch Changes from GitHub API
|
||||
working-directory: ./tools/fetch-changelog
|
||||
env:
|
||||
GITHUB_API_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
run: |
|
||||
node index.js \
|
||||
--from=${{ github.event.inputs.fromCommit }} \
|
||||
--to=${{ github.event.inputs.toCommit }} > changes.md
|
||||
echo
|
||||
echo "============================================================"
|
||||
echo
|
||||
cat changes.md
|
||||
echo
|
||||
echo "============================================================"
|
||||
echo
|
||||
echo "You may want to go to https://gist.github.com/ to upload the final changelog"
|
||||
echo "The next step will require an url because we can't easily pass multiline strings to actions"
|
||||
- uses: actions/upload-artifact@v2
|
||||
with:
|
||||
name: bitburner_changelog___DRAFT.md
|
||||
path: ./tools/fetch-changelog/changes.md
|
2
.gitignore
vendored
2
.gitignore
vendored
@ -5,10 +5,10 @@ Netburner.txt
|
||||
/doc/build
|
||||
/node_modules
|
||||
/electron/node_modules
|
||||
/dist/*.map
|
||||
/test/*.map
|
||||
/test/*.bundle.*
|
||||
/test/*.css
|
||||
/input/bitburner.api.json
|
||||
.cypress
|
||||
|
||||
# tmp folder for electron
|
||||
|
@ -6,7 +6,7 @@ Bitburner is a programming-based [incremental game](https://en.wikipedia.org/wik
|
||||
that revolves around hacking and cyberpunk themes.
|
||||
The game can be played at https://danielyxie.github.io/bitburner or installed through [Steam](https://store.steampowered.com/app/1812820/Bitburner/).
|
||||
|
||||
See the [frequently asked questions](./FAQ.md) for more information . To discuss the game or get help, join the [official discord server](https://discord.gg/TFc3hKD)
|
||||
See the [frequently asked questions](./doc/FAQ.md) for more information . To discuss the game or get help, join the [official discord server](https://discord.gg/TFc3hKD)
|
||||
|
||||
# Documentation
|
||||
|
||||
@ -18,13 +18,13 @@ The [in-game documentation](./markdown/bitburner.md) is generated from the [Type
|
||||
Anyone is welcome to contribute to the documentation by editing the [source
|
||||
files](/doc/source) and then making a pull request with your contributions.
|
||||
For further guidance, please refer to the "As A Documentor" section of
|
||||
[CONTRIBUTING](CONTRIBUTING.md).
|
||||
[CONTRIBUTING](./doc/CONTRIBUTING.md).
|
||||
|
||||
# Contribution
|
||||
|
||||
There are many ways to contribute to the game. It can be as simple as fixing
|
||||
a typo, correcting a bug, or improving the UI. For guidance on doing so,
|
||||
please refer to the [CONTRIBUTING](CONTRIBUTING.md) document.
|
||||
please refer to the [CONTRIBUTING](./doc/CONTRIBUTING.md) document.
|
||||
|
||||
You will retain all ownership of the Copyright of any contributions you make,
|
||||
and will have the same rights to use or license your contributions. By
|
||||
|
Before Width: | Height: | Size: 15 KiB After Width: | Height: | Size: 15 KiB |
12
cypress.json
12
cypress.json
@ -1,10 +1,14 @@
|
||||
{
|
||||
"baseUrl": "http://localhost:8000",
|
||||
"fixturesFolder": false,
|
||||
"trashAssetsBeforeRuns": true,
|
||||
"screenshotsFolder": ".cypress/screenshots",
|
||||
"videosFolder": ".cypress/videos",
|
||||
"videoUploadOnPasses": false,
|
||||
"viewportWidth": 1980,
|
||||
"viewportHeight": 1080
|
||||
"viewportHeight": 1080,
|
||||
"fixturesFolder": "test/cypress/fixtures",
|
||||
"integrationFolder": "test/cypress/integration",
|
||||
"pluginsFile": "test/cypress/plugins/index.js",
|
||||
"supportFile": "test/cypress/support/index.js",
|
||||
"screenshotsFolder": ".cypress/screenshots",
|
||||
"videosFolder": ".cypress/videos",
|
||||
"downloadsFolder": ".cypress/downloads"
|
||||
}
|
||||
|
@ -1,9 +0,0 @@
|
||||
export {};
|
||||
|
||||
beforeEach(() => {
|
||||
cy.visit("/");
|
||||
cy.clearLocalStorage();
|
||||
cy.window().then((win) => {
|
||||
win.indexedDB.deleteDatabase("bitburnerSave");
|
||||
});
|
||||
});
|
BIN
dist/favicon.ico
vendored
Normal file
BIN
dist/favicon.ico
vendored
Normal file
Binary file not shown.
After Width: | Height: | Size: 15 KiB |
20
dist/main.bundle.js
vendored
Normal file
20
dist/main.bundle.js
vendored
Normal file
File diff suppressed because one or more lines are too long
1
dist/main.bundle.js.map
vendored
Normal file
1
dist/main.bundle.js.map
vendored
Normal file
File diff suppressed because one or more lines are too long
36
dist/vendor.bundle.js
vendored
36
dist/vendor.bundle.js
vendored
File diff suppressed because one or more lines are too long
1
dist/vendor.bundle.js.map
vendored
Normal file
1
dist/vendor.bundle.js.map
vendored
Normal file
File diff suppressed because one or more lines are too long
@ -182,7 +182,7 @@ To contribute to and view your changes to the BitBurner documentation on [Read T
|
||||
Docs](http://bitburner.readthedocs.io/), you will
|
||||
need to have Python installed, along with [Sphinx](http://www.sphinx-doc.org).
|
||||
|
||||
To make change to the [in-game documentation](./markdown/bitburner.md), you will need to modify the [TypeScript definitions](./src/ScriptEditor/NetscriptDefinitions.d.ts), not the markdown files.
|
||||
To make change to the [in-game documentation](../markdown/bitburner.md), you will need to modify the [TypeScript definitions](../src/ScriptEditor/NetscriptDefinitions.d.ts), not the markdown files.
|
||||
|
||||
We are using [API Extractor](https://api-extractor.com/pages/tsdoc/doc_comment_syntax/) (tsdoc hints) to generate the markdown doc. Make your changes to the TypeScript definitions and then run `npm run doc`.
|
||||
|
@ -30,6 +30,11 @@ from faction to faction.
|
||||
|
||||
List of Factions and their Requirements
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
.. raw:: html
|
||||
|
||||
<details><summary><a>Early Game Factions</a></summary>
|
||||
|
||||
.. _gameplay_factions::
|
||||
|
||||
+---------------------+----------------+-----------------------------------------+-------------------------------+
|
||||
| Early Game | Faction Name | Requirements | Joining this Faction prevents |
|
||||
@ -46,7 +51,18 @@ List of Factions and their Requirements
|
||||
| | | * Total Hacknet RAM of 8 | |
|
||||
| | | * Total Hacknet Cores of 4 | |
|
||||
+---------------------+----------------+-----------------------------------------+-------------------------------+
|
||||
| City Factions | Sector-12 | * Be in Sector-12 | * Chongqing |
|
||||
.. raw:: html
|
||||
|
||||
</details>
|
||||
<details><summary><a>City Factions</a></summary>
|
||||
|
||||
.. _gameplay_factions::
|
||||
|
||||
+---------------------+----------------+-----------------------------------------+-------------------------------+
|
||||
| City Factions | Faction Name | Requirements | Joining this Faction prevents |
|
||||
| | | | you from joining: |
|
||||
+ +----------------+-----------------------------------------+-------------------------------+
|
||||
| | Sector-12 | * Be in Sector-12 | * Chongqing |
|
||||
| | | * $15m | * New Tokyo |
|
||||
| | | | * Ishima |
|
||||
| | | | * Volhaven |
|
||||
@ -74,8 +90,19 @@ List of Factions and their Requirements
|
||||
| | | | * New Tokyo |
|
||||
| | | | * Ishima |
|
||||
+---------------------+----------------+-----------------------------------------+-------------------------------+
|
||||
| Hacking | NiteSec | * Install a backdoor on the avmnite-02h | |
|
||||
| Groups | | server | |
|
||||
.. raw:: html
|
||||
|
||||
</details>
|
||||
<details><summary><a>Hacking Groups</a></summary>
|
||||
|
||||
.. _gameplay_factions::
|
||||
|
||||
+---------------------+----------------+-----------------------------------------+-------------------------------+
|
||||
| Hacking | Faction Name | Requirements | Joining this Faction prevents |
|
||||
| Groups | | | you from joining: |
|
||||
+ +----------------+-----------------------------------------+-------------------------------+
|
||||
| | NiteSec | * Install a backdoor on the avmnite-02h | |
|
||||
| | | server | |
|
||||
| | | | |
|
||||
+ +----------------+-----------------------------------------+-------------------------------+
|
||||
| | The Black Hand | * Install a backdoor on the I.I.I.I | |
|
||||
@ -86,7 +113,18 @@ List of Factions and their Requirements
|
||||
| | | server | |
|
||||
| | | | |
|
||||
+---------------------+----------------+-----------------------------------------+-------------------------------+
|
||||
| Megacorporations | ECorp | * Have 200k reputation with | |
|
||||
.. raw:: html
|
||||
|
||||
</details>
|
||||
<details><summary><a>Megacorporations</a></summary>
|
||||
|
||||
.. _gameplay_factions::
|
||||
|
||||
+---------------------+----------------+-----------------------------------------+-------------------------------+
|
||||
| Megacorporations | Faction Name | Requirements | Joining this Faction prevents |
|
||||
| | | | you from joining: |
|
||||
+ +----------------+-----------------------------------------+-------------------------------+
|
||||
| | ECorp | * Have 200k reputation with | |
|
||||
| | | the Corporation | |
|
||||
+ +----------------+-----------------------------------------+-------------------------------+
|
||||
| | MegaCorp | * Have 200k reputation with | |
|
||||
@ -118,8 +156,19 @@ List of Factions and their Requirements
|
||||
| | | * Install a backdoor on the | |
|
||||
| | | fulcrumassets server | |
|
||||
+---------------------+----------------+-----------------------------------------+-------------------------------+
|
||||
| Criminal | Slum Snakes | * All Combat Stats of 30 | |
|
||||
| Organizations | | * -9 Karma | |
|
||||
.. raw:: html
|
||||
|
||||
</details>
|
||||
<details><summary><a>Criminal Organizations</a></summary>
|
||||
|
||||
.. _gameplay_factions::
|
||||
|
||||
+---------------------+----------------+-----------------------------------------+-------------------------------+
|
||||
| Criminal | Faction Name | Requirements | Joining this Faction prevents |
|
||||
| Organizations | | | you from joining: |
|
||||
+ +----------------+-----------------------------------------+-------------------------------+
|
||||
| | Slum Snakes | * All Combat Stats of 30 | |
|
||||
| | | * -9 Karma | |
|
||||
| | | * $1m | |
|
||||
+ +----------------+-----------------------------------------+-------------------------------+
|
||||
| | Tetrads | * Be in Chongqing, New Tokyo, or Ishima | |
|
||||
@ -150,8 +199,19 @@ List of Factions and their Requirements
|
||||
| | | * -90 Karma | |
|
||||
| | | * Not working for CIA or NSA | |
|
||||
+---------------------+----------------+-----------------------------------------+-------------------------------+
|
||||
| Endgame | The Covenant | * 20 Augmentations | |
|
||||
| Factions | | * $75b | |
|
||||
.. raw:: html
|
||||
|
||||
</details>
|
||||
<details><summary><a>Endgame Factions</a></summary>
|
||||
|
||||
.. _gameplay_factions::
|
||||
|
||||
+---------------------+----------------+-----------------------------------------+-------------------------------+
|
||||
| Endgame | Faction Name | Requirements | Joining this Faction prevents |
|
||||
| Factions | | | you from joining: |
|
||||
+ +----------------+-----------------------------------------+-------------------------------+
|
||||
| | The Covenant | * 20 Augmentations | |
|
||||
| | | * $75b | |
|
||||
| | | * Hacking Level of 850 | |
|
||||
| | | * All Combat Stats of 850 | |
|
||||
+ +----------------+-----------------------------------------+-------------------------------+
|
||||
@ -165,3 +225,6 @@ List of Factions and their Requirements
|
||||
| | | * Hacking Level of 1500 | |
|
||||
| | | * All Combat Stats of 1200 | |
|
||||
+---------------------+----------------+-----------------------------------------+-------------------------------+
|
||||
.. raw:: html
|
||||
|
||||
</details><br>
|
||||
|
@ -23,4 +23,4 @@ Bug
|
||||
---
|
||||
|
||||
Otherwise, the game is probably frozen/stuck due to a bug. To report a bug, follow
|
||||
the guidelines `here <https://github.com/danielyxie/bitburner/blob/master/CONTRIBUTING.md#reporting-bugs>`_.
|
||||
the guidelines `here <https://github.com/danielyxie/bitburner/blob/master/doc/CONTRIBUTING.md#reporting-bugs>`_.
|
||||
|
@ -9,7 +9,7 @@ async function enableAchievementsInterval(window) {
|
||||
// This is backward but the game fills in an array called `document.achievements` and we retrieve it from
|
||||
// here. Hey if it works it works.
|
||||
const steamAchievements = greenworks.getAchievementNames();
|
||||
log.debug(`All Steam achievements ${JSON.stringify(steamAchievements)}`);
|
||||
log.silly(`All Steam achievements ${JSON.stringify(steamAchievements)}`);
|
||||
const playerAchieved = (await Promise.all(steamAchievements.map(checkSteamAchievement))).filter(name => !!name);
|
||||
log.debug(`Player has Steam achievements ${JSON.stringify(playerAchieved)}`);
|
||||
const intervalID = setInterval(async () => {
|
||||
|
@ -12,11 +12,13 @@ async function initialize(win) {
|
||||
window = win;
|
||||
server = http.createServer(async function (req, res) {
|
||||
let body = "";
|
||||
res.setHeader('Content-Type', 'application/json');
|
||||
|
||||
req.on("data", (chunk) => {
|
||||
body += chunk.toString(); // convert Buffer to string
|
||||
});
|
||||
req.on("end", () => {
|
||||
|
||||
req.on("end", async () => {
|
||||
const providedToken = req.headers?.authorization?.replace('Bearer ', '') ?? '';
|
||||
const isValid = providedToken === getAuthenticationToken();
|
||||
if (isValid) {
|
||||
@ -24,8 +26,11 @@ async function initialize(win) {
|
||||
} else {
|
||||
log.log('Invalid authentication token');
|
||||
res.writeHead(401);
|
||||
res.write('Invalid authentication token');
|
||||
res.end();
|
||||
|
||||
res.end(JSON.stringify({
|
||||
success: false,
|
||||
msg: 'Invalid authentication token'
|
||||
}));
|
||||
return;
|
||||
}
|
||||
|
||||
@ -35,17 +40,56 @@ async function initialize(win) {
|
||||
} catch (error) {
|
||||
log.warn(`Invalid body data`);
|
||||
res.writeHead(400);
|
||||
res.write('Invalid body data');
|
||||
res.end();
|
||||
res.end(JSON.stringify({
|
||||
success: false,
|
||||
msg: 'Invalid body data'
|
||||
}));
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (data) {
|
||||
window.webContents.executeJavaScript(`document.saveFile("${data.filename}", "${data.code}")`).then((result) => {
|
||||
res.write(result);
|
||||
res.end();
|
||||
});
|
||||
let result;
|
||||
switch(req.method) {
|
||||
// Request files
|
||||
case "GET":
|
||||
result = await window.webContents.executeJavaScript(`document.getFiles()`);
|
||||
break;
|
||||
|
||||
// Create or update files
|
||||
// Support POST for VScode implementation
|
||||
case "POST":
|
||||
case "PUT":
|
||||
if (!data) {
|
||||
log.warn(`Invalid script update request - No data`);
|
||||
res.writeHead(400);
|
||||
res.end(JSON.stringify({
|
||||
success: false,
|
||||
msg: 'Invalid script update request - No data'
|
||||
}));
|
||||
return;
|
||||
}
|
||||
|
||||
result = await window.webContents.executeJavaScript(`document.saveFile("${data.filename}", "${data.code}")`);
|
||||
break;
|
||||
|
||||
// Delete files
|
||||
case "DELETE":
|
||||
result = await window.webContents.executeJavaScript(`document.deleteFile("${data.filename}")`);
|
||||
break;
|
||||
}
|
||||
|
||||
if (!result.res) {
|
||||
//We've encountered an error
|
||||
res.writeHead(400);
|
||||
log.warn(`Api Server Error`, result.msg);
|
||||
}
|
||||
|
||||
res.end(JSON.stringify({
|
||||
success: result.res,
|
||||
msg: result.msg,
|
||||
data: result.data
|
||||
}));
|
||||
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -8,20 +8,42 @@ const api = require("./api-server");
|
||||
const cp = require("child_process");
|
||||
const path = require("path");
|
||||
const fs = require("fs");
|
||||
const { windowTracker } = require("./windowTracker");
|
||||
const { fileURLToPath } = require("url");
|
||||
|
||||
const debug = process.argv.includes("--debug");
|
||||
|
||||
async function createWindow(killall) {
|
||||
const setStopProcessHandler = global.app_handlers.stopProcess;
|
||||
|
||||
let icon;
|
||||
if (process.platform == 'linux') {
|
||||
icon = path.join(__dirname, 'icon.png');
|
||||
}
|
||||
|
||||
const tracker = windowTracker('main');
|
||||
const window = new BrowserWindow({
|
||||
icon,
|
||||
show: false,
|
||||
backgroundThrottling: false,
|
||||
backgroundColor: "#000000",
|
||||
title: 'Bitburner',
|
||||
x: tracker.state.x,
|
||||
y: tracker.state.y,
|
||||
width: tracker.state.width,
|
||||
height: tracker.state.height,
|
||||
minWidth: 600,
|
||||
minHeight: 400,
|
||||
webPreferences: {
|
||||
nativeWindowOpen: true,
|
||||
preload: path.join(__dirname, 'preload.js'),
|
||||
},
|
||||
});
|
||||
|
||||
setTimeout(() => tracker.track(window), 1000);
|
||||
if (tracker.state.isMaximized) window.maximize();
|
||||
|
||||
window.removeMenu();
|
||||
window.maximize();
|
||||
noScripts = killall ? { query: { noScripts: killall } } : {};
|
||||
window.loadFile("index.html", noScripts);
|
||||
window.show();
|
||||
|
137
electron/main.js
137
electron/main.js
@ -1,12 +1,19 @@
|
||||
/* eslint-disable no-process-exit */
|
||||
/* eslint-disable @typescript-eslint/no-var-requires */
|
||||
const { app, dialog, BrowserWindow } = require("electron");
|
||||
const { app, dialog, BrowserWindow, ipcMain } = require("electron");
|
||||
const log = require("electron-log");
|
||||
const greenworks = require("./greenworks");
|
||||
const api = require("./api-server");
|
||||
const gameWindow = require("./gameWindow");
|
||||
const achievements = require("./achievements");
|
||||
const utils = require("./utils");
|
||||
const storage = require("./storage");
|
||||
const debounce = require("lodash/debounce");
|
||||
const Config = require("electron-config");
|
||||
const config = new Config();
|
||||
|
||||
log.transports.file.level = config.get("file-log-level", "info");
|
||||
log.transports.console.level = config.get("console-log-level", "debug");
|
||||
|
||||
log.catchErrors();
|
||||
log.info(`Started app: ${JSON.stringify(process.argv)}`);
|
||||
@ -30,6 +37,8 @@ try {
|
||||
global.greenworksError = ex.message;
|
||||
}
|
||||
|
||||
let isRestoreDisabled = false;
|
||||
|
||||
function setStopProcessHandler(app, window, enabled) {
|
||||
const closingWindowHandler = async (e) => {
|
||||
// We need to prevent the default closing event to add custom logic
|
||||
@ -41,6 +50,18 @@ function setStopProcessHandler(app, window, enabled) {
|
||||
// Shutdown the http server
|
||||
api.disable();
|
||||
|
||||
// Trigger debounced saves right now before closing
|
||||
try {
|
||||
await saveToDisk.flush();
|
||||
} catch (error) {
|
||||
log.error(error);
|
||||
}
|
||||
try {
|
||||
await saveToCloud.flush();
|
||||
} catch (error) {
|
||||
log.error(error);
|
||||
}
|
||||
|
||||
// Because of a steam limitation, if the player has launched an external browser,
|
||||
// steam will keep displaying the game as "Running" in their UI as long as the browser is up.
|
||||
// So we'll alert the player to close their browser.
|
||||
@ -87,21 +108,106 @@ function setStopProcessHandler(app, window, enabled) {
|
||||
process.exit(0);
|
||||
};
|
||||
|
||||
const receivedGameReadyHandler = async (event, arg) => {
|
||||
if (!window) {
|
||||
log.warn("Window was undefined in game info handler");
|
||||
return;
|
||||
}
|
||||
|
||||
log.debug("Received game information", arg);
|
||||
window.gameInfo = { ...arg };
|
||||
await storage.prepareSaveFolders(window);
|
||||
|
||||
const restoreNewest = config.get("onload-restore-newest", true);
|
||||
if (restoreNewest && !isRestoreDisabled) {
|
||||
try {
|
||||
await storage.restoreIfNewerExists(window)
|
||||
} catch (error) {
|
||||
log.error("Could not restore newer file", error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const receivedDisableRestoreHandler = async (event, arg) => {
|
||||
if (!window) {
|
||||
log.warn("Window was undefined in disable import handler");
|
||||
return;
|
||||
}
|
||||
|
||||
log.debug(`Disabling auto-restore for ${arg.duration}ms.`);
|
||||
isRestoreDisabled = true;
|
||||
setTimeout(() => {
|
||||
isRestoreDisabled = false;
|
||||
log.debug("Re-enabling auto-restore");
|
||||
}, arg.duration);
|
||||
}
|
||||
|
||||
const receivedGameSavedHandler = async (event, arg) => {
|
||||
if (!window) {
|
||||
log.warn("Window was undefined in game saved handler");
|
||||
return;
|
||||
}
|
||||
|
||||
const { save, ...other } = arg;
|
||||
log.silly("Received game saved info", {...other, save: `${save.length} bytes`});
|
||||
|
||||
if (storage.isAutosaveEnabled()) {
|
||||
saveToDisk(save, arg.fileName);
|
||||
}
|
||||
if (storage.isCloudEnabled()) {
|
||||
const minimumPlaytime = 1000 * 60 * 15;
|
||||
const playtime = window.gameInfo.player.playtime;
|
||||
log.silly(window.gameInfo);
|
||||
if (playtime > minimumPlaytime) {
|
||||
saveToCloud(save);
|
||||
} else {
|
||||
log.debug(`Auto-save to cloud disabled for save game under ${minimumPlaytime}ms (${playtime}ms)`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const saveToCloud = debounce(async (save) => {
|
||||
log.debug("Saving to Steam Cloud ...")
|
||||
try {
|
||||
const playerId = window.gameInfo.player.identifier;
|
||||
await storage.pushGameSaveToSteamCloud(save, playerId);
|
||||
log.silly("Saved Game to Steam Cloud");
|
||||
} catch (error) {
|
||||
log.error(error);
|
||||
utils.writeToast(window, "Could not save to Steam Cloud.", "error", 5000);
|
||||
}
|
||||
}, config.get("cloud-save-min-time", 1000 * 60 * 15), { leading: true });
|
||||
|
||||
const saveToDisk = debounce(async (save, fileName) => {
|
||||
log.debug("Saving to Disk ...")
|
||||
try {
|
||||
const file = await storage.saveGameToDisk(window, { save, fileName });
|
||||
log.silly(`Saved Game to '${file.replaceAll('\\', '\\\\')}'`);
|
||||
} catch (error) {
|
||||
log.error(error);
|
||||
utils.writeToast(window, "Could not save to disk", "error", 5000);
|
||||
}
|
||||
}, config.get("disk-save-min-time", 1000 * 60 * 5), { leading: true });
|
||||
|
||||
if (enabled) {
|
||||
log.debug('Adding closing handlers');
|
||||
log.debug("Adding closing handlers");
|
||||
ipcMain.on("push-game-ready", receivedGameReadyHandler);
|
||||
ipcMain.on("push-game-saved", receivedGameSavedHandler);
|
||||
ipcMain.on("push-disable-restore", receivedDisableRestoreHandler)
|
||||
window.on("closed", clearWindowHandler);
|
||||
window.on("close", closingWindowHandler)
|
||||
app.on("window-all-closed", stopProcessHandler);
|
||||
} else {
|
||||
log.debug('Removing closing handlers');
|
||||
log.debug("Removing closing handlers");
|
||||
ipcMain.removeAllListeners();
|
||||
window.removeListener("closed", clearWindowHandler);
|
||||
window.removeListener("close", closingWindowHandler);
|
||||
app.removeListener("window-all-closed", stopProcessHandler);
|
||||
}
|
||||
}
|
||||
|
||||
function startWindow(noScript) {
|
||||
gameWindow.createWindow(noScript);
|
||||
async function startWindow(noScript) {
|
||||
return gameWindow.createWindow(noScript);
|
||||
}
|
||||
|
||||
global.app_handlers = {
|
||||
@ -110,7 +216,7 @@ global.app_handlers = {
|
||||
}
|
||||
|
||||
app.whenReady().then(async () => {
|
||||
log.info('Application is ready!');
|
||||
log.info("Application is ready!");
|
||||
|
||||
if (process.argv.includes("--export-save")) {
|
||||
const window = new BrowserWindow({ show: false });
|
||||
@ -119,15 +225,14 @@ app.whenReady().then(async () => {
|
||||
setStopProcessHandler(app, window, true);
|
||||
await utils.exportSave(window);
|
||||
} else {
|
||||
startWindow(process.argv.includes("--no-scripts"));
|
||||
}
|
||||
|
||||
if (global.greenworksError) {
|
||||
dialog.showMessageBox({
|
||||
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']
|
||||
});
|
||||
const window = await startWindow(process.argv.includes("--no-scripts"));
|
||||
if (global.greenworksError) {
|
||||
await dialog.showMessageBox(window, {
|
||||
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']
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
|
171
electron/menu.js
171
electron/menu.js
@ -1,11 +1,169 @@
|
||||
/* eslint-disable @typescript-eslint/no-var-requires */
|
||||
const { Menu, clipboard, dialog } = require("electron");
|
||||
const { app, Menu, clipboard, dialog, shell } = require("electron");
|
||||
const log = require("electron-log");
|
||||
const Config = require("electron-config");
|
||||
const api = require("./api-server");
|
||||
const utils = require("./utils");
|
||||
const storage = require("./storage");
|
||||
const config = new Config();
|
||||
|
||||
function getMenu(window) {
|
||||
return Menu.buildFromTemplate([
|
||||
{
|
||||
label: "File",
|
||||
submenu: [
|
||||
{
|
||||
label: "Save Game",
|
||||
click: () => window.webContents.send("trigger-save"),
|
||||
},
|
||||
{
|
||||
label: "Export Save",
|
||||
click: () => window.webContents.send("trigger-game-export"),
|
||||
},
|
||||
{
|
||||
label: "Export Scripts",
|
||||
click: async () => window.webContents.send("trigger-scripts-export"),
|
||||
},
|
||||
{
|
||||
type: "separator",
|
||||
},
|
||||
{
|
||||
label: "Load Last Save",
|
||||
click: async () => {
|
||||
try {
|
||||
const saveGame = await storage.loadLastFromDisk(window);
|
||||
window.webContents.send("push-save-request", { save: saveGame });
|
||||
} catch (error) {
|
||||
log.error(error);
|
||||
utils.writeToast(window, "Could not load last save from disk", "error", 5000);
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
label: "Load From File",
|
||||
click: async () => {
|
||||
const defaultPath = await storage.getSaveFolder(window);
|
||||
const result = await dialog.showOpenDialog(window, {
|
||||
title: "Load From File",
|
||||
defaultPath: defaultPath,
|
||||
buttonLabel: "Load",
|
||||
filters: [
|
||||
{ name: "Game Saves", extensions: ["json", "json.gz", "txt"] },
|
||||
{ name: "All", extensions: ["*"] },
|
||||
],
|
||||
properties: [
|
||||
"openFile", "dontAddToRecent",
|
||||
]
|
||||
});
|
||||
if (result.canceled) return;
|
||||
const file = result.filePaths[0];
|
||||
|
||||
try {
|
||||
const saveGame = await storage.loadFileFromDisk(file);
|
||||
window.webContents.send("push-save-request", { save: saveGame });
|
||||
} catch (error) {
|
||||
log.error(error);
|
||||
utils.writeToast(window, "Could not load save from disk", "error", 5000);
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
label: "Load From Steam Cloud",
|
||||
enabled: storage.isCloudEnabled(),
|
||||
click: async () => {
|
||||
try {
|
||||
const saveGame = await storage.getSteamCloudSaveString();
|
||||
await storage.pushSaveGameForImport(window, saveGame, false);
|
||||
} catch (error) {
|
||||
log.error(error);
|
||||
utils.writeToast(window, "Could not load from Steam Cloud", "error", 5000);
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
type: "separator",
|
||||
},
|
||||
{
|
||||
label: "Compress Disk Saves (.gz)",
|
||||
type: "checkbox",
|
||||
checked: storage.isSaveCompressionEnabled(),
|
||||
click: (menuItem) => {
|
||||
storage.setSaveCompressionConfig(menuItem.checked);
|
||||
utils.writeToast(window,
|
||||
`${menuItem.checked ? "Enabled" : "Disabled"} Save Compression`, "info", 5000);
|
||||
refreshMenu(window);
|
||||
},
|
||||
},
|
||||
{
|
||||
label: "Auto-Save to Disk",
|
||||
type: "checkbox",
|
||||
checked: storage.isAutosaveEnabled(),
|
||||
click: (menuItem) => {
|
||||
storage.setAutosaveConfig(menuItem.checked);
|
||||
utils.writeToast(window,
|
||||
`${menuItem.checked ? "Enabled" : "Disabled"} Auto-Save to Disk`, "info", 5000);
|
||||
refreshMenu(window);
|
||||
},
|
||||
},
|
||||
{
|
||||
label: "Auto-Save to Steam Cloud",
|
||||
type: "checkbox",
|
||||
enabled: !global.greenworksError,
|
||||
checked: storage.isCloudEnabled(),
|
||||
click: (menuItem) => {
|
||||
storage.setCloudEnabledConfig(menuItem.checked);
|
||||
utils.writeToast(window,
|
||||
`${menuItem.checked ? "Enabled" : "Disabled"} Auto-Save to Steam Cloud`, "info", 5000);
|
||||
refreshMenu(window);
|
||||
},
|
||||
},
|
||||
{
|
||||
label: "Restore Newest on Load",
|
||||
type: "checkbox",
|
||||
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);
|
||||
refreshMenu(window);
|
||||
},
|
||||
},
|
||||
{
|
||||
type: "separator",
|
||||
},
|
||||
{
|
||||
label: "Open Directory",
|
||||
submenu: [
|
||||
{
|
||||
label: "Open Game Directory",
|
||||
click: () => shell.openPath(app.getAppPath()),
|
||||
},
|
||||
{
|
||||
label: "Open Saves Directory",
|
||||
click: async () => {
|
||||
const path = await storage.getSaveFolder(window);
|
||||
shell.openPath(path);
|
||||
},
|
||||
},
|
||||
{
|
||||
label: "Open Logs Directory",
|
||||
click: () => shell.openPath(app.getPath("logs")),
|
||||
},
|
||||
{
|
||||
label: "Open Data Directory",
|
||||
click: () => shell.openPath(app.getPath("userData")),
|
||||
},
|
||||
]
|
||||
},
|
||||
{
|
||||
type: "separator",
|
||||
},
|
||||
{
|
||||
label: "Quit",
|
||||
click: () => app.quit(),
|
||||
},
|
||||
]
|
||||
},
|
||||
{
|
||||
label: "Edit",
|
||||
submenu: [
|
||||
@ -163,6 +321,17 @@ function getMenu(window) {
|
||||
label: "Activate",
|
||||
click: () => window.webContents.openDevTools(),
|
||||
},
|
||||
{
|
||||
label: "Delete Steam Cloud Data",
|
||||
enabled: !global.greenworksError,
|
||||
click: async () => {
|
||||
try {
|
||||
await storage.deleteCloudFile();
|
||||
} catch (error) {
|
||||
log.error(error);
|
||||
}
|
||||
}
|
||||
}
|
||||
],
|
||||
},
|
||||
]);
|
||||
|
13
electron/package-lock.json
generated
13
electron/package-lock.json
generated
@ -9,7 +9,8 @@
|
||||
"version": "1.0.0",
|
||||
"dependencies": {
|
||||
"electron-config": "^2.0.0",
|
||||
"electron-log": "^4.4.4"
|
||||
"electron-log": "^4.4.4",
|
||||
"lodash": "^4.17.21"
|
||||
}
|
||||
},
|
||||
"node_modules/conf": {
|
||||
@ -104,6 +105,11 @@
|
||||
"node": ">=4"
|
||||
}
|
||||
},
|
||||
"node_modules/lodash": {
|
||||
"version": "4.17.21",
|
||||
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz",
|
||||
"integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg=="
|
||||
},
|
||||
"node_modules/make-dir": {
|
||||
"version": "1.3.0",
|
||||
"resolved": "https://registry.npmjs.org/make-dir/-/make-dir-1.3.0.tgz",
|
||||
@ -259,6 +265,11 @@
|
||||
"path-exists": "^3.0.0"
|
||||
}
|
||||
},
|
||||
"lodash": {
|
||||
"version": "4.17.21",
|
||||
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz",
|
||||
"integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg=="
|
||||
},
|
||||
"make-dir": {
|
||||
"version": "1.3.0",
|
||||
"resolved": "https://registry.npmjs.org/make-dir/-/make-dir-1.3.0.tgz",
|
||||
|
@ -16,6 +16,8 @@
|
||||
"./dist/**/*",
|
||||
"./node_modules/**/*",
|
||||
"./public/**/*",
|
||||
"./src/**",
|
||||
"./lib/**,",
|
||||
"*.js"
|
||||
],
|
||||
"directories": {
|
||||
@ -23,6 +25,7 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"electron-config": "^2.0.0",
|
||||
"electron-log": "^4.4.4"
|
||||
"electron-log": "^4.4.4",
|
||||
"lodash": "^4.17.21"
|
||||
}
|
||||
}
|
||||
|
38
electron/preload.js
Normal file
38
electron/preload.js
Normal file
@ -0,0 +1,38 @@
|
||||
/* eslint-disable @typescript-eslint/no-var-requires */
|
||||
const { ipcRenderer, contextBridge } = require('electron')
|
||||
const log = require("electron-log");
|
||||
|
||||
contextBridge.exposeInMainWorld(
|
||||
"electronBridge", {
|
||||
send: (channel, data) => {
|
||||
log.log("Send on channel " + channel)
|
||||
// whitelist channels
|
||||
let validChannels = [
|
||||
"get-save-data-response",
|
||||
"get-save-info-response",
|
||||
"push-game-saved",
|
||||
"push-game-ready",
|
||||
"push-import-result",
|
||||
"push-disable-restore",
|
||||
];
|
||||
if (validChannels.includes(channel)) {
|
||||
ipcRenderer.send(channel, data);
|
||||
}
|
||||
},
|
||||
receive: (channel, func) => {
|
||||
log.log("Receive on channel " + channel)
|
||||
let validChannels = [
|
||||
"get-save-data-request",
|
||||
"get-save-info-request",
|
||||
"push-save-request",
|
||||
"trigger-save",
|
||||
"trigger-game-export",
|
||||
"trigger-scripts-export",
|
||||
];
|
||||
if (validChannels.includes(channel)) {
|
||||
// Deliberately strip event as it includes `sender`
|
||||
ipcRenderer.on(channel, (event, ...args) => func(...args));
|
||||
}
|
||||
}
|
||||
}
|
||||
);
|
386
electron/storage.js
Normal file
386
electron/storage.js
Normal file
@ -0,0 +1,386 @@
|
||||
/* eslint-disable @typescript-eslint/no-var-requires */
|
||||
const { app, ipcMain } = require("electron");
|
||||
const zlib = require("zlib");
|
||||
const path = require("path");
|
||||
const fs = require("fs/promises");
|
||||
const { promisify } = require("util");
|
||||
const gzip = promisify(zlib.gzip);
|
||||
const gunzip = promisify(zlib.gunzip);
|
||||
|
||||
const greenworks = require("./greenworks");
|
||||
const log = require("electron-log");
|
||||
const flatten = require("lodash/flatten");
|
||||
const Config = require("electron-config");
|
||||
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)));
|
||||
return (await Promise.all(stats)).reduce((accumulator, { size }) => accumulator + size, 0);
|
||||
}
|
||||
|
||||
const getDirFileStats = async (directory) => {
|
||||
const files = await fs.readdir(directory);
|
||||
const stats = files.map((f) => {
|
||||
const file = path.join(directory, f);
|
||||
return fs.stat(file).then((stat) => ({ file, stat }));
|
||||
});
|
||||
const data = (await Promise.all(stats));
|
||||
return data;
|
||||
};
|
||||
|
||||
const getNewestFile = async (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 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")
|
||||
await prepareFolders(rootFolder, currentFolder, backupsFolder);
|
||||
}
|
||||
|
||||
async function prepareFolders(...folders) {
|
||||
for (const folder of folders) {
|
||||
try {
|
||||
// Making sure the folder exists
|
||||
// eslint-disable-next-line no-await-in-loop
|
||||
await fs.stat(folder);
|
||||
} catch (error) {
|
||||
if (error.code === 'ENOENT') {
|
||||
log.warn(`'${folder}' not found, creating it...`);
|
||||
// eslint-disable-next-line no-await-in-loop
|
||||
await fs.mkdir(folder);
|
||||
} else {
|
||||
log.error(error);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function getFolderSizeInBytes(saveFolder) {
|
||||
try {
|
||||
return await dirSize(saveFolder);
|
||||
} catch (error) {
|
||||
log.error(error);
|
||||
}
|
||||
}
|
||||
|
||||
function setAutosaveConfig(value) {
|
||||
config.set("autosave-enabled", value);
|
||||
}
|
||||
|
||||
function isAutosaveEnabled() {
|
||||
return config.get("autosave-enabled", true);
|
||||
}
|
||||
|
||||
function setSaveCompressionConfig(value) {
|
||||
config.set("save-compression-enabled", value);
|
||||
}
|
||||
|
||||
function isSaveCompressionEnabled() {
|
||||
return config.get("save-compression-enabled", true);
|
||||
}
|
||||
|
||||
function setCloudEnabledConfig(value) {
|
||||
config.set("cloud-enabled", value);
|
||||
}
|
||||
|
||||
async function getSaveFolder(window, root = false) {
|
||||
if (root) return path.join(app.getPath("userData"), "/saves");
|
||||
const identifier = window.gameInfo?.player?.identifier ?? "";
|
||||
return path.join(app.getPath("userData"), "/saves", `/${identifier}`);
|
||||
}
|
||||
|
||||
function isCloudEnabled() {
|
||||
// If the Steam API could not be initialized on game start, we'll abort this.
|
||||
if (global.greenworksError) return false;
|
||||
|
||||
// If the user disables it in Steam there's nothing we can do
|
||||
if (!greenworks.isCloudEnabledForUser()) return false;
|
||||
|
||||
// Let's check the config file to see if it's been overriden
|
||||
const enabledInConf = config.get("cloud-enabled", true);
|
||||
if (!enabledInConf) return false;
|
||||
|
||||
const isAppEnabled = greenworks.isCloudEnabled();
|
||||
if (!isAppEnabled) greenworks.enableCloud(true);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
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');
|
||||
const file = greenworks.getFileNameAndSize(0);
|
||||
log.silly(`Found ${nbFiles} files.`)
|
||||
log.silly(`First File: ${file.name} (${file.size} bytes)`);
|
||||
return file.name;
|
||||
}
|
||||
|
||||
function getCloudFile() {
|
||||
const file = getFirstCloudFile();
|
||||
return new Promise((resolve, reject) => {
|
||||
greenworks.readTextFromFile(file, resolve, reject);
|
||||
});
|
||||
}
|
||||
|
||||
function deleteCloudFile() {
|
||||
const file = getFirstCloudFile();
|
||||
return new Promise((resolve, reject) => {
|
||||
greenworks.deleteFile(file, resolve, reject);
|
||||
});
|
||||
}
|
||||
|
||||
async function getSteamCloudQuota() {
|
||||
return new Promise((resolve, reject) => {
|
||||
greenworks.getCloudQuota(resolve, reject)
|
||||
});
|
||||
}
|
||||
|
||||
async function backupSteamDataToDisk(currentPlayerId) {
|
||||
const nbFiles = greenworks.getFileCount();
|
||||
if (nbFiles === 0) return;
|
||||
|
||||
const file = greenworks.getFileNameAndSize(0);
|
||||
const previousPlayerId = file.name.replace(".json.gz", "");
|
||||
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');
|
||||
saveContent = await gzip(buffer);
|
||||
await fs.writeFile(backupFile, saveContent, 'utf8');
|
||||
log.debug(`Saved backup game to '${backupFile}`);
|
||||
}
|
||||
}
|
||||
|
||||
async function pushGameSaveToSteamCloud(base64save, currentPlayerId) {
|
||||
if (!isCloudEnabled) return Promise.reject("Steam Cloud is not Enabled");
|
||||
|
||||
try {
|
||||
backupSteamDataToDisk(currentPlayerId);
|
||||
} catch (error) {
|
||||
log.error(error);
|
||||
}
|
||||
|
||||
const steamSaveName = `${currentPlayerId}.json.gz`;
|
||||
|
||||
// Let's decode the base64 string so GZIP is more efficient.
|
||||
const buffer = Buffer.from(base64save, "base64");
|
||||
const compressedBuffer = await gzip(buffer);
|
||||
// We can't use utf8 for some reason, steamworks is unhappy.
|
||||
const content = compressedBuffer.toString("base64");
|
||||
log.debug(`Uncompressed: ${base64save.length} bytes`);
|
||||
log.debug(`Compressed: ${content.length} bytes`);
|
||||
log.debug(`Saving to Steam Cloud as ${steamSaveName}`);
|
||||
|
||||
try {
|
||||
await saveCloudFile(steamSaveName, content);
|
||||
} catch (error) {
|
||||
log.error(error);
|
||||
}
|
||||
}
|
||||
|
||||
async function getSteamCloudSaveString() {
|
||||
if (!isCloudEnabled()) return Promise.reject("Steam Cloud is not Enabled");
|
||||
log.debug(`Fetching Save in Steam Cloud`);
|
||||
const cloudString = await getCloudFile();
|
||||
const gzippedBase64Buffer = Buffer.from(cloudString, "base64");
|
||||
const uncompressedBuffer = await gunzip(gzippedBase64Buffer);
|
||||
const content = uncompressedBuffer.toString("base64");
|
||||
log.debug(`Compressed: ${cloudString.length} bytes`);
|
||||
log.debug(`Uncompressed: ${content.length} bytes`);
|
||||
return content;
|
||||
}
|
||||
|
||||
async function saveGameToDisk(window, saveData) {
|
||||
const currentFolder = await getSaveFolder(window);
|
||||
let saveFolderSizeBytes = await getFolderSizeInBytes(currentFolder);
|
||||
const maxFolderSizeBytes = config.get("autosave-quota", 1e8); // 100Mb per playerIndentifier
|
||||
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)`)
|
||||
const shouldCompress = isSaveCompressionEnabled();
|
||||
const fileName = saveData.fileName;
|
||||
const file = path.join(currentFolder, fileName + (shouldCompress ? ".gz" : ""));
|
||||
try {
|
||||
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');
|
||||
saveContent = await gzip(buffer);
|
||||
}
|
||||
await fs.writeFile(file, saveContent, 'utf8');
|
||||
log.debug(`Saved Game to '${file}'`);
|
||||
log.debug(`Save Size: ${saveContent.length} bytes`);
|
||||
} catch (error) {
|
||||
log.error(error);
|
||||
}
|
||||
|
||||
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);
|
||||
|
||||
while (saveFolderSizeBytes > maxFolderSizeBytes && oldestFiles.length > 0) {
|
||||
const fileToRemove = oldestFiles.shift();
|
||||
log.debug(`Over Quota -> Removing "${fileToRemove}"`);
|
||||
try {
|
||||
// eslint-disable-next-line no-await-in-loop
|
||||
await fs.unlink(fileToRemove);
|
||||
} catch (error) {
|
||||
log.error(error);
|
||||
}
|
||||
|
||||
// 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)`)
|
||||
}
|
||||
|
||||
return file;
|
||||
}
|
||||
|
||||
async function loadLastFromDisk(window) {
|
||||
const folder = await getSaveFolder(window);
|
||||
const last = await getNewestFile(folder);
|
||||
log.debug(`Last modified file: "${last.file}" (${last.stat.mtime.toLocaleString()})`);
|
||||
return loadFileFromDisk(last.file);
|
||||
}
|
||||
|
||||
async function loadFileFromDisk(path) {
|
||||
const buffer = await fs.readFile(path);
|
||||
let content;
|
||||
if (path.endsWith('.gz')) {
|
||||
const uncompressedBuffer = await gunzip(buffer);
|
||||
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`)
|
||||
}
|
||||
return content;
|
||||
}
|
||||
|
||||
function getSaveInformation(window, save) {
|
||||
return new Promise((resolve) => {
|
||||
ipcMain.once("get-save-info-response", async (event, data) => {
|
||||
resolve(data);
|
||||
});
|
||||
window.webContents.send("get-save-info-request", save);
|
||||
});
|
||||
}
|
||||
|
||||
function getCurrentSave(window) {
|
||||
return new Promise((resolve) => {
|
||||
ipcMain.once('get-save-data-response', (event, data) => {
|
||||
resolve(data);
|
||||
});
|
||||
window.webContents.send('get-save-data-request');
|
||||
});
|
||||
}
|
||||
|
||||
function pushSaveGameForImport(window, save, automatic) {
|
||||
ipcMain.once("push-import-result", async (event, arg) => {
|
||||
log.debug(`Was save imported? ${arg.wasImported ? "Yes" : "No"}`);
|
||||
});
|
||||
window.webContents.send("push-save-request", { save, automatic });
|
||||
}
|
||||
|
||||
async function restoreIfNewerExists(window) {
|
||||
const currentSave = await getCurrentSave(window);
|
||||
const currentData = await getSaveInformation(window, currentSave.save);
|
||||
const steam = {};
|
||||
const disk = {};
|
||||
|
||||
try {
|
||||
steam.save = await getSteamCloudSaveString();
|
||||
steam.data = await getSaveInformation(window, steam.save);
|
||||
} catch (error) {
|
||||
log.error("Could not retrieve steam file");
|
||||
log.debug(error);
|
||||
}
|
||||
|
||||
try {
|
||||
const saves = (await getAllSaves()).
|
||||
sort((a, b) => b.stat.mtime.getTime() - a.stat.mtime.getTime());
|
||||
if (saves.length > 0) {
|
||||
disk.save = await loadFileFromDisk(saves[0].file);
|
||||
disk.data = await getSaveInformation(window, disk.save);
|
||||
}
|
||||
} catch(error) {
|
||||
log.error("Could not retrieve disk file");
|
||||
log.debug(error);
|
||||
}
|
||||
|
||||
const lowPlaytime = 1000 * 60 * 15;
|
||||
let bestMatch;
|
||||
if (!steam.data && !disk.data) {
|
||||
log.info("No data to import");
|
||||
} else {
|
||||
// We'll just compare using the lastSave field for now.
|
||||
if (!steam.data) {
|
||||
log.debug('Best potential save match: Disk');
|
||||
bestMatch = disk;
|
||||
} else if (!disk.data) {
|
||||
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)) {
|
||||
// We want to prioritze steam data if the playtime is very close
|
||||
log.debug('Best potential save match: Steam Cloud');
|
||||
bestMatch = steam;
|
||||
} else {
|
||||
log.debug('Best potential save match: disk');
|
||||
bestMatch = disk;
|
||||
}
|
||||
}
|
||||
if (bestMatch) {
|
||||
if (bestMatch.data.lastSave > currentData.lastSave + 5000) {
|
||||
// We add a few seconds to the currentSave's lastSave to prioritize it
|
||||
log.info("Found newer data than the current's save file");
|
||||
log.silly(bestMatch.data);
|
||||
await pushSaveGameForImport(window, bestMatch.save, true);
|
||||
return true;
|
||||
} else if(bestMatch.data.playtime > currentData.playtime && currentData.playtime < lowPlaytime) {
|
||||
log.info("Found older save, but with more playtime, and current less than 15 mins played");
|
||||
log.silly(bestMatch.data);
|
||||
await pushSaveGameForImport(window, bestMatch.save, true);
|
||||
return true;
|
||||
} else {
|
||||
log.debug("Current save data is the freshest");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
getCurrentSave, getSaveInformation,
|
||||
restoreIfNewerExists, pushSaveGameForImport,
|
||||
pushGameSaveToSteamCloud, getSteamCloudSaveString, getSteamCloudQuota, deleteCloudFile,
|
||||
saveGameToDisk, loadLastFromDisk, loadFileFromDisk,
|
||||
getSaveFolder, prepareSaveFolders, getAllSaves,
|
||||
isCloudEnabled, setCloudEnabledConfig,
|
||||
isAutosaveEnabled, setAutosaveConfig,
|
||||
isSaveCompressionEnabled, setSaveCompressionConfig,
|
||||
};
|
63
electron/windowTracker.js
Normal file
63
electron/windowTracker.js
Normal file
@ -0,0 +1,63 @@
|
||||
/* eslint-disable @typescript-eslint/no-var-requires */
|
||||
const { screen } = require("electron");
|
||||
const log = require("electron-log");
|
||||
const debounce = require("lodash/debounce");
|
||||
const Config = require("electron-config");
|
||||
const config = new Config();
|
||||
|
||||
// https://stackoverflow.com/a/68627253
|
||||
const windowTracker = (windowName) => {
|
||||
let window, windowState;
|
||||
|
||||
const setBounds = () => {
|
||||
// Restore from appConfig
|
||||
if (config.has(`window.${windowName}`)) {
|
||||
windowState = config.get(`window.${windowName}`);
|
||||
return;
|
||||
}
|
||||
|
||||
const size = screen.getPrimaryDisplay().workAreaSize;
|
||||
|
||||
// Default
|
||||
windowState = {
|
||||
x: undefined,
|
||||
y: undefined,
|
||||
width: size.width,
|
||||
height: size.height,
|
||||
isMaximized: true,
|
||||
};
|
||||
};
|
||||
|
||||
const saveState = debounce(() => {
|
||||
if (!windowState.isMaximized) {
|
||||
windowState = window.getBounds();
|
||||
}
|
||||
|
||||
windowState.isMaximized = window.isMaximized();
|
||||
log.silly(`Saving window.${windowName} to configs`);
|
||||
config.set(`window.${windowName}`, windowState);
|
||||
log.silly(windowState);
|
||||
}, 1000);
|
||||
|
||||
const track = (win) => {
|
||||
window = win;
|
||||
['resize', 'move', 'close'].forEach((event) => {
|
||||
win.on(event, saveState);
|
||||
});
|
||||
};
|
||||
|
||||
setBounds();
|
||||
|
||||
return {
|
||||
state: {
|
||||
x: windowState.x,
|
||||
y: windowState.y,
|
||||
width: windowState.width,
|
||||
height: windowState.height,
|
||||
isMaximized: windowState.isMaximized,
|
||||
},
|
||||
track,
|
||||
};
|
||||
};
|
||||
|
||||
module.exports = { windowTracker };
|
@ -73,5 +73,5 @@
|
||||
<link rel="shortcut icon" href="favicon.ico"></head>
|
||||
<body>
|
||||
<div id="root"/>
|
||||
<script type="text/javascript" src="dist/vendor.bundle.js"></script><script type="text/javascript" src="main.bundle.js"></script></body>
|
||||
<script type="text/javascript" src="dist/vendor.bundle.js"></script><script type="text/javascript" src="dist/main.bundle.js"></script></body>
|
||||
</html>
|
||||
|
34502
input/bitburner.api.json
34502
input/bitburner.api.json
File diff suppressed because it is too large
Load Diff
@ -8,4 +8,8 @@ module.exports = {
|
||||
'.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"
|
||||
}
|
||||
};
|
||||
|
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
1912
package-lock.json
generated
1912
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
29
package.json
29
package.json
@ -13,19 +13,10 @@
|
||||
"@emotion/react": "^11.4.1",
|
||||
"@emotion/styled": "^11.3.0",
|
||||
"@material-ui/core": "^4.12.3",
|
||||
"@microsoft/api-documenter": "^7.13.65",
|
||||
"@microsoft/api-extractor": "^7.18.17",
|
||||
"@monaco-editor/react": "^4.2.2",
|
||||
"@mui/icons-material": "^5.0.3",
|
||||
"@mui/material": "^5.0.3",
|
||||
"@mui/styles": "^5.0.1",
|
||||
"@types/acorn": "^4.0.6",
|
||||
"@types/escodegen": "^0.0.7",
|
||||
"@types/numeral": "0.0.25",
|
||||
"@types/react": "^17.0.21",
|
||||
"@types/react-beautiful-dnd": "^13.1.2",
|
||||
"@types/react-dom": "^17.0.9",
|
||||
"@types/react-resizable": "^1.7.3",
|
||||
"acorn": "^8.4.1",
|
||||
"acorn-walk": "^8.1.1",
|
||||
"arg": "^5.0.0",
|
||||
@ -45,7 +36,6 @@
|
||||
"notistack": "^2.0.2",
|
||||
"numeral": "2.0.6",
|
||||
"prop-types": "^15.8.0",
|
||||
"raw-loader": "^4.0.2",
|
||||
"react": "^17.0.2",
|
||||
"react-beautiful-dnd": "^13.1.0",
|
||||
"react-dom": "^17.0.2",
|
||||
@ -59,10 +49,19 @@
|
||||
"@babel/preset-env": "^7.15.0",
|
||||
"@babel/preset-react": "^7.0.0",
|
||||
"@babel/preset-typescript": "^7.15.0",
|
||||
"@microsoft/api-documenter": "^7.13.65",
|
||||
"@microsoft/api-extractor": "^7.18.17",
|
||||
"@pmmmwh/react-refresh-webpack-plugin": "^0.5.1",
|
||||
"@testing-library/cypress": "^8.0.1",
|
||||
"@types/acorn": "^4.0.6",
|
||||
"@types/escodegen": "^0.0.7",
|
||||
"@types/file-saver": "^2.0.3",
|
||||
"@types/lodash": "^4.14.168",
|
||||
"@types/numeral": "0.0.25",
|
||||
"@types/react": "^17.0.21",
|
||||
"@types/react-beautiful-dnd": "^13.1.2",
|
||||
"@types/react-dom": "^17.0.9",
|
||||
"@types/react-resizable": "^1.7.3",
|
||||
"@typescript-eslint/eslint-plugin": "^4.22.0",
|
||||
"@typescript-eslint/parser": "^4.22.0",
|
||||
"babel-jest": "^27.0.6",
|
||||
@ -71,6 +70,7 @@
|
||||
"electron": "^14.0.2",
|
||||
"electron-packager": "^15.4.0",
|
||||
"eslint": "^7.24.0",
|
||||
"file-loader": "^6.2.0",
|
||||
"fork-ts-checker-webpack-plugin": "^6.3.3",
|
||||
"html-webpack-plugin": "^3.2.0",
|
||||
"http-server": "^13.0.1",
|
||||
@ -79,6 +79,7 @@
|
||||
"lodash": "^4.17.21",
|
||||
"mini-css-extract-plugin": "^0.4.1",
|
||||
"prettier": "^2.3.2",
|
||||
"raw-loader": "^4.0.2",
|
||||
"react-refresh": "^0.10.0",
|
||||
"regenerator-runtime": "^0.13.9",
|
||||
"source-map": "^0.7.3",
|
||||
@ -102,7 +103,7 @@
|
||||
"cy:dev": "start-server-and-test start:dev http://localhost:8000 cy:open",
|
||||
"cy:open": "cypress open",
|
||||
"cy:run": "cypress run",
|
||||
"doc": "npx api-extractor run && npx api-documenter markdown",
|
||||
"doc": "npx api-extractor run && npx api-documenter markdown && rm input/bitburner.api.json && rm -r input",
|
||||
"format": "prettier --write .",
|
||||
"start": "http-server -p 8000",
|
||||
"start:dev": "webpack-dev-server --progress --env.devServer --mode development",
|
||||
@ -112,13 +113,17 @@
|
||||
"build:dev": "webpack --mode development",
|
||||
"lint": "eslint --fix . --ext js,jsx,ts,tsx",
|
||||
"lint:report": "eslint --ext js,jsx,ts,tsx .",
|
||||
"preinstall": "node ./scripts/engines-check.js",
|
||||
"preinstall": "node ./tools/engines-check/engines-check.js",
|
||||
"test": "jest",
|
||||
"test:watch": "jest --watch",
|
||||
"watch": "webpack --watch --mode production",
|
||||
"watch:dev": "webpack --watch --mode development",
|
||||
"electron": "sh ./package.sh",
|
||||
"electron:packager": "electron-packager .package bitburner --all --out .build --overwrite --icon .package/icon.png --no-prune",
|
||||
"electron:packager-all": "electron-packager .package bitburner --all --out .build --overwrite --icon .package/icon.png",
|
||||
"electron:packager-win": "electron-packager .package bitburner --platform win32 --arch x64 --out .build --overwrite --icon .package/icon.png",
|
||||
"electron:packager-mac": "electron-packager .package bitburner --platform darwin --arch x64 --out .build --overwrite --icon .package/icon.png",
|
||||
"electron:packager-linux": "electron-packager .package bitburner --platform linux --arch x64 --out .build --overwrite --icon .package/icon.png",
|
||||
"allbuild": "npm run build && npm run electron && git add --all && git commit --amend --no-edit && git push -f -u origin dev"
|
||||
}
|
||||
}
|
||||
|
26
package.sh
26
package.sh
@ -1,30 +1,18 @@
|
||||
#!/bin/sh
|
||||
|
||||
mkdir -p .package/dist/src/ThirdParty || true
|
||||
mkdir -p .package/src/ThirdParty || true
|
||||
mkdir -p .package/node_modules || true
|
||||
# Clear out any files remaining from old builds
|
||||
rm -rf .package
|
||||
|
||||
cp index.html .package
|
||||
mkdir -p .package/dist/
|
||||
cp -r electron/* .package
|
||||
cp -r dist/ext .package/dist
|
||||
cp -r dist/icons .package/dist
|
||||
|
||||
# The css files
|
||||
cp dist/vendor.css .package/dist
|
||||
cp main.css .package/main.css
|
||||
|
||||
# The js files.
|
||||
cp dist/vendor.bundle.js .package/dist/vendor.bundle.js
|
||||
cp main.bundle.js .package/main.bundle.js
|
||||
|
||||
# Source maps
|
||||
cp dist/vendor.bundle.js.map .package/dist/vendor.bundle.js.map
|
||||
cp main.bundle.js.map .package/main.bundle.js.map
|
||||
cp -r dist .package
|
||||
cp index.html .package/index.html
|
||||
|
||||
# Install electron sub-dependencies
|
||||
cd electron
|
||||
npm install
|
||||
cd ..
|
||||
|
||||
BUILD_PLATFORM="${1:-"all"}"
|
||||
# And finally build the app.
|
||||
npm run electron:packager
|
||||
npm run electron:packager-$BUILD_PLATFORM
|
||||
|
6
src/@types/global.d.ts
vendored
6
src/@types/global.d.ts
vendored
@ -1,2 +1,8 @@
|
||||
// Defined by webpack on startup or compilation
|
||||
declare let __COMMIT_HASH__: string;
|
||||
|
||||
// When using file-loader, we'll get a path to the resource
|
||||
declare module "*.png" {
|
||||
const value: string;
|
||||
export default value;
|
||||
}
|
||||
|
@ -2050,7 +2050,7 @@ function initAugmentations(): void {
|
||||
info:
|
||||
"A brain implant carefully assembled around the synapses, which " +
|
||||
"micromanages the activity and levels of various neuroreceptor " +
|
||||
"chemicals and modulates electrical acvitiy to optimize concentration, " +
|
||||
"chemicals and modulates electrical activity to optimize concentration, " +
|
||||
"allowing the user to multitask much more effectively.",
|
||||
stats: (
|
||||
<>
|
||||
|
@ -26,7 +26,7 @@ export function InstalledAugmentations(): React.ReactElement {
|
||||
|
||||
if (Settings.OwnedAugmentationsOrder === OwnedAugmentationsOrderSetting.Alphabetically) {
|
||||
sourceAugs.sort((aug1, aug2) => {
|
||||
return aug1.name <= aug2.name ? -1 : 1;
|
||||
return aug1.name.localeCompare(aug2.name);
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -85,7 +85,7 @@ function BitNodePortal(props: IPortalProps): React.ReactElement {
|
||||
</Typography>
|
||||
}
|
||||
>
|
||||
<span onClick={() => setPortalOpen(true)} className={cssClass}>
|
||||
<span onClick={() => setPortalOpen(true)} className={cssClass} aria-label={`enter-bitnode-${bitNode.number.toString()}`}>
|
||||
<b>O</b>
|
||||
</span>
|
||||
</Tooltip>
|
||||
|
@ -14,6 +14,7 @@ import Typography from "@mui/material/Typography";
|
||||
import TextField from "@mui/material/TextField";
|
||||
|
||||
const MAX_BET = 100e6;
|
||||
export const DECK_COUNT = 5; // 5-deck multideck
|
||||
|
||||
enum Result {
|
||||
Pending = "",
|
||||
@ -45,7 +46,7 @@ export class Blackjack extends Game<Props, State> {
|
||||
constructor(props: Props) {
|
||||
super(props);
|
||||
|
||||
this.deck = new Deck(5); // 5-deck multideck
|
||||
this.deck = new Deck(DECK_COUNT);
|
||||
|
||||
const initialBet = 1e6;
|
||||
|
||||
|
@ -40,13 +40,13 @@ const strategies: {
|
||||
} = {
|
||||
Red: {
|
||||
match: (n: number): boolean => {
|
||||
if (n === 0) return false;
|
||||
return redNumbers.includes(n);
|
||||
},
|
||||
payout: 1,
|
||||
},
|
||||
Black: {
|
||||
match: (n: number): boolean => {
|
||||
if (n === 0) return false;
|
||||
return !redNumbers.includes(n);
|
||||
},
|
||||
payout: 1,
|
||||
@ -118,12 +118,6 @@ export function Roulette(props: IProps): React.ReactElement {
|
||||
const [status, setStatus] = useState<string | JSX.Element>("waiting");
|
||||
const [n, setN] = useState(0);
|
||||
const [lock, setLock] = useState(true);
|
||||
const [strategy, setStrategy] = useState<Strategy>({
|
||||
payout: 0,
|
||||
match: (): boolean => {
|
||||
return false;
|
||||
},
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
const i = window.setInterval(step, 50);
|
||||
@ -156,13 +150,12 @@ export function Roulette(props: IProps): React.ReactElement {
|
||||
return `${n}${color}`;
|
||||
}
|
||||
|
||||
function play(s: Strategy): void {
|
||||
function play(strategy: Strategy): void {
|
||||
if (reachedLimit(props.p)) return;
|
||||
|
||||
setCanPlay(false);
|
||||
setLock(false);
|
||||
setStatus("playing");
|
||||
setStrategy(s);
|
||||
|
||||
setTimeout(() => {
|
||||
let n = Math.floor(rng.random() * 37);
|
||||
|
@ -159,18 +159,18 @@ 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) % symbols.length;
|
||||
copy[i] = (copy[i] - 1 >= 0) ? copy[i] - 1 : symbols.length - 1;
|
||||
stoppedOne = true;
|
||||
}
|
||||
|
||||
setIndex(copy);
|
||||
|
||||
if (stoppedOne && copy.every((e, i) => e === locks[i])) {
|
||||
checkWinnings();
|
||||
checkWinnings(getTable(copy, symbols));
|
||||
}
|
||||
}
|
||||
|
||||
function getTable(): string[][] {
|
||||
function getTable(index:number[], symbols:string[]): string[][] {
|
||||
return [
|
||||
[
|
||||
symbols[(index[0] + symbols.length - 1) % symbols.length],
|
||||
@ -209,8 +209,7 @@ export function SlotMachine(props: IProps): React.ReactElement {
|
||||
]);
|
||||
}
|
||||
|
||||
function checkWinnings(): void {
|
||||
const t = getTable();
|
||||
function checkWinnings(t:string[][]): void {
|
||||
const getPaylineData = function (payline: number[][]): string[] {
|
||||
const data = [];
|
||||
for (const point of payline) {
|
||||
@ -267,7 +266,7 @@ export function SlotMachine(props: IProps): React.ReactElement {
|
||||
setInvestment(investment);
|
||||
}
|
||||
|
||||
const t = getTable();
|
||||
const t = getTable(index, symbols);
|
||||
// prettier-ignore
|
||||
return (
|
||||
<>
|
||||
@ -288,7 +287,7 @@ export function SlotMachine(props: IProps): React.ReactElement {
|
||||
disabled={!canPlay}
|
||||
>Spin!</Button>)}}
|
||||
/>
|
||||
|
||||
|
||||
<Typography variant="h4">{status}</Typography>
|
||||
<Typography>Pay lines</Typography>
|
||||
|
||||
|
@ -26,6 +26,7 @@ import Table from "@mui/material/Table";
|
||||
import TableBody from "@mui/material/TableBody";
|
||||
import TableRow from "@mui/material/TableRow";
|
||||
import { TableCell } from "../../ui/React/Table";
|
||||
import { Box } from "@mui/material";
|
||||
|
||||
interface IProps {
|
||||
office: OfficeSpace;
|
||||
@ -430,51 +431,46 @@ export function IndustryOffice(props: IProps): React.ReactElement {
|
||||
<Typography>
|
||||
Size: {props.office.employees.length} / {props.office.size} employees
|
||||
</Typography>
|
||||
<Tooltip title={<Typography>Automatically hires an employee and gives him/her a random name</Typography>}>
|
||||
<span>
|
||||
<Button disabled={props.office.atCapacity()} onClick={autohireEmployeeButtonOnClick}>
|
||||
Hire Employee
|
||||
</Button>
|
||||
</span>
|
||||
</Tooltip>
|
||||
<br />
|
||||
<Tooltip title={<Typography>Upgrade the office's size so that it can hold more employees!</Typography>}>
|
||||
<span>
|
||||
<Button disabled={corp.funds < 0} onClick={() => setUpgradeOfficeSizeOpen(true)}>
|
||||
Upgrade size
|
||||
</Button>
|
||||
</span>
|
||||
</Tooltip>
|
||||
<UpgradeOfficeSizeModal
|
||||
rerender={props.rerender}
|
||||
office={props.office}
|
||||
open={upgradeOfficeSizeOpen}
|
||||
onClose={() => setUpgradeOfficeSizeOpen(false)}
|
||||
/>
|
||||
|
||||
{!division.hasResearch("AutoPartyManager") && (
|
||||
<>
|
||||
<Tooltip
|
||||
title={<Typography>Throw an office party to increase your employee's morale and happiness</Typography>}
|
||||
>
|
||||
<span>
|
||||
<Button disabled={corp.funds < 0} onClick={() => setThrowPartyOpen(true)}>
|
||||
Throw Party
|
||||
</Button>
|
||||
</span>
|
||||
<Box sx={{ display: 'grid', gridTemplateColumns: '1fr', width: 'fit-content' }}>
|
||||
<Box sx={{ gridTemplateColumns: 'repeat(3, 1fr)' }}>
|
||||
<Tooltip title={<Typography>Automatically hires an employee and gives him/her a random name</Typography>}>
|
||||
<Button disabled={props.office.atCapacity()} onClick={autohireEmployeeButtonOnClick}>
|
||||
Hire Employee
|
||||
</Button>
|
||||
</Tooltip>
|
||||
<ThrowPartyModal
|
||||
<Tooltip title={<Typography>Upgrade the office's size so that it can hold more employees!</Typography>}>
|
||||
<Button disabled={corp.funds < 0} onClick={() => setUpgradeOfficeSizeOpen(true)}>
|
||||
Upgrade size
|
||||
</Button>
|
||||
</Tooltip>
|
||||
<UpgradeOfficeSizeModal
|
||||
rerender={props.rerender}
|
||||
office={props.office}
|
||||
open={throwPartyOpen}
|
||||
onClose={() => setThrowPartyOpen(false)}
|
||||
open={upgradeOfficeSizeOpen}
|
||||
onClose={() => setUpgradeOfficeSizeOpen(false)}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
|
||||
<br />
|
||||
{!division.hasResearch("AutoPartyManager") && (
|
||||
<>
|
||||
<Tooltip
|
||||
title={<Typography>Throw an office party to increase your employee's morale and happiness</Typography>}
|
||||
>
|
||||
<Button disabled={corp.funds < 0} onClick={() => setThrowPartyOpen(true)}>
|
||||
Throw Party
|
||||
</Button>
|
||||
</Tooltip>
|
||||
<ThrowPartyModal
|
||||
rerender={props.rerender}
|
||||
office={props.office}
|
||||
open={throwPartyOpen}
|
||||
onClose={() => setThrowPartyOpen(false)}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
|
||||
<SwitchButton manualMode={employeeManualAssignMode} switchMode={setEmployeeManualAssignMode} />
|
||||
</Box>
|
||||
<SwitchButton manualMode={employeeManualAssignMode} switchMode={setEmployeeManualAssignMode} />
|
||||
</Box>
|
||||
{employeeManualAssignMode ? (
|
||||
<ManualManagement rerender={props.rerender} office={props.office} />
|
||||
) : (
|
||||
|
@ -139,13 +139,13 @@ function WarehouseRoot(props: IProps): React.ReactElement {
|
||||
{numeralWrapper.formatBigNumber(props.warehouse.size)}
|
||||
</Typography>
|
||||
</Tooltip>
|
||||
|
||||
<Button disabled={!canAffordUpgrade} onClick={upgradeWarehouseOnClick}>
|
||||
Upgrade Warehouse Size -
|
||||
<MoneyCost money={sizeUpgradeCost} corp={corp} />
|
||||
</Button>
|
||||
</Box>
|
||||
|
||||
<Button disabled={!canAffordUpgrade} onClick={upgradeWarehouseOnClick}>
|
||||
Upgrade Warehouse Size -
|
||||
<MoneyCost money={sizeUpgradeCost} corp={corp} />
|
||||
</Button>
|
||||
|
||||
<Typography>This industry uses the following equation for its production: </Typography>
|
||||
<br />
|
||||
<Typography>
|
||||
|
@ -112,7 +112,7 @@ export function MaterialElem(props: IMaterialProps): React.ReactElement {
|
||||
|
||||
return (
|
||||
<Paper>
|
||||
<Box display="flex">
|
||||
<Box sx={{ display: 'grid', gridTemplateColumns: '2fr 1fr', m: '5px' }}>
|
||||
<Box>
|
||||
<Tooltip
|
||||
title={
|
||||
@ -149,11 +149,10 @@ export function MaterialElem(props: IMaterialProps): React.ReactElement {
|
||||
</Tooltip>
|
||||
</Box>
|
||||
|
||||
<Box>
|
||||
<Box sx={{ "& button": { width: '100%' } }}>
|
||||
<Tooltip
|
||||
title={tutorial ? <Typography>Purchase your required materials to get production started!</Typography> : ""}
|
||||
>
|
||||
<span>
|
||||
<Button
|
||||
color={tutorial ? "error" : "primary"}
|
||||
onClick={() => setPurchaseMaterialOpen(true)}
|
||||
@ -161,7 +160,6 @@ export function MaterialElem(props: IMaterialProps): React.ReactElement {
|
||||
>
|
||||
{purchaseButtonText}
|
||||
</Button>
|
||||
</span>
|
||||
</Tooltip>
|
||||
<PurchaseMaterialModal
|
||||
mat={mat}
|
||||
@ -177,7 +175,6 @@ export function MaterialElem(props: IMaterialProps): React.ReactElement {
|
||||
<ExportModal mat={mat} open={exportOpen} onClose={() => setExportOpen(false)} />
|
||||
</>
|
||||
)}
|
||||
<br />
|
||||
|
||||
<Button
|
||||
color={division.prodMats.includes(props.mat.name) && !mat.sllman[0] ? "error" : "primary"}
|
||||
|
@ -89,19 +89,21 @@ export function Overview({ rerender }: IProps): React.ReactElement {
|
||||
<StatsTable rows={multRows} />
|
||||
<br />
|
||||
<BonusTime />
|
||||
<Tooltip
|
||||
title={
|
||||
<Typography>
|
||||
Get a copy of and read 'The Complete Handbook for Creating a Successful Corporation.' This is a .lit file
|
||||
that guides you through the beginning of setting up a Corporation and provides some tips/pointers for
|
||||
helping you get started with managing it.
|
||||
</Typography>
|
||||
}
|
||||
>
|
||||
<Button onClick={() => corp.getStarterGuide(player)}>Getting Started Guide</Button>
|
||||
</Tooltip>
|
||||
{corp.public ? <PublicButtons rerender={rerender} /> : <PrivateButtons rerender={rerender} />}
|
||||
<BribeButton />
|
||||
<Box sx={{ display: 'grid', gridTemplateColumns: 'repeat(3, 1fr)', width: 'fit-content' }}>
|
||||
<Tooltip
|
||||
title={
|
||||
<Typography>
|
||||
Get a copy of and read 'The Complete Handbook for Creating a Successful Corporation.' This is a .lit file
|
||||
that guides you through the beginning of setting up a Corporation and provides some tips/pointers for
|
||||
helping you get started with managing it.
|
||||
</Typography>
|
||||
}
|
||||
>
|
||||
<Button onClick={() => corp.getStarterGuide(player)}>Getting Started Guide</Button>
|
||||
</Tooltip>
|
||||
{corp.public ? <PublicButtons rerender={rerender} /> : <PrivateButtons rerender={rerender} />}
|
||||
<BribeButton />
|
||||
</Box>
|
||||
<br />
|
||||
<Upgrades rerender={rerender} />
|
||||
</>
|
||||
@ -125,11 +127,9 @@ function PrivateButtons({ rerender }: IPrivateButtonsProps): React.ReactElement
|
||||
return (
|
||||
<>
|
||||
<Tooltip title={<Typography>{findInvestorsTooltip}</Typography>}>
|
||||
<span>
|
||||
<Button disabled={!fundingAvailable} onClick={() => setFindInvestorsopen(true)}>
|
||||
Find Investors
|
||||
</Button>
|
||||
</span>
|
||||
<Button disabled={!fundingAvailable} onClick={() => setFindInvestorsopen(true)}>
|
||||
Find Investors
|
||||
</Button>
|
||||
</Tooltip>
|
||||
<Tooltip
|
||||
title={
|
||||
@ -143,7 +143,6 @@ function PrivateButtons({ rerender }: IPrivateButtonsProps): React.ReactElement
|
||||
</Tooltip>
|
||||
<FindInvestorsModal open={findInvestorsopen} onClose={() => setFindInvestorsopen(false)} rerender={rerender} />
|
||||
<GoPublicModal open={goPublicopen} onClose={() => setGoPublicopen(false)} rerender={rerender} />
|
||||
<br />
|
||||
</>
|
||||
);
|
||||
}
|
||||
@ -201,8 +200,8 @@ function PublicButtons({ rerender }: IPublicButtonsProps): React.ReactElement {
|
||||
const sellSharesTooltip = sellSharesOnCd
|
||||
? "Cannot sell shares for " + corp.convertCooldownToString(corp.shareSaleCooldown)
|
||||
: "Sell your shares in the company. The money earned from selling your " +
|
||||
"shares goes into your personal account, not the Corporation's. " +
|
||||
"This is one of the only ways to profit from your business venture.";
|
||||
"shares goes into your personal account, not the Corporation's. " +
|
||||
"This is one of the only ways to profit from your business venture.";
|
||||
|
||||
const issueNewSharesOnCd = corp.issueNewSharesCooldown > 0;
|
||||
const issueNewSharesTooltip = issueNewSharesOnCd
|
||||
@ -212,28 +211,21 @@ function PublicButtons({ rerender }: IPublicButtonsProps): React.ReactElement {
|
||||
return (
|
||||
<>
|
||||
<Tooltip title={<Typography>{sellSharesTooltip}</Typography>}>
|
||||
<span>
|
||||
<Button disabled={sellSharesOnCd} onClick={() => setSellSharesOpen(true)}>
|
||||
Sell Shares
|
||||
</Button>
|
||||
</span>
|
||||
<Button disabled={sellSharesOnCd} onClick={() => setSellSharesOpen(true)}>
|
||||
Sell Shares
|
||||
</Button>
|
||||
</Tooltip>
|
||||
<SellSharesModal open={sellSharesOpen} onClose={() => setSellSharesOpen(false)} rerender={rerender} />
|
||||
<Tooltip title={<Typography>Buy back shares you that previously issued or sold at market price.</Typography>}>
|
||||
<span>
|
||||
<Button disabled={corp.issuedShares < 1} onClick={() => setBuybackSharesOpen(true)}>
|
||||
Buyback shares
|
||||
</Button>
|
||||
</span>
|
||||
<Button disabled={corp.issuedShares < 1} onClick={() => setBuybackSharesOpen(true)}>
|
||||
Buyback shares
|
||||
</Button>
|
||||
</Tooltip>
|
||||
<BuybackSharesModal open={buybackSharesOpen} onClose={() => setBuybackSharesOpen(false)} rerender={rerender} />
|
||||
<br />
|
||||
<Tooltip title={<Typography>{issueNewSharesTooltip}</Typography>}>
|
||||
<span>
|
||||
<Button disabled={issueNewSharesOnCd} onClick={() => setIssueNewSharesOpen(true)}>
|
||||
Issue New Shares
|
||||
</Button>
|
||||
</span>
|
||||
<Button disabled={issueNewSharesOnCd} onClick={() => setIssueNewSharesOpen(true)}>
|
||||
Issue New Shares
|
||||
</Button>
|
||||
</Tooltip>
|
||||
<IssueNewSharesModal open={issueNewSharesOpen} onClose={() => setIssueNewSharesOpen(false)} />
|
||||
<Tooltip
|
||||
@ -242,7 +234,6 @@ function PublicButtons({ rerender }: IPublicButtonsProps): React.ReactElement {
|
||||
<Button onClick={() => setIssueDividendsOpen(true)}>Issue Dividends</Button>
|
||||
</Tooltip>
|
||||
<IssueDividendsModal open={issueDividendsOpen} onClose={() => setIssueDividendsOpen(false)} />
|
||||
<br />
|
||||
</>
|
||||
);
|
||||
}
|
||||
@ -269,11 +260,9 @@ function BribeButton(): React.ReactElement {
|
||||
: "Your Corporation is not powerful enough to bribe Faction leaders"
|
||||
}
|
||||
>
|
||||
<span>
|
||||
<Button disabled={!canBribe} onClick={openBribe}>
|
||||
Bribe Factions
|
||||
</Button>
|
||||
</span>
|
||||
<Button disabled={!canBribe} onClick={openBribe}>
|
||||
Bribe Factions
|
||||
</Button>
|
||||
</Tooltip>
|
||||
<BribeFactionModal open={open} onClose={() => setOpen(false)} />
|
||||
</>
|
||||
|
@ -81,7 +81,7 @@ export function ProductElem(props: IProductProps): React.ReactElement {
|
||||
);
|
||||
} else if (product.sCost) {
|
||||
if (isString(product.sCost)) {
|
||||
const sCost = (product.sCost as string).replace(/MP/g, product.pCost + "");
|
||||
const sCost = (product.sCost as string).replace(/MP/g, product.pCost + product.rat / product.mku + "");
|
||||
sellButtonText = (
|
||||
<>
|
||||
{sellButtonText} @ <Money money={eval(sCost)} />
|
||||
|
@ -6,17 +6,18 @@ import { IIndustry } from "../IIndustry";
|
||||
import { Research } from "../Actions";
|
||||
import { Node } from "../ResearchTree";
|
||||
import { ResearchMap } from "../ResearchMap";
|
||||
import { Settings } from "../../Settings/Settings";
|
||||
import { dialogBoxCreate } from "../../ui/React/DialogBox";
|
||||
import Typography from "@mui/material/Typography";
|
||||
import Tooltip from "@mui/material/Tooltip";
|
||||
import Button from "@mui/material/Button";
|
||||
import Box from "@mui/material/Box";
|
||||
|
||||
import ListItemButton from "@mui/material/ListItemButton";
|
||||
import ListItemText from "@mui/material/ListItemText";
|
||||
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';
|
||||
|
||||
interface INodeProps {
|
||||
n: Node | null;
|
||||
division: IIndustry;
|
||||
@ -42,8 +43,8 @@ function Upgrade({ n, division }: INodeProps): React.ReactElement {
|
||||
|
||||
dialogBoxCreate(
|
||||
`Researched ${n.text}. It may take a market cycle ` +
|
||||
`(~${CorporationConstants.SecsPerMarketCycle} seconds) before the effects of ` +
|
||||
`the Research apply.`,
|
||||
`(~${CorporationConstants.SecsPerMarketCycle} seconds) before the effects of ` +
|
||||
`the Research apply.`,
|
||||
);
|
||||
}
|
||||
|
||||
@ -52,8 +53,8 @@ function Upgrade({ n, division }: INodeProps): React.ReactElement {
|
||||
color = "info";
|
||||
}
|
||||
|
||||
const but = (
|
||||
<Box>
|
||||
const wrapInTooltip = (ele: React.ReactElement): React.ReactElement => {
|
||||
return (
|
||||
<Tooltip
|
||||
title={
|
||||
<Typography>
|
||||
@ -63,12 +64,22 @@ function Upgrade({ n, division }: INodeProps): React.ReactElement {
|
||||
</Typography>
|
||||
}
|
||||
>
|
||||
{ele}
|
||||
</Tooltip>
|
||||
)
|
||||
}
|
||||
|
||||
const but = (
|
||||
<Box>
|
||||
{wrapInTooltip(
|
||||
<span>
|
||||
<Button color={color} disabled={disabled && !n.researched} onClick={research}>
|
||||
{n.text}
|
||||
<Button color={color} disabled={disabled && !n.researched} onClick={research}
|
||||
style={{ width: '100%', textAlign: 'left', justifyContent: 'unset' }}
|
||||
>
|
||||
{n.researched && (<CheckIcon sx={{ mr: 1 }} />)}{n.text}
|
||||
</Button>
|
||||
</span>
|
||||
</Tooltip>
|
||||
)}
|
||||
</Box>
|
||||
);
|
||||
|
||||
@ -76,15 +87,25 @@ function Upgrade({ n, division }: INodeProps): React.ReactElement {
|
||||
|
||||
return (
|
||||
<Box>
|
||||
<Box display="flex">
|
||||
{but}
|
||||
<ListItemButton onClick={() => setOpen((old) => !old)}>
|
||||
<ListItemText />
|
||||
<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}
|
||||
</Button>
|
||||
</span>
|
||||
)}
|
||||
<Button onClick={() => setOpen((old) => !old)} sx={{ borderColor: Settings.theme.button, minWidth: 'fit-content' }}>
|
||||
{open ? <ExpandLess color="primary" /> : <ExpandMore color="primary" />}
|
||||
</ListItemButton>
|
||||
</Button>
|
||||
</Box>
|
||||
<Collapse in={open} unmountOnExit>
|
||||
<Box m={4}>
|
||||
<Box m={1}>
|
||||
{n.children.map((m) => (
|
||||
<Upgrade key={m.text} division={division} n={m} />
|
||||
))}
|
||||
@ -108,7 +129,7 @@ export function ResearchModal(props: IProps): React.ReactElement {
|
||||
return (
|
||||
<Modal open={props.open} onClose={props.onClose}>
|
||||
<Upgrade division={props.industry} n={researchTree.root} />
|
||||
<Typography>
|
||||
<Typography sx={{ mt: 1 }}>
|
||||
Research points: {props.industry.sciResearch.qty.toFixed(3)}
|
||||
<br />
|
||||
Multipliers from research:
|
||||
|
@ -8,6 +8,7 @@ import ExpandMoreIcon from "@mui/icons-material/ExpandMore";
|
||||
import Button from "@mui/material/Button";
|
||||
import Typography from "@mui/material/Typography";
|
||||
import { IPlayer } from "../../PersonObjects/IPlayer";
|
||||
import { Adjuster } from "./Adjuster";
|
||||
|
||||
interface IProps {
|
||||
player: IPlayer;
|
||||
@ -38,6 +39,12 @@ export function Sleeves(props: IProps): React.ReactElement {
|
||||
}
|
||||
}
|
||||
|
||||
function sleeveSetStoredCycles(cycles: number): void {
|
||||
for (let i = 0; i < props.player.sleeves.length; ++i) {
|
||||
props.player.sleeves[i].storedCycles = cycles;
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<Accordion TransitionProps={{ unmountOnExit: true }}>
|
||||
<AccordionSummary expandIcon={<ExpandMoreIcon />}>
|
||||
@ -68,6 +75,18 @@ export function Sleeves(props: IProps): React.ReactElement {
|
||||
<Button onClick={sleeveSyncClearAll}>Clear all</Button>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td colSpan={3}>
|
||||
<Adjuster
|
||||
label="Stored Cycles"
|
||||
placeholder="cycles"
|
||||
tons={() => sleeveSetStoredCycles(10000000)}
|
||||
add={sleeveSetStoredCycles}
|
||||
subtract={sleeveSetStoredCycles}
|
||||
reset={() => sleeveSetStoredCycles(0)}
|
||||
/>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</AccordionDetails>
|
||||
|
211
src/Electron.tsx
211
src/Electron.tsx
@ -1,11 +1,18 @@
|
||||
import { Player } from "./Player";
|
||||
import { Router } from "./ui/GameRoot";
|
||||
import { isScriptFilename } from "./Script/isScriptFilename";
|
||||
import { Script } from "./Script/Script";
|
||||
import { removeLeadingSlash } from "./Terminal/DirectoryHelpers";
|
||||
import { Terminal } from "./Terminal";
|
||||
import { SnackbarEvents } from "./ui/React/Snackbar";
|
||||
import { IMap } from "./types";
|
||||
import { IMap, IReturnStatus } from "./types";
|
||||
import { GetServer } from "./Server/AllServers";
|
||||
import { resolve } from "cypress/types/bluebird";
|
||||
import { ImportPlayerData, SaveData, saveObject } from "./SaveObject";
|
||||
import { Settings } from "./Settings/Settings";
|
||||
import { exportScripts } from "./Terminal/commands/download";
|
||||
import { CONSTANTS } from "./Constants";
|
||||
import { hash } from "./hash/hash";
|
||||
|
||||
export function initElectron(): void {
|
||||
const userAgent = navigator.userAgent.toLowerCase();
|
||||
@ -14,36 +21,81 @@ export function initElectron(): void {
|
||||
(document as any).achievements = [];
|
||||
initWebserver();
|
||||
initAppNotifier();
|
||||
initSaveFunctions();
|
||||
initElectronBridge();
|
||||
}
|
||||
}
|
||||
|
||||
function initWebserver(): void {
|
||||
(document as any).saveFile = function (filename: string, code: string): string {
|
||||
interface IReturnWebStatus extends IReturnStatus {
|
||||
data?: {
|
||||
[propName: string]: any;
|
||||
};
|
||||
}
|
||||
function normalizeFileName(filename: string): string {
|
||||
filename = filename.replace(/\/\/+/g, "/");
|
||||
filename = removeLeadingSlash(filename);
|
||||
if (filename.includes("/")) {
|
||||
filename = "/" + removeLeadingSlash(filename);
|
||||
}
|
||||
return filename;
|
||||
}
|
||||
|
||||
(document as any).getFiles = function (): IReturnWebStatus {
|
||||
const home = GetServer("home");
|
||||
if (home === null) {
|
||||
return {
|
||||
res: false,
|
||||
msg: "Home server does not exist.",
|
||||
};
|
||||
}
|
||||
return {
|
||||
res: true,
|
||||
data: {
|
||||
files: home.scripts.map((script) => ({
|
||||
filename: script.filename,
|
||||
code: script.code,
|
||||
ramUsage: script.ramUsage,
|
||||
})),
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
(document as any).deleteFile = function (filename: string): IReturnWebStatus {
|
||||
filename = normalizeFileName(filename);
|
||||
const home = GetServer("home");
|
||||
if (home === null) {
|
||||
return {
|
||||
res: false,
|
||||
msg: "Home server does not exist.",
|
||||
};
|
||||
}
|
||||
return home.removeFile(filename);
|
||||
};
|
||||
|
||||
(document as any).saveFile = function (filename: string, code: string): IReturnWebStatus {
|
||||
filename = normalizeFileName(filename);
|
||||
|
||||
code = Buffer.from(code, "base64").toString();
|
||||
const home = GetServer("home");
|
||||
if (home === null) return "'home' server not found.";
|
||||
if (isScriptFilename(filename)) {
|
||||
//If the current script already exists on the server, overwrite it
|
||||
for (let i = 0; i < home.scripts.length; i++) {
|
||||
if (filename == home.scripts[i].filename) {
|
||||
home.scripts[i].saveScript(Player, filename, code, "home", home.scripts);
|
||||
return "written";
|
||||
}
|
||||
}
|
||||
|
||||
//If the current script does NOT exist, create a new one
|
||||
const script = new Script();
|
||||
script.saveScript(Player, filename, code, "home", home.scripts);
|
||||
home.scripts.push(script);
|
||||
return "written";
|
||||
if (home === null) {
|
||||
return {
|
||||
res: false,
|
||||
msg: "Home server does not exist.",
|
||||
};
|
||||
}
|
||||
|
||||
return "not a script file";
|
||||
const { success, overwritten } = home.writeToScriptFile(Player, filename, code);
|
||||
let script;
|
||||
if (success) {
|
||||
script = home.getScript(filename);
|
||||
}
|
||||
return {
|
||||
res: success,
|
||||
data: {
|
||||
overwritten,
|
||||
ramUsage: script?.ramUsage,
|
||||
},
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
@ -67,6 +119,123 @@ function initAppNotifier(): void {
|
||||
};
|
||||
|
||||
// Will be consumud by the electron wrapper.
|
||||
// @ts-ignore
|
||||
window.appNotifier = funcs;
|
||||
(window as any).appNotifier = funcs;
|
||||
}
|
||||
|
||||
function initSaveFunctions(): void {
|
||||
const funcs = {
|
||||
triggerSave: (): Promise<void> => saveObject.saveGame(true),
|
||||
triggerGameExport: (): void => {
|
||||
try {
|
||||
saveObject.exportGame();
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
SnackbarEvents.emit("Could not export game.", "error", 2000);
|
||||
}
|
||||
},
|
||||
triggerScriptsExport: (): void => exportScripts("*", Player.getHomeComputer()),
|
||||
getSaveData: (): { save: string; fileName: string } => {
|
||||
return {
|
||||
save: saveObject.getSaveString(Settings.ExcludeRunningScriptsFromSave),
|
||||
fileName: saveObject.getSaveFileName(),
|
||||
};
|
||||
},
|
||||
getSaveInfo: async (base64save: string): Promise<ImportPlayerData | undefined> => {
|
||||
try {
|
||||
const data = await saveObject.getImportDataFromString(base64save);
|
||||
return data.playerData;
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
return;
|
||||
}
|
||||
},
|
||||
pushSaveData: (base64save: string, automatic = false): void => Router.toImportSave(base64save, automatic),
|
||||
};
|
||||
|
||||
// Will be consumud by the electron wrapper.
|
||||
(window as any).appSaveFns = funcs;
|
||||
}
|
||||
|
||||
function initElectronBridge(): void {
|
||||
const bridge = (window as any).electronBridge as any;
|
||||
if (!bridge) return;
|
||||
|
||||
bridge.receive("get-save-data-request", () => {
|
||||
const data = (window as any).appSaveFns.getSaveData();
|
||||
bridge.send("get-save-data-response", data);
|
||||
});
|
||||
bridge.receive("get-save-info-request", async (save: string) => {
|
||||
const data = await (window as any).appSaveFns.getSaveInfo(save);
|
||||
bridge.send("get-save-info-response", data);
|
||||
});
|
||||
bridge.receive("push-save-request", ({ save, automatic = false }: { save: string; automatic: boolean }) => {
|
||||
(window as any).appSaveFns.pushSaveData(save, automatic);
|
||||
});
|
||||
bridge.receive("trigger-save", () => {
|
||||
return (window as any).appSaveFns
|
||||
.triggerSave()
|
||||
.then(() => {
|
||||
bridge.send("save-completed");
|
||||
})
|
||||
.catch((error: any) => {
|
||||
console.log(error);
|
||||
SnackbarEvents.emit("Could not save game.", "error", 2000);
|
||||
});
|
||||
});
|
||||
bridge.receive("trigger-game-export", () => {
|
||||
try {
|
||||
(window as any).appSaveFns.triggerGameExport();
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
SnackbarEvents.emit("Could not export game.", "error", 2000);
|
||||
}
|
||||
});
|
||||
bridge.receive("trigger-scripts-export", () => {
|
||||
try {
|
||||
(window as any).appSaveFns.triggerScriptsExport();
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
SnackbarEvents.emit("Could not export scripts.", "error", 2000);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
export function pushGameSaved(data: SaveData): void {
|
||||
const bridge = (window as any).electronBridge as any;
|
||||
if (!bridge) return;
|
||||
|
||||
bridge.send("push-game-saved", data);
|
||||
}
|
||||
|
||||
export function pushGameReady(): void {
|
||||
const bridge = (window as any).electronBridge as any;
|
||||
if (!bridge) return;
|
||||
|
||||
// Send basic information to the electron wrapper
|
||||
bridge.send("push-game-ready", {
|
||||
player: {
|
||||
identifier: Player.identifier,
|
||||
playtime: Player.totalPlaytime,
|
||||
lastSave: Player.lastSave,
|
||||
},
|
||||
game: {
|
||||
version: CONSTANTS.VersionString,
|
||||
hash: hash(),
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
export function pushImportResult(wasImported: boolean): void {
|
||||
const bridge = (window as any).electronBridge as any;
|
||||
if (!bridge) return;
|
||||
|
||||
bridge.send("push-import-result", { wasImported });
|
||||
pushDisableRestore();
|
||||
}
|
||||
|
||||
export function pushDisableRestore(): void {
|
||||
const bridge = (window as any).electronBridge as any;
|
||||
if (!bridge) return;
|
||||
|
||||
bridge.send("push-disable-restore", { duration: 1000 * 60 });
|
||||
}
|
||||
|
@ -77,7 +77,7 @@ export function DonateOption(props: IProps): React.ReactElement {
|
||||
}
|
||||
|
||||
return (
|
||||
<Paper sx={{ my: 1, p: 1, width: "100%" }}>
|
||||
<Paper sx={{ my: 1, p: 1 }}>
|
||||
<Status />
|
||||
{props.disabled ? (
|
||||
<Typography>
|
||||
|
@ -1,17 +1,17 @@
|
||||
import React, { useState, useEffect } from "react";
|
||||
import Box from "@mui/material/Box";
|
||||
import Button from "@mui/material/Button";
|
||||
import Container from "@mui/material/Container";
|
||||
import Paper from "@mui/material/Paper";
|
||||
import TableBody from "@mui/material/TableBody";
|
||||
import TableRow from "@mui/material/TableRow";
|
||||
import Typography from "@mui/material/Typography";
|
||||
import React, { useEffect, useState } from "react";
|
||||
import { IPlayer } from "../../PersonObjects/IPlayer";
|
||||
import { Table, TableCell } from "../../ui/React/Table";
|
||||
import { IRouter } from "../../ui/Router";
|
||||
import { Factions } from "../Factions";
|
||||
import { Faction } from "../Faction";
|
||||
import { joinFaction } from "../FactionHelpers";
|
||||
|
||||
import Typography from "@mui/material/Typography";
|
||||
import Box from "@mui/material/Box";
|
||||
import Link from "@mui/material/Link";
|
||||
import Button from "@mui/material/Button";
|
||||
import TableBody from "@mui/material/TableBody";
|
||||
import { Table, TableCell } from "../../ui/React/Table";
|
||||
import TableRow from "@mui/material/TableRow";
|
||||
import { Factions } from "../Factions";
|
||||
|
||||
export const InvitationsSeen: string[] = [];
|
||||
|
||||
@ -48,42 +48,67 @@ export function FactionsRoot(props: IProps): React.ReactElement {
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<Container disableGutters maxWidth="md" sx={{ mx: 0, mb: 10 }}>
|
||||
<Typography variant="h4">Factions</Typography>
|
||||
<Typography>Lists all factions you have joined</Typography>
|
||||
<br />
|
||||
<Box display="flex" flexDirection="column">
|
||||
{props.player.factions.map((faction: string) => (
|
||||
<Link key={faction} variant="h6" onClick={() => openFaction(Factions[faction])}>
|
||||
{faction}
|
||||
</Link>
|
||||
))}
|
||||
</Box>
|
||||
<br />
|
||||
{props.player.factionInvitations.length > 0 && (
|
||||
<>
|
||||
<Typography variant="h5" color="primary">
|
||||
Outstanding Faction Invitations
|
||||
</Typography>
|
||||
<Typography>
|
||||
Lists factions you have been invited to. You can accept these faction invitations at any time.
|
||||
</Typography>
|
||||
<Table size="small" padding="none">
|
||||
<Typography mb={4}>
|
||||
Throughout the game you may receive invitations from factions. There are many different factions, and each
|
||||
faction has different criteria for determining its potential members. Joining a faction and furthering its cause
|
||||
is crucial to progressing in the game and unlocking endgame content.
|
||||
</Typography>
|
||||
|
||||
<Typography variant="h5" color="primary" mt={2} mb={1}>
|
||||
Factions you have joined:
|
||||
</Typography>
|
||||
{(props.player.factions.length > 0 && (
|
||||
<Paper sx={{ my: 1, p: 1, pb: 0, display: "inline-block" }}>
|
||||
<Table padding="none">
|
||||
<TableBody>
|
||||
{props.player.factionInvitations.map((faction: string) => (
|
||||
{props.player.factions.map((faction: string) => (
|
||||
<TableRow key={faction}>
|
||||
<TableCell>
|
||||
<Typography noWrap>{faction}</Typography>
|
||||
<Typography noWrap mb={1}>
|
||||
{faction}
|
||||
</Typography>
|
||||
</TableCell>
|
||||
<TableCell align="right">
|
||||
<Button onClick={(e) => acceptInvitation(e, faction)}>Join!</Button>
|
||||
<Box ml={1} mb={1}>
|
||||
<Button onClick={() => openFaction(Factions[faction])}>Details</Button>
|
||||
</Box>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
</Paper>
|
||||
)) || <Typography>You haven't joined any factions.</Typography>}
|
||||
<Typography variant="h5" color="primary" mt={4} mb={1}>
|
||||
Outstanding Faction Invitations
|
||||
</Typography>
|
||||
<Typography mb={1}>
|
||||
Factions you have been invited to. You can accept these faction invitations at any time:
|
||||
</Typography>
|
||||
{(props.player.factionInvitations.length > 0 && (
|
||||
<Paper sx={{ my: 1, mb: 4, p: 1, pb: 0, display: "inline-block" }}>
|
||||
<Table padding="none">
|
||||
<TableBody>
|
||||
{props.player.factionInvitations.map((faction: string) => (
|
||||
<TableRow key={faction}>
|
||||
<TableCell>
|
||||
<Typography noWrap mb={1}>
|
||||
{faction}
|
||||
</Typography>
|
||||
</TableCell>
|
||||
<TableCell align="right">
|
||||
<Box ml={1} mb={1}>
|
||||
<Button onClick={(e) => acceptInvitation(e, faction)}>Join!</Button>
|
||||
</Box>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</Paper>
|
||||
)) || <Typography>You have no outstanding faction invites.</Typography>}
|
||||
</Container>
|
||||
);
|
||||
}
|
||||
|
@ -19,7 +19,7 @@ type IProps = {
|
||||
export function Option(props: IProps): React.ReactElement {
|
||||
return (
|
||||
<Box>
|
||||
<Paper sx={{ my: 1, p: 1, width: "100%" }}>
|
||||
<Paper sx={{ my: 1, p: 1 }}>
|
||||
<Button onClick={props.onClick}>{props.buttonText}</Button>
|
||||
<Typography>{props.infoText}</Typography>
|
||||
</Paper>
|
||||
|
@ -31,7 +31,7 @@ export function AscensionModal(props: IProps): React.ReactElement {
|
||||
props.onAscend();
|
||||
const res = gang.ascendMember(props.member);
|
||||
dialogBoxCreate(
|
||||
<Typography>
|
||||
<>
|
||||
You ascended {props.member.name}!<br />
|
||||
<br />
|
||||
Your gang lost {numeralWrapper.formatRespect(res.respect)} respect.
|
||||
@ -51,7 +51,7 @@ export function AscensionModal(props: IProps): React.ReactElement {
|
||||
<br />
|
||||
Charisma: x{numeralWrapper.format(res.cha, "0.000")}
|
||||
<br />
|
||||
</Typography>,
|
||||
</>
|
||||
);
|
||||
props.onClose();
|
||||
}
|
||||
|
@ -2,20 +2,27 @@
|
||||
* React Component for the popup that manages gang members upgrades
|
||||
*/
|
||||
import React, { useState } from "react";
|
||||
import { formatNumber } from "../../utils/StringHelperFunctions";
|
||||
import { numeralWrapper } from "../../ui/numeralFormat";
|
||||
import { GangMemberUpgrades } from "../GangMemberUpgrades";
|
||||
import { GangMemberUpgrade } from "../GangMemberUpgrade";
|
||||
import { Money } from "../../ui/React/Money";
|
||||
import { useGang } from "./Context";
|
||||
import { GangMember } from "../GangMember";
|
||||
import { UpgradeType } from "../data/upgrades";
|
||||
import { use } from "../../ui/Context";
|
||||
import { generateTableRow } from "./GangMemberStats";
|
||||
|
||||
import Typography from "@mui/material/Typography";
|
||||
import Button from "@mui/material/Button";
|
||||
import Tooltip from "@mui/material/Tooltip";
|
||||
import Box from "@mui/material/Box";
|
||||
import Paper from "@mui/material/Paper";
|
||||
import Select, { SelectChangeEvent } from "@mui/material/Select";
|
||||
import { MenuItem, Table, TableBody, TextField } from "@mui/material";
|
||||
import SearchIcon from "@mui/icons-material/Search";
|
||||
|
||||
import { numeralWrapper } from "../../ui/numeralFormat";
|
||||
import { GangMemberUpgrades } from "../GangMemberUpgrades";
|
||||
import { GangMemberUpgrade } from "../GangMemberUpgrade";
|
||||
import { Money } from "../../ui/React/Money";
|
||||
import { GangMember } from "../GangMember";
|
||||
import { UpgradeType } from "../data/upgrades";
|
||||
import { use } from "../../ui/Context";
|
||||
import { Settings } from "../../Settings/Settings";
|
||||
import { characterOverviewStyles as useStyles } from "../../ui/React/CharacterOverview";
|
||||
|
||||
interface INextRevealProps {
|
||||
upgrades: string[];
|
||||
@ -46,12 +53,10 @@ function NextReveal(props: INextRevealProps): React.ReactElement {
|
||||
function PurchasedUpgrade({ upgName }: { upgName: string }): React.ReactElement {
|
||||
const upg = GangMemberUpgrades[upgName];
|
||||
return (
|
||||
<Paper sx={{ mx: 1, p: 1 }}>
|
||||
<Box display="flex">
|
||||
<Tooltip title={<Typography dangerouslySetInnerHTML={{ __html: upg.desc }} />}>
|
||||
<Typography>{upg.name}</Typography>
|
||||
</Tooltip>
|
||||
</Box>
|
||||
<Paper sx={{ p: 1 }}>
|
||||
<Tooltip title={<Typography dangerouslySetInnerHTML={{ __html: upg.desc }} />}>
|
||||
<Typography>{upg.name}</Typography>
|
||||
</Tooltip>
|
||||
</Paper>
|
||||
);
|
||||
}
|
||||
@ -72,8 +77,8 @@ function UpgradeButton(props: IUpgradeButtonProps): React.ReactElement {
|
||||
return (
|
||||
<Tooltip title={<Typography dangerouslySetInnerHTML={{ __html: props.upg.desc }} />}>
|
||||
<span>
|
||||
<Typography>{props.upg.name}</Typography>
|
||||
<Button onClick={onClick}>
|
||||
<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>
|
||||
@ -86,12 +91,16 @@ interface IPanelProps {
|
||||
}
|
||||
|
||||
function GangMemberUpgradePanel(props: IPanelProps): React.ReactElement {
|
||||
const classes = useStyles();
|
||||
const gang = useGang();
|
||||
const player = use.Player();
|
||||
const setRerender = useState(false)[1];
|
||||
const [currentCategory, setCurrentCategory] = useState("Weapons");
|
||||
|
||||
function rerender(): void {
|
||||
setRerender((old) => !old);
|
||||
}
|
||||
|
||||
function filterUpgrades(list: string[], type: UpgradeType): GangMemberUpgrade[] {
|
||||
return Object.keys(GangMemberUpgrades)
|
||||
.filter((upgName: string) => {
|
||||
@ -103,12 +112,26 @@ function GangMemberUpgradePanel(props: IPanelProps): React.ReactElement {
|
||||
})
|
||||
.map((upgName: string) => GangMemberUpgrades[upgName]);
|
||||
}
|
||||
|
||||
const onChange = (event: SelectChangeEvent<string>): void => {
|
||||
setCurrentCategory(event.target.value);
|
||||
rerender()
|
||||
}
|
||||
|
||||
const weaponUpgrades = filterUpgrades(props.member.upgrades, UpgradeType.Weapon);
|
||||
const armorUpgrades = filterUpgrades(props.member.upgrades, UpgradeType.Armor);
|
||||
const vehicleUpgrades = filterUpgrades(props.member.upgrades, UpgradeType.Vehicle);
|
||||
const rootkitUpgrades = filterUpgrades(props.member.upgrades, UpgradeType.Rootkit);
|
||||
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]
|
||||
};
|
||||
|
||||
const asc = {
|
||||
hack: props.member.calculateAscensionMult(props.member.hack_asc_points),
|
||||
str: props.member.calculateAscensionMult(props.member.str_asc_points),
|
||||
@ -119,26 +142,89 @@ function GangMemberUpgradePanel(props: IPanelProps): React.ReactElement {
|
||||
};
|
||||
return (
|
||||
<Paper>
|
||||
<Typography variant="h5" color="primary">
|
||||
{props.member.name} ({props.member.task})
|
||||
</Typography>
|
||||
<Typography>
|
||||
Hack: {props.member.hack} (x
|
||||
{formatNumber(props.member.hack_mult * asc.hack, 2)})<br />
|
||||
Str: {props.member.str} (x
|
||||
{formatNumber(props.member.str_mult * asc.str, 2)})<br />
|
||||
Def: {props.member.def} (x
|
||||
{formatNumber(props.member.def_mult * asc.def, 2)})<br />
|
||||
Dex: {props.member.dex} (x
|
||||
{formatNumber(props.member.dex_mult * asc.dex, 2)})<br />
|
||||
Agi: {props.member.agi} (x
|
||||
{formatNumber(props.member.agi_mult * asc.agi, 2)})<br />
|
||||
Cha: {props.member.cha} (x
|
||||
{formatNumber(props.member.cha_mult * asc.cha, 2)})
|
||||
</Typography>
|
||||
<Box display="flex" flexWrap="wrap">
|
||||
<Typography>Purchased Upgrades: </Typography>
|
||||
<br />
|
||||
<Box display="grid" sx={{ gridTemplateColumns: '1fr 1fr', m: 1, gap: 1 }}>
|
||||
<span>
|
||||
<Typography variant="h5" color="primary">
|
||||
{props.member.name} ({props.member.task})
|
||||
</Typography>
|
||||
<Tooltip
|
||||
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)
|
||||
<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)
|
||||
<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)
|
||||
<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)
|
||||
<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)
|
||||
<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)
|
||||
</Typography>
|
||||
}
|
||||
>
|
||||
<Table>
|
||||
<TableBody>
|
||||
{generateTableRow("Hacking", props.member.hack, props.member.hack_exp, Settings.theme.hack, classes)}
|
||||
{generateTableRow("Strength", props.member.str, props.member.str_exp, Settings.theme.combat, classes)}
|
||||
{generateTableRow("Defense", props.member.def, props.member.def_exp, Settings.theme.combat, classes)}
|
||||
{generateTableRow("Dexterity", props.member.dex, props.member.dex_exp, Settings.theme.combat, classes)}
|
||||
{generateTableRow("Agility", props.member.agi, props.member.agi_exp, Settings.theme.combat, classes)}
|
||||
{generateTableRow("Charisma", props.member.cha, props.member.cha_exp, Settings.theme.cha, classes)}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</Tooltip>
|
||||
|
||||
</span>
|
||||
|
||||
<span>
|
||||
<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>
|
||||
</MenuItem>
|
||||
))}
|
||||
</Select>
|
||||
|
||||
<Box sx={{ width: '100%' }}>
|
||||
{(categories[currentCategory][0] as GangMemberUpgrade[]).length === 0 && (
|
||||
<Typography>
|
||||
All upgrades owned!
|
||||
</Typography>
|
||||
)}
|
||||
<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}
|
||||
/>
|
||||
))}
|
||||
</Box>
|
||||
<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 }}>
|
||||
{props.member.upgrades.map((upg: string) => (
|
||||
<PurchasedUpgrade key={upg} upgName={upg} />
|
||||
))}
|
||||
@ -146,59 +232,22 @@ function GangMemberUpgradePanel(props: IPanelProps): React.ReactElement {
|
||||
<PurchasedUpgrade key={upg} upgName={upg} />
|
||||
))}
|
||||
</Box>
|
||||
<Box display="flex" justifyContent="space-around">
|
||||
<Box>
|
||||
<Typography variant="h6" color="primary">
|
||||
Weapons
|
||||
</Typography>
|
||||
{weaponUpgrades.map((upg) => (
|
||||
<UpgradeButton key={upg.name} rerender={rerender} member={props.member} upg={upg} />
|
||||
))}
|
||||
<NextReveal type={UpgradeType.Weapon} upgrades={props.member.upgrades} />
|
||||
</Box>
|
||||
<Box>
|
||||
<Typography variant="h6" color="primary">
|
||||
Armor
|
||||
</Typography>
|
||||
{armorUpgrades.map((upg) => (
|
||||
<UpgradeButton key={upg.name} rerender={rerender} member={props.member} upg={upg} />
|
||||
))}
|
||||
<NextReveal type={UpgradeType.Armor} upgrades={props.member.upgrades} />
|
||||
</Box>
|
||||
<Box>
|
||||
<Typography variant="h6" color="primary">
|
||||
Vehicles
|
||||
</Typography>
|
||||
{vehicleUpgrades.map((upg) => (
|
||||
<UpgradeButton key={upg.name} rerender={rerender} member={props.member} upg={upg} />
|
||||
))}
|
||||
<NextReveal type={UpgradeType.Vehicle} upgrades={props.member.upgrades} />
|
||||
</Box>
|
||||
<Box>
|
||||
<Typography variant="h6" color="primary">
|
||||
Rootkits
|
||||
</Typography>
|
||||
{rootkitUpgrades.map((upg) => (
|
||||
<UpgradeButton key={upg.name} rerender={rerender} member={props.member} upg={upg} />
|
||||
))}
|
||||
<NextReveal type={UpgradeType.Rootkit} upgrades={props.member.upgrades} />
|
||||
</Box>
|
||||
<Box>
|
||||
<Typography variant="h6" color="primary">
|
||||
Augmentations
|
||||
</Typography>
|
||||
{augUpgrades.map((upg) => (
|
||||
<UpgradeButton key={upg.name} rerender={rerender} member={props.member} upg={upg} />
|
||||
))}
|
||||
<NextReveal type={UpgradeType.Augmentation} upgrades={props.member.upgrades} />
|
||||
</Box>
|
||||
</Box>
|
||||
</Paper>
|
||||
</Paper >
|
||||
);
|
||||
}
|
||||
|
||||
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));
|
||||
|
||||
return (
|
||||
<>
|
||||
<Tooltip
|
||||
@ -209,11 +258,26 @@ export function EquipmentsSubpage(): React.ReactElement {
|
||||
</Typography>
|
||||
}
|
||||
>
|
||||
<Typography>Discount: -{numeralWrapper.formatPercentage(1 - 1 / gang.getDiscount())}</Typography>
|
||||
<Typography sx={{ m: 1 }}>Discount: -{numeralWrapper.formatPercentage(1 - 1 / gang.getDiscount())}</Typography>
|
||||
</Tooltip>
|
||||
{gang.members.map((member: GangMember) => (
|
||||
<GangMemberUpgradePanel key={member.name} member={member} />
|
||||
))}
|
||||
|
||||
<TextField
|
||||
value={filter}
|
||||
onChange={handleFilterChange}
|
||||
autoFocus
|
||||
InputProps={{
|
||||
startAdornment: <SearchIcon />,
|
||||
spellCheck: false
|
||||
}}
|
||||
placeholder="Filter by member name"
|
||||
sx={{ m: 1, width: '15%' }}
|
||||
/>
|
||||
|
||||
<Box display="grid" sx={{ gridTemplateColumns: '1fr 1fr', width: 'fit-content' }}>
|
||||
{members.map((member: GangMember) => (
|
||||
<GangMemberUpgradePanel key={member.name} member={member} />
|
||||
))}
|
||||
</Box>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
@ -1,36 +0,0 @@
|
||||
/**
|
||||
* React Component for a gang member on the management subpage.
|
||||
*/
|
||||
import React, { useState } from "react";
|
||||
import { GangMember } from "../GangMember";
|
||||
import { GangMemberAccordionContent } from "./GangMemberAccordionContent";
|
||||
|
||||
import Box from "@mui/material/Box";
|
||||
|
||||
import Typography from "@mui/material/Typography";
|
||||
import ListItemButton from "@mui/material/ListItemButton";
|
||||
import ListItemText from "@mui/material/ListItemText";
|
||||
import Paper from "@mui/material/Paper";
|
||||
import Collapse from "@mui/material/Collapse";
|
||||
import ExpandMore from "@mui/icons-material/ExpandMore";
|
||||
import ExpandLess from "@mui/icons-material/ExpandLess";
|
||||
interface IProps {
|
||||
member: GangMember;
|
||||
}
|
||||
|
||||
export function GangMemberAccordion(props: IProps): React.ReactElement {
|
||||
const [open, setOpen] = useState(true);
|
||||
return (
|
||||
<Box component={Paper}>
|
||||
<ListItemButton onClick={() => setOpen((old) => !old)}>
|
||||
<ListItemText primary={<Typography>{props.member.name}</Typography>} />
|
||||
{open ? <ExpandLess color="primary" /> : <ExpandMore color="primary" />}
|
||||
</ListItemButton>
|
||||
<Collapse in={open} unmountOnExit>
|
||||
<Box sx={{ mx: 4 }}>
|
||||
<GangMemberAccordionContent member={props.member} />
|
||||
</Box>
|
||||
</Collapse>
|
||||
</Box>
|
||||
);
|
||||
}
|
@ -1,31 +0,0 @@
|
||||
/**
|
||||
* React Component for the content of the accordion of gang members on the
|
||||
* management subpage.
|
||||
*/
|
||||
import React, { useState } from "react";
|
||||
import { GangMemberStats } from "./GangMemberStats";
|
||||
import { TaskSelector } from "./TaskSelector";
|
||||
import { TaskDescription } from "./TaskDescription";
|
||||
import { GangMember } from "../GangMember";
|
||||
import Grid from "@mui/material/Grid";
|
||||
|
||||
interface IProps {
|
||||
member: GangMember;
|
||||
}
|
||||
|
||||
export function GangMemberAccordionContent(props: IProps): React.ReactElement {
|
||||
const setRerender = useState(false)[1];
|
||||
return (
|
||||
<Grid container>
|
||||
<Grid item xs={4}>
|
||||
<GangMemberStats onAscend={() => setRerender((old) => !old)} member={props.member} />
|
||||
</Grid>
|
||||
<Grid item xs={4}>
|
||||
<TaskSelector onTaskChange={() => setRerender((old) => !old)} member={props.member} />
|
||||
</Grid>
|
||||
<Grid item xs={4}>
|
||||
<TaskDescription member={props.member} />
|
||||
</Grid>
|
||||
</Grid>
|
||||
);
|
||||
}
|
26
src/Gang/ui/GangMemberCard.tsx
Normal file
26
src/Gang/ui/GangMemberCard.tsx
Normal file
@ -0,0 +1,26 @@
|
||||
/**
|
||||
* React Component for a gang member on the management subpage.
|
||||
*/
|
||||
import React from "react";
|
||||
import { GangMember } from "../GangMember";
|
||||
import { GangMemberCardContent } from "./GangMemberCardContent";
|
||||
|
||||
import Box from "@mui/material/Box";
|
||||
|
||||
import ListItemText from "@mui/material/ListItemText";
|
||||
import Paper from "@mui/material/Paper";
|
||||
|
||||
interface IProps {
|
||||
member: GangMember;
|
||||
}
|
||||
|
||||
export function GangMemberCard(props: IProps): React.ReactElement {
|
||||
return (
|
||||
<Box component={Paper} sx={{ width: 'auto' }}>
|
||||
<Box sx={{ m: 1 }}>
|
||||
<ListItemText primary={<b>{props.member.name}</b>} />
|
||||
<GangMemberCardContent member={props.member} />
|
||||
</Box>
|
||||
</Box>
|
||||
);
|
||||
}
|
62
src/Gang/ui/GangMemberCardContent.tsx
Normal file
62
src/Gang/ui/GangMemberCardContent.tsx
Normal file
@ -0,0 +1,62 @@
|
||||
/**
|
||||
* React Component for the content of the accordion of gang members on the
|
||||
* management subpage.
|
||||
*/
|
||||
import React, { useState } from "react";
|
||||
import { GangMemberStats } from "./GangMemberStats";
|
||||
import { TaskSelector } from "./TaskSelector";
|
||||
import { AscensionModal } from "./AscensionModal";
|
||||
|
||||
import { Box } from "@mui/system";
|
||||
import { Button, Typography } from "@mui/material";
|
||||
import HelpIcon from "@mui/icons-material/Help";
|
||||
|
||||
import { GangMember } from "../GangMember";
|
||||
import { StaticModal } from "../../ui/React/StaticModal";
|
||||
|
||||
interface IProps {
|
||||
member: GangMember;
|
||||
}
|
||||
|
||||
export function GangMemberCardContent(props: IProps): React.ReactElement {
|
||||
const setRerender = useState(false)[1];
|
||||
const [helpOpen, setHelpOpen] = useState(false);
|
||||
const [ascendOpen, setAscendOpen] = useState(false);
|
||||
|
||||
return (
|
||||
<>
|
||||
{props.member.canAscend() && (
|
||||
<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 }}>
|
||||
<HelpIcon />
|
||||
</Button>
|
||||
<StaticModal open={helpOpen} onClose={() => setHelpOpen(false)}>
|
||||
<Typography>
|
||||
Ascending a Gang Member resets the member's progress and stats in exchange for a permanent boost to their
|
||||
stat multipliers.
|
||||
<br />
|
||||
<br />
|
||||
The additional stat multiplier that the Gang Member gains upon ascension is based on the amount of exp
|
||||
they have.
|
||||
<br />
|
||||
<br />
|
||||
Upon ascension, the member will lose all of its non-Augmentation Equipment and your gang will lose respect
|
||||
equal to the total respect earned by the member.
|
||||
</Typography>
|
||||
</StaticModal>
|
||||
</Box>
|
||||
)}
|
||||
<Box display="grid" sx={{ gridTemplateColumns: '1fr 1fr', width: '100%', gap: 1 }}>
|
||||
<GangMemberStats member={props.member} />
|
||||
<TaskSelector onTaskChange={() => setRerender((old) => !old)} member={props.member} />
|
||||
</Box>
|
||||
</>
|
||||
);
|
||||
}
|
@ -2,23 +2,63 @@
|
||||
* React Component for the list of gang members on the management subpage.
|
||||
*/
|
||||
import React, { useState } from "react";
|
||||
import { GangMemberAccordion } from "./GangMemberAccordion";
|
||||
import { GangMember } from "../GangMember";
|
||||
import { GangMemberCard } from "./GangMemberCard";
|
||||
import { RecruitButton } from "./RecruitButton";
|
||||
import { useGang } from "./Context";
|
||||
|
||||
import { Box, TextField } from "@mui/material";
|
||||
import SearchIcon from "@mui/icons-material/Search";
|
||||
|
||||
import { GangMember } from "../GangMember";
|
||||
import { OptionSwitch } from "../../ui/React/OptionSwitch";
|
||||
|
||||
export function GangMemberList(): React.ReactElement {
|
||||
const gang = useGang();
|
||||
const setRerender = useState(false)[1];
|
||||
const [filter, setFilter] = useState("");
|
||||
const [ascendOnly, setAscendOnly] = useState(false);
|
||||
|
||||
const handleFilterChange = (event: React.ChangeEvent<HTMLInputElement>): void => {
|
||||
setFilter(event.target.value.toLowerCase());
|
||||
}
|
||||
|
||||
const members = gang.members
|
||||
.filter((member) => member && member.name.toLowerCase().includes(filter))
|
||||
.filter((member) => {
|
||||
if (ascendOnly) return member.canAscend();
|
||||
return true;
|
||||
});
|
||||
|
||||
return (
|
||||
<>
|
||||
<RecruitButton onRecruit={() => setRerender((old) => !old)} />
|
||||
<ul>
|
||||
{gang.members.map((member: GangMember) => (
|
||||
<GangMemberAccordion key={member.name} member={member} />
|
||||
<TextField
|
||||
value={filter}
|
||||
onChange={handleFilterChange}
|
||||
autoFocus
|
||||
InputProps={{
|
||||
startAdornment: <SearchIcon />,
|
||||
spellCheck: false
|
||||
}}
|
||||
placeholder="Filter by member name"
|
||||
sx={{ m: 1, width: '15%' }}
|
||||
/>
|
||||
<OptionSwitch
|
||||
checked={ascendOnly}
|
||||
onChange={(newValue) => (setAscendOnly(newValue))}
|
||||
text="Show only ascendable"
|
||||
tooltip={
|
||||
<>
|
||||
Filter the members list by whether or not the member
|
||||
can be ascended.
|
||||
</>
|
||||
}
|
||||
/>
|
||||
<Box display="grid" sx={{ gridTemplateColumns: 'repeat(2, 1fr)' }}>
|
||||
{members.map((member: GangMember) => (
|
||||
<GangMemberCard key={member.name} member={member} />
|
||||
))}
|
||||
</ul>
|
||||
</Box>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
@ -2,26 +2,53 @@
|
||||
* React Component for the first part of a gang member details.
|
||||
* Contains skills and exp.
|
||||
*/
|
||||
import React, { useState } from "react";
|
||||
import { formatNumber } from "../../utils/StringHelperFunctions";
|
||||
import { numeralWrapper } from "../../ui/numeralFormat";
|
||||
import { GangMember } from "../GangMember";
|
||||
import { AscensionModal } from "./AscensionModal";
|
||||
import React from "react";
|
||||
import { useGang } from "./Context";
|
||||
|
||||
import Typography from "@mui/material/Typography";
|
||||
import Tooltip from "@mui/material/Tooltip";
|
||||
import Button from "@mui/material/Button";
|
||||
import { StaticModal } from "../../ui/React/StaticModal";
|
||||
import IconButton from "@mui/material/IconButton";
|
||||
import HelpIcon from "@mui/icons-material/Help";
|
||||
import {
|
||||
Table,
|
||||
TableBody,
|
||||
TableCell,
|
||||
TableRow,
|
||||
} from "@mui/material";
|
||||
|
||||
import { numeralWrapper } from "../../ui/numeralFormat";
|
||||
import { GangMember } from "../GangMember";
|
||||
import { Settings } from "../../Settings/Settings";
|
||||
import { formatNumber } from "../../utils/StringHelperFunctions";
|
||||
import { MoneyRate } from "../../ui/React/MoneyRate";
|
||||
import { characterOverviewStyles as useStyles } from "../../ui/React/CharacterOverview";
|
||||
|
||||
interface IProps {
|
||||
member: GangMember;
|
||||
onAscend: () => void;
|
||||
}
|
||||
|
||||
export const generateTableRow = (
|
||||
name: string,
|
||||
level: number,
|
||||
exp: number,
|
||||
color: string,
|
||||
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
|
||||
classes: any
|
||||
): React.ReactElement => {
|
||||
return (
|
||||
<TableRow>
|
||||
<TableCell classes={{ root: classes.cellNone }}>
|
||||
<Typography style={{ color: color }}>{name}</Typography>
|
||||
</TableCell>
|
||||
<TableCell align="right" classes={{ root: classes.cellNone }}>
|
||||
<Typography style={{ color: color }}>
|
||||
{formatNumber(level, 0)} ({numeralWrapper.formatExp(exp)} exp)
|
||||
</Typography>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
)
|
||||
}
|
||||
|
||||
export function GangMemberStats(props: IProps): React.ReactElement {
|
||||
const [helpOpen, setHelpOpen] = useState(false);
|
||||
const [ascendOpen, setAscendOpen] = useState(false);
|
||||
const classes = useStyles();
|
||||
|
||||
const asc = {
|
||||
hack: props.member.calculateAscensionMult(props.member.hack_asc_points),
|
||||
@ -32,6 +59,16 @@ export function GangMemberStats(props: IProps): React.ReactElement {
|
||||
cha: props.member.calculateAscensionMult(props.member.cha_asc_points),
|
||||
};
|
||||
|
||||
|
||||
|
||||
const gang = useGang();
|
||||
const data = [
|
||||
[`Money:`, <MoneyRate money={5 * props.member.calculateMoneyGain(gang)} />],
|
||||
[`Respect:`, `${numeralWrapper.formatRespect(5 * props.member.calculateRespectGain(gang))} / sec`],
|
||||
[`Wanted Level:`, `${numeralWrapper.formatWanted(5 * props.member.calculateWantedLevelGain(gang))} / sec`],
|
||||
[`Total Respect:`, `${numeralWrapper.formatRespect(props.member.earnedRespect)}`],
|
||||
];
|
||||
|
||||
return (
|
||||
<>
|
||||
<Tooltip
|
||||
@ -63,50 +100,32 @@ export function GangMemberStats(props: IProps): React.ReactElement {
|
||||
</Typography>
|
||||
}
|
||||
>
|
||||
<Typography>
|
||||
Hacking: {formatNumber(props.member.hack, 0)} ({numeralWrapper.formatExp(props.member.hack_exp)} exp)
|
||||
<br />
|
||||
Strength: {formatNumber(props.member.str, 0)} ({numeralWrapper.formatExp(props.member.str_exp)} exp)
|
||||
<br />
|
||||
Defense: {formatNumber(props.member.def, 0)} ({numeralWrapper.formatExp(props.member.def_exp)} exp)
|
||||
<br />
|
||||
Dexterity: {formatNumber(props.member.dex, 0)} ({numeralWrapper.formatExp(props.member.dex_exp)} exp)
|
||||
<br />
|
||||
Agility: {formatNumber(props.member.agi, 0)} ({numeralWrapper.formatExp(props.member.agi_exp)} exp)
|
||||
<br />
|
||||
Charisma: {formatNumber(props.member.cha, 0)} ({numeralWrapper.formatExp(props.member.cha_exp)} exp)
|
||||
<br />
|
||||
</Typography>
|
||||
<Table sx={{ display: 'table', mb: 1, width: '100%' }}>
|
||||
<TableBody>
|
||||
{generateTableRow("Hacking", props.member.hack, props.member.hack_exp, Settings.theme.hack, classes)}
|
||||
{generateTableRow("Strength", props.member.str, props.member.str_exp, Settings.theme.combat, classes)}
|
||||
{generateTableRow("Defense", props.member.def, props.member.def_exp, Settings.theme.combat, classes)}
|
||||
{generateTableRow("Dexterity", props.member.dex, props.member.dex_exp, Settings.theme.combat, classes)}
|
||||
{generateTableRow("Agility", props.member.agi, props.member.agi_exp, Settings.theme.combat, classes)}
|
||||
{generateTableRow("Charisma", props.member.cha, props.member.cha_exp, Settings.theme.cha, classes)}
|
||||
<TableRow>
|
||||
<TableCell classes={{ root: classes.cellNone }}>
|
||||
<br />
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
{data.map(([a, b]) => (
|
||||
<TableRow key={a.toString() + b.toString()}>
|
||||
<TableCell classes={{ root: classes.cellNone }}>
|
||||
<Typography>{a}</Typography>
|
||||
</TableCell>
|
||||
<TableCell align="right" classes={{ root: classes.cellNone }}>
|
||||
<Typography>{b}</Typography>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</Tooltip>
|
||||
<br />
|
||||
{props.member.canAscend() && (
|
||||
<>
|
||||
<Button onClick={() => setAscendOpen(true)}>Ascend</Button>
|
||||
<AscensionModal
|
||||
open={ascendOpen}
|
||||
onClose={() => setAscendOpen(false)}
|
||||
member={props.member}
|
||||
onAscend={props.onAscend}
|
||||
/>
|
||||
<IconButton onClick={() => setHelpOpen(true)}>
|
||||
<HelpIcon />
|
||||
</IconButton>
|
||||
<StaticModal open={helpOpen} onClose={() => setHelpOpen(false)}>
|
||||
<Typography>
|
||||
Ascending a Gang Member resets the member's progress and stats in exchange for a permanent boost to their
|
||||
stat multipliers.
|
||||
<br />
|
||||
<br />
|
||||
The additional stat multiplier that the Gang Member gains upon ascension is based on the amount of exp
|
||||
they have.
|
||||
<br />
|
||||
<br />
|
||||
Upon ascension, the member will lose all of its non-Augmentation Equipment and your gang will lose respect
|
||||
equal to the total respect earned by the member.
|
||||
</Typography>
|
||||
</StaticModal>
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
@ -24,18 +24,20 @@ export function RecruitButton(props: IProps): React.ReactElement {
|
||||
if (!gang.canRecruitMember()) {
|
||||
const respect = gang.getRespectNeededToRecruitMember();
|
||||
return (
|
||||
<Box display="flex" alignItems="center">
|
||||
<Button sx={{ mx: 1 }} disabled>
|
||||
<Box display="flex" alignItems="center" sx={{ mx: 1 }}>
|
||||
<Button disabled>
|
||||
Recruit Gang Member
|
||||
</Button>
|
||||
<Typography>{numeralWrapper.formatRespect(respect)} respect needed to recruit next member</Typography>
|
||||
<Typography sx={{ ml: 1 }}>{numeralWrapper.formatRespect(respect)} respect needed to recruit next member</Typography>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<Button onClick={() => setOpen(true)}>Recruit Gang Member</Button>
|
||||
<Box sx={{ mx: 1 }}>
|
||||
<Button onClick={() => setOpen(true)}>Recruit Gang Member</Button>
|
||||
</Box>
|
||||
<RecruitModal open={open} onClose={() => setOpen(false)} onRecruit={props.onRecruit} />
|
||||
</>
|
||||
);
|
||||
|
@ -3,14 +3,15 @@
|
||||
* the task selector as well as some stats.
|
||||
*/
|
||||
import React, { useState } from "react";
|
||||
import { numeralWrapper } from "../../ui/numeralFormat";
|
||||
import { StatsTable } from "../../ui/React/StatsTable";
|
||||
import { MoneyRate } from "../../ui/React/MoneyRate";
|
||||
import { useGang } from "./Context";
|
||||
import { GangMember } from "../GangMember";
|
||||
import { TaskDescription } from "./TaskDescription";
|
||||
|
||||
import { Box } from "@mui/material";
|
||||
import MenuItem from "@mui/material/MenuItem";
|
||||
import Select, { SelectChangeEvent } from "@mui/material/Select";
|
||||
|
||||
import { GangMember } from "../GangMember";
|
||||
|
||||
interface IProps {
|
||||
member: GangMember;
|
||||
onTaskChange: () => void;
|
||||
@ -29,16 +30,9 @@ export function TaskSelector(props: IProps): React.ReactElement {
|
||||
|
||||
const tasks = gang.getAllTaskNames();
|
||||
|
||||
const data = [
|
||||
[`Money:`, <MoneyRate money={5 * props.member.calculateMoneyGain(gang)} />],
|
||||
[`Respect:`, `${numeralWrapper.formatRespect(5 * props.member.calculateRespectGain(gang))} / sec`],
|
||||
[`Wanted Level:`, `${numeralWrapper.formatWanted(5 * props.member.calculateWantedLevelGain(gang))} / sec`],
|
||||
[`Total Respect:`, `${numeralWrapper.formatRespect(props.member.earnedRespect)}`],
|
||||
];
|
||||
|
||||
return (
|
||||
<>
|
||||
<Select onChange={onChange} value={currentTask}>
|
||||
<Box>
|
||||
<Select onChange={onChange} value={currentTask} sx={{ width: '100%' }}>
|
||||
<MenuItem key={0} value={"Unassigned"}>
|
||||
Unassigned
|
||||
</MenuItem>
|
||||
@ -48,8 +42,7 @@ export function TaskSelector(props: IProps): React.ReactElement {
|
||||
</MenuItem>
|
||||
))}
|
||||
</Select>
|
||||
|
||||
<StatsTable rows={data} />
|
||||
</>
|
||||
<TaskDescription member={props.member} />
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
@ -34,9 +34,9 @@ export function calculateHackingExpGain(server: Server, player: IPlayer): number
|
||||
server.baseDifficulty = server.hackDifficulty;
|
||||
}
|
||||
let expGain = baseExpGain;
|
||||
expGain += server.baseDifficulty * player.hacking_exp_mult * diffFactor;
|
||||
expGain += server.baseDifficulty * diffFactor;
|
||||
|
||||
return expGain * BitNodeMultipliers.HackExpGain;
|
||||
return expGain * player.hacking_exp_mult * BitNodeMultipliers.HackExpGain;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -5,7 +5,7 @@
|
||||
*/
|
||||
import React, { useState } from "react";
|
||||
import Button from "@mui/material/Button";
|
||||
import { Blackjack } from "../../Casino/Blackjack";
|
||||
import { Blackjack, DECK_COUNT } from "../../Casino/Blackjack";
|
||||
import { CoinFlip } from "../../Casino/CoinFlip";
|
||||
import { Roulette } from "../../Casino/Roulette";
|
||||
import { SlotMachine } from "../../Casino/SlotMachine";
|
||||
@ -38,7 +38,7 @@ export function CasinoLocation(props: IProps): React.ReactElement {
|
||||
<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>
|
||||
<Button onClick={() => updateGame(GameType.Blackjack)}>Play blackjack</Button>
|
||||
<Button onClick={() => updateGame(GameType.Blackjack)}>Play blackjack ({DECK_COUNT} decks)</Button>
|
||||
</Box>
|
||||
)}
|
||||
{game !== GameType.None && (
|
||||
|
@ -114,6 +114,7 @@ export const RamCosts: IMap<any> = {
|
||||
weaken: RamCostConstants.ScriptWeakenRamCost,
|
||||
weakenAnalyze: RamCostConstants.ScriptWeakenAnalyzeRamCost,
|
||||
print: 0,
|
||||
printf: 0,
|
||||
tprint: 0,
|
||||
clearLog: 0,
|
||||
disableLog: 0,
|
||||
|
@ -553,7 +553,7 @@ export function NetscriptFunctions(workerScript: WorkerScript): NS {
|
||||
if (isNaN(hackAmount)) {
|
||||
throw makeRuntimeErrorMsg(
|
||||
"hackAnalyzeThreads",
|
||||
`Invalid growth argument passed into hackAnalyzeThreads: ${hackAmount}. Must be numeric.`,
|
||||
`Invalid hackAmount argument passed into hackAnalyzeThreads: ${hackAmount}. Must be numeric.`,
|
||||
);
|
||||
}
|
||||
|
||||
@ -751,6 +751,12 @@ export function NetscriptFunctions(workerScript: WorkerScript): NS {
|
||||
}
|
||||
workerScript.print(argsToString(args));
|
||||
},
|
||||
printf: function (format: string, ...args: any[]): void {
|
||||
if (typeof format !== "string") {
|
||||
throw makeRuntimeErrorMsg("printf", "First argument must be string for the format.");
|
||||
}
|
||||
workerScript.print(vsprintf(format, args));
|
||||
},
|
||||
tprint: function (...args: any[]): void {
|
||||
if (args.length === 0) {
|
||||
throw makeRuntimeErrorMsg("tprint", "Takes at least 1 argument.");
|
||||
@ -1676,7 +1682,12 @@ export function NetscriptFunctions(workerScript: WorkerScript): NS {
|
||||
|
||||
const cost = getPurchaseServerCost(ram);
|
||||
if (cost === Infinity) {
|
||||
workerScript.log("purchaseServer", () => `Invalid argument: ram='${ram}' must be a positive power of 2`);
|
||||
if(ram > getPurchaseServerMaxRam()){
|
||||
workerScript.log("purchaseServer", () => `Invalid argument: ram='${ram}' must not be greater than getPurchaseServerMaxRam`);
|
||||
}else{
|
||||
workerScript.log("purchaseServer", () => `Invalid argument: ram='${ram}' must be a positive power of 2`);
|
||||
}
|
||||
|
||||
return "";
|
||||
}
|
||||
|
||||
|
@ -49,6 +49,7 @@ import {
|
||||
SetMaterialMarketTA2,
|
||||
SetProductMarketTA1,
|
||||
SetProductMarketTA2,
|
||||
SetSmartSupplyUseLeftovers,
|
||||
} from "../Corporation/Actions";
|
||||
import { CorporationUnlockUpgrades } from "../Corporation/data/CorporationUnlockUpgrades";
|
||||
import { CorporationUpgrades } from "../Corporation/data/CorporationUpgrades";
|
||||
@ -410,6 +411,16 @@ export function NetscriptCorporation(
|
||||
const warehouse = getWarehouse(divisionName, cityName);
|
||||
SetSmartSupply(warehouse, enabled);
|
||||
},
|
||||
setSmartSupplyUseLeftovers: function (adivisionName: any, acityName: any, amaterialName: any, aenabled: any): void {
|
||||
checkAccess("setSmartSupplyUseLeftovers", 7);
|
||||
const divisionName = helper.string("setSmartSupply", "divisionName", adivisionName);
|
||||
const cityName = helper.string("sellProduct", "cityName", acityName);
|
||||
const materialName = helper.string("sellProduct", "materialName", amaterialName);
|
||||
const enabled = helper.boolean(aenabled);
|
||||
const warehouse = getWarehouse(divisionName, cityName);
|
||||
const material = getMaterial(divisionName, cityName, materialName);
|
||||
SetSmartSupplyUseLeftovers(warehouse, material, enabled);
|
||||
},
|
||||
buyMaterial: function (adivisionName: any, acityName: any, amaterialName: any, aamt: any): void {
|
||||
checkAccess("buyMaterial", 7);
|
||||
const divisionName = helper.string("buyMaterial", "divisionName", adivisionName);
|
||||
|
@ -81,41 +81,41 @@ export function NetscriptFormulas(player: IPlayer, workerScript: WorkerScript, h
|
||||
return {
|
||||
skills: {
|
||||
calculateSkill: function (exp: any, mult: any = 1): any {
|
||||
checkFormulasAccess("basic.calculateSkill");
|
||||
checkFormulasAccess("skills.calculateSkill");
|
||||
return calculateSkill(exp, mult);
|
||||
},
|
||||
calculateExp: function (skill: any, mult: any = 1): any {
|
||||
checkFormulasAccess("basic.calculateExp");
|
||||
checkFormulasAccess("skills.calculateExp");
|
||||
return calculateExp(skill, mult);
|
||||
},
|
||||
},
|
||||
hacking: {
|
||||
hackChance: function (server: any, player: any): any {
|
||||
checkFormulasAccess("basic.hackChance");
|
||||
checkFormulasAccess("hacking.hackChance");
|
||||
return calculateHackingChance(server, player);
|
||||
},
|
||||
hackExp: function (server: any, player: any): any {
|
||||
checkFormulasAccess("basic.hackExp");
|
||||
checkFormulasAccess("hacking.hackExp");
|
||||
return calculateHackingExpGain(server, player);
|
||||
},
|
||||
hackPercent: function (server: any, player: any): any {
|
||||
checkFormulasAccess("basic.hackPercent");
|
||||
checkFormulasAccess("hacking.hackPercent");
|
||||
return calculatePercentMoneyHacked(server, player);
|
||||
},
|
||||
growPercent: function (server: any, threads: any, player: any, cores: any = 1): any {
|
||||
checkFormulasAccess("basic.growPercent");
|
||||
checkFormulasAccess("hacking.growPercent");
|
||||
return calculateServerGrowth(server, threads, player, cores);
|
||||
},
|
||||
hackTime: function (server: any, player: any): any {
|
||||
checkFormulasAccess("basic.hackTime");
|
||||
checkFormulasAccess("hacking.hackTime");
|
||||
return calculateHackingTime(server, player) * 1000;
|
||||
},
|
||||
growTime: function (server: any, player: any): any {
|
||||
checkFormulasAccess("basic.growTime");
|
||||
checkFormulasAccess("hacking.growTime");
|
||||
return calculateGrowTime(server, player) * 1000;
|
||||
},
|
||||
weakenTime: function (server: any, player: any): any {
|
||||
checkFormulasAccess("basic.weakenTime");
|
||||
checkFormulasAccess("hacking.weakenTime");
|
||||
return calculateWeakenTime(server, player) * 1000;
|
||||
},
|
||||
},
|
||||
@ -188,21 +188,27 @@ export function NetscriptFormulas(player: IPlayer, workerScript: WorkerScript, h
|
||||
},
|
||||
gang: {
|
||||
wantedPenalty(gang: any): number {
|
||||
checkFormulasAccess("gang.wantedPenalty");
|
||||
return calculateWantedPenalty(gang);
|
||||
},
|
||||
respectGain: function (gang: any, member: any, task: any): number {
|
||||
checkFormulasAccess("gang.respectGain");
|
||||
return calculateRespectGain(gang, member, task);
|
||||
},
|
||||
wantedLevelGain: function (gang: any, member: any, task: any): number {
|
||||
checkFormulasAccess("gang.wantedLevelGain");
|
||||
return calculateWantedLevelGain(gang, member, task);
|
||||
},
|
||||
moneyGain: function (gang: any, member: any, task: any): number {
|
||||
checkFormulasAccess("gang.moneyGain");
|
||||
return calculateMoneyGain(gang, member, task);
|
||||
},
|
||||
ascensionPointsGain: function (exp: any): number {
|
||||
checkFormulasAccess("gang.ascensionPointsGain");
|
||||
return calculateAscensionPointsGain(exp);
|
||||
},
|
||||
ascensionMultiplier: function (points: any): number {
|
||||
checkFormulasAccess("gang.ascensionMultiplier");
|
||||
return calculateAscensionMult(points);
|
||||
},
|
||||
},
|
||||
|
@ -474,7 +474,8 @@ export function NetscriptSingularity(
|
||||
case CityName.Ishima:
|
||||
case CityName.Volhaven:
|
||||
if (player.money < CONSTANTS.TravelCost) {
|
||||
throw helper.makeRuntimeErrorMsg("travelToCity", "Not enough money to travel.");
|
||||
workerScript.log("travelToCity", () => "Not enough money to travel.");
|
||||
return false
|
||||
}
|
||||
player.loseMoney(CONSTANTS.TravelCost, "other");
|
||||
player.city = cityname;
|
||||
@ -482,8 +483,7 @@ export function NetscriptSingularity(
|
||||
player.gainIntelligenceExp(CONSTANTS.IntelligenceSingFnBaseExpGain / 50000);
|
||||
return true;
|
||||
default:
|
||||
workerScript.log("travelToCity", () => `Invalid city name: '${cityname}'.`);
|
||||
return false;
|
||||
throw helper.makeRuntimeErrorMsg("travelToCity", `Invalid city name: '${cityname}'.`);
|
||||
}
|
||||
},
|
||||
|
||||
|
@ -4,9 +4,9 @@ import { IPlayer } from "../PersonObjects/IPlayer";
|
||||
import { getRamCost } from "../Netscript/RamCostGenerator";
|
||||
import { GameInfo, IStyleSettings, UserInterface as IUserInterface, UserInterfaceTheme } from "../ScriptEditor/NetscriptDefinitions";
|
||||
import { Settings } from "../Settings/Settings";
|
||||
import { ThemeEvents } from "../ui/React/Theme";
|
||||
import { defaultTheme } from "../Settings/Themes";
|
||||
import { defaultStyles } from "../Settings/Styles";
|
||||
import { ThemeEvents } from "../Themes/ui/Theme";
|
||||
import { defaultTheme } from "../Themes/Themes";
|
||||
import { defaultStyles } from "../Themes/Styles";
|
||||
import { CONSTANTS } from "../Constants";
|
||||
import { hash } from "../hash/hash";
|
||||
|
||||
|
@ -72,6 +72,7 @@ export interface IPlayer {
|
||||
sourceFiles: IPlayerOwnedSourceFile[];
|
||||
exploits: Exploit[];
|
||||
achievements: PlayerAchievement[];
|
||||
terminalCommandHistory: string[];
|
||||
lastUpdate: number;
|
||||
totalPlaytime: number;
|
||||
|
||||
|
@ -35,7 +35,9 @@ import { CityName } from "../../Locations/data/CityNames";
|
||||
import { MoneySourceTracker } from "../../utils/MoneySourceTracker";
|
||||
import { Reviver, Generic_toJSON, Generic_fromJSON } from "../../utils/JSONReviver";
|
||||
import { ISkillProgress } from "../formulas/skill";
|
||||
import { PlayerAchievement } from '../../Achievements/Achievements';
|
||||
import { PlayerAchievement } from "../../Achievements/Achievements";
|
||||
import { cyrb53 } from "../../utils/StringHelperFunctions";
|
||||
import { getRandomInt } from "../../utils/helpers/getRandomInt";
|
||||
|
||||
export class PlayerObject implements IPlayer {
|
||||
// Class members
|
||||
@ -77,7 +79,10 @@ export class PlayerObject implements IPlayer {
|
||||
sourceFiles: IPlayerOwnedSourceFile[];
|
||||
exploits: Exploit[];
|
||||
achievements: PlayerAchievement[];
|
||||
terminalCommandHistory: string[];
|
||||
identifier: string;
|
||||
lastUpdate: number;
|
||||
lastSave: number;
|
||||
totalPlaytime: number;
|
||||
|
||||
// Stats
|
||||
@ -459,7 +464,9 @@ export class PlayerObject implements IPlayer {
|
||||
|
||||
//Used to store the last update time.
|
||||
this.lastUpdate = 0;
|
||||
this.lastSave = 0;
|
||||
this.totalPlaytime = 0;
|
||||
|
||||
this.playtimeSinceLastAug = 0;
|
||||
this.playtimeSinceLastBitnode = 0;
|
||||
|
||||
@ -471,6 +478,17 @@ export class PlayerObject implements IPlayer {
|
||||
|
||||
this.exploits = [];
|
||||
this.achievements = [];
|
||||
this.terminalCommandHistory = [];
|
||||
|
||||
// Let's get a hash of some semi-random stuff so we have something unique.
|
||||
this.identifier = cyrb53(
|
||||
"I-" +
|
||||
new Date().getTime() +
|
||||
navigator.userAgent +
|
||||
window.innerWidth +
|
||||
window.innerHeight +
|
||||
getRandomInt(100, 999),
|
||||
);
|
||||
|
||||
this.init = generalMethods.init;
|
||||
this.prestigeAugmentation = generalMethods.prestigeAugmentation;
|
||||
|
@ -1,5 +1,8 @@
|
||||
import { Sleeve } from "../Sleeve";
|
||||
import { numeralWrapper } from "../../../ui/numeralFormat";
|
||||
import { convertTimeMsToTimeElapsedString } from "../../../utils/StringHelperFunctions";
|
||||
import { CONSTANTS } from "../../../Constants";
|
||||
import { Typography } from "@mui/material";
|
||||
import { StatsTable } from "../../../ui/React/StatsTable";
|
||||
import { Modal } from "../../../ui/React/Modal";
|
||||
import React from "react";
|
||||
@ -80,6 +83,13 @@ export function MoreStatsModal(props: IProps): React.ReactElement {
|
||||
]}
|
||||
title="Multipliers:"
|
||||
/>
|
||||
|
||||
{/* Check for storedCycles to be a bit over 0 to prevent jittering */}
|
||||
{props.sleeve.storedCycles > 10 && (
|
||||
<Typography sx={{ py: 2 }}>
|
||||
Bonus Time: {convertTimeMsToTimeElapsedString(props.sleeve.storedCycles * CONSTANTS.MilliPerCycle)}
|
||||
</Typography>
|
||||
)}
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
|
@ -22,11 +22,44 @@ import { v1APIBreak } from "./utils/v1APIBreak";
|
||||
import { AugmentationNames } from "./Augmentation/data/AugmentationNames";
|
||||
import { PlayerOwnedAugmentation } from "./Augmentation/PlayerOwnedAugmentation";
|
||||
import { LocationName } from "./Locations/data/LocationNames";
|
||||
import { SxProps } from "@mui/system";
|
||||
import { PlayerObject } from "./PersonObjects/Player/PlayerObject";
|
||||
import { pushGameSaved } from "./Electron";
|
||||
|
||||
/* SaveObject.js
|
||||
* Defines the object used to save/load games
|
||||
*/
|
||||
|
||||
export interface SaveData {
|
||||
playerIdentifier: string;
|
||||
fileName: string;
|
||||
save: string;
|
||||
savedOn: number;
|
||||
}
|
||||
|
||||
export interface ImportData {
|
||||
base64: string;
|
||||
parsed: any;
|
||||
playerData?: ImportPlayerData;
|
||||
}
|
||||
|
||||
export interface ImportPlayerData {
|
||||
identifier: string;
|
||||
lastSave: number;
|
||||
totalPlaytime: number;
|
||||
|
||||
money: number;
|
||||
hacking: number;
|
||||
|
||||
augmentations: number;
|
||||
factions: number;
|
||||
achievements: number;
|
||||
|
||||
bitNode: number;
|
||||
bitNodeLevel: number;
|
||||
sourceFiles: number;
|
||||
}
|
||||
|
||||
class BitburnerSaveObject {
|
||||
PlayerSave = "";
|
||||
AllServersSave = "";
|
||||
@ -41,7 +74,6 @@ class BitburnerSaveObject {
|
||||
AllGangsSave = "";
|
||||
LastExportBonus = "";
|
||||
StaneksGiftSave = "";
|
||||
SaveTimestamp = "";
|
||||
|
||||
getSaveString(excludeRunningScripts = false): string {
|
||||
this.PlayerSave = JSON.stringify(Player);
|
||||
@ -57,7 +89,6 @@ class BitburnerSaveObject {
|
||||
this.VersionSave = JSON.stringify(CONSTANTS.VersionNumber);
|
||||
this.LastExportBonus = JSON.stringify(ExportBonus.LastExportBonus);
|
||||
this.StaneksGiftSave = JSON.stringify(staneksGift);
|
||||
this.SaveTimestamp = new Date().getTime().toString();
|
||||
|
||||
if (Player.inGang()) {
|
||||
this.AllGangsSave = JSON.stringify(AllGangs);
|
||||
@ -67,28 +98,134 @@ class BitburnerSaveObject {
|
||||
return saveString;
|
||||
}
|
||||
|
||||
saveGame(emitToastEvent = true): void {
|
||||
saveGame(emitToastEvent = true): Promise<void> {
|
||||
const savedOn = new Date().getTime();
|
||||
Player.lastSave = savedOn;
|
||||
const saveString = this.getSaveString(Settings.ExcludeRunningScriptsFromSave);
|
||||
return new Promise((resolve, reject) => {
|
||||
save(saveString)
|
||||
.then(() => {
|
||||
const saveData: SaveData = {
|
||||
playerIdentifier: Player.identifier,
|
||||
fileName: this.getSaveFileName(),
|
||||
save: saveString,
|
||||
savedOn,
|
||||
};
|
||||
pushGameSaved(saveData);
|
||||
|
||||
save(saveString)
|
||||
.then(() => {
|
||||
if (emitToastEvent) {
|
||||
SnackbarEvents.emit("Game Saved!", "info", 2000);
|
||||
}
|
||||
})
|
||||
.catch((err) => console.error(err));
|
||||
if (emitToastEvent) {
|
||||
SnackbarEvents.emit("Game Saved!", "info", 2000);
|
||||
}
|
||||
return resolve();
|
||||
})
|
||||
.catch((err) => {
|
||||
console.error(err);
|
||||
return reject();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
getSaveFileName(isRecovery = false): string {
|
||||
// Save file name is based on current timestamp and BitNode
|
||||
const epochTime = Math.round(Date.now() / 1000);
|
||||
const bn = Player.bitNodeN;
|
||||
let filename = `bitburnerSave_${epochTime}_BN${bn}x${SourceFileFlags[bn]}.json`;
|
||||
if (isRecovery) filename = "RECOVERY" + filename;
|
||||
return filename;
|
||||
}
|
||||
|
||||
exportGame(): void {
|
||||
const saveString = this.getSaveString(Settings.ExcludeRunningScriptsFromSave);
|
||||
|
||||
// Save file name is based on current timestamp and BitNode
|
||||
const epochTime = Math.round(Date.now() / 1000);
|
||||
const bn = Player.bitNodeN;
|
||||
const filename = `bitburnerSave_${epochTime}_BN${bn}x${SourceFileFlags[bn]}.json`;
|
||||
const filename = this.getSaveFileName();
|
||||
download(filename, saveString);
|
||||
}
|
||||
|
||||
importGame(base64Save: string, reload = true): Promise<void> {
|
||||
if (!base64Save || base64Save === "") throw new Error("Invalid import string");
|
||||
return save(base64Save).then(() => {
|
||||
if (reload) setTimeout(() => location.reload(), 1000);
|
||||
return Promise.resolve();
|
||||
});
|
||||
}
|
||||
|
||||
getImportStringFromFile(files: FileList | null): Promise<string> {
|
||||
if (files === null) return Promise.reject(new Error("No file selected"));
|
||||
const file = files[0];
|
||||
if (!file) return Promise.reject(new Error("Invalid file selected"));
|
||||
|
||||
const reader = new FileReader();
|
||||
const promise: Promise<string> = new Promise((resolve, reject) => {
|
||||
reader.onload = function (this: FileReader, e: ProgressEvent<FileReader>) {
|
||||
const target = e.target;
|
||||
if (target === null) {
|
||||
return reject(new Error("Error importing file"));
|
||||
}
|
||||
const result = target.result;
|
||||
if (typeof result !== "string" || result === null) {
|
||||
return reject(new Error("FileReader event was not type string"));
|
||||
}
|
||||
const contents = result;
|
||||
resolve(contents);
|
||||
};
|
||||
});
|
||||
reader.readAsText(file);
|
||||
return promise;
|
||||
}
|
||||
|
||||
async getImportDataFromString(base64Save: string): Promise<ImportData> {
|
||||
if (!base64Save || base64Save === "") throw new Error("Invalid import string");
|
||||
|
||||
let newSave;
|
||||
try {
|
||||
newSave = window.atob(base64Save);
|
||||
newSave = newSave.trim();
|
||||
} catch (error) {
|
||||
console.error(error); // We'll handle below
|
||||
}
|
||||
|
||||
if (!newSave || newSave === "") {
|
||||
return Promise.reject(new Error("Save game had not content or was not base64 encoded"));
|
||||
}
|
||||
|
||||
let parsedSave;
|
||||
try {
|
||||
parsedSave = JSON.parse(newSave);
|
||||
} catch (error) {
|
||||
console.log(error); // We'll handle below
|
||||
}
|
||||
|
||||
if (!parsedSave || parsedSave.ctor !== "BitburnerSaveObject" || !parsedSave.data) {
|
||||
return Promise.reject(new Error("Save game did not seem valid"));
|
||||
}
|
||||
|
||||
const data: ImportData = {
|
||||
base64: base64Save,
|
||||
parsed: parsedSave,
|
||||
};
|
||||
|
||||
const importedPlayer = PlayerObject.fromJSON(JSON.parse(parsedSave.data.PlayerSave));
|
||||
|
||||
const playerData: ImportPlayerData = {
|
||||
identifier: importedPlayer.identifier,
|
||||
lastSave: importedPlayer.lastSave,
|
||||
totalPlaytime: importedPlayer.totalPlaytime,
|
||||
|
||||
money: importedPlayer.money,
|
||||
hacking: importedPlayer.hacking,
|
||||
|
||||
augmentations: importedPlayer.augmentations?.reduce<number>((total, current) => (total += current.level), 0) ?? 0,
|
||||
factions: importedPlayer.factions?.length ?? 0,
|
||||
achievements: importedPlayer.achievements?.length ?? 0,
|
||||
|
||||
bitNode: importedPlayer.bitNodeN,
|
||||
bitNodeLevel: importedPlayer.sourceFileLvl(Player.bitNodeN) + 1,
|
||||
sourceFiles: importedPlayer.sourceFiles?.reduce<number>((total, current) => (total += current.lvl), 0) ?? 0,
|
||||
};
|
||||
|
||||
data.playerData = playerData;
|
||||
return Promise.resolve(data);
|
||||
}
|
||||
|
||||
toJSON(): any {
|
||||
return Generic_toJSON("BitburnerSaveObject", this);
|
||||
}
|
||||
@ -371,6 +508,18 @@ function createScamUpdateText(): void {
|
||||
}
|
||||
}
|
||||
|
||||
const resets: SxProps = {
|
||||
"& h1, & h2, & h3, & h4, & p, & a, & ul": {
|
||||
margin: 0,
|
||||
color: Settings.theme.primary,
|
||||
whiteSpace: "initial",
|
||||
},
|
||||
"& ul": {
|
||||
paddingLeft: "1.5em",
|
||||
lineHeight: 1.5,
|
||||
},
|
||||
};
|
||||
|
||||
function createNewUpdateText(): void {
|
||||
setTimeout(
|
||||
() =>
|
||||
@ -379,6 +528,7 @@ function createNewUpdateText(): void {
|
||||
"Please report any bugs/issues through the github repository " +
|
||||
"or the Bitburner subreddit (reddit.com/r/bitburner).<br><br>" +
|
||||
CONSTANTS.LatestUpdate,
|
||||
resets,
|
||||
),
|
||||
1000,
|
||||
);
|
||||
@ -391,6 +541,7 @@ function createBetaUpdateText(): void {
|
||||
"Please report any bugs/issues through the github repository (https://github.com/danielyxie/bitburner/issues) " +
|
||||
"or the Bitburner subreddit (reddit.com/r/bitburner).<br><br>" +
|
||||
CONSTANTS.LatestUpdate,
|
||||
resets,
|
||||
);
|
||||
}
|
||||
|
||||
|
42
src/ScriptEditor/NetscriptDefinitions.d.ts
vendored
42
src/ScriptEditor/NetscriptDefinitions.d.ts
vendored
@ -163,7 +163,7 @@ export interface CrimeStats {
|
||||
/** How much money is given */
|
||||
money: number;
|
||||
/** Name of crime */
|
||||
name: number;
|
||||
name: string;
|
||||
/** Milliseconds it takes to attempt the crime */
|
||||
time: number;
|
||||
/** Description of the crime activity */
|
||||
@ -3667,6 +3667,7 @@ interface SkillsFormulas {
|
||||
interface HackingFormulas {
|
||||
/**
|
||||
* Calculate hack chance.
|
||||
* (Ex: 0.25 would indicate a 25% chance of success.)
|
||||
* @param server - Server info from {@link NS.getServer | getServer}
|
||||
* @param player - Player info from {@link NS.getPlayer | getPlayer}
|
||||
* @returns The calculated hack chance.
|
||||
@ -3683,6 +3684,7 @@ interface HackingFormulas {
|
||||
hackExp(server: Server, player: Player): number;
|
||||
/**
|
||||
* Calculate hack percent for one thread.
|
||||
* (Ex: 0.25 would steal 25% of the server's current value.)
|
||||
* @remarks
|
||||
* Multiply by thread to get total percent hacked.
|
||||
* @param server - Server info from {@link NS.getServer | getServer}
|
||||
@ -3691,7 +3693,8 @@ interface HackingFormulas {
|
||||
*/
|
||||
hackPercent(server: Server, player: Player): number;
|
||||
/**
|
||||
* Calculate the percent a server would grow.
|
||||
* Calculate the percent a server would grow to.
|
||||
* (Ex: 3.0 would would grow the server to 300% of its current value.)
|
||||
* @param server - Server info from {@link NS.getServer | getServer}
|
||||
* @param threads - Amount of thread.
|
||||
* @param player - Player info from {@link NS.getPlayer | getPlayer}
|
||||
@ -4231,13 +4234,11 @@ export interface NS extends Singularity {
|
||||
* ```ts
|
||||
* // NS1:
|
||||
* var earnedMoney = hack("foodnstuff");
|
||||
* earnedMoney = earnedMoney + hack("foodnstuff", { threads: 5 }); // Only use 5 threads to hack
|
||||
* ```
|
||||
* @example
|
||||
* ```ts
|
||||
* // NS2:
|
||||
* let earnedMoney = await ns.hack("foodnstuff");
|
||||
* earnedMoney += await ns.hack("foodnstuff", { threads: 5 }); // Only use 5 threads to hack
|
||||
* ```
|
||||
* @param host - Hostname of the target server to hack.
|
||||
* @param opts - Optional parameters for configuring function behavior.
|
||||
@ -4265,16 +4266,14 @@ export interface NS extends Singularity {
|
||||
* @example
|
||||
* ```ts
|
||||
* // NS1:
|
||||
* var availableMoney = getServerMoneyAvailable("foodnstuff");
|
||||
* var currentMoney = getServerMoneyAvailable("foodnstuff");
|
||||
* currentMoney = currentMoney * (1 + grow("foodnstuff"));
|
||||
* currentMoney = currentMoney * (1 + grow("foodnstuff", { threads: 5 })); // Only use 5 threads to grow
|
||||
* ```
|
||||
* @example
|
||||
* ```ts
|
||||
* // NS2:
|
||||
* let availableMoney = ns.getServerMoneyAvailable("foodnstuff");
|
||||
* let currentMoney = ns.getServerMoneyAvailable("foodnstuff");
|
||||
* currentMoney *= (1 + await ns.grow("foodnstuff"));
|
||||
* currentMoney *= (1 + await ns.grow("foodnstuff", { threads: 5 })); // Only use 5 threads to grow
|
||||
* ```
|
||||
* @param host - Hostname of the target server to grow.
|
||||
* @param opts - Optional parameters for configuring function behavior.
|
||||
@ -4300,14 +4299,12 @@ export interface NS extends Singularity {
|
||||
* // NS1:
|
||||
* var currentSecurity = getServerSecurityLevel("foodnstuff");
|
||||
* currentSecurity = currentSecurity - weaken("foodnstuff");
|
||||
* currentSecurity = currentSecurity - weaken("foodnstuff", { threads: 5 }); // Only use 5 threads to weaken
|
||||
* ```
|
||||
* @example
|
||||
* ```ts
|
||||
* // NS2:
|
||||
* let currentSecurity = ns.getServerSecurityLevel("foodnstuff");
|
||||
* currentSecurity -= await ns.weaken("foodnstuff");
|
||||
* currentSecurity -= await ns.weaken("foodnstuff", { threads: 5 }); // Only use 5 threads to weaken
|
||||
* ```
|
||||
* @param host - Hostname of the target server to weaken.
|
||||
* @param opts - Optional parameters for configuring function behavior.
|
||||
@ -4494,6 +4491,17 @@ export interface NS extends Singularity {
|
||||
*/
|
||||
print(...args: any[]): void;
|
||||
|
||||
/**
|
||||
* Prints a formatted string to the script’s logs.
|
||||
* @remarks
|
||||
* RAM cost: 0 GB
|
||||
*
|
||||
* see: https://github.com/alexei/sprintf.js
|
||||
* @param format - format of the message
|
||||
* @param args - Value(s) to be printed.
|
||||
*/
|
||||
printf(format: string, ...args: any[]): void;
|
||||
|
||||
/**
|
||||
* Prints one or more values or variables to the Terminal.
|
||||
* @remarks
|
||||
@ -5451,7 +5459,7 @@ export interface NS extends Singularity {
|
||||
* @param filename - Optional. Filename or PID of the script.
|
||||
* @param hostname - Optional. Name of host server the script is running on.
|
||||
* @param args - Arguments to identify the script
|
||||
* @returns info about a running script
|
||||
* @returns The info about the running script if found, and null otherwise.
|
||||
*/
|
||||
getRunningScript(filename?: FilenameOrPID, hostname?: string, ...args: (string | number)[]): RunningScript;
|
||||
|
||||
@ -6234,14 +6242,14 @@ export interface OfficeAPI {
|
||||
/**
|
||||
* Get the cost to unlock research
|
||||
* @param divisionName - Name of the division
|
||||
* @param cityName - Name of the city
|
||||
* @param researchName - Name of the research
|
||||
* @returns cost
|
||||
*/
|
||||
getResearchCost(divisionName: string, researchName: string): number;
|
||||
/**
|
||||
* Gets if you have unlocked a research
|
||||
* @param divisionName - Name of the division
|
||||
* @param cityName - Name of the city
|
||||
* @param researchName - Name of the research
|
||||
* @returns true is unlocked, false if not
|
||||
*/
|
||||
hasResearched(divisionName: string, researchName: string): boolean;
|
||||
@ -6310,6 +6318,14 @@ export interface WarehouseAPI {
|
||||
* @param enabled - smart supply enabled
|
||||
*/
|
||||
setSmartSupply(divisionName: string, cityName: string, enabled: boolean): void;
|
||||
/**
|
||||
* Set whether smart supply uses leftovers before buying
|
||||
* @param divisionName - Name of the division
|
||||
* @param cityName - Name of the city
|
||||
* @param materialName - Name of the material
|
||||
* @param enabled - smart supply use leftovers enabled
|
||||
*/
|
||||
setSmartSupplyUseLeftovers(divisionName: string, cityName: string, materialName: string, enabled: boolean): void;
|
||||
/**
|
||||
* Set material buy data
|
||||
* @param divisionName - Name of the division
|
||||
|
@ -33,6 +33,8 @@ import Typography from "@mui/material/Typography";
|
||||
import Link from "@mui/material/Link";
|
||||
import Box from "@mui/material/Box";
|
||||
import SettingsIcon from "@mui/icons-material/Settings";
|
||||
import SyncIcon from "@mui/icons-material/Sync";
|
||||
import CloseIcon from "@mui/icons-material/Close";
|
||||
import Table from "@mui/material/Table";
|
||||
import TableCell from "@mui/material/TableCell";
|
||||
import TableRow from "@mui/material/TableRow";
|
||||
@ -41,6 +43,7 @@ import { PromptEvent } from "../../ui/React/PromptManager";
|
||||
import { Modal } from "../../ui/React/Modal";
|
||||
|
||||
import libSource from "!!raw-loader!../NetscriptDefinitions.d.ts";
|
||||
import { Tooltip } from "@mui/material";
|
||||
|
||||
interface IProps {
|
||||
// Map of filename -> code
|
||||
@ -696,17 +699,58 @@ export function Root(props: IProps): React.ReactElement {
|
||||
}
|
||||
}
|
||||
|
||||
function onTabUpdate(index: number): void {
|
||||
const openScript = openScripts[index];
|
||||
const serverScriptCode = getServerCode(index);
|
||||
if (serverScriptCode === null) return;
|
||||
|
||||
if (openScript.code !== serverScriptCode) {
|
||||
PromptEvent.emit({
|
||||
txt:
|
||||
"Do you want to overwrite the current editor content with the contents of " +
|
||||
openScript.fileName +
|
||||
" on the server? This cannot be undone.",
|
||||
resolve: (result: boolean) => {
|
||||
if (result) {
|
||||
// Save changes
|
||||
openScript.code = serverScriptCode;
|
||||
|
||||
// Switch to target tab
|
||||
onTabClick(index);
|
||||
|
||||
if (editorRef.current !== null && openScript !== null) {
|
||||
if (openScript.model === undefined || openScript.model.isDisposed()) {
|
||||
regenerateModel(openScript);
|
||||
}
|
||||
editorRef.current.setModel(openScript.model);
|
||||
|
||||
editorRef.current.setValue(openScript.code);
|
||||
updateRAM(openScript.code);
|
||||
editorRef.current.focus();
|
||||
}
|
||||
}
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function dirty(index: number): string {
|
||||
const openScript = openScripts[index];
|
||||
const serverScriptCode = getServerCode(index);
|
||||
if (serverScriptCode === null) return " *";
|
||||
|
||||
// The server code is stored with its starting & trailing whitespace removed
|
||||
const openScriptFormatted = Script.formatCode(openScript.code);
|
||||
return serverScriptCode !== openScriptFormatted ? " *" : "";
|
||||
}
|
||||
|
||||
function getServerCode(index: number): string | null {
|
||||
const openScript = openScripts[index];
|
||||
const server = GetServer(openScript.hostname);
|
||||
if (server === null) throw new Error(`Server '${openScript.hostname}' should not be null, but it is.`);
|
||||
|
||||
const serverScript = server.scripts.find((s) => s.filename === openScript.fileName);
|
||||
if (serverScript === undefined) return " *";
|
||||
|
||||
// The server code is stored with its starting & trailing whitespace removed
|
||||
const openScriptFormatted = Script.formatCode(openScript.code);
|
||||
return serverScript.code !== openScriptFormatted ? " *" : "";
|
||||
return serverScript?.code ?? null;
|
||||
}
|
||||
|
||||
// Toolbars are roughly 112px:
|
||||
@ -738,62 +782,78 @@ export function Root(props: IProps): React.ReactElement {
|
||||
overflowX: "scroll",
|
||||
}}
|
||||
>
|
||||
{openScripts.map(({ fileName, hostname }, index) => (
|
||||
<Draggable
|
||||
key={fileName + hostname}
|
||||
draggableId={fileName + hostname}
|
||||
index={index}
|
||||
disableInteractiveElementBlocking={true}
|
||||
>
|
||||
{(provided) => (
|
||||
<div
|
||||
ref={provided.innerRef}
|
||||
{...provided.draggableProps}
|
||||
{...provided.dragHandleProps}
|
||||
style={{
|
||||
...provided.draggableProps.style,
|
||||
marginRight: "5px",
|
||||
flexShrink: 0,
|
||||
}}
|
||||
>
|
||||
<Button
|
||||
onClick={() => onTabClick(index)}
|
||||
style={
|
||||
currentScript?.fileName === openScripts[index].fileName
|
||||
? {
|
||||
background: Settings.theme.button,
|
||||
color: Settings.theme.primary,
|
||||
}
|
||||
: {
|
||||
background: Settings.theme.backgroundsecondary,
|
||||
color: Settings.theme.secondary,
|
||||
}
|
||||
}
|
||||
>
|
||||
{hostname}:~/{fileName} {dirty(index)}
|
||||
</Button>
|
||||
<Button
|
||||
onClick={() => onTabClose(index)}
|
||||
{openScripts.map(({ fileName, hostname }, index) => {
|
||||
const iconButtonStyle = {
|
||||
maxWidth: "25px",
|
||||
minWidth: "25px",
|
||||
minHeight: "38.5px",
|
||||
maxHeight: "38.5px",
|
||||
...(currentScript?.fileName === openScripts[index].fileName
|
||||
? {
|
||||
background: Settings.theme.button,
|
||||
borderColor: Settings.theme.button,
|
||||
color: Settings.theme.primary,
|
||||
}
|
||||
: {
|
||||
background: Settings.theme.backgroundsecondary,
|
||||
borderColor: Settings.theme.backgroundsecondary,
|
||||
color: Settings.theme.secondary,
|
||||
}),
|
||||
};
|
||||
return (
|
||||
<Draggable
|
||||
key={fileName + hostname}
|
||||
draggableId={fileName + hostname}
|
||||
index={index}
|
||||
disableInteractiveElementBlocking={true}
|
||||
>
|
||||
{(provided) => (
|
||||
<div
|
||||
ref={provided.innerRef}
|
||||
{...provided.draggableProps}
|
||||
{...provided.dragHandleProps}
|
||||
style={{
|
||||
maxWidth: "20px",
|
||||
minWidth: "20px",
|
||||
...(currentScript?.fileName === openScripts[index].fileName
|
||||
? {
|
||||
background: Settings.theme.button,
|
||||
color: Settings.theme.primary,
|
||||
}
|
||||
: {
|
||||
background: Settings.theme.backgroundsecondary,
|
||||
color: Settings.theme.secondary,
|
||||
}),
|
||||
...provided.draggableProps.style,
|
||||
marginRight: "5px",
|
||||
flexShrink: 0,
|
||||
border: "1px solid " + Settings.theme.well,
|
||||
}}
|
||||
>
|
||||
x
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</Draggable>
|
||||
))}
|
||||
<Button
|
||||
onClick={() => onTabClick(index)}
|
||||
onMouseDown={(e) => {
|
||||
e.preventDefault();
|
||||
if (e.button === 1) onTabClose(index);
|
||||
}}
|
||||
style={{
|
||||
...(currentScript?.fileName === openScripts[index].fileName
|
||||
? {
|
||||
background: Settings.theme.button,
|
||||
borderColor: Settings.theme.button,
|
||||
color: Settings.theme.primary,
|
||||
}
|
||||
: {
|
||||
background: Settings.theme.backgroundsecondary,
|
||||
borderColor: Settings.theme.backgroundsecondary,
|
||||
color: Settings.theme.secondary,
|
||||
}),
|
||||
}}
|
||||
>
|
||||
{hostname}:~/{fileName} {dirty(index)}
|
||||
</Button>
|
||||
<Tooltip title="Overwrite editor content with saved file content">
|
||||
<Button onClick={() => onTabUpdate(index)} style={iconButtonStyle}>
|
||||
<SyncIcon fontSize="small" />
|
||||
</Button>
|
||||
</Tooltip>
|
||||
<Button onClick={() => onTabClose(index)} style={iconButtonStyle}>
|
||||
<CloseIcon fontSize="small" />
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</Draggable>
|
||||
);
|
||||
})}
|
||||
{provided.placeholder}
|
||||
</Box>
|
||||
)}
|
||||
|
@ -30,7 +30,7 @@ export function getPurchaseServerCost(ram: number): number {
|
||||
|
||||
const upg = Math.max(0, Math.log(sanitizedRam) / Math.log(2) - 6);
|
||||
|
||||
return (
|
||||
return Math.round(
|
||||
sanitizedRam *
|
||||
CONSTANTS.BaseCostFor1GBOfRamServer *
|
||||
BitNodeMultipliers.PurchasedServerCost *
|
||||
|
@ -1,7 +1,7 @@
|
||||
import { ISelfInitializer, ISelfLoading } from "../types";
|
||||
import { OwnedAugmentationsOrderSetting, PurchaseAugmentationsOrderSetting } from "./SettingEnums";
|
||||
import { defaultTheme, ITheme } from "./Themes";
|
||||
import { defaultStyles } from "./Styles";
|
||||
import { defaultTheme, ITheme } from "../Themes/Themes";
|
||||
import { defaultStyles } from "../Themes/Styles";
|
||||
import { WordWrapOptions } from "../ScriptEditor/ui/Options";
|
||||
import { OverviewSettings } from "../ui/React/Overview";
|
||||
import { IStyleSettings } from "../ScriptEditor/NetscriptDefinitions";
|
||||
|
@ -1,613 +0,0 @@
|
||||
import { IMap } from "../types";
|
||||
|
||||
export interface ITheme {
|
||||
[key: string]: string | undefined;
|
||||
primarylight: string;
|
||||
primary: string;
|
||||
primarydark: string;
|
||||
successlight: string;
|
||||
success: string;
|
||||
successdark: string;
|
||||
errorlight: string;
|
||||
error: string;
|
||||
errordark: string;
|
||||
secondarylight: string;
|
||||
secondary: string;
|
||||
secondarydark: string;
|
||||
warninglight: string;
|
||||
warning: string;
|
||||
warningdark: string;
|
||||
infolight: string;
|
||||
info: string;
|
||||
infodark: string;
|
||||
welllight: string;
|
||||
well: string;
|
||||
white: string;
|
||||
black: string;
|
||||
hp: string;
|
||||
money: string;
|
||||
hack: string;
|
||||
combat: string;
|
||||
cha: string;
|
||||
int: string;
|
||||
rep: string;
|
||||
disabled: string;
|
||||
backgroundprimary: string;
|
||||
backgroundsecondary: string;
|
||||
button: string;
|
||||
}
|
||||
|
||||
export interface IPredefinedTheme {
|
||||
colors: ITheme;
|
||||
name?: string;
|
||||
credit?: string;
|
||||
description?: string;
|
||||
reference?: string;
|
||||
}
|
||||
|
||||
export const defaultTheme: ITheme = {
|
||||
primarylight: "#0f0",
|
||||
primary: "#0c0",
|
||||
primarydark: "#090",
|
||||
successlight: "#0f0",
|
||||
success: "#0c0",
|
||||
successdark: "#090",
|
||||
errorlight: "#f00",
|
||||
error: "#c00",
|
||||
errordark: "#900",
|
||||
secondarylight: "#AAA",
|
||||
secondary: "#888",
|
||||
secondarydark: "#666",
|
||||
warninglight: "#ff0",
|
||||
warning: "#cc0",
|
||||
warningdark: "#990",
|
||||
infolight: "#69f",
|
||||
info: "#36c",
|
||||
infodark: "#039",
|
||||
welllight: "#444",
|
||||
well: "#222",
|
||||
white: "#fff",
|
||||
black: "#000",
|
||||
hp: "#dd3434",
|
||||
money: "#ffd700",
|
||||
hack: "#adff2f",
|
||||
combat: "#faffdf",
|
||||
cha: "#a671d1",
|
||||
int: "#6495ed",
|
||||
rep: "#faffdf",
|
||||
disabled: "#66cfbc",
|
||||
backgroundprimary: "#000",
|
||||
backgroundsecondary: "#000",
|
||||
button: "#333",
|
||||
};
|
||||
|
||||
export const getPredefinedThemes = (): IMap<IPredefinedTheme> => ({
|
||||
Default: {
|
||||
colors: defaultTheme,
|
||||
},
|
||||
Monokai: {
|
||||
description: "Monokai'ish",
|
||||
colors: {
|
||||
primarylight: "#FFF",
|
||||
primary: "#F8F8F2",
|
||||
primarydark: "#FAFAEB",
|
||||
successlight: "#ADE146",
|
||||
success: "#A6E22E",
|
||||
successdark: "#98E104",
|
||||
errorlight: "#FF69A0",
|
||||
error: "#F92672",
|
||||
errordark: "#D10F56",
|
||||
secondarylight: "#AAA",
|
||||
secondary: "#888",
|
||||
secondarydark: "#666",
|
||||
warninglight: "#E1D992",
|
||||
warning: "#E6DB74",
|
||||
warningdark: "#EDDD54",
|
||||
infolight: "#92E1F1",
|
||||
info: "#66D9EF",
|
||||
infodark: "#31CDED",
|
||||
welllight: "#444",
|
||||
well: "#222",
|
||||
white: "#fff",
|
||||
black: "#000",
|
||||
hp: "#F92672",
|
||||
money: "#E6DB74",
|
||||
hack: "#A6E22E",
|
||||
combat: "#75715E",
|
||||
cha: "#AE81FF",
|
||||
int: "#66D9EF",
|
||||
rep: "#E69F66",
|
||||
disabled: "#66cfbc",
|
||||
backgroundprimary: "#272822",
|
||||
backgroundsecondary: "#1B1C18",
|
||||
button: "#333",
|
||||
},
|
||||
},
|
||||
|
||||
Warmer: {
|
||||
credit: "hexnaught",
|
||||
description: "Warmer, softer theme",
|
||||
reference: "https://discord.com/channels/415207508303544321/921991895230611466/921999581020028938",
|
||||
colors: {
|
||||
primarylight: "#EA9062",
|
||||
primary: "#DD7B4A",
|
||||
primarydark: "#D3591C",
|
||||
successlight: "#6ACF6A",
|
||||
success: "#43BF43",
|
||||
successdark: "#3E913E",
|
||||
errorlight: "#C15757",
|
||||
error: "#B34141",
|
||||
errordark: "#752525",
|
||||
secondarylight: "#AAA",
|
||||
secondary: "#888",
|
||||
secondarydark: "#666",
|
||||
warninglight: "#E6E69D",
|
||||
warning: "#DADA56",
|
||||
warningdark: "#A1A106",
|
||||
infolight: "#69f",
|
||||
info: "#36c",
|
||||
infodark: "#039",
|
||||
welllight: "#444",
|
||||
well: "#222",
|
||||
white: "#fff",
|
||||
black: "#000",
|
||||
hp: "#dd3434",
|
||||
money: "#ffd700",
|
||||
hack: "#adff2f",
|
||||
combat: "#faffdf",
|
||||
cha: "#AD84CF",
|
||||
int: "#6495ed",
|
||||
rep: "#faffdf",
|
||||
disabled: "#76C6B7",
|
||||
backgroundprimary: "#000",
|
||||
backgroundsecondary: "#000",
|
||||
button: "#333",
|
||||
},
|
||||
},
|
||||
|
||||
"Dark+": {
|
||||
credit: "LoganMD",
|
||||
description: "VSCode Dark+",
|
||||
reference: "https://discord.com/channels/415207508303544321/921991895230611466/921999975867617310",
|
||||
colors: {
|
||||
primarylight: "#E0E0BC",
|
||||
primary: "#CCCCAE",
|
||||
primarydark: "#B8B89C",
|
||||
successlight: "#00F000",
|
||||
success: "#00D200",
|
||||
successdark: "#00B400",
|
||||
errorlight: "#F00000",
|
||||
error: "#C80000",
|
||||
errordark: "#A00000",
|
||||
secondarylight: "#B4AEAE",
|
||||
secondary: "#969090",
|
||||
secondarydark: "#787272",
|
||||
warninglight: "#F0F000",
|
||||
warning: "#C8C800",
|
||||
warningdark: "#A0A000",
|
||||
infolight: "#69f",
|
||||
info: "#36c",
|
||||
infodark: "#039",
|
||||
welllight: "#444",
|
||||
well: "#222",
|
||||
white: "#fff",
|
||||
black: "#1E1E1E",
|
||||
hp: "#dd3434",
|
||||
money: "#ffd700",
|
||||
hack: "#adff2f",
|
||||
combat: "#faffdf",
|
||||
cha: "#a671d1",
|
||||
int: "#6495ed",
|
||||
rep: "#faffdf",
|
||||
disabled: "#66cfbc",
|
||||
backgroundprimary: "#1E1E1E",
|
||||
backgroundsecondary: "#252525",
|
||||
button: "#333",
|
||||
},
|
||||
},
|
||||
|
||||
"Mayukai Dark": {
|
||||
credit: "Festive Noire",
|
||||
description: "Mayukai Dark-esque",
|
||||
reference: "https://discord.com/channels/415207508303544321/921991895230611466/922037502334889994",
|
||||
colors: {
|
||||
primarylight: "#DDDFC5",
|
||||
primary: "#CDCFB6",
|
||||
primarydark: "#9D9F8C",
|
||||
successlight: "#00EF00",
|
||||
success: "#00A500",
|
||||
successdark: "#007A00",
|
||||
errorlight: "#F92672",
|
||||
error: "#CA1C5C",
|
||||
errordark: "#90274A",
|
||||
secondarylight: "#AAA",
|
||||
secondary: "#888",
|
||||
secondarydark: "#666",
|
||||
warninglight: "#D3D300",
|
||||
warning: "#cc0",
|
||||
warningdark: "#990",
|
||||
infolight: "#69f",
|
||||
info: "#36c",
|
||||
infodark: "#039",
|
||||
welllight: "#444",
|
||||
well: "#00010A",
|
||||
white: "#fff",
|
||||
black: "#020509",
|
||||
hp: "#dd3434",
|
||||
money: "#ffd700",
|
||||
hack: "#8CCF27",
|
||||
combat: "#faffdf",
|
||||
cha: "#a671d1",
|
||||
int: "#6495ed",
|
||||
rep: "#faffdf",
|
||||
disabled: "#66cfbc",
|
||||
backgroundprimary: "#080C11",
|
||||
backgroundsecondary: "#03080F",
|
||||
button: "#00010A",
|
||||
},
|
||||
},
|
||||
|
||||
Purple: {
|
||||
credit: "zer0ney",
|
||||
description: "Essentially all defaults except for purple replacing the main colors",
|
||||
reference: "https://discord.com/channels/415207508303544321/921991895230611466/922091815849570395",
|
||||
colors: {
|
||||
primarylight: "#BA55D3",
|
||||
primary: "#9370DB",
|
||||
primarydark: "#8A2BE2",
|
||||
successlight: "#BA55D3",
|
||||
success: "#9370DB",
|
||||
successdark: "#8A2BE2",
|
||||
errorlight: "#f00",
|
||||
error: "#c00",
|
||||
errordark: "#900",
|
||||
secondarylight: "#AAA",
|
||||
secondary: "#888",
|
||||
secondarydark: "#666",
|
||||
warninglight: "#ff0",
|
||||
warning: "#cc0",
|
||||
warningdark: "#990",
|
||||
infolight: "#69f",
|
||||
info: "#36c",
|
||||
infodark: "#039",
|
||||
welllight: "#444",
|
||||
well: "#222",
|
||||
white: "#fff",
|
||||
black: "#000",
|
||||
hp: "#dd3434",
|
||||
money: "#ffd700",
|
||||
hack: "#adff2f",
|
||||
combat: "#faffdf",
|
||||
cha: "#a671d1",
|
||||
int: "#6495ed",
|
||||
rep: "#faffdf",
|
||||
disabled: "#66cfbc",
|
||||
backgroundprimary: "#000",
|
||||
backgroundsecondary: "#000",
|
||||
button: "#333",
|
||||
},
|
||||
},
|
||||
|
||||
"Smooth Green": {
|
||||
credit: "Swidt",
|
||||
description: "A nice green theme that doesn't hurt your eyes.",
|
||||
reference: "https://discord.com/channels/415207508303544321/921991895230611466/922243957986033725",
|
||||
colors: {
|
||||
primarylight: "#E0E0BC",
|
||||
primary: "#B0D9A3",
|
||||
primarydark: "#B8B89C",
|
||||
successlight: "#00F000",
|
||||
success: "#6BC16B",
|
||||
successdark: "#00B400",
|
||||
errorlight: "#F00000",
|
||||
error: "#3D713D",
|
||||
errordark: "#A00000",
|
||||
secondarylight: "#B4AEAE",
|
||||
secondary: "#8FAF85",
|
||||
secondarydark: "#787272",
|
||||
warninglight: "#F0F000",
|
||||
warning: "#38F100",
|
||||
warningdark: "#A0A000",
|
||||
infolight: "#69f",
|
||||
info: "#36c",
|
||||
infodark: "#039",
|
||||
welllight: "#444",
|
||||
well: "#2F3C2B",
|
||||
white: "#fff",
|
||||
black: "#1E1E1E",
|
||||
hp: "#dd3434",
|
||||
money: "#4AA52E",
|
||||
hack: "#adff2f",
|
||||
combat: "#faffdf",
|
||||
cha: "#a671d1",
|
||||
int: "#6495ed",
|
||||
rep: "#35A135",
|
||||
disabled: "#66cfbc",
|
||||
backgroundprimary: "#1E1E1E",
|
||||
backgroundsecondary: "#252525",
|
||||
button: "#2F3C2B",
|
||||
},
|
||||
},
|
||||
|
||||
Dracula: {
|
||||
credit: "H3draut3r",
|
||||
reference: "https://discord.com/channels/415207508303544321/921991895230611466/922296307836678144",
|
||||
colors: {
|
||||
primarylight: "#7082B8",
|
||||
primary: "#F8F8F2",
|
||||
primarydark: "#FF79C6",
|
||||
successlight: "#0f0",
|
||||
success: "#0c0",
|
||||
successdark: "#090",
|
||||
errorlight: "#FD4545",
|
||||
error: "#FF2D2D",
|
||||
errordark: "#C62424",
|
||||
secondarylight: "#AAA",
|
||||
secondary: "#8BE9FD",
|
||||
secondarydark: "#666",
|
||||
warninglight: "#FFC281",
|
||||
warning: "#FFB86C",
|
||||
warningdark: "#E6A055",
|
||||
infolight: "#A0A0FF",
|
||||
info: "#7070FF",
|
||||
infodark: "#4040FF",
|
||||
welllight: "#44475A",
|
||||
well: "#363948",
|
||||
white: "#fff",
|
||||
black: "#282A36",
|
||||
hp: "#D34448",
|
||||
money: "#50FA7B",
|
||||
hack: "#F1FA8C",
|
||||
combat: "#BD93F9",
|
||||
cha: "#FF79C6",
|
||||
int: "#6495ed",
|
||||
rep: "#faffdf",
|
||||
disabled: "#66cfbc",
|
||||
backgroundprimary: "#282A36",
|
||||
backgroundsecondary: "#21222C",
|
||||
button: "#21222C",
|
||||
},
|
||||
},
|
||||
|
||||
"Dark Blue": {
|
||||
credit: "Saynt_Garmo",
|
||||
reference: "https://discord.com/channels/415207508303544321/921991895230611466/923084732718264340",
|
||||
colors: {
|
||||
primarylight: "#023DDE",
|
||||
primary: "#4A41C8",
|
||||
primarydark: "#005299",
|
||||
successlight: "#00FF00",
|
||||
success: "#D1DAD1",
|
||||
successdark: "#BFCABF",
|
||||
errorlight: "#f00",
|
||||
error: "#c00",
|
||||
errordark: "#900",
|
||||
secondarylight: "#AAA",
|
||||
secondary: "#888",
|
||||
secondarydark: "#666",
|
||||
warninglight: "#ff0",
|
||||
warning: "#cc0",
|
||||
warningdark: "#990",
|
||||
infolight: "#69f",
|
||||
info: "#36c",
|
||||
infodark: "#039",
|
||||
welllight: "#444",
|
||||
well: "#040505",
|
||||
white: "#fff",
|
||||
black: "#000000",
|
||||
hp: "#dd3434",
|
||||
money: "#ffd700",
|
||||
hack: "#adff2f",
|
||||
combat: "#faffdf",
|
||||
cha: "#a671d1",
|
||||
int: "#6495ed",
|
||||
rep: "#faffdf",
|
||||
disabled: "#66cfbc",
|
||||
backgroundprimary: "#091419",
|
||||
backgroundsecondary: "#000000",
|
||||
button: "#000000",
|
||||
},
|
||||
},
|
||||
|
||||
Discord: {
|
||||
credit: "Thermite",
|
||||
description: "Discord inspired theme",
|
||||
reference: "https://discord.com/channels/415207508303544321/921991895230611466/924305252017143818",
|
||||
colors: {
|
||||
primarylight: "#7389DC",
|
||||
primary: "#7389DC",
|
||||
primarydark: "#5964F1",
|
||||
successlight: "#00CC00",
|
||||
success: "#20DF20",
|
||||
successdark: "#0CB80C",
|
||||
errorlight: "#EA5558",
|
||||
error: "#EC4145",
|
||||
errordark: "#E82528",
|
||||
secondarylight: "#C3C3C3",
|
||||
secondary: "#9C9C9C",
|
||||
secondarydark: "#4E4E4E",
|
||||
warninglight: "#ff0",
|
||||
warning: "#cc0",
|
||||
warningdark: "#990",
|
||||
infolight: "#69f",
|
||||
info: "#36c",
|
||||
infodark: "#1C4FB3",
|
||||
welllight: "#999999",
|
||||
well: "#35383C",
|
||||
white: "#FFFFFF",
|
||||
black: "#202225",
|
||||
hp: "#FF5656",
|
||||
money: "#43FF43",
|
||||
hack: "#FFAB3D",
|
||||
combat: "#8A90FD",
|
||||
cha: "#FF51D9",
|
||||
int: "#6495ed",
|
||||
rep: "#FFFF30",
|
||||
disabled: "#474B51",
|
||||
backgroundprimary: "#2F3136",
|
||||
backgroundsecondary: "#35393E",
|
||||
button: "#333",
|
||||
},
|
||||
},
|
||||
|
||||
"One Dark": {
|
||||
credit: "Dexalt142",
|
||||
reference: "https://discord.com/channels/415207508303544321/921991895230611466/924650660694208512",
|
||||
colors: {
|
||||
primarylight: "#98C379",
|
||||
primary: "#98C379",
|
||||
primarydark: "#98C379",
|
||||
successlight: "#98C379",
|
||||
success: "#98C379",
|
||||
successdark: "#98C379",
|
||||
errorlight: "#E06C75",
|
||||
error: "#BE5046",
|
||||
errordark: "#BE5046",
|
||||
secondarylight: "#AAA",
|
||||
secondary: "#888",
|
||||
secondarydark: "#666",
|
||||
warninglight: "#E5C07B",
|
||||
warning: "#E5C07B",
|
||||
warningdark: "#D19A66",
|
||||
infolight: "#61AFEF",
|
||||
info: "#61AFEF",
|
||||
infodark: "#61AFEF",
|
||||
welllight: "#4B5263",
|
||||
well: "#282C34",
|
||||
white: "#ABB2BF",
|
||||
black: "#282C34",
|
||||
hp: "#E06C75",
|
||||
money: "#E5C07B",
|
||||
hack: "#98C379",
|
||||
combat: "#ABB2BF",
|
||||
cha: "#C678DD",
|
||||
int: "#61AFEF",
|
||||
rep: "#ABB2BF",
|
||||
disabled: "#56B6C2",
|
||||
backgroundprimary: "#282C34",
|
||||
backgroundsecondary: "#21252B",
|
||||
button: "#4B5263",
|
||||
},
|
||||
},
|
||||
|
||||
"Muted Gold & Blue": {
|
||||
credit: "Sloth",
|
||||
reference: "https://discord.com/channels/415207508303544321/921991895230611466/924672660758208563",
|
||||
colors: {
|
||||
primarylight: "#E3B54A",
|
||||
primary: "#CAA243",
|
||||
primarydark: "#7E6937",
|
||||
successlight: "#82FF82",
|
||||
success: "#6FDA6F",
|
||||
successdark: "#64C364",
|
||||
errorlight: "#FD5555",
|
||||
error: "#D84A4A",
|
||||
errordark: "#AC3939",
|
||||
secondarylight: "#D8D0B8",
|
||||
secondary: "#B1AA95",
|
||||
secondarydark: "#736E5E",
|
||||
warninglight: "#ff0",
|
||||
warning: "#cc0",
|
||||
warningdark: "#990",
|
||||
infolight: "#69f",
|
||||
info: "#36c",
|
||||
infodark: "#039",
|
||||
welllight: "#444",
|
||||
well: "#111111",
|
||||
white: "#fff",
|
||||
black: "#070300",
|
||||
hp: "#dd3434",
|
||||
money: "#ffd700",
|
||||
hack: "#adff2f",
|
||||
combat: "#faffdf",
|
||||
cha: "#a671d1",
|
||||
int: "#6495ed",
|
||||
rep: "#faffdf",
|
||||
disabled: "#66cfbc",
|
||||
backgroundprimary: "#0A0A0E",
|
||||
backgroundsecondary: "#0E0E10",
|
||||
button: "#222222",
|
||||
},
|
||||
},
|
||||
|
||||
"Default Lite": {
|
||||
credit: "NmuGmu",
|
||||
description: "Less eye-straining default theme",
|
||||
reference: "https://discord.com/channels/415207508303544321/921991895230611466/925263801564151888",
|
||||
colors: {
|
||||
primarylight: "#28CF28",
|
||||
primary: "#21A821",
|
||||
primarydark: "#177317",
|
||||
successlight: "#1CFF1C",
|
||||
success: "#16CA16",
|
||||
successdark: "#0D910D",
|
||||
errorlight: "#FF3B3B",
|
||||
error: "#C32D2D",
|
||||
errordark: "#8E2121",
|
||||
secondarylight: "#B3B3B3",
|
||||
secondary: "#838383",
|
||||
secondarydark: "#676767",
|
||||
warninglight: "#FFFF3A",
|
||||
warning: "#C3C32A",
|
||||
warningdark: "#8C8C1E",
|
||||
infolight: "#64CBFF",
|
||||
info: "#3399CC",
|
||||
infodark: "#246D91",
|
||||
welllight: "#404040",
|
||||
well: "#1C1C1C",
|
||||
white: "#C3C3C3",
|
||||
black: "#0A0B0B",
|
||||
hp: "#C62E2E",
|
||||
money: "#D6BB27",
|
||||
hack: "#ADFF2F",
|
||||
combat: "#E8EDCD",
|
||||
cha: "#8B5FAF",
|
||||
int: "#537CC8",
|
||||
rep: "#E8EDCD",
|
||||
disabled: "#5AB5A5",
|
||||
backgroundprimary: "#0C0D0E",
|
||||
backgroundsecondary: "#121415",
|
||||
button: "#252829",
|
||||
},
|
||||
},
|
||||
|
||||
Light: {
|
||||
credit: "matt",
|
||||
reference: "https://discord.com/channels/415207508303544321/921991895230611466/926114005456658432",
|
||||
colors: {
|
||||
primarylight: "#535353",
|
||||
primary: "#1A1A1A",
|
||||
primarydark: "#0d0d0d",
|
||||
successlight: "#63c439",
|
||||
success: "#428226",
|
||||
successdark: "#2E5A1B",
|
||||
errorlight: "#df7051",
|
||||
error: "#C94824",
|
||||
errordark: "#91341B",
|
||||
secondarylight: "#b3b3b3",
|
||||
secondary: "#9B9B9B",
|
||||
secondarydark: "#7A7979",
|
||||
warninglight: "#e8d464",
|
||||
warning: "#C6AD20",
|
||||
warningdark: "#9F8A16",
|
||||
infolight: "#6299cf",
|
||||
info: "#3778B7",
|
||||
infodark: "#30689C",
|
||||
welllight: "#f9f9f9",
|
||||
well: "#eaeaea",
|
||||
white: "#F7F7F7",
|
||||
black: "#F7F7F7",
|
||||
hp: "#BF5C41",
|
||||
money: "#E1B121",
|
||||
hack: "#47BC38",
|
||||
combat: "#656262",
|
||||
cha: "#A568AC",
|
||||
int: "#889BCF",
|
||||
rep: "#656262",
|
||||
disabled: "#70B4BF",
|
||||
backgroundprimary: "#F7F7F7",
|
||||
backgroundsecondary: "#f9f9f9",
|
||||
button: "#eaeaea",
|
||||
},
|
||||
},
|
||||
});
|
@ -21,6 +21,7 @@ export const TerminalHelpText: string[] = [
|
||||
" grow Spoof money in a servers bank account, increasing the amount available.",
|
||||
" hack Hack the current machine",
|
||||
" help [command] Display this help text, or the help text for a command",
|
||||
" history [-c] Display the terminal history",
|
||||
" home Connect to home computer",
|
||||
" hostname Displays the hostname of the machine",
|
||||
" kill [script/pid] [args...] Stops the specified script on the current server ",
|
||||
@ -255,6 +256,12 @@ export const HelpTexts: IMap<string[]> = {
|
||||
" help scan-analyze",
|
||||
" ",
|
||||
],
|
||||
history: [
|
||||
"Usage: history [-c]",
|
||||
" ",
|
||||
"Without arguments, displays the terminal command history. To clear the history, pass in the '-c' argument.",
|
||||
" ",
|
||||
],
|
||||
home: [
|
||||
"Usage: home", " ", "Connect to your home computer. This will work no matter what server you are currently connected to.", " ",
|
||||
],
|
||||
|
@ -48,6 +48,7 @@ import { free } from "./commands/free";
|
||||
import { grow } from "./commands/grow";
|
||||
import { hack } from "./commands/hack";
|
||||
import { help } from "./commands/help";
|
||||
import { history } from "./commands/history";
|
||||
import { home } from "./commands/home";
|
||||
import { hostname } from "./commands/hostname";
|
||||
import { kill } from "./commands/kill";
|
||||
@ -143,7 +144,7 @@ export class Terminal implements ITerminal {
|
||||
startGrow(player: IPlayer): void {
|
||||
const server = player.getCurrentServer();
|
||||
if (server instanceof HacknetServer) {
|
||||
this.error("Cannot hack this kind of server");
|
||||
this.error("Cannot grow this kind of server");
|
||||
return;
|
||||
}
|
||||
if (!(server instanceof Server)) throw new Error("server should be normal server");
|
||||
@ -152,7 +153,7 @@ export class Terminal implements ITerminal {
|
||||
startWeaken(player: IPlayer): void {
|
||||
const server = player.getCurrentServer();
|
||||
if (server instanceof HacknetServer) {
|
||||
this.error("Cannot hack this kind of server");
|
||||
this.error("Cannot weaken this kind of server");
|
||||
return;
|
||||
}
|
||||
if (!(server instanceof Server)) throw new Error("server should be normal server");
|
||||
@ -241,7 +242,7 @@ export class Terminal implements ITerminal {
|
||||
if (cancelled) return;
|
||||
|
||||
if (server instanceof HacknetServer) {
|
||||
this.error("Cannot hack this kind of server");
|
||||
this.error("Cannot grow this kind of server");
|
||||
return;
|
||||
}
|
||||
if (!(server instanceof Server)) throw new Error("server should be normal server");
|
||||
@ -268,7 +269,7 @@ export class Terminal implements ITerminal {
|
||||
if (cancelled) return;
|
||||
|
||||
if (server instanceof HacknetServer) {
|
||||
this.error("Cannot hack this kind of server");
|
||||
this.error("Cannot weaken this kind of server");
|
||||
return;
|
||||
}
|
||||
if (!(server instanceof Server)) throw new Error("server should be normal server");
|
||||
@ -576,6 +577,7 @@ export class Terminal implements ITerminal {
|
||||
if (this.commandHistory.length > 50) {
|
||||
this.commandHistory.splice(0, 1);
|
||||
}
|
||||
player.terminalCommandHistory = this.commandHistory;
|
||||
}
|
||||
this.commandHistoryIndex = this.commandHistory.length;
|
||||
const allCommands = ParseCommands(commands);
|
||||
@ -785,6 +787,7 @@ export class Terminal implements ITerminal {
|
||||
grow: grow,
|
||||
hack: hack,
|
||||
help: help,
|
||||
history: history,
|
||||
home: home,
|
||||
hostname: hostname,
|
||||
kill: kill,
|
||||
|
@ -6,6 +6,37 @@ import { isScriptFilename } from "../../Script/isScriptFilename";
|
||||
import FileSaver from "file-saver";
|
||||
import JSZip from "jszip";
|
||||
|
||||
export function exportScripts(pattern: string, server: BaseServer): void {
|
||||
const matchEnding = pattern.length == 1 || pattern === "*.*" ? null : pattern.slice(1); // Treat *.* the same as *
|
||||
const zip = new JSZip();
|
||||
// Helper function to zip any file contents whose name matches the pattern
|
||||
const zipFiles = (fileNames: string[], fileContents: string[]): void => {
|
||||
for (let i = 0; i < fileContents.length; ++i) {
|
||||
let name = fileNames[i];
|
||||
if (name.startsWith("/")) name = name.slice(1);
|
||||
if (!matchEnding || name.endsWith(matchEnding))
|
||||
zip.file(name, new Blob([fileContents[i]], { type: "text/plain" }));
|
||||
}
|
||||
};
|
||||
// In the case of script files, we pull from the server.scripts array
|
||||
if (!matchEnding || isScriptFilename(matchEnding))
|
||||
zipFiles(
|
||||
server.scripts.map((s) => s.filename),
|
||||
server.scripts.map((s) => s.code),
|
||||
);
|
||||
// In the case of text files, we pull from the server.scripts array
|
||||
if (!matchEnding || matchEnding.endsWith(".txt"))
|
||||
zipFiles(
|
||||
server.textFiles.map((s) => s.fn),
|
||||
server.textFiles.map((s) => s.text),
|
||||
);
|
||||
|
||||
// Return an error if no files matched, rather than an empty zip folder
|
||||
if (Object.keys(zip.files).length == 0) throw new Error(`No files match the pattern ${pattern}`);
|
||||
const zipFn = `bitburner${isScriptFilename(pattern) ? "Scripts" : pattern === "*.txt" ? "Texts" : "Files"}.zip`;
|
||||
zip.generateAsync({ type: "blob" }).then((content: any) => FileSaver.saveAs(content, zipFn));
|
||||
}
|
||||
|
||||
export function download(
|
||||
terminal: ITerminal,
|
||||
router: IRouter,
|
||||
@ -21,34 +52,12 @@ export function download(
|
||||
const fn = args[0] + "";
|
||||
// If the parameter starts with *, download all files that match the wildcard pattern
|
||||
if (fn.startsWith("*")) {
|
||||
const matchEnding = fn.length == 1 || fn === "*.*" ? null : fn.slice(1); // Treat *.* the same as *
|
||||
const zip = new JSZip();
|
||||
// Helper function to zip any file contents whose name matches the pattern
|
||||
const zipFiles = (fileNames: string[], fileContents: string[]): void => {
|
||||
for (let i = 0; i < fileContents.length; ++i) {
|
||||
let name = fileNames[i];
|
||||
if (name.startsWith("/")) name = name.slice(1);
|
||||
if (!matchEnding || name.endsWith(matchEnding))
|
||||
zip.file(name, new Blob([fileContents[i]], { type: "text/plain" }));
|
||||
}
|
||||
};
|
||||
// In the case of script files, we pull from the server.scripts array
|
||||
if (!matchEnding || isScriptFilename(matchEnding))
|
||||
zipFiles(
|
||||
server.scripts.map((s) => s.filename),
|
||||
server.scripts.map((s) => s.code),
|
||||
);
|
||||
// In the case of text files, we pull from the server.scripts array
|
||||
if (!matchEnding || matchEnding.endsWith(".txt"))
|
||||
zipFiles(
|
||||
server.textFiles.map((s) => s.fn),
|
||||
server.textFiles.map((s) => s.text),
|
||||
);
|
||||
// Return an error if no files matched, rather than an empty zip folder
|
||||
if (Object.keys(zip.files).length == 0) return terminal.error(`No files match the pattern ${fn}`);
|
||||
const zipFn = `bitburner${isScriptFilename(fn) ? "Scripts" : fn === "*.txt" ? "Texts" : "Files"}.zip`;
|
||||
zip.generateAsync({ type: "blob" }).then((content: any) => FileSaver.saveAs(content, zipFn));
|
||||
return;
|
||||
try {
|
||||
exportScripts(fn, server);
|
||||
return;
|
||||
} catch (error: any) {
|
||||
return terminal.error(error.message);
|
||||
}
|
||||
} else if (isScriptFilename(fn)) {
|
||||
// Download a single script
|
||||
const script = terminal.getScript(player, fn);
|
||||
|
27
src/Terminal/commands/history.ts
Normal file
27
src/Terminal/commands/history.ts
Normal file
@ -0,0 +1,27 @@
|
||||
import { ITerminal } from "../ITerminal";
|
||||
import { IRouter } from "../../ui/Router";
|
||||
import { IPlayer } from "../../PersonObjects/IPlayer";
|
||||
import { BaseServer } from "../../Server/BaseServer";
|
||||
|
||||
export function history(
|
||||
terminal: ITerminal,
|
||||
router: IRouter,
|
||||
player: IPlayer,
|
||||
server: BaseServer,
|
||||
args: (string | number | boolean)[],
|
||||
): void {
|
||||
if (args.length === 0) {
|
||||
terminal.commandHistory.forEach((command, index) => {
|
||||
terminal.print(`${index.toString().padStart(2)} ${command}`);
|
||||
});
|
||||
return;
|
||||
}
|
||||
const arg = args[0] + "";
|
||||
if (arg === "-c" || arg === "--clear") {
|
||||
player.terminalCommandHistory = [];
|
||||
terminal.commandHistory = [];
|
||||
terminal.commandHistoryIndex = 1;
|
||||
} else {
|
||||
terminal.error("Incorrect usage of history command. usage: history [-c]");
|
||||
}
|
||||
}
|
@ -52,6 +52,12 @@ export function TerminalInput({ terminal, router, player }: IProps): React.React
|
||||
const [possibilities, setPossibilities] = useState<string[]>([]);
|
||||
const classes = useStyles();
|
||||
|
||||
// If we have no data in the current terminal history, let's initialize it from the player save
|
||||
if (terminal.commandHistory.length === 0 && player.terminalCommandHistory.length > 0) {
|
||||
terminal.commandHistory = player.terminalCommandHistory;
|
||||
terminal.commandHistoryIndex = terminal.commandHistory.length;
|
||||
}
|
||||
|
||||
// Need to run after state updates, for example if we need to move cursor
|
||||
// *after* we modify input
|
||||
useEffect(() => {
|
||||
|
18
src/Themes/README.md
Normal file
18
src/Themes/README.md
Normal file
@ -0,0 +1,18 @@
|
||||
# Themes
|
||||
|
||||
Feel free to contribute a new theme by submitting a pull request to the game!
|
||||
|
||||
See [CONTRIBUTING.md](/doc/CONTRIBUTING.md) for details.
|
||||
|
||||
## How create a new theme
|
||||
|
||||
1. Duplicate one of the folders in `/src/Themes/data` and give it a new name (keep the hyphenated format)
|
||||
2. Modify the data in the new `/src/Themes/data/new-folder/index.ts` file
|
||||
3. Replace the screenshot.png with one of your theme
|
||||
4. Add the import/export into the `/src/Themes/data/index.ts` file
|
||||
|
||||
The themes are ordered according to the export order in `index.ts`
|
||||
|
||||
## Other resources
|
||||
|
||||
There is an external script called `theme-browser` which may include more themes than those shown here. Head over the [bitpacker](https://github.com/davidsiems/bitpacker) repository for details.
|
56
src/Themes/Themes.ts
Normal file
56
src/Themes/Themes.ts
Normal file
@ -0,0 +1,56 @@
|
||||
import { IMap } from "../types";
|
||||
import * as predefined from "./data";
|
||||
|
||||
export interface ITheme {
|
||||
[key: string]: string | undefined;
|
||||
primarylight: string;
|
||||
primary: string;
|
||||
primarydark: string;
|
||||
successlight: string;
|
||||
success: string;
|
||||
successdark: string;
|
||||
errorlight: string;
|
||||
error: string;
|
||||
errordark: string;
|
||||
secondarylight: string;
|
||||
secondary: string;
|
||||
secondarydark: string;
|
||||
warninglight: string;
|
||||
warning: string;
|
||||
warningdark: string;
|
||||
infolight: string;
|
||||
info: string;
|
||||
infodark: string;
|
||||
welllight: string;
|
||||
well: string;
|
||||
white: string;
|
||||
black: string;
|
||||
hp: string;
|
||||
money: string;
|
||||
hack: string;
|
||||
combat: string;
|
||||
cha: string;
|
||||
int: string;
|
||||
rep: string;
|
||||
disabled: string;
|
||||
backgroundprimary: string;
|
||||
backgroundsecondary: string;
|
||||
button: string;
|
||||
}
|
||||
|
||||
export interface IPredefinedTheme {
|
||||
colors: ITheme;
|
||||
name: string;
|
||||
credit: string;
|
||||
screenshot: string;
|
||||
description: string;
|
||||
reference?: string;
|
||||
}
|
||||
|
||||
export const defaultTheme: ITheme = {
|
||||
...predefined.Default.colors,
|
||||
};
|
||||
|
||||
export const getPredefinedThemes = (): IMap<IPredefinedTheme> => ({
|
||||
...predefined,
|
||||
});
|
45
src/Themes/data/dark-blue/index.ts
Normal file
45
src/Themes/data/dark-blue/index.ts
Normal file
@ -0,0 +1,45 @@
|
||||
import { IPredefinedTheme } from "../../Themes";
|
||||
import img1 from "./screenshot.png";
|
||||
|
||||
export const Theme: IPredefinedTheme = {
|
||||
name: "Dark Blue",
|
||||
description: "Very dark with a blue/purplelly primary",
|
||||
credit: "Saynt_Garmo",
|
||||
reference: "https://discord.com/channels/415207508303544321/921991895230611466/923084732718264340",
|
||||
screenshot: img1,
|
||||
colors: {
|
||||
primarylight: "#023DDE",
|
||||
primary: "#4A41C8",
|
||||
primarydark: "#005299",
|
||||
successlight: "#00FF00",
|
||||
success: "#D1DAD1",
|
||||
successdark: "#BFCABF",
|
||||
errorlight: "#f00",
|
||||
error: "#c00",
|
||||
errordark: "#900",
|
||||
secondarylight: "#AAA",
|
||||
secondary: "#888",
|
||||
secondarydark: "#666",
|
||||
warninglight: "#ff0",
|
||||
warning: "#cc0",
|
||||
warningdark: "#990",
|
||||
infolight: "#69f",
|
||||
info: "#36c",
|
||||
infodark: "#039",
|
||||
welllight: "#444",
|
||||
well: "#040505",
|
||||
white: "#fff",
|
||||
black: "#000000",
|
||||
hp: "#dd3434",
|
||||
money: "#ffd700",
|
||||
hack: "#adff2f",
|
||||
combat: "#faffdf",
|
||||
cha: "#a671d1",
|
||||
int: "#6495ed",
|
||||
rep: "#faffdf",
|
||||
disabled: "#66cfbc",
|
||||
backgroundprimary: "#091419",
|
||||
backgroundsecondary: "#000000",
|
||||
button: "#000000",
|
||||
},
|
||||
};
|
BIN
src/Themes/data/dark-blue/screenshot.png
Normal file
BIN
src/Themes/data/dark-blue/screenshot.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 116 KiB |
45
src/Themes/data/dark-plus/index.ts
Normal file
45
src/Themes/data/dark-plus/index.ts
Normal file
@ -0,0 +1,45 @@
|
||||
import { IPredefinedTheme } from "../../Themes";
|
||||
import img1 from "./screenshot.png";
|
||||
|
||||
export const Theme: IPredefinedTheme = {
|
||||
name: "Dark+",
|
||||
credit: "LoganMD",
|
||||
description: "VSCode Dark+",
|
||||
reference: "https://discord.com/channels/415207508303544321/921991895230611466/921999975867617310",
|
||||
screenshot: img1,
|
||||
colors: {
|
||||
primarylight: "#E0E0BC",
|
||||
primary: "#CCCCAE",
|
||||
primarydark: "#B8B89C",
|
||||
successlight: "#00F000",
|
||||
success: "#00D200",
|
||||
successdark: "#00B400",
|
||||
errorlight: "#F00000",
|
||||
error: "#C80000",
|
||||
errordark: "#A00000",
|
||||
secondarylight: "#B4AEAE",
|
||||
secondary: "#969090",
|
||||
secondarydark: "#787272",
|
||||
warninglight: "#F0F000",
|
||||
warning: "#C8C800",
|
||||
warningdark: "#A0A000",
|
||||
infolight: "#69f",
|
||||
info: "#36c",
|
||||
infodark: "#039",
|
||||
welllight: "#444",
|
||||
well: "#222",
|
||||
white: "#fff",
|
||||
black: "#1E1E1E",
|
||||
hp: "#dd3434",
|
||||
money: "#ffd700",
|
||||
hack: "#adff2f",
|
||||
combat: "#faffdf",
|
||||
cha: "#a671d1",
|
||||
int: "#6495ed",
|
||||
rep: "#faffdf",
|
||||
disabled: "#66cfbc",
|
||||
backgroundprimary: "#1E1E1E",
|
||||
backgroundsecondary: "#252525",
|
||||
button: "#333",
|
||||
},
|
||||
};
|
BIN
src/Themes/data/dark-plus/screenshot.png
Normal file
BIN
src/Themes/data/dark-plus/screenshot.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 117 KiB |
45
src/Themes/data/default-lite/index.ts
Normal file
45
src/Themes/data/default-lite/index.ts
Normal file
@ -0,0 +1,45 @@
|
||||
import { IPredefinedTheme } from "../../Themes";
|
||||
import img1 from "./screenshot.png";
|
||||
|
||||
export const Theme: IPredefinedTheme = {
|
||||
name: "Default-lite",
|
||||
description: "Less eye-straining default theme",
|
||||
credit: "NmuGmu",
|
||||
reference: "https://discord.com/channels/415207508303544321/921991895230611466/925263801564151888",
|
||||
screenshot: img1,
|
||||
colors: {
|
||||
primarylight: "#28CF28",
|
||||
primary: "#21A821",
|
||||
primarydark: "#177317",
|
||||
successlight: "#1CFF1C",
|
||||
success: "#16CA16",
|
||||
successdark: "#0D910D",
|
||||
errorlight: "#FF3B3B",
|
||||
error: "#C32D2D",
|
||||
errordark: "#8E2121",
|
||||
secondarylight: "#B3B3B3",
|
||||
secondary: "#838383",
|
||||
secondarydark: "#676767",
|
||||
warninglight: "#FFFF3A",
|
||||
warning: "#C3C32A",
|
||||
warningdark: "#8C8C1E",
|
||||
infolight: "#64CBFF",
|
||||
info: "#3399CC",
|
||||
infodark: "#246D91",
|
||||
welllight: "#404040",
|
||||
well: "#1C1C1C",
|
||||
white: "#C3C3C3",
|
||||
black: "#0A0B0B",
|
||||
hp: "#C62E2E",
|
||||
money: "#D6BB27",
|
||||
hack: "#ADFF2F",
|
||||
combat: "#E8EDCD",
|
||||
cha: "#8B5FAF",
|
||||
int: "#537CC8",
|
||||
rep: "#E8EDCD",
|
||||
disabled: "#5AB5A5",
|
||||
backgroundprimary: "#0C0D0E",
|
||||
backgroundsecondary: "#121415",
|
||||
button: "#252829",
|
||||
},
|
||||
};
|
BIN
src/Themes/data/default-lite/screenshot.png
Normal file
BIN
src/Themes/data/default-lite/screenshot.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 111 KiB |
44
src/Themes/data/default/index.ts
Normal file
44
src/Themes/data/default/index.ts
Normal file
@ -0,0 +1,44 @@
|
||||
import { IPredefinedTheme } from "../../Themes";
|
||||
import img1 from "./screenshot.png";
|
||||
|
||||
export const Theme: IPredefinedTheme = {
|
||||
name: 'Default',
|
||||
description: 'Default game theme, most supported',
|
||||
credit: 'hydroflame',
|
||||
screenshot: img1,
|
||||
colors: {
|
||||
primarylight: "#0f0",
|
||||
primary: "#0c0",
|
||||
primarydark: "#090",
|
||||
successlight: "#0f0",
|
||||
success: "#0c0",
|
||||
successdark: "#090",
|
||||
errorlight: "#f00",
|
||||
error: "#c00",
|
||||
errordark: "#900",
|
||||
secondarylight: "#AAA",
|
||||
secondary: "#888",
|
||||
secondarydark: "#666",
|
||||
warninglight: "#ff0",
|
||||
warning: "#cc0",
|
||||
warningdark: "#990",
|
||||
infolight: "#69f",
|
||||
info: "#36c",
|
||||
infodark: "#039",
|
||||
welllight: "#444",
|
||||
well: "#222",
|
||||
white: "#fff",
|
||||
black: "#000",
|
||||
hp: "#dd3434",
|
||||
money: "#ffd700",
|
||||
hack: "#adff2f",
|
||||
combat: "#faffdf",
|
||||
cha: "#a671d1",
|
||||
int: "#6495ed",
|
||||
rep: "#faffdf",
|
||||
disabled: "#66cfbc",
|
||||
backgroundprimary: "#000",
|
||||
backgroundsecondary: "#000",
|
||||
button: "#333",
|
||||
},
|
||||
};
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user