mirror of
https://github.com/bitburner-official/bitburner-src.git
synced 2024-11-24 00:23:49 +01:00
Merge branch 'dev' into add-ns-getRecentScripts
This commit is contained in:
commit
2add4e9112
@ -1,27 +1,26 @@
|
|||||||
node_modules/
|
node_modules/
|
||||||
dist/
|
|
||||||
input/
|
|
||||||
|
|
||||||
.dist
|
.dist
|
||||||
.tmp
|
.tmp
|
||||||
.package
|
.package
|
||||||
|
.build
|
||||||
assets/
|
|
||||||
css/
|
|
||||||
.cypress/
|
.cypress/
|
||||||
cypress/
|
|
||||||
|
dist/
|
||||||
|
input/
|
||||||
|
assets/
|
||||||
doc/
|
doc/
|
||||||
markdown/
|
markdown/
|
||||||
netscript_tests/
|
|
||||||
scripts/
|
|
||||||
|
test/netscript/
|
||||||
|
|
||||||
electron/lib
|
electron/lib
|
||||||
electron/greenworks.js
|
electron/greenworks.js
|
||||||
src/ThirdParty/*
|
src/ThirdParty/*
|
||||||
src/JSInterpreter.js
|
src/JSInterpreter.js
|
||||||
|
tools/engines-check/
|
||||||
|
|
||||||
test/*.bundle.*
|
|
||||||
editor.main.js
|
editor.main.js
|
||||||
main.bundle.js
|
main.bundle.js
|
||||||
webpack.config.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
|
/doc/build
|
||||||
/node_modules
|
/node_modules
|
||||||
/electron/node_modules
|
/electron/node_modules
|
||||||
/dist/*.map
|
|
||||||
/test/*.map
|
/test/*.map
|
||||||
/test/*.bundle.*
|
/test/*.bundle.*
|
||||||
/test/*.css
|
/test/*.css
|
||||||
|
/input/bitburner.api.json
|
||||||
.cypress
|
.cypress
|
||||||
|
|
||||||
# tmp folder for electron
|
# 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.
|
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/).
|
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
|
# 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
|
Anyone is welcome to contribute to the documentation by editing the [source
|
||||||
files](/doc/source) and then making a pull request with your contributions.
|
files](/doc/source) and then making a pull request with your contributions.
|
||||||
For further guidance, please refer to the "As A Documentor" section of
|
For further guidance, please refer to the "As A Documentor" section of
|
||||||
[CONTRIBUTING](CONTRIBUTING.md).
|
[CONTRIBUTING](./doc/CONTRIBUTING.md).
|
||||||
|
|
||||||
# Contribution
|
# Contribution
|
||||||
|
|
||||||
There are many ways to contribute to the game. It can be as simple as fixing
|
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,
|
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,
|
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
|
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",
|
"baseUrl": "http://localhost:8000",
|
||||||
"fixturesFolder": false,
|
|
||||||
"trashAssetsBeforeRuns": true,
|
"trashAssetsBeforeRuns": true,
|
||||||
"screenshotsFolder": ".cypress/screenshots",
|
|
||||||
"videosFolder": ".cypress/videos",
|
|
||||||
"videoUploadOnPasses": false,
|
"videoUploadOnPasses": false,
|
||||||
"viewportWidth": 1980,
|
"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
|
Docs](http://bitburner.readthedocs.io/), you will
|
||||||
need to have Python installed, along with [Sphinx](http://www.sphinx-doc.org).
|
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`.
|
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
|
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 |
|
| 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 RAM of 8 | |
|
||||||
| | | * Total Hacknet Cores of 4 | |
|
| | | * 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 |
|
| | | * $15m | * New Tokyo |
|
||||||
| | | | * Ishima |
|
| | | | * Ishima |
|
||||||
| | | | * Volhaven |
|
| | | | * Volhaven |
|
||||||
@ -74,8 +90,19 @@ List of Factions and their Requirements
|
|||||||
| | | | * New Tokyo |
|
| | | | * New Tokyo |
|
||||||
| | | | * Ishima |
|
| | | | * Ishima |
|
||||||
+---------------------+----------------+-----------------------------------------+-------------------------------+
|
+---------------------+----------------+-----------------------------------------+-------------------------------+
|
||||||
| Hacking | NiteSec | * Install a backdoor on the avmnite-02h | |
|
.. raw:: html
|
||||||
| Groups | | server | |
|
|
||||||
|
</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 | |
|
| | The Black Hand | * Install a backdoor on the I.I.I.I | |
|
||||||
@ -86,7 +113,18 @@ List of Factions and their Requirements
|
|||||||
| | | server | |
|
| | | 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 | |
|
| | | the Corporation | |
|
||||||
+ +----------------+-----------------------------------------+-------------------------------+
|
+ +----------------+-----------------------------------------+-------------------------------+
|
||||||
| | MegaCorp | * Have 200k reputation with | |
|
| | MegaCorp | * Have 200k reputation with | |
|
||||||
@ -118,8 +156,19 @@ List of Factions and their Requirements
|
|||||||
| | | * Install a backdoor on the | |
|
| | | * Install a backdoor on the | |
|
||||||
| | | fulcrumassets server | |
|
| | | fulcrumassets server | |
|
||||||
+---------------------+----------------+-----------------------------------------+-------------------------------+
|
+---------------------+----------------+-----------------------------------------+-------------------------------+
|
||||||
| Criminal | Slum Snakes | * All Combat Stats of 30 | |
|
.. raw:: html
|
||||||
| Organizations | | * -9 Karma | |
|
|
||||||
|
</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 | |
|
| | | * $1m | |
|
||||||
+ +----------------+-----------------------------------------+-------------------------------+
|
+ +----------------+-----------------------------------------+-------------------------------+
|
||||||
| | Tetrads | * Be in Chongqing, New Tokyo, or Ishima | |
|
| | Tetrads | * Be in Chongqing, New Tokyo, or Ishima | |
|
||||||
@ -150,8 +199,19 @@ List of Factions and their Requirements
|
|||||||
| | | * -90 Karma | |
|
| | | * -90 Karma | |
|
||||||
| | | * Not working for CIA or NSA | |
|
| | | * Not working for CIA or NSA | |
|
||||||
+---------------------+----------------+-----------------------------------------+-------------------------------+
|
+---------------------+----------------+-----------------------------------------+-------------------------------+
|
||||||
| Endgame | The Covenant | * 20 Augmentations | |
|
.. raw:: html
|
||||||
| Factions | | * $75b | |
|
|
||||||
|
</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 | |
|
| | | * Hacking Level of 850 | |
|
||||||
| | | * All Combat Stats of 850 | |
|
| | | * All Combat Stats of 850 | |
|
||||||
+ +----------------+-----------------------------------------+-------------------------------+
|
+ +----------------+-----------------------------------------+-------------------------------+
|
||||||
@ -165,3 +225,6 @@ List of Factions and their Requirements
|
|||||||
| | | * Hacking Level of 1500 | |
|
| | | * Hacking Level of 1500 | |
|
||||||
| | | * All Combat Stats of 1200 | |
|
| | | * 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
|
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
|
// This is backward but the game fills in an array called `document.achievements` and we retrieve it from
|
||||||
// here. Hey if it works it works.
|
// here. Hey if it works it works.
|
||||||
const steamAchievements = greenworks.getAchievementNames();
|
const steamAchievements = greenworks.getAchievementNames();
|
||||||
log.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);
|
const playerAchieved = (await Promise.all(steamAchievements.map(checkSteamAchievement))).filter(name => !!name);
|
||||||
log.debug(`Player has Steam achievements ${JSON.stringify(playerAchieved)}`);
|
log.debug(`Player has Steam achievements ${JSON.stringify(playerAchieved)}`);
|
||||||
const intervalID = setInterval(async () => {
|
const intervalID = setInterval(async () => {
|
||||||
|
@ -12,11 +12,13 @@ async function initialize(win) {
|
|||||||
window = win;
|
window = win;
|
||||||
server = http.createServer(async function (req, res) {
|
server = http.createServer(async function (req, res) {
|
||||||
let body = "";
|
let body = "";
|
||||||
|
res.setHeader('Content-Type', 'application/json');
|
||||||
|
|
||||||
req.on("data", (chunk) => {
|
req.on("data", (chunk) => {
|
||||||
body += chunk.toString(); // convert Buffer to string
|
body += chunk.toString(); // convert Buffer to string
|
||||||
});
|
});
|
||||||
req.on("end", () => {
|
|
||||||
|
req.on("end", async () => {
|
||||||
const providedToken = req.headers?.authorization?.replace('Bearer ', '') ?? '';
|
const providedToken = req.headers?.authorization?.replace('Bearer ', '') ?? '';
|
||||||
const isValid = providedToken === getAuthenticationToken();
|
const isValid = providedToken === getAuthenticationToken();
|
||||||
if (isValid) {
|
if (isValid) {
|
||||||
@ -24,8 +26,11 @@ async function initialize(win) {
|
|||||||
} else {
|
} else {
|
||||||
log.log('Invalid authentication token');
|
log.log('Invalid authentication token');
|
||||||
res.writeHead(401);
|
res.writeHead(401);
|
||||||
res.write('Invalid authentication token');
|
|
||||||
res.end();
|
res.end(JSON.stringify({
|
||||||
|
success: false,
|
||||||
|
msg: 'Invalid authentication token'
|
||||||
|
}));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -35,17 +40,56 @@ async function initialize(win) {
|
|||||||
} catch (error) {
|
} catch (error) {
|
||||||
log.warn(`Invalid body data`);
|
log.warn(`Invalid body data`);
|
||||||
res.writeHead(400);
|
res.writeHead(400);
|
||||||
res.write('Invalid body data');
|
res.end(JSON.stringify({
|
||||||
res.end();
|
success: false,
|
||||||
|
msg: 'Invalid body data'
|
||||||
|
}));
|
||||||
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (data) {
|
let result;
|
||||||
window.webContents.executeJavaScript(`document.saveFile("${data.filename}", "${data.code}")`).then((result) => {
|
switch(req.method) {
|
||||||
res.write(result);
|
// Request files
|
||||||
res.end();
|
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 cp = require("child_process");
|
||||||
const path = require("path");
|
const path = require("path");
|
||||||
const fs = require("fs");
|
const fs = require("fs");
|
||||||
|
const { windowTracker } = require("./windowTracker");
|
||||||
const { fileURLToPath } = require("url");
|
const { fileURLToPath } = require("url");
|
||||||
|
|
||||||
const debug = process.argv.includes("--debug");
|
const debug = process.argv.includes("--debug");
|
||||||
|
|
||||||
async function createWindow(killall) {
|
async function createWindow(killall) {
|
||||||
const setStopProcessHandler = global.app_handlers.stopProcess;
|
const setStopProcessHandler = global.app_handlers.stopProcess;
|
||||||
|
|
||||||
|
let icon;
|
||||||
|
if (process.platform == 'linux') {
|
||||||
|
icon = path.join(__dirname, 'icon.png');
|
||||||
|
}
|
||||||
|
|
||||||
|
const tracker = windowTracker('main');
|
||||||
const window = new BrowserWindow({
|
const window = new BrowserWindow({
|
||||||
|
icon,
|
||||||
show: false,
|
show: false,
|
||||||
backgroundThrottling: false,
|
backgroundThrottling: false,
|
||||||
backgroundColor: "#000000",
|
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.removeMenu();
|
||||||
window.maximize();
|
|
||||||
noScripts = killall ? { query: { noScripts: killall } } : {};
|
noScripts = killall ? { query: { noScripts: killall } } : {};
|
||||||
window.loadFile("index.html", noScripts);
|
window.loadFile("index.html", noScripts);
|
||||||
window.show();
|
window.show();
|
||||||
|
137
electron/main.js
137
electron/main.js
@ -1,12 +1,19 @@
|
|||||||
/* eslint-disable no-process-exit */
|
/* eslint-disable no-process-exit */
|
||||||
/* eslint-disable @typescript-eslint/no-var-requires */
|
/* 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 log = require("electron-log");
|
||||||
const greenworks = require("./greenworks");
|
const greenworks = require("./greenworks");
|
||||||
const api = require("./api-server");
|
const api = require("./api-server");
|
||||||
const gameWindow = require("./gameWindow");
|
const gameWindow = require("./gameWindow");
|
||||||
const achievements = require("./achievements");
|
const achievements = require("./achievements");
|
||||||
const utils = require("./utils");
|
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.catchErrors();
|
||||||
log.info(`Started app: ${JSON.stringify(process.argv)}`);
|
log.info(`Started app: ${JSON.stringify(process.argv)}`);
|
||||||
@ -30,6 +37,8 @@ try {
|
|||||||
global.greenworksError = ex.message;
|
global.greenworksError = ex.message;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let isRestoreDisabled = false;
|
||||||
|
|
||||||
function setStopProcessHandler(app, window, enabled) {
|
function setStopProcessHandler(app, window, enabled) {
|
||||||
const closingWindowHandler = async (e) => {
|
const closingWindowHandler = async (e) => {
|
||||||
// We need to prevent the default closing event to add custom logic
|
// 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
|
// Shutdown the http server
|
||||||
api.disable();
|
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,
|
// 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.
|
// 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.
|
// So we'll alert the player to close their browser.
|
||||||
@ -87,21 +108,106 @@ function setStopProcessHandler(app, window, enabled) {
|
|||||||
process.exit(0);
|
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) {
|
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("closed", clearWindowHandler);
|
||||||
window.on("close", closingWindowHandler)
|
window.on("close", closingWindowHandler)
|
||||||
app.on("window-all-closed", stopProcessHandler);
|
app.on("window-all-closed", stopProcessHandler);
|
||||||
} else {
|
} else {
|
||||||
log.debug('Removing closing handlers');
|
log.debug("Removing closing handlers");
|
||||||
|
ipcMain.removeAllListeners();
|
||||||
window.removeListener("closed", clearWindowHandler);
|
window.removeListener("closed", clearWindowHandler);
|
||||||
window.removeListener("close", closingWindowHandler);
|
window.removeListener("close", closingWindowHandler);
|
||||||
app.removeListener("window-all-closed", stopProcessHandler);
|
app.removeListener("window-all-closed", stopProcessHandler);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function startWindow(noScript) {
|
async function startWindow(noScript) {
|
||||||
gameWindow.createWindow(noScript);
|
return gameWindow.createWindow(noScript);
|
||||||
}
|
}
|
||||||
|
|
||||||
global.app_handlers = {
|
global.app_handlers = {
|
||||||
@ -110,7 +216,7 @@ global.app_handlers = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
app.whenReady().then(async () => {
|
app.whenReady().then(async () => {
|
||||||
log.info('Application is ready!');
|
log.info("Application is ready!");
|
||||||
|
|
||||||
if (process.argv.includes("--export-save")) {
|
if (process.argv.includes("--export-save")) {
|
||||||
const window = new BrowserWindow({ show: false });
|
const window = new BrowserWindow({ show: false });
|
||||||
@ -119,15 +225,14 @@ app.whenReady().then(async () => {
|
|||||||
setStopProcessHandler(app, window, true);
|
setStopProcessHandler(app, window, true);
|
||||||
await utils.exportSave(window);
|
await utils.exportSave(window);
|
||||||
} else {
|
} else {
|
||||||
startWindow(process.argv.includes("--no-scripts"));
|
const window = await startWindow(process.argv.includes("--no-scripts"));
|
||||||
}
|
if (global.greenworksError) {
|
||||||
|
await dialog.showMessageBox(window, {
|
||||||
if (global.greenworksError) {
|
title: "Bitburner",
|
||||||
dialog.showMessageBox({
|
message: "Could not connect to Steam",
|
||||||
title: 'Bitburner',
|
detail: `${global.greenworksError}\n\nYou won't be able to receive achievements until this is resolved and you restart the game.`,
|
||||||
message: 'Could not connect to Steam',
|
type: 'warning', buttons: ['OK']
|
||||||
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 */
|
/* 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 log = require("electron-log");
|
||||||
|
const Config = require("electron-config");
|
||||||
const api = require("./api-server");
|
const api = require("./api-server");
|
||||||
const utils = require("./utils");
|
const utils = require("./utils");
|
||||||
|
const storage = require("./storage");
|
||||||
|
const config = new Config();
|
||||||
|
|
||||||
function getMenu(window) {
|
function getMenu(window) {
|
||||||
return Menu.buildFromTemplate([
|
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",
|
label: "Edit",
|
||||||
submenu: [
|
submenu: [
|
||||||
@ -163,6 +321,17 @@ function getMenu(window) {
|
|||||||
label: "Activate",
|
label: "Activate",
|
||||||
click: () => window.webContents.openDevTools(),
|
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",
|
"version": "1.0.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"electron-config": "^2.0.0",
|
"electron-config": "^2.0.0",
|
||||||
"electron-log": "^4.4.4"
|
"electron-log": "^4.4.4",
|
||||||
|
"lodash": "^4.17.21"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/conf": {
|
"node_modules/conf": {
|
||||||
@ -104,6 +105,11 @@
|
|||||||
"node": ">=4"
|
"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": {
|
"node_modules/make-dir": {
|
||||||
"version": "1.3.0",
|
"version": "1.3.0",
|
||||||
"resolved": "https://registry.npmjs.org/make-dir/-/make-dir-1.3.0.tgz",
|
"resolved": "https://registry.npmjs.org/make-dir/-/make-dir-1.3.0.tgz",
|
||||||
@ -259,6 +265,11 @@
|
|||||||
"path-exists": "^3.0.0"
|
"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": {
|
"make-dir": {
|
||||||
"version": "1.3.0",
|
"version": "1.3.0",
|
||||||
"resolved": "https://registry.npmjs.org/make-dir/-/make-dir-1.3.0.tgz",
|
"resolved": "https://registry.npmjs.org/make-dir/-/make-dir-1.3.0.tgz",
|
||||||
|
@ -16,6 +16,8 @@
|
|||||||
"./dist/**/*",
|
"./dist/**/*",
|
||||||
"./node_modules/**/*",
|
"./node_modules/**/*",
|
||||||
"./public/**/*",
|
"./public/**/*",
|
||||||
|
"./src/**",
|
||||||
|
"./lib/**,",
|
||||||
"*.js"
|
"*.js"
|
||||||
],
|
],
|
||||||
"directories": {
|
"directories": {
|
||||||
@ -23,6 +25,7 @@
|
|||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"electron-config": "^2.0.0",
|
"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>
|
<link rel="shortcut icon" href="favicon.ico"></head>
|
||||||
<body>
|
<body>
|
||||||
<div id="root"/>
|
<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>
|
</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',
|
'.cypress', 'node_modules', 'dist',
|
||||||
],
|
],
|
||||||
testEnvironment: "jsdom",
|
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/react": "^11.4.1",
|
||||||
"@emotion/styled": "^11.3.0",
|
"@emotion/styled": "^11.3.0",
|
||||||
"@material-ui/core": "^4.12.3",
|
"@material-ui/core": "^4.12.3",
|
||||||
"@microsoft/api-documenter": "^7.13.65",
|
|
||||||
"@microsoft/api-extractor": "^7.18.17",
|
|
||||||
"@monaco-editor/react": "^4.2.2",
|
"@monaco-editor/react": "^4.2.2",
|
||||||
"@mui/icons-material": "^5.0.3",
|
"@mui/icons-material": "^5.0.3",
|
||||||
"@mui/material": "^5.0.3",
|
"@mui/material": "^5.0.3",
|
||||||
"@mui/styles": "^5.0.1",
|
"@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": "^8.4.1",
|
||||||
"acorn-walk": "^8.1.1",
|
"acorn-walk": "^8.1.1",
|
||||||
"arg": "^5.0.0",
|
"arg": "^5.0.0",
|
||||||
@ -45,7 +36,6 @@
|
|||||||
"notistack": "^2.0.2",
|
"notistack": "^2.0.2",
|
||||||
"numeral": "2.0.6",
|
"numeral": "2.0.6",
|
||||||
"prop-types": "^15.8.0",
|
"prop-types": "^15.8.0",
|
||||||
"raw-loader": "^4.0.2",
|
|
||||||
"react": "^17.0.2",
|
"react": "^17.0.2",
|
||||||
"react-beautiful-dnd": "^13.1.0",
|
"react-beautiful-dnd": "^13.1.0",
|
||||||
"react-dom": "^17.0.2",
|
"react-dom": "^17.0.2",
|
||||||
@ -59,10 +49,19 @@
|
|||||||
"@babel/preset-env": "^7.15.0",
|
"@babel/preset-env": "^7.15.0",
|
||||||
"@babel/preset-react": "^7.0.0",
|
"@babel/preset-react": "^7.0.0",
|
||||||
"@babel/preset-typescript": "^7.15.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",
|
"@pmmmwh/react-refresh-webpack-plugin": "^0.5.1",
|
||||||
"@testing-library/cypress": "^8.0.1",
|
"@testing-library/cypress": "^8.0.1",
|
||||||
|
"@types/acorn": "^4.0.6",
|
||||||
|
"@types/escodegen": "^0.0.7",
|
||||||
"@types/file-saver": "^2.0.3",
|
"@types/file-saver": "^2.0.3",
|
||||||
"@types/lodash": "^4.14.168",
|
"@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/eslint-plugin": "^4.22.0",
|
||||||
"@typescript-eslint/parser": "^4.22.0",
|
"@typescript-eslint/parser": "^4.22.0",
|
||||||
"babel-jest": "^27.0.6",
|
"babel-jest": "^27.0.6",
|
||||||
@ -71,6 +70,7 @@
|
|||||||
"electron": "^14.0.2",
|
"electron": "^14.0.2",
|
||||||
"electron-packager": "^15.4.0",
|
"electron-packager": "^15.4.0",
|
||||||
"eslint": "^7.24.0",
|
"eslint": "^7.24.0",
|
||||||
|
"file-loader": "^6.2.0",
|
||||||
"fork-ts-checker-webpack-plugin": "^6.3.3",
|
"fork-ts-checker-webpack-plugin": "^6.3.3",
|
||||||
"html-webpack-plugin": "^3.2.0",
|
"html-webpack-plugin": "^3.2.0",
|
||||||
"http-server": "^13.0.1",
|
"http-server": "^13.0.1",
|
||||||
@ -79,6 +79,7 @@
|
|||||||
"lodash": "^4.17.21",
|
"lodash": "^4.17.21",
|
||||||
"mini-css-extract-plugin": "^0.4.1",
|
"mini-css-extract-plugin": "^0.4.1",
|
||||||
"prettier": "^2.3.2",
|
"prettier": "^2.3.2",
|
||||||
|
"raw-loader": "^4.0.2",
|
||||||
"react-refresh": "^0.10.0",
|
"react-refresh": "^0.10.0",
|
||||||
"regenerator-runtime": "^0.13.9",
|
"regenerator-runtime": "^0.13.9",
|
||||||
"source-map": "^0.7.3",
|
"source-map": "^0.7.3",
|
||||||
@ -102,7 +103,7 @@
|
|||||||
"cy:dev": "start-server-and-test start:dev http://localhost:8000 cy:open",
|
"cy:dev": "start-server-and-test start:dev http://localhost:8000 cy:open",
|
||||||
"cy:open": "cypress open",
|
"cy:open": "cypress open",
|
||||||
"cy:run": "cypress run",
|
"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 .",
|
"format": "prettier --write .",
|
||||||
"start": "http-server -p 8000",
|
"start": "http-server -p 8000",
|
||||||
"start:dev": "webpack-dev-server --progress --env.devServer --mode development",
|
"start:dev": "webpack-dev-server --progress --env.devServer --mode development",
|
||||||
@ -112,13 +113,17 @@
|
|||||||
"build:dev": "webpack --mode development",
|
"build:dev": "webpack --mode development",
|
||||||
"lint": "eslint --fix . --ext js,jsx,ts,tsx",
|
"lint": "eslint --fix . --ext js,jsx,ts,tsx",
|
||||||
"lint:report": "eslint --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": "jest",
|
||||||
"test:watch": "jest --watch",
|
"test:watch": "jest --watch",
|
||||||
"watch": "webpack --watch --mode production",
|
"watch": "webpack --watch --mode production",
|
||||||
"watch:dev": "webpack --watch --mode development",
|
"watch:dev": "webpack --watch --mode development",
|
||||||
"electron": "sh ./package.sh",
|
"electron": "sh ./package.sh",
|
||||||
"electron:packager": "electron-packager .package bitburner --all --out .build --overwrite --icon .package/icon.png --no-prune",
|
"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"
|
"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
|
#!/bin/sh
|
||||||
|
|
||||||
mkdir -p .package/dist/src/ThirdParty || true
|
# Clear out any files remaining from old builds
|
||||||
mkdir -p .package/src/ThirdParty || true
|
rm -rf .package
|
||||||
mkdir -p .package/node_modules || true
|
|
||||||
|
|
||||||
cp index.html .package
|
mkdir -p .package/dist/
|
||||||
cp -r electron/* .package
|
cp -r electron/* .package
|
||||||
cp -r dist/ext .package/dist
|
cp -r dist .package
|
||||||
cp -r dist/icons .package/dist
|
cp index.html .package/index.html
|
||||||
|
|
||||||
# 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
|
|
||||||
|
|
||||||
# Install electron sub-dependencies
|
# Install electron sub-dependencies
|
||||||
cd electron
|
cd electron
|
||||||
npm install
|
npm install
|
||||||
cd ..
|
cd ..
|
||||||
|
|
||||||
|
BUILD_PLATFORM="${1:-"all"}"
|
||||||
# And finally build the app.
|
# 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
|
// Defined by webpack on startup or compilation
|
||||||
declare let __COMMIT_HASH__: string;
|
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:
|
info:
|
||||||
"A brain implant carefully assembled around the synapses, which " +
|
"A brain implant carefully assembled around the synapses, which " +
|
||||||
"micromanages the activity and levels of various neuroreceptor " +
|
"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.",
|
"allowing the user to multitask much more effectively.",
|
||||||
stats: (
|
stats: (
|
||||||
<>
|
<>
|
||||||
|
@ -26,7 +26,7 @@ export function InstalledAugmentations(): React.ReactElement {
|
|||||||
|
|
||||||
if (Settings.OwnedAugmentationsOrder === OwnedAugmentationsOrderSetting.Alphabetically) {
|
if (Settings.OwnedAugmentationsOrder === OwnedAugmentationsOrderSetting.Alphabetically) {
|
||||||
sourceAugs.sort((aug1, aug2) => {
|
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>
|
</Typography>
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<span onClick={() => setPortalOpen(true)} className={cssClass}>
|
<span onClick={() => setPortalOpen(true)} className={cssClass} aria-label={`enter-bitnode-${bitNode.number.toString()}`}>
|
||||||
<b>O</b>
|
<b>O</b>
|
||||||
</span>
|
</span>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
|
@ -14,6 +14,7 @@ import Typography from "@mui/material/Typography";
|
|||||||
import TextField from "@mui/material/TextField";
|
import TextField from "@mui/material/TextField";
|
||||||
|
|
||||||
const MAX_BET = 100e6;
|
const MAX_BET = 100e6;
|
||||||
|
export const DECK_COUNT = 5; // 5-deck multideck
|
||||||
|
|
||||||
enum Result {
|
enum Result {
|
||||||
Pending = "",
|
Pending = "",
|
||||||
@ -45,7 +46,7 @@ export class Blackjack extends Game<Props, State> {
|
|||||||
constructor(props: Props) {
|
constructor(props: Props) {
|
||||||
super(props);
|
super(props);
|
||||||
|
|
||||||
this.deck = new Deck(5); // 5-deck multideck
|
this.deck = new Deck(DECK_COUNT);
|
||||||
|
|
||||||
const initialBet = 1e6;
|
const initialBet = 1e6;
|
||||||
|
|
||||||
|
@ -40,13 +40,13 @@ const strategies: {
|
|||||||
} = {
|
} = {
|
||||||
Red: {
|
Red: {
|
||||||
match: (n: number): boolean => {
|
match: (n: number): boolean => {
|
||||||
if (n === 0) return false;
|
|
||||||
return redNumbers.includes(n);
|
return redNumbers.includes(n);
|
||||||
},
|
},
|
||||||
payout: 1,
|
payout: 1,
|
||||||
},
|
},
|
||||||
Black: {
|
Black: {
|
||||||
match: (n: number): boolean => {
|
match: (n: number): boolean => {
|
||||||
|
if (n === 0) return false;
|
||||||
return !redNumbers.includes(n);
|
return !redNumbers.includes(n);
|
||||||
},
|
},
|
||||||
payout: 1,
|
payout: 1,
|
||||||
@ -118,12 +118,6 @@ export function Roulette(props: IProps): React.ReactElement {
|
|||||||
const [status, setStatus] = useState<string | JSX.Element>("waiting");
|
const [status, setStatus] = useState<string | JSX.Element>("waiting");
|
||||||
const [n, setN] = useState(0);
|
const [n, setN] = useState(0);
|
||||||
const [lock, setLock] = useState(true);
|
const [lock, setLock] = useState(true);
|
||||||
const [strategy, setStrategy] = useState<Strategy>({
|
|
||||||
payout: 0,
|
|
||||||
match: (): boolean => {
|
|
||||||
return false;
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const i = window.setInterval(step, 50);
|
const i = window.setInterval(step, 50);
|
||||||
@ -156,13 +150,12 @@ export function Roulette(props: IProps): React.ReactElement {
|
|||||||
return `${n}${color}`;
|
return `${n}${color}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
function play(s: Strategy): void {
|
function play(strategy: Strategy): void {
|
||||||
if (reachedLimit(props.p)) return;
|
if (reachedLimit(props.p)) return;
|
||||||
|
|
||||||
setCanPlay(false);
|
setCanPlay(false);
|
||||||
setLock(false);
|
setLock(false);
|
||||||
setStatus("playing");
|
setStatus("playing");
|
||||||
setStrategy(s);
|
|
||||||
|
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
let n = Math.floor(rng.random() * 37);
|
let n = Math.floor(rng.random() * 37);
|
||||||
|
@ -159,18 +159,18 @@ export function SlotMachine(props: IProps): React.ReactElement {
|
|||||||
const copy = index.slice();
|
const copy = index.slice();
|
||||||
for (let i = 0; i < copy.length; i++) {
|
for (let i = 0; i < copy.length; i++) {
|
||||||
if (copy[i] === locks[i] && !stoppedOne) continue;
|
if (copy[i] === locks[i] && !stoppedOne) continue;
|
||||||
copy[i] = (copy[i] + 1) % symbols.length;
|
copy[i] = (copy[i] - 1 >= 0) ? copy[i] - 1 : symbols.length - 1;
|
||||||
stoppedOne = true;
|
stoppedOne = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
setIndex(copy);
|
setIndex(copy);
|
||||||
|
|
||||||
if (stoppedOne && copy.every((e, i) => e === locks[i])) {
|
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 [
|
return [
|
||||||
[
|
[
|
||||||
symbols[(index[0] + symbols.length - 1) % symbols.length],
|
symbols[(index[0] + symbols.length - 1) % symbols.length],
|
||||||
@ -209,8 +209,7 @@ export function SlotMachine(props: IProps): React.ReactElement {
|
|||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
function checkWinnings(): void {
|
function checkWinnings(t:string[][]): void {
|
||||||
const t = getTable();
|
|
||||||
const getPaylineData = function (payline: number[][]): string[] {
|
const getPaylineData = function (payline: number[][]): string[] {
|
||||||
const data = [];
|
const data = [];
|
||||||
for (const point of payline) {
|
for (const point of payline) {
|
||||||
@ -267,7 +266,7 @@ export function SlotMachine(props: IProps): React.ReactElement {
|
|||||||
setInvestment(investment);
|
setInvestment(investment);
|
||||||
}
|
}
|
||||||
|
|
||||||
const t = getTable();
|
const t = getTable(index, symbols);
|
||||||
// prettier-ignore
|
// prettier-ignore
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
@ -288,7 +287,7 @@ export function SlotMachine(props: IProps): React.ReactElement {
|
|||||||
disabled={!canPlay}
|
disabled={!canPlay}
|
||||||
>Spin!</Button>)}}
|
>Spin!</Button>)}}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<Typography variant="h4">{status}</Typography>
|
<Typography variant="h4">{status}</Typography>
|
||||||
<Typography>Pay lines</Typography>
|
<Typography>Pay lines</Typography>
|
||||||
|
|
||||||
|
@ -26,6 +26,7 @@ import Table from "@mui/material/Table";
|
|||||||
import TableBody from "@mui/material/TableBody";
|
import TableBody from "@mui/material/TableBody";
|
||||||
import TableRow from "@mui/material/TableRow";
|
import TableRow from "@mui/material/TableRow";
|
||||||
import { TableCell } from "../../ui/React/Table";
|
import { TableCell } from "../../ui/React/Table";
|
||||||
|
import { Box } from "@mui/material";
|
||||||
|
|
||||||
interface IProps {
|
interface IProps {
|
||||||
office: OfficeSpace;
|
office: OfficeSpace;
|
||||||
@ -430,51 +431,46 @@ export function IndustryOffice(props: IProps): React.ReactElement {
|
|||||||
<Typography>
|
<Typography>
|
||||||
Size: {props.office.employees.length} / {props.office.size} employees
|
Size: {props.office.employees.length} / {props.office.size} employees
|
||||||
</Typography>
|
</Typography>
|
||||||
<Tooltip title={<Typography>Automatically hires an employee and gives him/her a random name</Typography>}>
|
<Box sx={{ display: 'grid', gridTemplateColumns: '1fr', width: 'fit-content' }}>
|
||||||
<span>
|
<Box sx={{ gridTemplateColumns: 'repeat(3, 1fr)' }}>
|
||||||
<Button disabled={props.office.atCapacity()} onClick={autohireEmployeeButtonOnClick}>
|
<Tooltip title={<Typography>Automatically hires an employee and gives him/her a random name</Typography>}>
|
||||||
Hire Employee
|
<Button disabled={props.office.atCapacity()} onClick={autohireEmployeeButtonOnClick}>
|
||||||
</Button>
|
Hire Employee
|
||||||
</span>
|
</Button>
|
||||||
</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>
|
|
||||||
</Tooltip>
|
</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}
|
rerender={props.rerender}
|
||||||
office={props.office}
|
office={props.office}
|
||||||
open={throwPartyOpen}
|
open={upgradeOfficeSizeOpen}
|
||||||
onClose={() => setThrowPartyOpen(false)}
|
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 ? (
|
{employeeManualAssignMode ? (
|
||||||
<ManualManagement rerender={props.rerender} office={props.office} />
|
<ManualManagement rerender={props.rerender} office={props.office} />
|
||||||
) : (
|
) : (
|
||||||
|
@ -139,13 +139,13 @@ function WarehouseRoot(props: IProps): React.ReactElement {
|
|||||||
{numeralWrapper.formatBigNumber(props.warehouse.size)}
|
{numeralWrapper.formatBigNumber(props.warehouse.size)}
|
||||||
</Typography>
|
</Typography>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
|
|
||||||
<Button disabled={!canAffordUpgrade} onClick={upgradeWarehouseOnClick}>
|
|
||||||
Upgrade Warehouse Size -
|
|
||||||
<MoneyCost money={sizeUpgradeCost} corp={corp} />
|
|
||||||
</Button>
|
|
||||||
</Box>
|
</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>
|
<Typography>This industry uses the following equation for its production: </Typography>
|
||||||
<br />
|
<br />
|
||||||
<Typography>
|
<Typography>
|
||||||
|
@ -112,7 +112,7 @@ export function MaterialElem(props: IMaterialProps): React.ReactElement {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<Paper>
|
<Paper>
|
||||||
<Box display="flex">
|
<Box sx={{ display: 'grid', gridTemplateColumns: '2fr 1fr', m: '5px' }}>
|
||||||
<Box>
|
<Box>
|
||||||
<Tooltip
|
<Tooltip
|
||||||
title={
|
title={
|
||||||
@ -149,11 +149,10 @@ export function MaterialElem(props: IMaterialProps): React.ReactElement {
|
|||||||
</Tooltip>
|
</Tooltip>
|
||||||
</Box>
|
</Box>
|
||||||
|
|
||||||
<Box>
|
<Box sx={{ "& button": { width: '100%' } }}>
|
||||||
<Tooltip
|
<Tooltip
|
||||||
title={tutorial ? <Typography>Purchase your required materials to get production started!</Typography> : ""}
|
title={tutorial ? <Typography>Purchase your required materials to get production started!</Typography> : ""}
|
||||||
>
|
>
|
||||||
<span>
|
|
||||||
<Button
|
<Button
|
||||||
color={tutorial ? "error" : "primary"}
|
color={tutorial ? "error" : "primary"}
|
||||||
onClick={() => setPurchaseMaterialOpen(true)}
|
onClick={() => setPurchaseMaterialOpen(true)}
|
||||||
@ -161,7 +160,6 @@ export function MaterialElem(props: IMaterialProps): React.ReactElement {
|
|||||||
>
|
>
|
||||||
{purchaseButtonText}
|
{purchaseButtonText}
|
||||||
</Button>
|
</Button>
|
||||||
</span>
|
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
<PurchaseMaterialModal
|
<PurchaseMaterialModal
|
||||||
mat={mat}
|
mat={mat}
|
||||||
@ -177,7 +175,6 @@ export function MaterialElem(props: IMaterialProps): React.ReactElement {
|
|||||||
<ExportModal mat={mat} open={exportOpen} onClose={() => setExportOpen(false)} />
|
<ExportModal mat={mat} open={exportOpen} onClose={() => setExportOpen(false)} />
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
<br />
|
|
||||||
|
|
||||||
<Button
|
<Button
|
||||||
color={division.prodMats.includes(props.mat.name) && !mat.sllman[0] ? "error" : "primary"}
|
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} />
|
<StatsTable rows={multRows} />
|
||||||
<br />
|
<br />
|
||||||
<BonusTime />
|
<BonusTime />
|
||||||
<Tooltip
|
<Box sx={{ display: 'grid', gridTemplateColumns: 'repeat(3, 1fr)', width: 'fit-content' }}>
|
||||||
title={
|
<Tooltip
|
||||||
<Typography>
|
title={
|
||||||
Get a copy of and read 'The Complete Handbook for Creating a Successful Corporation.' This is a .lit file
|
<Typography>
|
||||||
that guides you through the beginning of setting up a Corporation and provides some tips/pointers for
|
Get a copy of and read 'The Complete Handbook for Creating a Successful Corporation.' This is a .lit file
|
||||||
helping you get started with managing it.
|
that guides you through the beginning of setting up a Corporation and provides some tips/pointers for
|
||||||
</Typography>
|
helping you get started with managing it.
|
||||||
}
|
</Typography>
|
||||||
>
|
}
|
||||||
<Button onClick={() => corp.getStarterGuide(player)}>Getting Started Guide</Button>
|
>
|
||||||
</Tooltip>
|
<Button onClick={() => corp.getStarterGuide(player)}>Getting Started Guide</Button>
|
||||||
{corp.public ? <PublicButtons rerender={rerender} /> : <PrivateButtons rerender={rerender} />}
|
</Tooltip>
|
||||||
<BribeButton />
|
{corp.public ? <PublicButtons rerender={rerender} /> : <PrivateButtons rerender={rerender} />}
|
||||||
|
<BribeButton />
|
||||||
|
</Box>
|
||||||
<br />
|
<br />
|
||||||
<Upgrades rerender={rerender} />
|
<Upgrades rerender={rerender} />
|
||||||
</>
|
</>
|
||||||
@ -125,11 +127,9 @@ function PrivateButtons({ rerender }: IPrivateButtonsProps): React.ReactElement
|
|||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Tooltip title={<Typography>{findInvestorsTooltip}</Typography>}>
|
<Tooltip title={<Typography>{findInvestorsTooltip}</Typography>}>
|
||||||
<span>
|
<Button disabled={!fundingAvailable} onClick={() => setFindInvestorsopen(true)}>
|
||||||
<Button disabled={!fundingAvailable} onClick={() => setFindInvestorsopen(true)}>
|
Find Investors
|
||||||
Find Investors
|
</Button>
|
||||||
</Button>
|
|
||||||
</span>
|
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
<Tooltip
|
<Tooltip
|
||||||
title={
|
title={
|
||||||
@ -143,7 +143,6 @@ function PrivateButtons({ rerender }: IPrivateButtonsProps): React.ReactElement
|
|||||||
</Tooltip>
|
</Tooltip>
|
||||||
<FindInvestorsModal open={findInvestorsopen} onClose={() => setFindInvestorsopen(false)} rerender={rerender} />
|
<FindInvestorsModal open={findInvestorsopen} onClose={() => setFindInvestorsopen(false)} rerender={rerender} />
|
||||||
<GoPublicModal open={goPublicopen} onClose={() => setGoPublicopen(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
|
const sellSharesTooltip = sellSharesOnCd
|
||||||
? "Cannot sell shares for " + corp.convertCooldownToString(corp.shareSaleCooldown)
|
? "Cannot sell shares for " + corp.convertCooldownToString(corp.shareSaleCooldown)
|
||||||
: "Sell your shares in the company. The money earned from selling your " +
|
: "Sell your shares in the company. The money earned from selling your " +
|
||||||
"shares goes into your personal account, not the Corporation's. " +
|
"shares goes into your personal account, not the Corporation's. " +
|
||||||
"This is one of the only ways to profit from your business venture.";
|
"This is one of the only ways to profit from your business venture.";
|
||||||
|
|
||||||
const issueNewSharesOnCd = corp.issueNewSharesCooldown > 0;
|
const issueNewSharesOnCd = corp.issueNewSharesCooldown > 0;
|
||||||
const issueNewSharesTooltip = issueNewSharesOnCd
|
const issueNewSharesTooltip = issueNewSharesOnCd
|
||||||
@ -212,28 +211,21 @@ function PublicButtons({ rerender }: IPublicButtonsProps): React.ReactElement {
|
|||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Tooltip title={<Typography>{sellSharesTooltip}</Typography>}>
|
<Tooltip title={<Typography>{sellSharesTooltip}</Typography>}>
|
||||||
<span>
|
<Button disabled={sellSharesOnCd} onClick={() => setSellSharesOpen(true)}>
|
||||||
<Button disabled={sellSharesOnCd} onClick={() => setSellSharesOpen(true)}>
|
Sell Shares
|
||||||
Sell Shares
|
</Button>
|
||||||
</Button>
|
|
||||||
</span>
|
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
<SellSharesModal open={sellSharesOpen} onClose={() => setSellSharesOpen(false)} rerender={rerender} />
|
<SellSharesModal open={sellSharesOpen} onClose={() => setSellSharesOpen(false)} rerender={rerender} />
|
||||||
<Tooltip title={<Typography>Buy back shares you that previously issued or sold at market price.</Typography>}>
|
<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)}>
|
||||||
<Button disabled={corp.issuedShares < 1} onClick={() => setBuybackSharesOpen(true)}>
|
Buyback shares
|
||||||
Buyback shares
|
</Button>
|
||||||
</Button>
|
|
||||||
</span>
|
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
<BuybackSharesModal open={buybackSharesOpen} onClose={() => setBuybackSharesOpen(false)} rerender={rerender} />
|
<BuybackSharesModal open={buybackSharesOpen} onClose={() => setBuybackSharesOpen(false)} rerender={rerender} />
|
||||||
<br />
|
|
||||||
<Tooltip title={<Typography>{issueNewSharesTooltip}</Typography>}>
|
<Tooltip title={<Typography>{issueNewSharesTooltip}</Typography>}>
|
||||||
<span>
|
<Button disabled={issueNewSharesOnCd} onClick={() => setIssueNewSharesOpen(true)}>
|
||||||
<Button disabled={issueNewSharesOnCd} onClick={() => setIssueNewSharesOpen(true)}>
|
Issue New Shares
|
||||||
Issue New Shares
|
</Button>
|
||||||
</Button>
|
|
||||||
</span>
|
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
<IssueNewSharesModal open={issueNewSharesOpen} onClose={() => setIssueNewSharesOpen(false)} />
|
<IssueNewSharesModal open={issueNewSharesOpen} onClose={() => setIssueNewSharesOpen(false)} />
|
||||||
<Tooltip
|
<Tooltip
|
||||||
@ -242,7 +234,6 @@ function PublicButtons({ rerender }: IPublicButtonsProps): React.ReactElement {
|
|||||||
<Button onClick={() => setIssueDividendsOpen(true)}>Issue Dividends</Button>
|
<Button onClick={() => setIssueDividendsOpen(true)}>Issue Dividends</Button>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
<IssueDividendsModal open={issueDividendsOpen} onClose={() => setIssueDividendsOpen(false)} />
|
<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"
|
: "Your Corporation is not powerful enough to bribe Faction leaders"
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<span>
|
<Button disabled={!canBribe} onClick={openBribe}>
|
||||||
<Button disabled={!canBribe} onClick={openBribe}>
|
Bribe Factions
|
||||||
Bribe Factions
|
</Button>
|
||||||
</Button>
|
|
||||||
</span>
|
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
<BribeFactionModal open={open} onClose={() => setOpen(false)} />
|
<BribeFactionModal open={open} onClose={() => setOpen(false)} />
|
||||||
</>
|
</>
|
||||||
|
@ -81,7 +81,7 @@ export function ProductElem(props: IProductProps): React.ReactElement {
|
|||||||
);
|
);
|
||||||
} else if (product.sCost) {
|
} else if (product.sCost) {
|
||||||
if (isString(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 = (
|
||||||
<>
|
<>
|
||||||
{sellButtonText} @ <Money money={eval(sCost)} />
|
{sellButtonText} @ <Money money={eval(sCost)} />
|
||||||
|
@ -6,17 +6,18 @@ import { IIndustry } from "../IIndustry";
|
|||||||
import { Research } from "../Actions";
|
import { Research } from "../Actions";
|
||||||
import { Node } from "../ResearchTree";
|
import { Node } from "../ResearchTree";
|
||||||
import { ResearchMap } from "../ResearchMap";
|
import { ResearchMap } from "../ResearchMap";
|
||||||
|
import { Settings } from "../../Settings/Settings";
|
||||||
import { dialogBoxCreate } from "../../ui/React/DialogBox";
|
import { dialogBoxCreate } from "../../ui/React/DialogBox";
|
||||||
import Typography from "@mui/material/Typography";
|
import Typography from "@mui/material/Typography";
|
||||||
import Tooltip from "@mui/material/Tooltip";
|
import Tooltip from "@mui/material/Tooltip";
|
||||||
import Button from "@mui/material/Button";
|
import Button from "@mui/material/Button";
|
||||||
import Box from "@mui/material/Box";
|
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 Collapse from "@mui/material/Collapse";
|
||||||
import ExpandMore from "@mui/icons-material/ExpandMore";
|
import ExpandMore from "@mui/icons-material/ExpandMore";
|
||||||
import ExpandLess from "@mui/icons-material/ExpandLess";
|
import ExpandLess from "@mui/icons-material/ExpandLess";
|
||||||
|
import CheckIcon from '@mui/icons-material/Check';
|
||||||
|
|
||||||
interface INodeProps {
|
interface INodeProps {
|
||||||
n: Node | null;
|
n: Node | null;
|
||||||
division: IIndustry;
|
division: IIndustry;
|
||||||
@ -42,8 +43,8 @@ function Upgrade({ n, division }: INodeProps): React.ReactElement {
|
|||||||
|
|
||||||
dialogBoxCreate(
|
dialogBoxCreate(
|
||||||
`Researched ${n.text}. It may take a market cycle ` +
|
`Researched ${n.text}. It may take a market cycle ` +
|
||||||
`(~${CorporationConstants.SecsPerMarketCycle} seconds) before the effects of ` +
|
`(~${CorporationConstants.SecsPerMarketCycle} seconds) before the effects of ` +
|
||||||
`the Research apply.`,
|
`the Research apply.`,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -52,8 +53,8 @@ function Upgrade({ n, division }: INodeProps): React.ReactElement {
|
|||||||
color = "info";
|
color = "info";
|
||||||
}
|
}
|
||||||
|
|
||||||
const but = (
|
const wrapInTooltip = (ele: React.ReactElement): React.ReactElement => {
|
||||||
<Box>
|
return (
|
||||||
<Tooltip
|
<Tooltip
|
||||||
title={
|
title={
|
||||||
<Typography>
|
<Typography>
|
||||||
@ -63,12 +64,22 @@ function Upgrade({ n, division }: INodeProps): React.ReactElement {
|
|||||||
</Typography>
|
</Typography>
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
|
{ele}
|
||||||
|
</Tooltip>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const but = (
|
||||||
|
<Box>
|
||||||
|
{wrapInTooltip(
|
||||||
<span>
|
<span>
|
||||||
<Button color={color} disabled={disabled && !n.researched} onClick={research}>
|
<Button color={color} disabled={disabled && !n.researched} onClick={research}
|
||||||
{n.text}
|
style={{ width: '100%', textAlign: 'left', justifyContent: 'unset' }}
|
||||||
|
>
|
||||||
|
{n.researched && (<CheckIcon sx={{ mr: 1 }} />)}{n.text}
|
||||||
</Button>
|
</Button>
|
||||||
</span>
|
</span>
|
||||||
</Tooltip>
|
)}
|
||||||
</Box>
|
</Box>
|
||||||
);
|
);
|
||||||
|
|
||||||
@ -76,15 +87,25 @@ function Upgrade({ n, division }: INodeProps): React.ReactElement {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<Box>
|
<Box>
|
||||||
<Box display="flex">
|
<Box display="flex" sx={{ border: '1px solid ' + Settings.theme.well }}>
|
||||||
{but}
|
{wrapInTooltip(
|
||||||
<ListItemButton onClick={() => setOpen((old) => !old)}>
|
<span style={{ width: '100%' }}>
|
||||||
<ListItemText />
|
<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" />}
|
{open ? <ExpandLess color="primary" /> : <ExpandMore color="primary" />}
|
||||||
</ListItemButton>
|
</Button>
|
||||||
</Box>
|
</Box>
|
||||||
<Collapse in={open} unmountOnExit>
|
<Collapse in={open} unmountOnExit>
|
||||||
<Box m={4}>
|
<Box m={1}>
|
||||||
{n.children.map((m) => (
|
{n.children.map((m) => (
|
||||||
<Upgrade key={m.text} division={division} n={m} />
|
<Upgrade key={m.text} division={division} n={m} />
|
||||||
))}
|
))}
|
||||||
@ -108,7 +129,7 @@ export function ResearchModal(props: IProps): React.ReactElement {
|
|||||||
return (
|
return (
|
||||||
<Modal open={props.open} onClose={props.onClose}>
|
<Modal open={props.open} onClose={props.onClose}>
|
||||||
<Upgrade division={props.industry} n={researchTree.root} />
|
<Upgrade division={props.industry} n={researchTree.root} />
|
||||||
<Typography>
|
<Typography sx={{ mt: 1 }}>
|
||||||
Research points: {props.industry.sciResearch.qty.toFixed(3)}
|
Research points: {props.industry.sciResearch.qty.toFixed(3)}
|
||||||
<br />
|
<br />
|
||||||
Multipliers from research:
|
Multipliers from research:
|
||||||
|
@ -8,6 +8,7 @@ import ExpandMoreIcon from "@mui/icons-material/ExpandMore";
|
|||||||
import Button from "@mui/material/Button";
|
import Button from "@mui/material/Button";
|
||||||
import Typography from "@mui/material/Typography";
|
import Typography from "@mui/material/Typography";
|
||||||
import { IPlayer } from "../../PersonObjects/IPlayer";
|
import { IPlayer } from "../../PersonObjects/IPlayer";
|
||||||
|
import { Adjuster } from "./Adjuster";
|
||||||
|
|
||||||
interface IProps {
|
interface IProps {
|
||||||
player: IPlayer;
|
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 (
|
return (
|
||||||
<Accordion TransitionProps={{ unmountOnExit: true }}>
|
<Accordion TransitionProps={{ unmountOnExit: true }}>
|
||||||
<AccordionSummary expandIcon={<ExpandMoreIcon />}>
|
<AccordionSummary expandIcon={<ExpandMoreIcon />}>
|
||||||
@ -68,6 +75,18 @@ export function Sleeves(props: IProps): React.ReactElement {
|
|||||||
<Button onClick={sleeveSyncClearAll}>Clear all</Button>
|
<Button onClick={sleeveSyncClearAll}>Clear all</Button>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td colSpan={3}>
|
||||||
|
<Adjuster
|
||||||
|
label="Stored Cycles"
|
||||||
|
placeholder="cycles"
|
||||||
|
tons={() => sleeveSetStoredCycles(10000000)}
|
||||||
|
add={sleeveSetStoredCycles}
|
||||||
|
subtract={sleeveSetStoredCycles}
|
||||||
|
reset={() => sleeveSetStoredCycles(0)}
|
||||||
|
/>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
</AccordionDetails>
|
</AccordionDetails>
|
||||||
|
211
src/Electron.tsx
211
src/Electron.tsx
@ -1,11 +1,18 @@
|
|||||||
import { Player } from "./Player";
|
import { Player } from "./Player";
|
||||||
|
import { Router } from "./ui/GameRoot";
|
||||||
import { isScriptFilename } from "./Script/isScriptFilename";
|
import { isScriptFilename } from "./Script/isScriptFilename";
|
||||||
import { Script } from "./Script/Script";
|
import { Script } from "./Script/Script";
|
||||||
import { removeLeadingSlash } from "./Terminal/DirectoryHelpers";
|
import { removeLeadingSlash } from "./Terminal/DirectoryHelpers";
|
||||||
import { Terminal } from "./Terminal";
|
import { Terminal } from "./Terminal";
|
||||||
import { SnackbarEvents } from "./ui/React/Snackbar";
|
import { SnackbarEvents } from "./ui/React/Snackbar";
|
||||||
import { IMap } from "./types";
|
import { IMap, IReturnStatus } from "./types";
|
||||||
import { GetServer } from "./Server/AllServers";
|
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 {
|
export function initElectron(): void {
|
||||||
const userAgent = navigator.userAgent.toLowerCase();
|
const userAgent = navigator.userAgent.toLowerCase();
|
||||||
@ -14,36 +21,81 @@ export function initElectron(): void {
|
|||||||
(document as any).achievements = [];
|
(document as any).achievements = [];
|
||||||
initWebserver();
|
initWebserver();
|
||||||
initAppNotifier();
|
initAppNotifier();
|
||||||
|
initSaveFunctions();
|
||||||
|
initElectronBridge();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function initWebserver(): void {
|
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 = filename.replace(/\/\/+/g, "/");
|
||||||
filename = removeLeadingSlash(filename);
|
filename = removeLeadingSlash(filename);
|
||||||
if (filename.includes("/")) {
|
if (filename.includes("/")) {
|
||||||
filename = "/" + removeLeadingSlash(filename);
|
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();
|
code = Buffer.from(code, "base64").toString();
|
||||||
const home = GetServer("home");
|
const home = GetServer("home");
|
||||||
if (home === null) return "'home' server not found.";
|
if (home === null) {
|
||||||
if (isScriptFilename(filename)) {
|
return {
|
||||||
//If the current script already exists on the server, overwrite it
|
res: false,
|
||||||
for (let i = 0; i < home.scripts.length; i++) {
|
msg: "Home server does not exist.",
|
||||||
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";
|
|
||||||
}
|
}
|
||||||
|
const { success, overwritten } = home.writeToScriptFile(Player, filename, code);
|
||||||
return "not a script file";
|
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.
|
// Will be consumud by the electron wrapper.
|
||||||
// @ts-ignore
|
(window as any).appNotifier = funcs;
|
||||||
window.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 (
|
return (
|
||||||
<Paper sx={{ my: 1, p: 1, width: "100%" }}>
|
<Paper sx={{ my: 1, p: 1 }}>
|
||||||
<Status />
|
<Status />
|
||||||
{props.disabled ? (
|
{props.disabled ? (
|
||||||
<Typography>
|
<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 { IPlayer } from "../../PersonObjects/IPlayer";
|
||||||
|
import { Table, TableCell } from "../../ui/React/Table";
|
||||||
import { IRouter } from "../../ui/Router";
|
import { IRouter } from "../../ui/Router";
|
||||||
import { Factions } from "../Factions";
|
|
||||||
import { Faction } from "../Faction";
|
import { Faction } from "../Faction";
|
||||||
import { joinFaction } from "../FactionHelpers";
|
import { joinFaction } from "../FactionHelpers";
|
||||||
|
import { Factions } from "../Factions";
|
||||||
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";
|
|
||||||
|
|
||||||
export const InvitationsSeen: string[] = [];
|
export const InvitationsSeen: string[] = [];
|
||||||
|
|
||||||
@ -48,42 +48,67 @@ export function FactionsRoot(props: IProps): React.ReactElement {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<Container disableGutters maxWidth="md" sx={{ mx: 0, mb: 10 }}>
|
||||||
<Typography variant="h4">Factions</Typography>
|
<Typography variant="h4">Factions</Typography>
|
||||||
<Typography>Lists all factions you have joined</Typography>
|
<Typography mb={4}>
|
||||||
<br />
|
Throughout the game you may receive invitations from factions. There are many different factions, and each
|
||||||
<Box display="flex" flexDirection="column">
|
faction has different criteria for determining its potential members. Joining a faction and furthering its cause
|
||||||
{props.player.factions.map((faction: string) => (
|
is crucial to progressing in the game and unlocking endgame content.
|
||||||
<Link key={faction} variant="h6" onClick={() => openFaction(Factions[faction])}>
|
</Typography>
|
||||||
{faction}
|
|
||||||
</Link>
|
<Typography variant="h5" color="primary" mt={2} mb={1}>
|
||||||
))}
|
Factions you have joined:
|
||||||
</Box>
|
</Typography>
|
||||||
<br />
|
{(props.player.factions.length > 0 && (
|
||||||
{props.player.factionInvitations.length > 0 && (
|
<Paper sx={{ my: 1, p: 1, pb: 0, display: "inline-block" }}>
|
||||||
<>
|
<Table padding="none">
|
||||||
<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">
|
|
||||||
<TableBody>
|
<TableBody>
|
||||||
{props.player.factionInvitations.map((faction: string) => (
|
{props.player.factions.map((faction: string) => (
|
||||||
<TableRow key={faction}>
|
<TableRow key={faction}>
|
||||||
<TableCell>
|
<TableCell>
|
||||||
<Typography noWrap>{faction}</Typography>
|
<Typography noWrap mb={1}>
|
||||||
|
{faction}
|
||||||
|
</Typography>
|
||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell align="right">
|
<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>
|
</TableCell>
|
||||||
</TableRow>
|
</TableRow>
|
||||||
))}
|
))}
|
||||||
</TableBody>
|
</TableBody>
|
||||||
</Table>
|
</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 {
|
export function Option(props: IProps): React.ReactElement {
|
||||||
return (
|
return (
|
||||||
<Box>
|
<Box>
|
||||||
<Paper sx={{ my: 1, p: 1, width: "100%" }}>
|
<Paper sx={{ my: 1, p: 1 }}>
|
||||||
<Button onClick={props.onClick}>{props.buttonText}</Button>
|
<Button onClick={props.onClick}>{props.buttonText}</Button>
|
||||||
<Typography>{props.infoText}</Typography>
|
<Typography>{props.infoText}</Typography>
|
||||||
</Paper>
|
</Paper>
|
||||||
|
@ -31,7 +31,7 @@ export function AscensionModal(props: IProps): React.ReactElement {
|
|||||||
props.onAscend();
|
props.onAscend();
|
||||||
const res = gang.ascendMember(props.member);
|
const res = gang.ascendMember(props.member);
|
||||||
dialogBoxCreate(
|
dialogBoxCreate(
|
||||||
<Typography>
|
<>
|
||||||
You ascended {props.member.name}!<br />
|
You ascended {props.member.name}!<br />
|
||||||
<br />
|
<br />
|
||||||
Your gang lost {numeralWrapper.formatRespect(res.respect)} respect.
|
Your gang lost {numeralWrapper.formatRespect(res.respect)} respect.
|
||||||
@ -51,7 +51,7 @@ export function AscensionModal(props: IProps): React.ReactElement {
|
|||||||
<br />
|
<br />
|
||||||
Charisma: x{numeralWrapper.format(res.cha, "0.000")}
|
Charisma: x{numeralWrapper.format(res.cha, "0.000")}
|
||||||
<br />
|
<br />
|
||||||
</Typography>,
|
</>
|
||||||
);
|
);
|
||||||
props.onClose();
|
props.onClose();
|
||||||
}
|
}
|
||||||
|
@ -2,20 +2,27 @@
|
|||||||
* React Component for the popup that manages gang members upgrades
|
* React Component for the popup that manages gang members upgrades
|
||||||
*/
|
*/
|
||||||
import React, { useState } from "react";
|
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 { useGang } from "./Context";
|
||||||
import { GangMember } from "../GangMember";
|
import { generateTableRow } from "./GangMemberStats";
|
||||||
import { UpgradeType } from "../data/upgrades";
|
|
||||||
import { use } from "../../ui/Context";
|
|
||||||
import Typography from "@mui/material/Typography";
|
import Typography from "@mui/material/Typography";
|
||||||
import Button from "@mui/material/Button";
|
import Button from "@mui/material/Button";
|
||||||
import Tooltip from "@mui/material/Tooltip";
|
import Tooltip from "@mui/material/Tooltip";
|
||||||
import Box from "@mui/material/Box";
|
import Box from "@mui/material/Box";
|
||||||
import Paper from "@mui/material/Paper";
|
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 {
|
interface INextRevealProps {
|
||||||
upgrades: string[];
|
upgrades: string[];
|
||||||
@ -46,12 +53,10 @@ function NextReveal(props: INextRevealProps): React.ReactElement {
|
|||||||
function PurchasedUpgrade({ upgName }: { upgName: string }): React.ReactElement {
|
function PurchasedUpgrade({ upgName }: { upgName: string }): React.ReactElement {
|
||||||
const upg = GangMemberUpgrades[upgName];
|
const upg = GangMemberUpgrades[upgName];
|
||||||
return (
|
return (
|
||||||
<Paper sx={{ mx: 1, p: 1 }}>
|
<Paper sx={{ p: 1 }}>
|
||||||
<Box display="flex">
|
<Tooltip title={<Typography dangerouslySetInnerHTML={{ __html: upg.desc }} />}>
|
||||||
<Tooltip title={<Typography dangerouslySetInnerHTML={{ __html: upg.desc }} />}>
|
<Typography>{upg.name}</Typography>
|
||||||
<Typography>{upg.name}</Typography>
|
</Tooltip>
|
||||||
</Tooltip>
|
|
||||||
</Box>
|
|
||||||
</Paper>
|
</Paper>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -72,8 +77,8 @@ function UpgradeButton(props: IUpgradeButtonProps): React.ReactElement {
|
|||||||
return (
|
return (
|
||||||
<Tooltip title={<Typography dangerouslySetInnerHTML={{ __html: props.upg.desc }} />}>
|
<Tooltip title={<Typography dangerouslySetInnerHTML={{ __html: props.upg.desc }} />}>
|
||||||
<span>
|
<span>
|
||||||
<Typography>{props.upg.name}</Typography>
|
<Button onClick={onClick} sx={{ display: 'flex', flexDirection: 'column', width: '100%', height: '100%' }}>
|
||||||
<Button onClick={onClick}>
|
<Typography sx={{ display: 'block' }}>{props.upg.name}</Typography>
|
||||||
<Money money={gang.getUpgradeCost(props.upg)} />
|
<Money money={gang.getUpgradeCost(props.upg)} />
|
||||||
</Button>
|
</Button>
|
||||||
</span>
|
</span>
|
||||||
@ -86,12 +91,16 @@ interface IPanelProps {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function GangMemberUpgradePanel(props: IPanelProps): React.ReactElement {
|
function GangMemberUpgradePanel(props: IPanelProps): React.ReactElement {
|
||||||
|
const classes = useStyles();
|
||||||
const gang = useGang();
|
const gang = useGang();
|
||||||
const player = use.Player();
|
const player = use.Player();
|
||||||
const setRerender = useState(false)[1];
|
const setRerender = useState(false)[1];
|
||||||
|
const [currentCategory, setCurrentCategory] = useState("Weapons");
|
||||||
|
|
||||||
function rerender(): void {
|
function rerender(): void {
|
||||||
setRerender((old) => !old);
|
setRerender((old) => !old);
|
||||||
}
|
}
|
||||||
|
|
||||||
function filterUpgrades(list: string[], type: UpgradeType): GangMemberUpgrade[] {
|
function filterUpgrades(list: string[], type: UpgradeType): GangMemberUpgrade[] {
|
||||||
return Object.keys(GangMemberUpgrades)
|
return Object.keys(GangMemberUpgrades)
|
||||||
.filter((upgName: string) => {
|
.filter((upgName: string) => {
|
||||||
@ -103,12 +112,26 @@ function GangMemberUpgradePanel(props: IPanelProps): React.ReactElement {
|
|||||||
})
|
})
|
||||||
.map((upgName: string) => GangMemberUpgrades[upgName]);
|
.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 weaponUpgrades = filterUpgrades(props.member.upgrades, UpgradeType.Weapon);
|
||||||
const armorUpgrades = filterUpgrades(props.member.upgrades, UpgradeType.Armor);
|
const armorUpgrades = filterUpgrades(props.member.upgrades, UpgradeType.Armor);
|
||||||
const vehicleUpgrades = filterUpgrades(props.member.upgrades, UpgradeType.Vehicle);
|
const vehicleUpgrades = filterUpgrades(props.member.upgrades, UpgradeType.Vehicle);
|
||||||
const rootkitUpgrades = filterUpgrades(props.member.upgrades, UpgradeType.Rootkit);
|
const rootkitUpgrades = filterUpgrades(props.member.upgrades, UpgradeType.Rootkit);
|
||||||
const augUpgrades = filterUpgrades(props.member.augmentations, UpgradeType.Augmentation);
|
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 = {
|
const asc = {
|
||||||
hack: props.member.calculateAscensionMult(props.member.hack_asc_points),
|
hack: props.member.calculateAscensionMult(props.member.hack_asc_points),
|
||||||
str: props.member.calculateAscensionMult(props.member.str_asc_points),
|
str: props.member.calculateAscensionMult(props.member.str_asc_points),
|
||||||
@ -119,26 +142,89 @@ function GangMemberUpgradePanel(props: IPanelProps): React.ReactElement {
|
|||||||
};
|
};
|
||||||
return (
|
return (
|
||||||
<Paper>
|
<Paper>
|
||||||
<Typography variant="h5" color="primary">
|
<Box display="grid" sx={{ gridTemplateColumns: '1fr 1fr', m: 1, gap: 1 }}>
|
||||||
{props.member.name} ({props.member.task})
|
<span>
|
||||||
</Typography>
|
<Typography variant="h5" color="primary">
|
||||||
<Typography>
|
{props.member.name} ({props.member.task})
|
||||||
Hack: {props.member.hack} (x
|
</Typography>
|
||||||
{formatNumber(props.member.hack_mult * asc.hack, 2)})<br />
|
<Tooltip
|
||||||
Str: {props.member.str} (x
|
title={
|
||||||
{formatNumber(props.member.str_mult * asc.str, 2)})<br />
|
<Typography>
|
||||||
Def: {props.member.def} (x
|
Hk: x{numeralWrapper.formatMultiplier(props.member.hack_mult * asc.hack)}(x
|
||||||
{formatNumber(props.member.def_mult * asc.def, 2)})<br />
|
{numeralWrapper.formatMultiplier(props.member.hack_mult)} Eq, x{numeralWrapper.formatMultiplier(asc.hack)}{" "}
|
||||||
Dex: {props.member.dex} (x
|
Asc)
|
||||||
{formatNumber(props.member.dex_mult * asc.dex, 2)})<br />
|
<br />
|
||||||
Agi: {props.member.agi} (x
|
St: x{numeralWrapper.formatMultiplier(props.member.str_mult * asc.str)}
|
||||||
{formatNumber(props.member.agi_mult * asc.agi, 2)})<br />
|
(x{numeralWrapper.formatMultiplier(props.member.str_mult)} Eq, x{numeralWrapper.formatMultiplier(asc.str)}{" "}
|
||||||
Cha: {props.member.cha} (x
|
Asc)
|
||||||
{formatNumber(props.member.cha_mult * asc.cha, 2)})
|
<br />
|
||||||
</Typography>
|
Df: x{numeralWrapper.formatMultiplier(props.member.def_mult * asc.def)}
|
||||||
<Box display="flex" flexWrap="wrap">
|
(x{numeralWrapper.formatMultiplier(props.member.def_mult)} Eq, x{numeralWrapper.formatMultiplier(asc.def)}{" "}
|
||||||
<Typography>Purchased Upgrades: </Typography>
|
Asc)
|
||||||
<br />
|
<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) => (
|
{props.member.upgrades.map((upg: string) => (
|
||||||
<PurchasedUpgrade key={upg} upgName={upg} />
|
<PurchasedUpgrade key={upg} upgName={upg} />
|
||||||
))}
|
))}
|
||||||
@ -146,59 +232,22 @@ function GangMemberUpgradePanel(props: IPanelProps): React.ReactElement {
|
|||||||
<PurchasedUpgrade key={upg} upgName={upg} />
|
<PurchasedUpgrade key={upg} upgName={upg} />
|
||||||
))}
|
))}
|
||||||
</Box>
|
</Box>
|
||||||
<Box display="flex" justifyContent="space-around">
|
</Paper >
|
||||||
<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>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function EquipmentsSubpage(): React.ReactElement {
|
export function EquipmentsSubpage(): React.ReactElement {
|
||||||
const gang = useGang();
|
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 (
|
return (
|
||||||
<>
|
<>
|
||||||
<Tooltip
|
<Tooltip
|
||||||
@ -209,11 +258,26 @@ export function EquipmentsSubpage(): React.ReactElement {
|
|||||||
</Typography>
|
</Typography>
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<Typography>Discount: -{numeralWrapper.formatPercentage(1 - 1 / gang.getDiscount())}</Typography>
|
<Typography sx={{ m: 1 }}>Discount: -{numeralWrapper.formatPercentage(1 - 1 / gang.getDiscount())}</Typography>
|
||||||
</Tooltip>
|
</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.
|
* React Component for the list of gang members on the management subpage.
|
||||||
*/
|
*/
|
||||||
import React, { useState } from "react";
|
import React, { useState } from "react";
|
||||||
import { GangMemberAccordion } from "./GangMemberAccordion";
|
import { GangMemberCard } from "./GangMemberCard";
|
||||||
import { GangMember } from "../GangMember";
|
|
||||||
import { RecruitButton } from "./RecruitButton";
|
import { RecruitButton } from "./RecruitButton";
|
||||||
import { useGang } from "./Context";
|
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 {
|
export function GangMemberList(): React.ReactElement {
|
||||||
const gang = useGang();
|
const gang = useGang();
|
||||||
const setRerender = useState(false)[1];
|
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 (
|
return (
|
||||||
<>
|
<>
|
||||||
<RecruitButton onRecruit={() => setRerender((old) => !old)} />
|
<RecruitButton onRecruit={() => setRerender((old) => !old)} />
|
||||||
<ul>
|
<TextField
|
||||||
{gang.members.map((member: GangMember) => (
|
value={filter}
|
||||||
<GangMemberAccordion key={member.name} member={member} />
|
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.
|
* React Component for the first part of a gang member details.
|
||||||
* Contains skills and exp.
|
* Contains skills and exp.
|
||||||
*/
|
*/
|
||||||
import React, { useState } from "react";
|
import React from "react";
|
||||||
import { formatNumber } from "../../utils/StringHelperFunctions";
|
import { useGang } from "./Context";
|
||||||
import { numeralWrapper } from "../../ui/numeralFormat";
|
|
||||||
import { GangMember } from "../GangMember";
|
|
||||||
import { AscensionModal } from "./AscensionModal";
|
|
||||||
import Typography from "@mui/material/Typography";
|
import Typography from "@mui/material/Typography";
|
||||||
import Tooltip from "@mui/material/Tooltip";
|
import Tooltip from "@mui/material/Tooltip";
|
||||||
import Button from "@mui/material/Button";
|
import {
|
||||||
import { StaticModal } from "../../ui/React/StaticModal";
|
Table,
|
||||||
import IconButton from "@mui/material/IconButton";
|
TableBody,
|
||||||
import HelpIcon from "@mui/icons-material/Help";
|
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 {
|
interface IProps {
|
||||||
member: GangMember;
|
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 {
|
export function GangMemberStats(props: IProps): React.ReactElement {
|
||||||
const [helpOpen, setHelpOpen] = useState(false);
|
const classes = useStyles();
|
||||||
const [ascendOpen, setAscendOpen] = useState(false);
|
|
||||||
|
|
||||||
const asc = {
|
const asc = {
|
||||||
hack: props.member.calculateAscensionMult(props.member.hack_asc_points),
|
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),
|
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 (
|
return (
|
||||||
<>
|
<>
|
||||||
<Tooltip
|
<Tooltip
|
||||||
@ -63,50 +100,32 @@ export function GangMemberStats(props: IProps): React.ReactElement {
|
|||||||
</Typography>
|
</Typography>
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<Typography>
|
<Table sx={{ display: 'table', mb: 1, width: '100%' }}>
|
||||||
Hacking: {formatNumber(props.member.hack, 0)} ({numeralWrapper.formatExp(props.member.hack_exp)} exp)
|
<TableBody>
|
||||||
<br />
|
{generateTableRow("Hacking", props.member.hack, props.member.hack_exp, Settings.theme.hack, classes)}
|
||||||
Strength: {formatNumber(props.member.str, 0)} ({numeralWrapper.formatExp(props.member.str_exp)} exp)
|
{generateTableRow("Strength", props.member.str, props.member.str_exp, Settings.theme.combat, classes)}
|
||||||
<br />
|
{generateTableRow("Defense", props.member.def, props.member.def_exp, Settings.theme.combat, classes)}
|
||||||
Defense: {formatNumber(props.member.def, 0)} ({numeralWrapper.formatExp(props.member.def_exp)} exp)
|
{generateTableRow("Dexterity", props.member.dex, props.member.dex_exp, Settings.theme.combat, classes)}
|
||||||
<br />
|
{generateTableRow("Agility", props.member.agi, props.member.agi_exp, Settings.theme.combat, classes)}
|
||||||
Dexterity: {formatNumber(props.member.dex, 0)} ({numeralWrapper.formatExp(props.member.dex_exp)} exp)
|
{generateTableRow("Charisma", props.member.cha, props.member.cha_exp, Settings.theme.cha, classes)}
|
||||||
<br />
|
<TableRow>
|
||||||
Agility: {formatNumber(props.member.agi, 0)} ({numeralWrapper.formatExp(props.member.agi_exp)} exp)
|
<TableCell classes={{ root: classes.cellNone }}>
|
||||||
<br />
|
<br />
|
||||||
Charisma: {formatNumber(props.member.cha, 0)} ({numeralWrapper.formatExp(props.member.cha_exp)} exp)
|
</TableCell>
|
||||||
<br />
|
</TableRow>
|
||||||
</Typography>
|
{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>
|
</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()) {
|
if (!gang.canRecruitMember()) {
|
||||||
const respect = gang.getRespectNeededToRecruitMember();
|
const respect = gang.getRespectNeededToRecruitMember();
|
||||||
return (
|
return (
|
||||||
<Box display="flex" alignItems="center">
|
<Box display="flex" alignItems="center" sx={{ mx: 1 }}>
|
||||||
<Button sx={{ mx: 1 }} disabled>
|
<Button disabled>
|
||||||
Recruit Gang Member
|
Recruit Gang Member
|
||||||
</Button>
|
</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>
|
</Box>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
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} />
|
<RecruitModal open={open} onClose={() => setOpen(false)} onRecruit={props.onRecruit} />
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
@ -3,14 +3,15 @@
|
|||||||
* the task selector as well as some stats.
|
* the task selector as well as some stats.
|
||||||
*/
|
*/
|
||||||
import React, { useState } from "react";
|
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 { useGang } from "./Context";
|
||||||
import { GangMember } from "../GangMember";
|
import { TaskDescription } from "./TaskDescription";
|
||||||
|
|
||||||
|
import { Box } from "@mui/material";
|
||||||
import MenuItem from "@mui/material/MenuItem";
|
import MenuItem from "@mui/material/MenuItem";
|
||||||
import Select, { SelectChangeEvent } from "@mui/material/Select";
|
import Select, { SelectChangeEvent } from "@mui/material/Select";
|
||||||
|
|
||||||
|
import { GangMember } from "../GangMember";
|
||||||
|
|
||||||
interface IProps {
|
interface IProps {
|
||||||
member: GangMember;
|
member: GangMember;
|
||||||
onTaskChange: () => void;
|
onTaskChange: () => void;
|
||||||
@ -29,16 +30,9 @@ export function TaskSelector(props: IProps): React.ReactElement {
|
|||||||
|
|
||||||
const tasks = gang.getAllTaskNames();
|
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 (
|
return (
|
||||||
<>
|
<Box>
|
||||||
<Select onChange={onChange} value={currentTask}>
|
<Select onChange={onChange} value={currentTask} sx={{ width: '100%' }}>
|
||||||
<MenuItem key={0} value={"Unassigned"}>
|
<MenuItem key={0} value={"Unassigned"}>
|
||||||
Unassigned
|
Unassigned
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
@ -48,8 +42,7 @@ export function TaskSelector(props: IProps): React.ReactElement {
|
|||||||
</MenuItem>
|
</MenuItem>
|
||||||
))}
|
))}
|
||||||
</Select>
|
</Select>
|
||||||
|
<TaskDescription member={props.member} />
|
||||||
<StatsTable rows={data} />
|
</Box>
|
||||||
</>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -34,9 +34,9 @@ export function calculateHackingExpGain(server: Server, player: IPlayer): number
|
|||||||
server.baseDifficulty = server.hackDifficulty;
|
server.baseDifficulty = server.hackDifficulty;
|
||||||
}
|
}
|
||||||
let expGain = baseExpGain;
|
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 React, { useState } from "react";
|
||||||
import Button from "@mui/material/Button";
|
import Button from "@mui/material/Button";
|
||||||
import { Blackjack } from "../../Casino/Blackjack";
|
import { Blackjack, DECK_COUNT } from "../../Casino/Blackjack";
|
||||||
import { CoinFlip } from "../../Casino/CoinFlip";
|
import { CoinFlip } from "../../Casino/CoinFlip";
|
||||||
import { Roulette } from "../../Casino/Roulette";
|
import { Roulette } from "../../Casino/Roulette";
|
||||||
import { SlotMachine } from "../../Casino/SlotMachine";
|
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.Coin)}>Play coin flip</Button>
|
||||||
<Button onClick={() => updateGame(GameType.Slots)}>Play slots</Button>
|
<Button onClick={() => updateGame(GameType.Slots)}>Play slots</Button>
|
||||||
<Button onClick={() => updateGame(GameType.Roulette)}>Play roulette</Button>
|
<Button onClick={() => updateGame(GameType.Roulette)}>Play roulette</Button>
|
||||||
<Button onClick={() => updateGame(GameType.Blackjack)}>Play blackjack</Button>
|
<Button onClick={() => updateGame(GameType.Blackjack)}>Play blackjack ({DECK_COUNT} decks)</Button>
|
||||||
</Box>
|
</Box>
|
||||||
)}
|
)}
|
||||||
{game !== GameType.None && (
|
{game !== GameType.None && (
|
||||||
|
@ -114,6 +114,7 @@ export const RamCosts: IMap<any> = {
|
|||||||
weaken: RamCostConstants.ScriptWeakenRamCost,
|
weaken: RamCostConstants.ScriptWeakenRamCost,
|
||||||
weakenAnalyze: RamCostConstants.ScriptWeakenAnalyzeRamCost,
|
weakenAnalyze: RamCostConstants.ScriptWeakenAnalyzeRamCost,
|
||||||
print: 0,
|
print: 0,
|
||||||
|
printf: 0,
|
||||||
tprint: 0,
|
tprint: 0,
|
||||||
clearLog: 0,
|
clearLog: 0,
|
||||||
disableLog: 0,
|
disableLog: 0,
|
||||||
|
@ -553,7 +553,7 @@ export function NetscriptFunctions(workerScript: WorkerScript): NS {
|
|||||||
if (isNaN(hackAmount)) {
|
if (isNaN(hackAmount)) {
|
||||||
throw makeRuntimeErrorMsg(
|
throw makeRuntimeErrorMsg(
|
||||||
"hackAnalyzeThreads",
|
"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));
|
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 {
|
tprint: function (...args: any[]): void {
|
||||||
if (args.length === 0) {
|
if (args.length === 0) {
|
||||||
throw makeRuntimeErrorMsg("tprint", "Takes at least 1 argument.");
|
throw makeRuntimeErrorMsg("tprint", "Takes at least 1 argument.");
|
||||||
@ -1676,7 +1682,12 @@ export function NetscriptFunctions(workerScript: WorkerScript): NS {
|
|||||||
|
|
||||||
const cost = getPurchaseServerCost(ram);
|
const cost = getPurchaseServerCost(ram);
|
||||||
if (cost === Infinity) {
|
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 "";
|
return "";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -49,6 +49,7 @@ import {
|
|||||||
SetMaterialMarketTA2,
|
SetMaterialMarketTA2,
|
||||||
SetProductMarketTA1,
|
SetProductMarketTA1,
|
||||||
SetProductMarketTA2,
|
SetProductMarketTA2,
|
||||||
|
SetSmartSupplyUseLeftovers,
|
||||||
} from "../Corporation/Actions";
|
} from "../Corporation/Actions";
|
||||||
import { CorporationUnlockUpgrades } from "../Corporation/data/CorporationUnlockUpgrades";
|
import { CorporationUnlockUpgrades } from "../Corporation/data/CorporationUnlockUpgrades";
|
||||||
import { CorporationUpgrades } from "../Corporation/data/CorporationUpgrades";
|
import { CorporationUpgrades } from "../Corporation/data/CorporationUpgrades";
|
||||||
@ -410,6 +411,16 @@ export function NetscriptCorporation(
|
|||||||
const warehouse = getWarehouse(divisionName, cityName);
|
const warehouse = getWarehouse(divisionName, cityName);
|
||||||
SetSmartSupply(warehouse, enabled);
|
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 {
|
buyMaterial: function (adivisionName: any, acityName: any, amaterialName: any, aamt: any): void {
|
||||||
checkAccess("buyMaterial", 7);
|
checkAccess("buyMaterial", 7);
|
||||||
const divisionName = helper.string("buyMaterial", "divisionName", adivisionName);
|
const divisionName = helper.string("buyMaterial", "divisionName", adivisionName);
|
||||||
|
@ -81,41 +81,41 @@ export function NetscriptFormulas(player: IPlayer, workerScript: WorkerScript, h
|
|||||||
return {
|
return {
|
||||||
skills: {
|
skills: {
|
||||||
calculateSkill: function (exp: any, mult: any = 1): any {
|
calculateSkill: function (exp: any, mult: any = 1): any {
|
||||||
checkFormulasAccess("basic.calculateSkill");
|
checkFormulasAccess("skills.calculateSkill");
|
||||||
return calculateSkill(exp, mult);
|
return calculateSkill(exp, mult);
|
||||||
},
|
},
|
||||||
calculateExp: function (skill: any, mult: any = 1): any {
|
calculateExp: function (skill: any, mult: any = 1): any {
|
||||||
checkFormulasAccess("basic.calculateExp");
|
checkFormulasAccess("skills.calculateExp");
|
||||||
return calculateExp(skill, mult);
|
return calculateExp(skill, mult);
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
hacking: {
|
hacking: {
|
||||||
hackChance: function (server: any, player: any): any {
|
hackChance: function (server: any, player: any): any {
|
||||||
checkFormulasAccess("basic.hackChance");
|
checkFormulasAccess("hacking.hackChance");
|
||||||
return calculateHackingChance(server, player);
|
return calculateHackingChance(server, player);
|
||||||
},
|
},
|
||||||
hackExp: function (server: any, player: any): any {
|
hackExp: function (server: any, player: any): any {
|
||||||
checkFormulasAccess("basic.hackExp");
|
checkFormulasAccess("hacking.hackExp");
|
||||||
return calculateHackingExpGain(server, player);
|
return calculateHackingExpGain(server, player);
|
||||||
},
|
},
|
||||||
hackPercent: function (server: any, player: any): any {
|
hackPercent: function (server: any, player: any): any {
|
||||||
checkFormulasAccess("basic.hackPercent");
|
checkFormulasAccess("hacking.hackPercent");
|
||||||
return calculatePercentMoneyHacked(server, player);
|
return calculatePercentMoneyHacked(server, player);
|
||||||
},
|
},
|
||||||
growPercent: function (server: any, threads: any, player: any, cores: any = 1): any {
|
growPercent: function (server: any, threads: any, player: any, cores: any = 1): any {
|
||||||
checkFormulasAccess("basic.growPercent");
|
checkFormulasAccess("hacking.growPercent");
|
||||||
return calculateServerGrowth(server, threads, player, cores);
|
return calculateServerGrowth(server, threads, player, cores);
|
||||||
},
|
},
|
||||||
hackTime: function (server: any, player: any): any {
|
hackTime: function (server: any, player: any): any {
|
||||||
checkFormulasAccess("basic.hackTime");
|
checkFormulasAccess("hacking.hackTime");
|
||||||
return calculateHackingTime(server, player) * 1000;
|
return calculateHackingTime(server, player) * 1000;
|
||||||
},
|
},
|
||||||
growTime: function (server: any, player: any): any {
|
growTime: function (server: any, player: any): any {
|
||||||
checkFormulasAccess("basic.growTime");
|
checkFormulasAccess("hacking.growTime");
|
||||||
return calculateGrowTime(server, player) * 1000;
|
return calculateGrowTime(server, player) * 1000;
|
||||||
},
|
},
|
||||||
weakenTime: function (server: any, player: any): any {
|
weakenTime: function (server: any, player: any): any {
|
||||||
checkFormulasAccess("basic.weakenTime");
|
checkFormulasAccess("hacking.weakenTime");
|
||||||
return calculateWeakenTime(server, player) * 1000;
|
return calculateWeakenTime(server, player) * 1000;
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@ -188,21 +188,27 @@ export function NetscriptFormulas(player: IPlayer, workerScript: WorkerScript, h
|
|||||||
},
|
},
|
||||||
gang: {
|
gang: {
|
||||||
wantedPenalty(gang: any): number {
|
wantedPenalty(gang: any): number {
|
||||||
|
checkFormulasAccess("gang.wantedPenalty");
|
||||||
return calculateWantedPenalty(gang);
|
return calculateWantedPenalty(gang);
|
||||||
},
|
},
|
||||||
respectGain: function (gang: any, member: any, task: any): number {
|
respectGain: function (gang: any, member: any, task: any): number {
|
||||||
|
checkFormulasAccess("gang.respectGain");
|
||||||
return calculateRespectGain(gang, member, task);
|
return calculateRespectGain(gang, member, task);
|
||||||
},
|
},
|
||||||
wantedLevelGain: function (gang: any, member: any, task: any): number {
|
wantedLevelGain: function (gang: any, member: any, task: any): number {
|
||||||
|
checkFormulasAccess("gang.wantedLevelGain");
|
||||||
return calculateWantedLevelGain(gang, member, task);
|
return calculateWantedLevelGain(gang, member, task);
|
||||||
},
|
},
|
||||||
moneyGain: function (gang: any, member: any, task: any): number {
|
moneyGain: function (gang: any, member: any, task: any): number {
|
||||||
|
checkFormulasAccess("gang.moneyGain");
|
||||||
return calculateMoneyGain(gang, member, task);
|
return calculateMoneyGain(gang, member, task);
|
||||||
},
|
},
|
||||||
ascensionPointsGain: function (exp: any): number {
|
ascensionPointsGain: function (exp: any): number {
|
||||||
|
checkFormulasAccess("gang.ascensionPointsGain");
|
||||||
return calculateAscensionPointsGain(exp);
|
return calculateAscensionPointsGain(exp);
|
||||||
},
|
},
|
||||||
ascensionMultiplier: function (points: any): number {
|
ascensionMultiplier: function (points: any): number {
|
||||||
|
checkFormulasAccess("gang.ascensionMultiplier");
|
||||||
return calculateAscensionMult(points);
|
return calculateAscensionMult(points);
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
@ -474,7 +474,8 @@ export function NetscriptSingularity(
|
|||||||
case CityName.Ishima:
|
case CityName.Ishima:
|
||||||
case CityName.Volhaven:
|
case CityName.Volhaven:
|
||||||
if (player.money < CONSTANTS.TravelCost) {
|
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.loseMoney(CONSTANTS.TravelCost, "other");
|
||||||
player.city = cityname;
|
player.city = cityname;
|
||||||
@ -482,8 +483,7 @@ export function NetscriptSingularity(
|
|||||||
player.gainIntelligenceExp(CONSTANTS.IntelligenceSingFnBaseExpGain / 50000);
|
player.gainIntelligenceExp(CONSTANTS.IntelligenceSingFnBaseExpGain / 50000);
|
||||||
return true;
|
return true;
|
||||||
default:
|
default:
|
||||||
workerScript.log("travelToCity", () => `Invalid city name: '${cityname}'.`);
|
throw helper.makeRuntimeErrorMsg("travelToCity", `Invalid city name: '${cityname}'.`);
|
||||||
return false;
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
@ -4,9 +4,9 @@ import { IPlayer } from "../PersonObjects/IPlayer";
|
|||||||
import { getRamCost } from "../Netscript/RamCostGenerator";
|
import { getRamCost } from "../Netscript/RamCostGenerator";
|
||||||
import { GameInfo, IStyleSettings, UserInterface as IUserInterface, UserInterfaceTheme } from "../ScriptEditor/NetscriptDefinitions";
|
import { GameInfo, IStyleSettings, UserInterface as IUserInterface, UserInterfaceTheme } from "../ScriptEditor/NetscriptDefinitions";
|
||||||
import { Settings } from "../Settings/Settings";
|
import { Settings } from "../Settings/Settings";
|
||||||
import { ThemeEvents } from "../ui/React/Theme";
|
import { ThemeEvents } from "../Themes/ui/Theme";
|
||||||
import { defaultTheme } from "../Settings/Themes";
|
import { defaultTheme } from "../Themes/Themes";
|
||||||
import { defaultStyles } from "../Settings/Styles";
|
import { defaultStyles } from "../Themes/Styles";
|
||||||
import { CONSTANTS } from "../Constants";
|
import { CONSTANTS } from "../Constants";
|
||||||
import { hash } from "../hash/hash";
|
import { hash } from "../hash/hash";
|
||||||
|
|
||||||
|
@ -72,6 +72,7 @@ export interface IPlayer {
|
|||||||
sourceFiles: IPlayerOwnedSourceFile[];
|
sourceFiles: IPlayerOwnedSourceFile[];
|
||||||
exploits: Exploit[];
|
exploits: Exploit[];
|
||||||
achievements: PlayerAchievement[];
|
achievements: PlayerAchievement[];
|
||||||
|
terminalCommandHistory: string[];
|
||||||
lastUpdate: number;
|
lastUpdate: number;
|
||||||
totalPlaytime: number;
|
totalPlaytime: number;
|
||||||
|
|
||||||
|
@ -35,7 +35,9 @@ import { CityName } from "../../Locations/data/CityNames";
|
|||||||
import { MoneySourceTracker } from "../../utils/MoneySourceTracker";
|
import { MoneySourceTracker } from "../../utils/MoneySourceTracker";
|
||||||
import { Reviver, Generic_toJSON, Generic_fromJSON } from "../../utils/JSONReviver";
|
import { Reviver, Generic_toJSON, Generic_fromJSON } from "../../utils/JSONReviver";
|
||||||
import { ISkillProgress } from "../formulas/skill";
|
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 {
|
export class PlayerObject implements IPlayer {
|
||||||
// Class members
|
// Class members
|
||||||
@ -77,7 +79,10 @@ export class PlayerObject implements IPlayer {
|
|||||||
sourceFiles: IPlayerOwnedSourceFile[];
|
sourceFiles: IPlayerOwnedSourceFile[];
|
||||||
exploits: Exploit[];
|
exploits: Exploit[];
|
||||||
achievements: PlayerAchievement[];
|
achievements: PlayerAchievement[];
|
||||||
|
terminalCommandHistory: string[];
|
||||||
|
identifier: string;
|
||||||
lastUpdate: number;
|
lastUpdate: number;
|
||||||
|
lastSave: number;
|
||||||
totalPlaytime: number;
|
totalPlaytime: number;
|
||||||
|
|
||||||
// Stats
|
// Stats
|
||||||
@ -459,7 +464,9 @@ export class PlayerObject implements IPlayer {
|
|||||||
|
|
||||||
//Used to store the last update time.
|
//Used to store the last update time.
|
||||||
this.lastUpdate = 0;
|
this.lastUpdate = 0;
|
||||||
|
this.lastSave = 0;
|
||||||
this.totalPlaytime = 0;
|
this.totalPlaytime = 0;
|
||||||
|
|
||||||
this.playtimeSinceLastAug = 0;
|
this.playtimeSinceLastAug = 0;
|
||||||
this.playtimeSinceLastBitnode = 0;
|
this.playtimeSinceLastBitnode = 0;
|
||||||
|
|
||||||
@ -471,6 +478,17 @@ export class PlayerObject implements IPlayer {
|
|||||||
|
|
||||||
this.exploits = [];
|
this.exploits = [];
|
||||||
this.achievements = [];
|
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.init = generalMethods.init;
|
||||||
this.prestigeAugmentation = generalMethods.prestigeAugmentation;
|
this.prestigeAugmentation = generalMethods.prestigeAugmentation;
|
||||||
|
@ -1,5 +1,8 @@
|
|||||||
import { Sleeve } from "../Sleeve";
|
import { Sleeve } from "../Sleeve";
|
||||||
import { numeralWrapper } from "../../../ui/numeralFormat";
|
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 { StatsTable } from "../../../ui/React/StatsTable";
|
||||||
import { Modal } from "../../../ui/React/Modal";
|
import { Modal } from "../../../ui/React/Modal";
|
||||||
import React from "react";
|
import React from "react";
|
||||||
@ -80,6 +83,13 @@ export function MoreStatsModal(props: IProps): React.ReactElement {
|
|||||||
]}
|
]}
|
||||||
title="Multipliers:"
|
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>
|
</Modal>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -22,11 +22,44 @@ import { v1APIBreak } from "./utils/v1APIBreak";
|
|||||||
import { AugmentationNames } from "./Augmentation/data/AugmentationNames";
|
import { AugmentationNames } from "./Augmentation/data/AugmentationNames";
|
||||||
import { PlayerOwnedAugmentation } from "./Augmentation/PlayerOwnedAugmentation";
|
import { PlayerOwnedAugmentation } from "./Augmentation/PlayerOwnedAugmentation";
|
||||||
import { LocationName } from "./Locations/data/LocationNames";
|
import { LocationName } from "./Locations/data/LocationNames";
|
||||||
|
import { SxProps } from "@mui/system";
|
||||||
|
import { PlayerObject } from "./PersonObjects/Player/PlayerObject";
|
||||||
|
import { pushGameSaved } from "./Electron";
|
||||||
|
|
||||||
/* SaveObject.js
|
/* SaveObject.js
|
||||||
* Defines the object used to save/load games
|
* 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 {
|
class BitburnerSaveObject {
|
||||||
PlayerSave = "";
|
PlayerSave = "";
|
||||||
AllServersSave = "";
|
AllServersSave = "";
|
||||||
@ -41,7 +74,6 @@ class BitburnerSaveObject {
|
|||||||
AllGangsSave = "";
|
AllGangsSave = "";
|
||||||
LastExportBonus = "";
|
LastExportBonus = "";
|
||||||
StaneksGiftSave = "";
|
StaneksGiftSave = "";
|
||||||
SaveTimestamp = "";
|
|
||||||
|
|
||||||
getSaveString(excludeRunningScripts = false): string {
|
getSaveString(excludeRunningScripts = false): string {
|
||||||
this.PlayerSave = JSON.stringify(Player);
|
this.PlayerSave = JSON.stringify(Player);
|
||||||
@ -57,7 +89,6 @@ class BitburnerSaveObject {
|
|||||||
this.VersionSave = JSON.stringify(CONSTANTS.VersionNumber);
|
this.VersionSave = JSON.stringify(CONSTANTS.VersionNumber);
|
||||||
this.LastExportBonus = JSON.stringify(ExportBonus.LastExportBonus);
|
this.LastExportBonus = JSON.stringify(ExportBonus.LastExportBonus);
|
||||||
this.StaneksGiftSave = JSON.stringify(staneksGift);
|
this.StaneksGiftSave = JSON.stringify(staneksGift);
|
||||||
this.SaveTimestamp = new Date().getTime().toString();
|
|
||||||
|
|
||||||
if (Player.inGang()) {
|
if (Player.inGang()) {
|
||||||
this.AllGangsSave = JSON.stringify(AllGangs);
|
this.AllGangsSave = JSON.stringify(AllGangs);
|
||||||
@ -67,28 +98,134 @@ class BitburnerSaveObject {
|
|||||||
return saveString;
|
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);
|
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)
|
if (emitToastEvent) {
|
||||||
.then(() => {
|
SnackbarEvents.emit("Game Saved!", "info", 2000);
|
||||||
if (emitToastEvent) {
|
}
|
||||||
SnackbarEvents.emit("Game Saved!", "info", 2000);
|
return resolve();
|
||||||
}
|
})
|
||||||
})
|
.catch((err) => {
|
||||||
.catch((err) => console.error(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 {
|
exportGame(): void {
|
||||||
const saveString = this.getSaveString(Settings.ExcludeRunningScriptsFromSave);
|
const saveString = this.getSaveString(Settings.ExcludeRunningScriptsFromSave);
|
||||||
|
const filename = this.getSaveFileName();
|
||||||
// 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`;
|
|
||||||
download(filename, saveString);
|
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 {
|
toJSON(): any {
|
||||||
return Generic_toJSON("BitburnerSaveObject", this);
|
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 {
|
function createNewUpdateText(): void {
|
||||||
setTimeout(
|
setTimeout(
|
||||||
() =>
|
() =>
|
||||||
@ -379,6 +528,7 @@ function createNewUpdateText(): void {
|
|||||||
"Please report any bugs/issues through the github repository " +
|
"Please report any bugs/issues through the github repository " +
|
||||||
"or the Bitburner subreddit (reddit.com/r/bitburner).<br><br>" +
|
"or the Bitburner subreddit (reddit.com/r/bitburner).<br><br>" +
|
||||||
CONSTANTS.LatestUpdate,
|
CONSTANTS.LatestUpdate,
|
||||||
|
resets,
|
||||||
),
|
),
|
||||||
1000,
|
1000,
|
||||||
);
|
);
|
||||||
@ -391,6 +541,7 @@ function createBetaUpdateText(): void {
|
|||||||
"Please report any bugs/issues through the github repository (https://github.com/danielyxie/bitburner/issues) " +
|
"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>" +
|
"or the Bitburner subreddit (reddit.com/r/bitburner).<br><br>" +
|
||||||
CONSTANTS.LatestUpdate,
|
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 */
|
/** How much money is given */
|
||||||
money: number;
|
money: number;
|
||||||
/** Name of crime */
|
/** Name of crime */
|
||||||
name: number;
|
name: string;
|
||||||
/** Milliseconds it takes to attempt the crime */
|
/** Milliseconds it takes to attempt the crime */
|
||||||
time: number;
|
time: number;
|
||||||
/** Description of the crime activity */
|
/** Description of the crime activity */
|
||||||
@ -3667,6 +3667,7 @@ interface SkillsFormulas {
|
|||||||
interface HackingFormulas {
|
interface HackingFormulas {
|
||||||
/**
|
/**
|
||||||
* Calculate hack chance.
|
* Calculate hack chance.
|
||||||
|
* (Ex: 0.25 would indicate a 25% chance of success.)
|
||||||
* @param server - Server info from {@link NS.getServer | getServer}
|
* @param server - Server info from {@link NS.getServer | getServer}
|
||||||
* @param player - Player info from {@link NS.getPlayer | getPlayer}
|
* @param player - Player info from {@link NS.getPlayer | getPlayer}
|
||||||
* @returns The calculated hack chance.
|
* @returns The calculated hack chance.
|
||||||
@ -3683,6 +3684,7 @@ interface HackingFormulas {
|
|||||||
hackExp(server: Server, player: Player): number;
|
hackExp(server: Server, player: Player): number;
|
||||||
/**
|
/**
|
||||||
* Calculate hack percent for one thread.
|
* Calculate hack percent for one thread.
|
||||||
|
* (Ex: 0.25 would steal 25% of the server's current value.)
|
||||||
* @remarks
|
* @remarks
|
||||||
* Multiply by thread to get total percent hacked.
|
* Multiply by thread to get total percent hacked.
|
||||||
* @param server - Server info from {@link NS.getServer | getServer}
|
* @param server - Server info from {@link NS.getServer | getServer}
|
||||||
@ -3691,7 +3693,8 @@ interface HackingFormulas {
|
|||||||
*/
|
*/
|
||||||
hackPercent(server: Server, player: Player): number;
|
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 server - Server info from {@link NS.getServer | getServer}
|
||||||
* @param threads - Amount of thread.
|
* @param threads - Amount of thread.
|
||||||
* @param player - Player info from {@link NS.getPlayer | getPlayer}
|
* @param player - Player info from {@link NS.getPlayer | getPlayer}
|
||||||
@ -4231,13 +4234,11 @@ export interface NS extends Singularity {
|
|||||||
* ```ts
|
* ```ts
|
||||||
* // NS1:
|
* // NS1:
|
||||||
* var earnedMoney = hack("foodnstuff");
|
* var earnedMoney = hack("foodnstuff");
|
||||||
* earnedMoney = earnedMoney + hack("foodnstuff", { threads: 5 }); // Only use 5 threads to hack
|
|
||||||
* ```
|
* ```
|
||||||
* @example
|
* @example
|
||||||
* ```ts
|
* ```ts
|
||||||
* // NS2:
|
* // NS2:
|
||||||
* let earnedMoney = await ns.hack("foodnstuff");
|
* 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 host - Hostname of the target server to hack.
|
||||||
* @param opts - Optional parameters for configuring function behavior.
|
* @param opts - Optional parameters for configuring function behavior.
|
||||||
@ -4265,16 +4266,14 @@ export interface NS extends Singularity {
|
|||||||
* @example
|
* @example
|
||||||
* ```ts
|
* ```ts
|
||||||
* // NS1:
|
* // NS1:
|
||||||
* var availableMoney = getServerMoneyAvailable("foodnstuff");
|
* var currentMoney = getServerMoneyAvailable("foodnstuff");
|
||||||
* currentMoney = currentMoney * (1 + grow("foodnstuff"));
|
* currentMoney = currentMoney * (1 + grow("foodnstuff"));
|
||||||
* currentMoney = currentMoney * (1 + grow("foodnstuff", { threads: 5 })); // Only use 5 threads to grow
|
|
||||||
* ```
|
* ```
|
||||||
* @example
|
* @example
|
||||||
* ```ts
|
* ```ts
|
||||||
* // NS2:
|
* // NS2:
|
||||||
* let availableMoney = ns.getServerMoneyAvailable("foodnstuff");
|
* let currentMoney = ns.getServerMoneyAvailable("foodnstuff");
|
||||||
* currentMoney *= (1 + await ns.grow("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 host - Hostname of the target server to grow.
|
||||||
* @param opts - Optional parameters for configuring function behavior.
|
* @param opts - Optional parameters for configuring function behavior.
|
||||||
@ -4300,14 +4299,12 @@ export interface NS extends Singularity {
|
|||||||
* // NS1:
|
* // NS1:
|
||||||
* var currentSecurity = getServerSecurityLevel("foodnstuff");
|
* var currentSecurity = getServerSecurityLevel("foodnstuff");
|
||||||
* currentSecurity = currentSecurity - weaken("foodnstuff");
|
* currentSecurity = currentSecurity - weaken("foodnstuff");
|
||||||
* currentSecurity = currentSecurity - weaken("foodnstuff", { threads: 5 }); // Only use 5 threads to weaken
|
|
||||||
* ```
|
* ```
|
||||||
* @example
|
* @example
|
||||||
* ```ts
|
* ```ts
|
||||||
* // NS2:
|
* // NS2:
|
||||||
* let currentSecurity = ns.getServerSecurityLevel("foodnstuff");
|
* let currentSecurity = ns.getServerSecurityLevel("foodnstuff");
|
||||||
* currentSecurity -= await ns.weaken("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 host - Hostname of the target server to weaken.
|
||||||
* @param opts - Optional parameters for configuring function behavior.
|
* @param opts - Optional parameters for configuring function behavior.
|
||||||
@ -4494,6 +4491,17 @@ export interface NS extends Singularity {
|
|||||||
*/
|
*/
|
||||||
print(...args: any[]): void;
|
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.
|
* Prints one or more values or variables to the Terminal.
|
||||||
* @remarks
|
* @remarks
|
||||||
@ -5451,7 +5459,7 @@ export interface NS extends Singularity {
|
|||||||
* @param filename - Optional. Filename or PID of the script.
|
* @param filename - Optional. Filename or PID of the script.
|
||||||
* @param hostname - Optional. Name of host server the script is running on.
|
* @param hostname - Optional. Name of host server the script is running on.
|
||||||
* @param args - Arguments to identify the script
|
* @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;
|
getRunningScript(filename?: FilenameOrPID, hostname?: string, ...args: (string | number)[]): RunningScript;
|
||||||
|
|
||||||
@ -6234,14 +6242,14 @@ export interface OfficeAPI {
|
|||||||
/**
|
/**
|
||||||
* Get the cost to unlock research
|
* Get the cost to unlock research
|
||||||
* @param divisionName - Name of the division
|
* @param divisionName - Name of the division
|
||||||
* @param cityName - Name of the city
|
* @param researchName - Name of the research
|
||||||
* @returns cost
|
* @returns cost
|
||||||
*/
|
*/
|
||||||
getResearchCost(divisionName: string, researchName: string): number;
|
getResearchCost(divisionName: string, researchName: string): number;
|
||||||
/**
|
/**
|
||||||
* Gets if you have unlocked a research
|
* Gets if you have unlocked a research
|
||||||
* @param divisionName - Name of the division
|
* @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
|
* @returns true is unlocked, false if not
|
||||||
*/
|
*/
|
||||||
hasResearched(divisionName: string, researchName: string): boolean;
|
hasResearched(divisionName: string, researchName: string): boolean;
|
||||||
@ -6310,6 +6318,14 @@ export interface WarehouseAPI {
|
|||||||
* @param enabled - smart supply enabled
|
* @param enabled - smart supply enabled
|
||||||
*/
|
*/
|
||||||
setSmartSupply(divisionName: string, cityName: string, enabled: boolean): void;
|
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
|
* Set material buy data
|
||||||
* @param divisionName - Name of the division
|
* @param divisionName - Name of the division
|
||||||
|
@ -33,6 +33,8 @@ import Typography from "@mui/material/Typography";
|
|||||||
import Link from "@mui/material/Link";
|
import Link from "@mui/material/Link";
|
||||||
import Box from "@mui/material/Box";
|
import Box from "@mui/material/Box";
|
||||||
import SettingsIcon from "@mui/icons-material/Settings";
|
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 Table from "@mui/material/Table";
|
||||||
import TableCell from "@mui/material/TableCell";
|
import TableCell from "@mui/material/TableCell";
|
||||||
import TableRow from "@mui/material/TableRow";
|
import TableRow from "@mui/material/TableRow";
|
||||||
@ -41,6 +43,7 @@ import { PromptEvent } from "../../ui/React/PromptManager";
|
|||||||
import { Modal } from "../../ui/React/Modal";
|
import { Modal } from "../../ui/React/Modal";
|
||||||
|
|
||||||
import libSource from "!!raw-loader!../NetscriptDefinitions.d.ts";
|
import libSource from "!!raw-loader!../NetscriptDefinitions.d.ts";
|
||||||
|
import { Tooltip } from "@mui/material";
|
||||||
|
|
||||||
interface IProps {
|
interface IProps {
|
||||||
// Map of filename -> code
|
// 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 {
|
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 openScript = openScripts[index];
|
||||||
const server = GetServer(openScript.hostname);
|
const server = GetServer(openScript.hostname);
|
||||||
if (server === null) throw new Error(`Server '${openScript.hostname}' should not be null, but it is.`);
|
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);
|
const serverScript = server.scripts.find((s) => s.filename === openScript.fileName);
|
||||||
if (serverScript === undefined) return " *";
|
return serverScript?.code ?? null;
|
||||||
|
|
||||||
// The server code is stored with its starting & trailing whitespace removed
|
|
||||||
const openScriptFormatted = Script.formatCode(openScript.code);
|
|
||||||
return serverScript.code !== openScriptFormatted ? " *" : "";
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Toolbars are roughly 112px:
|
// Toolbars are roughly 112px:
|
||||||
@ -738,62 +782,78 @@ export function Root(props: IProps): React.ReactElement {
|
|||||||
overflowX: "scroll",
|
overflowX: "scroll",
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{openScripts.map(({ fileName, hostname }, index) => (
|
{openScripts.map(({ fileName, hostname }, index) => {
|
||||||
<Draggable
|
const iconButtonStyle = {
|
||||||
key={fileName + hostname}
|
maxWidth: "25px",
|
||||||
draggableId={fileName + hostname}
|
minWidth: "25px",
|
||||||
index={index}
|
minHeight: "38.5px",
|
||||||
disableInteractiveElementBlocking={true}
|
maxHeight: "38.5px",
|
||||||
>
|
...(currentScript?.fileName === openScripts[index].fileName
|
||||||
{(provided) => (
|
? {
|
||||||
<div
|
background: Settings.theme.button,
|
||||||
ref={provided.innerRef}
|
borderColor: Settings.theme.button,
|
||||||
{...provided.draggableProps}
|
color: Settings.theme.primary,
|
||||||
{...provided.dragHandleProps}
|
}
|
||||||
style={{
|
: {
|
||||||
...provided.draggableProps.style,
|
background: Settings.theme.backgroundsecondary,
|
||||||
marginRight: "5px",
|
borderColor: Settings.theme.backgroundsecondary,
|
||||||
flexShrink: 0,
|
color: Settings.theme.secondary,
|
||||||
}}
|
}),
|
||||||
>
|
};
|
||||||
<Button
|
return (
|
||||||
onClick={() => onTabClick(index)}
|
<Draggable
|
||||||
style={
|
key={fileName + hostname}
|
||||||
currentScript?.fileName === openScripts[index].fileName
|
draggableId={fileName + hostname}
|
||||||
? {
|
index={index}
|
||||||
background: Settings.theme.button,
|
disableInteractiveElementBlocking={true}
|
||||||
color: Settings.theme.primary,
|
>
|
||||||
}
|
{(provided) => (
|
||||||
: {
|
<div
|
||||||
background: Settings.theme.backgroundsecondary,
|
ref={provided.innerRef}
|
||||||
color: Settings.theme.secondary,
|
{...provided.draggableProps}
|
||||||
}
|
{...provided.dragHandleProps}
|
||||||
}
|
|
||||||
>
|
|
||||||
{hostname}:~/{fileName} {dirty(index)}
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
onClick={() => onTabClose(index)}
|
|
||||||
style={{
|
style={{
|
||||||
maxWidth: "20px",
|
...provided.draggableProps.style,
|
||||||
minWidth: "20px",
|
marginRight: "5px",
|
||||||
...(currentScript?.fileName === openScripts[index].fileName
|
flexShrink: 0,
|
||||||
? {
|
border: "1px solid " + Settings.theme.well,
|
||||||
background: Settings.theme.button,
|
|
||||||
color: Settings.theme.primary,
|
|
||||||
}
|
|
||||||
: {
|
|
||||||
background: Settings.theme.backgroundsecondary,
|
|
||||||
color: Settings.theme.secondary,
|
|
||||||
}),
|
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
x
|
<Button
|
||||||
</Button>
|
onClick={() => onTabClick(index)}
|
||||||
</div>
|
onMouseDown={(e) => {
|
||||||
)}
|
e.preventDefault();
|
||||||
</Draggable>
|
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}
|
{provided.placeholder}
|
||||||
</Box>
|
</Box>
|
||||||
)}
|
)}
|
||||||
|
@ -30,7 +30,7 @@ export function getPurchaseServerCost(ram: number): number {
|
|||||||
|
|
||||||
const upg = Math.max(0, Math.log(sanitizedRam) / Math.log(2) - 6);
|
const upg = Math.max(0, Math.log(sanitizedRam) / Math.log(2) - 6);
|
||||||
|
|
||||||
return (
|
return Math.round(
|
||||||
sanitizedRam *
|
sanitizedRam *
|
||||||
CONSTANTS.BaseCostFor1GBOfRamServer *
|
CONSTANTS.BaseCostFor1GBOfRamServer *
|
||||||
BitNodeMultipliers.PurchasedServerCost *
|
BitNodeMultipliers.PurchasedServerCost *
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
import { ISelfInitializer, ISelfLoading } from "../types";
|
import { ISelfInitializer, ISelfLoading } from "../types";
|
||||||
import { OwnedAugmentationsOrderSetting, PurchaseAugmentationsOrderSetting } from "./SettingEnums";
|
import { OwnedAugmentationsOrderSetting, PurchaseAugmentationsOrderSetting } from "./SettingEnums";
|
||||||
import { defaultTheme, ITheme } from "./Themes";
|
import { defaultTheme, ITheme } from "../Themes/Themes";
|
||||||
import { defaultStyles } from "./Styles";
|
import { defaultStyles } from "../Themes/Styles";
|
||||||
import { WordWrapOptions } from "../ScriptEditor/ui/Options";
|
import { WordWrapOptions } from "../ScriptEditor/ui/Options";
|
||||||
import { OverviewSettings } from "../ui/React/Overview";
|
import { OverviewSettings } from "../ui/React/Overview";
|
||||||
import { IStyleSettings } from "../ScriptEditor/NetscriptDefinitions";
|
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.",
|
" grow Spoof money in a servers bank account, increasing the amount available.",
|
||||||
" hack Hack the current machine",
|
" hack Hack the current machine",
|
||||||
" help [command] Display this help text, or the help text for a command",
|
" help [command] Display this help text, or the help text for a command",
|
||||||
|
" history [-c] Display the terminal history",
|
||||||
" home Connect to home computer",
|
" home Connect to home computer",
|
||||||
" hostname Displays the hostname of the machine",
|
" hostname Displays the hostname of the machine",
|
||||||
" kill [script/pid] [args...] Stops the specified script on the current server ",
|
" kill [script/pid] [args...] Stops the specified script on the current server ",
|
||||||
@ -255,6 +256,12 @@ export const HelpTexts: IMap<string[]> = {
|
|||||||
" help scan-analyze",
|
" help scan-analyze",
|
||||||
" ",
|
" ",
|
||||||
],
|
],
|
||||||
|
history: [
|
||||||
|
"Usage: history [-c]",
|
||||||
|
" ",
|
||||||
|
"Without arguments, displays the terminal command history. To clear the history, pass in the '-c' argument.",
|
||||||
|
" ",
|
||||||
|
],
|
||||||
home: [
|
home: [
|
||||||
"Usage: home", " ", "Connect to your home computer. This will work no matter what server you are currently connected to.", " ",
|
"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 { grow } from "./commands/grow";
|
||||||
import { hack } from "./commands/hack";
|
import { hack } from "./commands/hack";
|
||||||
import { help } from "./commands/help";
|
import { help } from "./commands/help";
|
||||||
|
import { history } from "./commands/history";
|
||||||
import { home } from "./commands/home";
|
import { home } from "./commands/home";
|
||||||
import { hostname } from "./commands/hostname";
|
import { hostname } from "./commands/hostname";
|
||||||
import { kill } from "./commands/kill";
|
import { kill } from "./commands/kill";
|
||||||
@ -143,7 +144,7 @@ export class Terminal implements ITerminal {
|
|||||||
startGrow(player: IPlayer): void {
|
startGrow(player: IPlayer): void {
|
||||||
const server = player.getCurrentServer();
|
const server = player.getCurrentServer();
|
||||||
if (server instanceof HacknetServer) {
|
if (server instanceof HacknetServer) {
|
||||||
this.error("Cannot hack this kind of server");
|
this.error("Cannot grow this kind of server");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (!(server instanceof Server)) throw new Error("server should be normal server");
|
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 {
|
startWeaken(player: IPlayer): void {
|
||||||
const server = player.getCurrentServer();
|
const server = player.getCurrentServer();
|
||||||
if (server instanceof HacknetServer) {
|
if (server instanceof HacknetServer) {
|
||||||
this.error("Cannot hack this kind of server");
|
this.error("Cannot weaken this kind of server");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (!(server instanceof Server)) throw new Error("server should be normal server");
|
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 (cancelled) return;
|
||||||
|
|
||||||
if (server instanceof HacknetServer) {
|
if (server instanceof HacknetServer) {
|
||||||
this.error("Cannot hack this kind of server");
|
this.error("Cannot grow this kind of server");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (!(server instanceof Server)) throw new Error("server should be normal server");
|
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 (cancelled) return;
|
||||||
|
|
||||||
if (server instanceof HacknetServer) {
|
if (server instanceof HacknetServer) {
|
||||||
this.error("Cannot hack this kind of server");
|
this.error("Cannot weaken this kind of server");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (!(server instanceof Server)) throw new Error("server should be normal server");
|
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) {
|
if (this.commandHistory.length > 50) {
|
||||||
this.commandHistory.splice(0, 1);
|
this.commandHistory.splice(0, 1);
|
||||||
}
|
}
|
||||||
|
player.terminalCommandHistory = this.commandHistory;
|
||||||
}
|
}
|
||||||
this.commandHistoryIndex = this.commandHistory.length;
|
this.commandHistoryIndex = this.commandHistory.length;
|
||||||
const allCommands = ParseCommands(commands);
|
const allCommands = ParseCommands(commands);
|
||||||
@ -785,6 +787,7 @@ export class Terminal implements ITerminal {
|
|||||||
grow: grow,
|
grow: grow,
|
||||||
hack: hack,
|
hack: hack,
|
||||||
help: help,
|
help: help,
|
||||||
|
history: history,
|
||||||
home: home,
|
home: home,
|
||||||
hostname: hostname,
|
hostname: hostname,
|
||||||
kill: kill,
|
kill: kill,
|
||||||
|
@ -6,6 +6,37 @@ import { isScriptFilename } from "../../Script/isScriptFilename";
|
|||||||
import FileSaver from "file-saver";
|
import FileSaver from "file-saver";
|
||||||
import JSZip from "jszip";
|
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(
|
export function download(
|
||||||
terminal: ITerminal,
|
terminal: ITerminal,
|
||||||
router: IRouter,
|
router: IRouter,
|
||||||
@ -21,34 +52,12 @@ export function download(
|
|||||||
const fn = args[0] + "";
|
const fn = args[0] + "";
|
||||||
// If the parameter starts with *, download all files that match the wildcard pattern
|
// If the parameter starts with *, download all files that match the wildcard pattern
|
||||||
if (fn.startsWith("*")) {
|
if (fn.startsWith("*")) {
|
||||||
const matchEnding = fn.length == 1 || fn === "*.*" ? null : fn.slice(1); // Treat *.* the same as *
|
try {
|
||||||
const zip = new JSZip();
|
exportScripts(fn, server);
|
||||||
// Helper function to zip any file contents whose name matches the pattern
|
return;
|
||||||
const zipFiles = (fileNames: string[], fileContents: string[]): void => {
|
} catch (error: any) {
|
||||||
for (let i = 0; i < fileContents.length; ++i) {
|
return terminal.error(error.message);
|
||||||
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;
|
|
||||||
} else if (isScriptFilename(fn)) {
|
} else if (isScriptFilename(fn)) {
|
||||||
// Download a single script
|
// Download a single script
|
||||||
const script = terminal.getScript(player, fn);
|
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 [possibilities, setPossibilities] = useState<string[]>([]);
|
||||||
const classes = useStyles();
|
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
|
// Need to run after state updates, for example if we need to move cursor
|
||||||
// *after* we modify input
|
// *after* we modify input
|
||||||
useEffect(() => {
|
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