@ -1,27 +1,26 @@
|
||||
node_modules/
|
||||
dist/
|
||||
input/
|
||||
|
||||
.dist
|
||||
.tmp
|
||||
.package
|
||||
|
||||
assets/
|
||||
css/
|
||||
.build
|
||||
.cypress/
|
||||
cypress/
|
||||
|
||||
dist/
|
||||
input/
|
||||
assets/
|
||||
doc/
|
||||
markdown/
|
||||
netscript_tests/
|
||||
scripts/
|
||||
|
||||
|
||||
test/netscript/
|
||||
|
||||
electron/lib
|
||||
electron/greenworks.js
|
||||
src/ThirdParty/*
|
||||
src/JSInterpreter.js
|
||||
tools/engines-check/
|
||||
|
||||
test/*.bundle.*
|
||||
editor.main.js
|
||||
main.bundle.js
|
||||
webpack.config.js
|
||||
webpack.config-test.js
|
||||
|
100
.github/workflows/bump-version.yml
vendored
Normal file
@ -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
@ -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
@ -5,10 +5,10 @@ Netburner.txt
|
||||
/doc/build
|
||||
/node_modules
|
||||
/electron/node_modules
|
||||
/dist/*.map
|
||||
/test/*.map
|
||||
/test/*.bundle.*
|
||||
/test/*.css
|
||||
/input/bitburner.api.json
|
||||
.cypress
|
||||
|
||||
# tmp folder for electron
|
||||
|
@ -6,7 +6,7 @@ Bitburner is a programming-based [incremental game](https://en.wikipedia.org/wik
|
||||
that revolves around hacking and cyberpunk themes.
|
||||
The game can be played at https://danielyxie.github.io/bitburner or installed through [Steam](https://store.steampowered.com/app/1812820/Bitburner/).
|
||||
|
||||
See the [frequently asked questions](./FAQ.md) for more information . To discuss the game or get help, join the [official discord server](https://discord.gg/TFc3hKD)
|
||||
See the [frequently asked questions](./doc/FAQ.md) for more information . To discuss the game or get help, join the [official discord server](https://discord.gg/TFc3hKD)
|
||||
|
||||
# Documentation
|
||||
|
||||
@ -18,13 +18,13 @@ The [in-game documentation](./markdown/bitburner.md) is generated from the [Type
|
||||
Anyone is welcome to contribute to the documentation by editing the [source
|
||||
files](/doc/source) and then making a pull request with your contributions.
|
||||
For further guidance, please refer to the "As A Documentor" section of
|
||||
[CONTRIBUTING](CONTRIBUTING.md).
|
||||
[CONTRIBUTING](./doc/CONTRIBUTING.md).
|
||||
|
||||
# Contribution
|
||||
|
||||
There are many ways to contribute to the game. It can be as simple as fixing
|
||||
a typo, correcting a bug, or improving the UI. For guidance on doing so,
|
||||
please refer to the [CONTRIBUTING](CONTRIBUTING.md) document.
|
||||
please refer to the [CONTRIBUTING](./doc/CONTRIBUTING.md) document.
|
||||
|
||||
You will retain all ownership of the Copyright of any contributions you make,
|
||||
and will have the same rights to use or license your contributions. By
|
||||
|
BIN
assets/favicon.ico
Normal file
After Width: | Height: | Size: 15 KiB |
12
cypress.json
@ -1,10 +1,14 @@
|
||||
{
|
||||
"baseUrl": "http://localhost:8000",
|
||||
"fixturesFolder": false,
|
||||
"trashAssetsBeforeRuns": true,
|
||||
"screenshotsFolder": ".cypress/screenshots",
|
||||
"videosFolder": ".cypress/videos",
|
||||
"videoUploadOnPasses": false,
|
||||
"viewportWidth": 1980,
|
||||
"viewportHeight": 1080
|
||||
"viewportHeight": 1080,
|
||||
"fixturesFolder": "test/cypress/fixtures",
|
||||
"integrationFolder": "test/cypress/integration",
|
||||
"pluginsFile": "test/cypress/plugins/index.js",
|
||||
"supportFile": "test/cypress/support/index.js",
|
||||
"screenshotsFolder": ".cypress/screenshots",
|
||||
"videosFolder": ".cypress/videos",
|
||||
"downloadsFolder": ".cypress/downloads"
|
||||
}
|
||||
|
@ -1,9 +0,0 @@
|
||||
export {};
|
||||
|
||||
beforeEach(() => {
|
||||
cy.visit("/");
|
||||
cy.clearLocalStorage();
|
||||
cy.window().then((win) => {
|
||||
win.indexedDB.deleteDatabase("bitburnerSave");
|
||||
});
|
||||
});
|
22
dist/bitburner.d.ts
vendored
@ -958,6 +958,16 @@ export declare interface Corporation extends WarehouseAPI, OfficeAPI {
|
||||
* @param percent - Percent of profit to issue as dividends.
|
||||
*/
|
||||
issueDividends(percent: number): void;
|
||||
/**
|
||||
* Buyback Shares
|
||||
* @param amt - Number of shares to attempt to buyback.
|
||||
*/
|
||||
buyBackShares(amt: number): void;
|
||||
/**
|
||||
* Sell Shares
|
||||
* @param amt - Number of shares to attempt to sell.
|
||||
*/
|
||||
sellShares(amt: number): void;
|
||||
}
|
||||
|
||||
/**
|
||||
@ -3748,7 +3758,7 @@ export declare interface NS extends Singularity {
|
||||
* @remarks
|
||||
* RAM cost: 0.3 GB
|
||||
*
|
||||
* Running with no args returns curent script.
|
||||
* Running with no args returns current script.
|
||||
* If you use a PID as the first parameter, the hostname and args parameters are unnecessary.
|
||||
*
|
||||
* @param filename - Optional. Filename or PID of the script.
|
||||
@ -4807,7 +4817,7 @@ export declare interface Server {
|
||||
/** IP Address. Must be unique */
|
||||
ip: string;
|
||||
|
||||
/** Flag indicating whether player is curently connected to this server */
|
||||
/** Flag indicating whether player is currently connected to this server */
|
||||
isConnectedTo: boolean;
|
||||
|
||||
/** RAM (GB) available on this server */
|
||||
@ -6635,6 +6645,14 @@ export declare interface WarehouseAPI {
|
||||
* @param amt - Amount of material to buy
|
||||
*/
|
||||
buyMaterial(divisionName: string, cityName: string, materialName: string, amt: number): void;
|
||||
/**
|
||||
* Set material to bulk buy
|
||||
* @param divisionName - Name of the division
|
||||
* @param cityName - Name of the city
|
||||
* @param materialName - Name of the material
|
||||
* @param amt - Amount of material to buy
|
||||
*/
|
||||
bulkPurchase(divisionName: string, cityName: string, materialName: string, amt: number): void;
|
||||
/**
|
||||
* Get warehouse data
|
||||
* @param divisionName - Name of the division
|
||||
|
20
dist/engine.bundle.js
vendored
BIN
dist/favicon.ico
vendored
Normal file
After Width: | Height: | Size: 15 KiB |
BIN
dist/images/297df8c0e47764ea113951318b2acf55.png
vendored
Normal file
After Width: | Height: | Size: 118 KiB |
BIN
dist/images/447bc31e61f55e7eff875be3e9a81f1a.png
vendored
Normal file
After Width: | Height: | Size: 120 KiB |
BIN
dist/images/4e0e750f2f09de58219773edd46cbbf5.png
vendored
Normal file
After Width: | Height: | Size: 118 KiB |
BIN
dist/images/5aa87b7de67a77c914088783b055e1cf.png
vendored
Normal file
After Width: | Height: | Size: 119 KiB |
BIN
dist/images/66f4b86d86164fc117bd6d648e4eaa6f.png
vendored
Normal file
After Width: | Height: | Size: 116 KiB |
BIN
dist/images/6caf35202b10b52e1fc2743f674c33e8.png
vendored
Normal file
After Width: | Height: | Size: 111 KiB |
BIN
dist/images/83b2443ab7e7d346766c8f6bc5afc7a7.png
vendored
Normal file
After Width: | Height: | Size: 112 KiB |
BIN
dist/images/85a7b2896acb62be76f3ea7100fe9012.png
vendored
Normal file
After Width: | Height: | Size: 121 KiB |
BIN
dist/images/9f96a5084f4e5f1a6c0041b41b34d62d.png
vendored
Normal file
After Width: | Height: | Size: 118 KiB |
BIN
dist/images/a1110d6c8d16a14c4570411750248399.png
vendored
Normal file
After Width: | Height: | Size: 120 KiB |
BIN
dist/images/c7164b072d62c91c27c6d607b5207e7b.png
vendored
Normal file
After Width: | Height: | Size: 121 KiB |
BIN
dist/images/cb88977ea837bccb9cceb727adc78302.png
vendored
Normal file
After Width: | Height: | Size: 118 KiB |
BIN
dist/images/e66b0c327f97d08e4253f52234d659eb.png
vendored
Normal file
After Width: | Height: | Size: 120 KiB |
BIN
dist/images/e97de4daa946331c7e99dee9c05d629c.png
vendored
Normal file
After Width: | Height: | Size: 117 KiB |
20
dist/main.bundle.js
vendored
Normal file
1
dist/main.bundle.js.map
vendored
Normal file
36
dist/vendor.bundle.js
vendored
1
dist/vendor.bundle.js.map
vendored
Normal file
@ -182,7 +182,7 @@ To contribute to and view your changes to the BitBurner documentation on [Read T
|
||||
Docs](http://bitburner.readthedocs.io/), you will
|
||||
need to have Python installed, along with [Sphinx](http://www.sphinx-doc.org).
|
||||
|
||||
To make change to the [in-game documentation](./markdown/bitburner.md), you will need to modify the [TypeScript definitions](./src/ScriptEditor/NetscriptDefinitions.d.ts), not the markdown files.
|
||||
To make change to the [in-game documentation](../markdown/bitburner.md), you will need to modify the [TypeScript definitions](../src/ScriptEditor/NetscriptDefinitions.d.ts), not the markdown files.
|
||||
|
||||
We are using [API Extractor](https://api-extractor.com/pages/tsdoc/doc_comment_syntax/) (tsdoc hints) to generate the markdown doc. Make your changes to the TypeScript definitions and then run `npm run doc`.
|
||||
|
@ -215,6 +215,7 @@ The list contains the name of (i.e. the value returned by
|
||||
| | | The answer should be provided as an array of strings containing the valid expressions. |
|
||||
| | | |
|
||||
| | | NOTE: Numbers in an expression cannot have leading 0's |
|
||||
| | | NOTE: The order of evaluation expects script operator precedence |
|
||||
| | | |
|
||||
| | | Examples: |
|
||||
| | | Input: digits = "123", target = 6 |
|
||||
|
@ -7,6 +7,16 @@ these companies, you can apply for jobs.
|
||||
|
||||
Working a job lets you earn money, experience, and reputation with that company.
|
||||
|
||||
While working for a company, you can click "Do something else simultaneously" to be able
|
||||
to do things while you continue to work in the background. There is a 20% penalty to the
|
||||
related gains. Clicking the "Focus" button under the overview will return you to the
|
||||
current work.
|
||||
|
||||
Reputation is required to apply for a promotion. This reputation is not counted towards
|
||||
your career until the shift ends, either due to the time spent or clicking the
|
||||
"Stop Working" button. For most jobs there is a penalty of 50% of the reputation gained
|
||||
if you stop your shift early.
|
||||
|
||||
Information about all Companies
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
TODO
|
||||
|
@ -33,6 +33,11 @@ from faction to faction.
|
||||
|
||||
List of Factions and their Requirements
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
.. raw:: html
|
||||
|
||||
<details><summary><a>Early Game Factions</a></summary>
|
||||
|
||||
.. _gameplay_factions::
|
||||
|
||||
+---------------------+----------------+-----------------------------------------+-------------------------------+
|
||||
| Early Game | Faction Name | Requirements | Joining this Faction prevents |
|
||||
@ -49,7 +54,18 @@ List of Factions and their Requirements
|
||||
| | | * Total Hacknet RAM of 8 | |
|
||||
| | | * Total Hacknet Cores of 4 | |
|
||||
+---------------------+----------------+-----------------------------------------+-------------------------------+
|
||||
| City Factions | Sector-12 | * Be in Sector-12 | * Chongqing |
|
||||
.. raw:: html
|
||||
|
||||
</details>
|
||||
<details><summary><a>City Factions</a></summary>
|
||||
|
||||
.. _gameplay_factions::
|
||||
|
||||
+---------------------+----------------+-----------------------------------------+-------------------------------+
|
||||
| City Factions | Faction Name | Requirements | Joining this Faction prevents |
|
||||
| | | | you from joining: |
|
||||
+ +----------------+-----------------------------------------+-------------------------------+
|
||||
| | Sector-12 | * Be in Sector-12 | * Chongqing |
|
||||
| | | * $15m | * New Tokyo |
|
||||
| | | | * Ishima |
|
||||
| | | | * Volhaven |
|
||||
@ -77,8 +93,19 @@ List of Factions and their Requirements
|
||||
| | | | * New Tokyo |
|
||||
| | | | * Ishima |
|
||||
+---------------------+----------------+-----------------------------------------+-------------------------------+
|
||||
| Hacking | NiteSec | * Install a backdoor on the avmnite-02h | |
|
||||
| Groups | | server | |
|
||||
.. raw:: html
|
||||
|
||||
</details>
|
||||
<details><summary><a>Hacking Groups</a></summary>
|
||||
|
||||
.. _gameplay_factions::
|
||||
|
||||
+---------------------+----------------+-----------------------------------------+-------------------------------+
|
||||
| Hacking | Faction Name | Requirements | Joining this Faction prevents |
|
||||
| Groups | | | you from joining: |
|
||||
+ +----------------+-----------------------------------------+-------------------------------+
|
||||
| | NiteSec | * Install a backdoor on the avmnite-02h | |
|
||||
| | | server | |
|
||||
| | | | |
|
||||
+ +----------------+-----------------------------------------+-------------------------------+
|
||||
| | The Black Hand | * Install a backdoor on the I.I.I.I | |
|
||||
@ -89,7 +116,18 @@ List of Factions and their Requirements
|
||||
| | | server | |
|
||||
| | | | |
|
||||
+---------------------+----------------+-----------------------------------------+-------------------------------+
|
||||
| Megacorporations | ECorp | * Have 200k reputation with | |
|
||||
.. raw:: html
|
||||
|
||||
</details>
|
||||
<details><summary><a>Megacorporations</a></summary>
|
||||
|
||||
.. _gameplay_factions::
|
||||
|
||||
+---------------------+----------------+-----------------------------------------+-------------------------------+
|
||||
| Megacorporations | Faction Name | Requirements | Joining this Faction prevents |
|
||||
| | | | you from joining: |
|
||||
+ +----------------+-----------------------------------------+-------------------------------+
|
||||
| | ECorp | * Have 200k reputation with | |
|
||||
| | | the Corporation | |
|
||||
+ +----------------+-----------------------------------------+-------------------------------+
|
||||
| | MegaCorp | * Have 200k reputation with | |
|
||||
@ -121,8 +159,19 @@ List of Factions and their Requirements
|
||||
| | | * Install a backdoor on the | |
|
||||
| | | fulcrumassets server | |
|
||||
+---------------------+----------------+-----------------------------------------+-------------------------------+
|
||||
| Criminal | Slum Snakes | * All Combat Stats of 30 | |
|
||||
| Organizations | | * -9 Karma | |
|
||||
.. raw:: html
|
||||
|
||||
</details>
|
||||
<details><summary><a>Criminal Organizations</a></summary>
|
||||
|
||||
.. _gameplay_factions::
|
||||
|
||||
+---------------------+----------------+-----------------------------------------+-------------------------------+
|
||||
| Criminal | Faction Name | Requirements | Joining this Faction prevents |
|
||||
| Organizations | | | you from joining: |
|
||||
+ +----------------+-----------------------------------------+-------------------------------+
|
||||
| | Slum Snakes | * All Combat Stats of 30 | |
|
||||
| | | * -9 Karma | |
|
||||
| | | * $1m | |
|
||||
+ +----------------+-----------------------------------------+-------------------------------+
|
||||
| | Tetrads | * Be in Chongqing, New Tokyo, or Ishima | |
|
||||
@ -153,8 +202,19 @@ List of Factions and their Requirements
|
||||
| | | * -90 Karma | |
|
||||
| | | * Not working for CIA or NSA | |
|
||||
+---------------------+----------------+-----------------------------------------+-------------------------------+
|
||||
| Endgame | The Covenant | * 20 Augmentations | |
|
||||
| Factions | | * $75b | |
|
||||
.. raw:: html
|
||||
|
||||
</details>
|
||||
<details><summary><a>Endgame Factions</a></summary>
|
||||
|
||||
.. _gameplay_factions::
|
||||
|
||||
+---------------------+----------------+-----------------------------------------+-------------------------------+
|
||||
| Endgame | Faction Name | Requirements | Joining this Faction prevents |
|
||||
| Factions | | | you from joining: |
|
||||
+ +----------------+-----------------------------------------+-------------------------------+
|
||||
| | The Covenant | * 20 Augmentations | |
|
||||
| | | * $75b | |
|
||||
| | | * Hacking Level of 850 | |
|
||||
| | | * All Combat Stats of 850 | |
|
||||
+ +----------------+-----------------------------------------+-------------------------------+
|
||||
@ -168,3 +228,6 @@ List of Factions and their Requirements
|
||||
| | | * Hacking Level of 1500 | |
|
||||
| | | * All Combat Stats of 1200 | |
|
||||
+---------------------+----------------+-----------------------------------------+-------------------------------+
|
||||
.. raw:: html
|
||||
|
||||
</details><br>
|
||||
|
@ -107,3 +107,14 @@ starting security, rounded to the nearest integer. To be more precise::
|
||||
|
||||
This means that a server's security level will not fall below this
|
||||
value if you are trying to weaken() it.
|
||||
|
||||
Backdoors
|
||||
^^^^^^^^^
|
||||
Servers that can be hacked can also have backdoors installed. These backdoors
|
||||
will provide you with a benefit; the services may be cheaper, penalties may
|
||||
be reduced or there may be other results. Honeypots exist and will let factions
|
||||
know when you have succeeded at backdooring their system. Once you have a
|
||||
backdoor installed, you can connect to that server directly.
|
||||
|
||||
When you visit a location in the city and see that the name is partially scrambled,
|
||||
this indicates that you have backdoored the server related to the location.
|
||||
|
@ -3,6 +3,182 @@
|
||||
Changelog
|
||||
=========
|
||||
|
||||
v1.5.0 - Steam Cloud integration
|
||||
--------------------------------
|
||||
|
||||
** Steam Cloud Saving **
|
||||
|
||||
* Added support for steam cloud saving (@MartinFournier)
|
||||
|
||||
** UI **
|
||||
|
||||
* background now matches game primary color (@nickofolas)
|
||||
* page title contains version (@MartinFourier)
|
||||
* Major text editor improvements (@nickofolas)
|
||||
* Display bonus time on sleeve page (@MartinFourier)
|
||||
* Several UI improvements (@nickofolas, @smolgumball, @DrCuriosity, @phyzical)
|
||||
* Fix aug display in alpha (@Dominik Winter)
|
||||
* Fix display of corporation product equation (@SagePtr)
|
||||
* Make Bitverse more accessible (@ChrissiQ)
|
||||
* Make corporation warehouse more accessible (@ChrissiQ)
|
||||
* Make tab style more consistent (@nikfolas)
|
||||
|
||||
** Netscript **
|
||||
|
||||
* Fix bug with async.
|
||||
* Add 'printf' ns function (@Ninetailed)
|
||||
* Remove blob caching.
|
||||
* Fix formulas access check (@Ornedan)
|
||||
* Fix bug in exp calculation (@qcorradi)
|
||||
* Fix NaN comparison (@qcorradi)
|
||||
* Fix travelToCity with bad argument (@SlyCedix)
|
||||
* Fix bug where augs could not be purchased via sing (@reacocard)
|
||||
* Fix rounding error in donateToFaction (@Risenafis)
|
||||
* Fix bug with weakenAnalyze (@rhobes)
|
||||
* Prevent exploit with atExit (@Ornedan)
|
||||
* Double 'share' power
|
||||
|
||||
** Corporations **
|
||||
|
||||
* Fix bugs with corp API (@pigalot)
|
||||
* Add smart supply func to corp API (@pd)
|
||||
|
||||
** Misc. **
|
||||
|
||||
* The file API now allows GET and DELETE (@lordducky)
|
||||
* Force achievement calculation on BN completion (@SagePtr)
|
||||
* Cleanup in repository (@MartinFourier)
|
||||
* Several improvements to the electron version (@MartinFourier)
|
||||
* Fix bug with casino roulette (@jamie-mac)
|
||||
* Terminal history persists in savefile (@MartinFourier)
|
||||
* Fix tests (@jamie-mac)
|
||||
* Fix crash with electron windows tracker (@smolgumball)
|
||||
* Fix BN6/7 passive reputation gain (@BrianLDev)
|
||||
* Fix Sleeve not resetting on install (@waffleattack)
|
||||
* Sort joined factions (@jjayeon)
|
||||
* Update documentation / typo (@lethern, @Meowdoleon, @JohnnyUrosevic, @JosephDavidTalbot,
|
||||
@pd, @lethern, @lordducky, @zeddrak, @fearnlj01, @reasonablytall, @MatthewTh0,
|
||||
@SagePtr, @manniL, @Jedimaster4559, @loganville, @Arrow2thekn33, @wdpk, @fwolfst,
|
||||
@fschoenfeldt, @Waladil, @AdamTReineke, @citrusmunch, @factubsio, @ashtongreen,
|
||||
@ChrissiQ, @DJ-Laser, @waffleattack, @ApamNapat, @CrafterKolyan, @DSteve595)
|
||||
* Nerf noodle bar.
|
||||
|
||||
v1.4.0 - 2022-01-18 Sharing is caring
|
||||
-------------------------------------
|
||||
|
||||
** Computer sharing **
|
||||
|
||||
* A new mechanic has been added, it's is invoked by calling the new function 'share'.
|
||||
This mechanic helps you farm reputation faster.
|
||||
|
||||
** gang **
|
||||
|
||||
* Installing augs means losing a little bit of ascension multipliers.
|
||||
|
||||
** Misc. **
|
||||
|
||||
* Prevent gang API from performing actions for the type of gang they are not. (@TheMas3212)
|
||||
* Fix donation to gang faction. (@TheMas3212)
|
||||
* Fix gang check crashing the game. (@TheMas3212)
|
||||
* Make time compression more robust.
|
||||
* Fix bug with scp.
|
||||
* Add zoom to steam version. (@MartinFourier)
|
||||
* Fix donateToFaction accepts donation of NaN. (@woody-lam-cwl)
|
||||
* Show correct hash capacity gain on cache level upgrade tooltip. (@woody-lam-cwl)
|
||||
* Fix tests (@woody-lam-cwl)
|
||||
* Fix cache tooltip (@woody-lam-cwl)
|
||||
* Added script to prettify save file for debugging (@MartinFourier)
|
||||
* Update documentation / typos (@theit8514, @thadguidry, @tigercat2000, @SlyCedix, @Spacejoker, @KenJohansson,
|
||||
@Ornedan, @JustAnOkapi, @nickofolas, @philarmstead, @TheMas3212, @dcragusa, @XxKingsxX-Pinu,
|
||||
@paiv, @smolgumball, @zeddrak, @stinky-lizard, @nickofolas, @Feodoric, @daanflore,
|
||||
@markusariliu, @mstruebing, @erplsf, @waffleattack, @Dexalt142, @AIT-OLPE, @deathly809, @BuckAMayzing,
|
||||
@MartinFourier, @pigalot, @lethern)
|
||||
* Fix BN3+ achievement (@SagePtr)
|
||||
* Fix reputation carry over bug (@TheMas3212)
|
||||
* Add button to exit infiltrations (@TheMas3212)
|
||||
* Add dev menu achievement check (@TheMas3212)
|
||||
* Add 'host' config for electron server (@MartinFourier)
|
||||
* Suppress save toast only works for autosave (@MartinFourier)
|
||||
* Fix some achievements not triggering with 'backdoor' (@SagePtr)
|
||||
* Update Neuroflux Governor description.
|
||||
* Fix bug with electron server.
|
||||
* Fix bug with corporation employee assignment function (@Ornedan)
|
||||
* Add detailed information to terminal 'mem' command (@MartinFourier)
|
||||
* Add savestamp to savefile (@MartinFourier)
|
||||
* Dev menu can apply export bonus (@MartinFourier)
|
||||
* Icarus message no longer applies on top of itself (@Feodoric)
|
||||
* purchase augment via API can no longer buy Neuroflux when it shouldn't (@Feodoric)
|
||||
* Syntax highlighter should be smarter (@neuralsim)
|
||||
* Fix some miscalculation when calculating money stolen (@zeddrak)
|
||||
* Fix max cache achievement working with 0 cache (@MartinFourier)
|
||||
* Add achievements in the game, not just steam (@MartinFourier)
|
||||
* Overflow hash converts to money automatically (@MartinFourier)
|
||||
* Make mathjax load locally (@MartinFourier)
|
||||
* Make favor calculation more efficient (@kittycat2002)
|
||||
* Fix some scripts crashing the game on startup (@MartinFourier)
|
||||
* Toasts will appear above tail window (@MartinFourier)
|
||||
* Fix issue that can cause terminal actions to start on one server and end on another (@MartinFourier)
|
||||
* Fix 'fileExists' not correctly matching file names (@TheMas3212)
|
||||
* Refactor some code to be more efficient (@TheMas3212)
|
||||
* Fix exp gain for terminal grow and weaken (@nickofolas)
|
||||
* Refactor script death code to reject waiting promises instead of resolving (@Ornedan)
|
||||
* HP recalculates on defense exp gain (@TheMas3212)
|
||||
* Fix log for ascendMember (@TheMas3212)
|
||||
* Netscript ports clear on reset (@TheMas3212)
|
||||
* Fix bug related to company (@TheMas3212)
|
||||
* Fix bug where corporation handbook would not be correctly added (@TheMas3212)
|
||||
* Servers in hash upgrades are sorted alpha (@MartinFourier)
|
||||
* Fix very old save not properly migrating augmentation renamed in 0.56 (@MartinFourier)
|
||||
* Add font height and line height in theme settings (@MartinFourier)
|
||||
* Fix crash when quitting job (@MartinFourier)
|
||||
* Added save file validation system (@TheMas3212)
|
||||
* React and ReactDOM are now global objects (@pigalot)
|
||||
* 'nano' supports globs (@smolgumball)
|
||||
* Character overview can be dragged (@MartinFourier)
|
||||
* Job page updates in real time (@nickofolas)
|
||||
* Company favor gain uses the same calculation as faction, this is just performance
|
||||
the value didn't change (@nickofolas)
|
||||
* ns2 files work with more import options (@theit8514)
|
||||
* Allow autocomplete for partial executables (@nickofolas)
|
||||
* Add support for contract completion (@nickofolas)
|
||||
* 'ls' link are clickable (@smolgumball)
|
||||
* Prevent steam from opening external LOCAL files (@MartinFourier)
|
||||
* Fix a bug with autocomplete (@Feodoric)
|
||||
* Optimise achievement checks (@Feodoric)
|
||||
* Hacknet server achievements grant associated hacknet node achievement (@Feodoric)
|
||||
* Fix display bug with hacknet (@Feodoric)
|
||||
* 'analyze' now says if the server is backdoored (@deathly809)
|
||||
* Add option to exclude running script from save (@MartinFourier)
|
||||
* Game now catches more errors and redirects to recovery page (@MartinFourier)
|
||||
* Fix bug with autocomplete (@nickofolas)
|
||||
* Add tooltip to unfocus work (@nickofolas)
|
||||
* Add detailst overview (@MartinFourier)
|
||||
* Fix focus bug (@deathly809)
|
||||
* Fix some NaN handling (@deathly809)
|
||||
* Added 'mv' ns function (@deathly809)
|
||||
* Add focus argument to some singularity functions (@nickofolas)
|
||||
* Fix some functions not disabling log correctly (@deathly809)
|
||||
* General UI improvements (@nickofolas)
|
||||
* Handle steamworks errors gravefully (@MartinFourier)
|
||||
* Fix some react component not unmounting correctly (@MartinFourier)
|
||||
* 'help' autocompletes (@nickofolas)
|
||||
* No longer push all achievements to steam (@Ornedan)
|
||||
* Recovery page has more information (@MartinFourier)
|
||||
* Added 'getGameInfo' ns function (@MartinFourier)
|
||||
* SF3.3 unlocks all corp API (@pigalot)
|
||||
* Major improvements to corp API (@pigalot)
|
||||
* Prevent seed money outside BN3 (@pigalot)
|
||||
* Fix bug where using keyboard shortcuts would crash if the feature is not available (@MartinFourier)\
|
||||
* Sidebar remains opened/closed on save (@MartinFourier)
|
||||
* Added tooltip to sidebar when closed (@MartinFourier)
|
||||
* Fix bug where Formulas.exe is not available when starting BN5 (@TheMas3212)
|
||||
* Fix CI (@tvanderpol)
|
||||
* Change shortcuts to match sidebar (@MartinFourier)
|
||||
* Format gang respect (@attrib)
|
||||
* Add modal to text editor with ram details (@nickofolas)
|
||||
* Fix several bugs with singularity focus (@nickofolas)
|
||||
* Nerf noodle bar.
|
||||
|
||||
v1.3.0 - 2022-01-04 Cleaning up
|
||||
-------------------------------
|
||||
|
||||
|
@ -64,9 +64,9 @@ documentation_title = '{0} Documentation'.format(project)
|
||||
# built documents.
|
||||
#
|
||||
# The short X.Y version.
|
||||
version = '1.3'
|
||||
version = '1.5'
|
||||
# The full version, including alpha/beta/rc tags.
|
||||
release = '1.3.0'
|
||||
release = '1.5.0'
|
||||
|
||||
# The language for content autogenerated by Sphinx. Refer to documentation
|
||||
# for a list of supported languages.
|
||||
|
@ -23,4 +23,4 @@ Bug
|
||||
---
|
||||
|
||||
Otherwise, the game is probably frozen/stuck due to a bug. To report a bug, follow
|
||||
the guidelines `here <https://github.com/danielyxie/bitburner/blob/master/CONTRIBUTING.md#reporting-bugs>`_.
|
||||
the guidelines `here <https://github.com/danielyxie/bitburner/blob/master/doc/CONTRIBUTING.md#reporting-bugs>`_.
|
||||
|
@ -117,7 +117,7 @@ Source-File
|
||||
:Max Level: 3
|
||||
|
||||
This Source-File lets you access and use the Singularity Functions in other BitNodes.
|
||||
Each level of this Source-File will open up more Singularity Functions that you can use.
|
||||
Each level of this Source-File will reduce RAM costs.
|
||||
|
||||
Difficulty:
|
||||
Depending on what Source-Files you have unlocked before attempting this BitNode,
|
||||
|
@ -9,7 +9,7 @@ async function enableAchievementsInterval(window) {
|
||||
// This is backward but the game fills in an array called `document.achievements` and we retrieve it from
|
||||
// here. Hey if it works it works.
|
||||
const steamAchievements = greenworks.getAchievementNames();
|
||||
log.debug(`All Steam achievements ${JSON.stringify(steamAchievements)}`);
|
||||
log.silly(`All Steam achievements ${JSON.stringify(steamAchievements)}`);
|
||||
const playerAchieved = (await Promise.all(steamAchievements.map(checkSteamAchievement))).filter(name => !!name);
|
||||
log.debug(`Player has Steam achievements ${JSON.stringify(playerAchieved)}`);
|
||||
const intervalID = setInterval(async () => {
|
||||
|
@ -12,11 +12,13 @@ async function initialize(win) {
|
||||
window = win;
|
||||
server = http.createServer(async function (req, res) {
|
||||
let body = "";
|
||||
res.setHeader('Content-Type', 'application/json');
|
||||
|
||||
req.on("data", (chunk) => {
|
||||
body += chunk.toString(); // convert Buffer to string
|
||||
});
|
||||
req.on("end", () => {
|
||||
|
||||
req.on("end", async () => {
|
||||
const providedToken = req.headers?.authorization?.replace('Bearer ', '') ?? '';
|
||||
const isValid = providedToken === getAuthenticationToken();
|
||||
if (isValid) {
|
||||
@ -24,8 +26,11 @@ async function initialize(win) {
|
||||
} else {
|
||||
log.log('Invalid authentication token');
|
||||
res.writeHead(401);
|
||||
res.write('Invalid authentication token');
|
||||
res.end();
|
||||
|
||||
res.end(JSON.stringify({
|
||||
success: false,
|
||||
msg: 'Invalid authentication token'
|
||||
}));
|
||||
return;
|
||||
}
|
||||
|
||||
@ -35,17 +40,56 @@ async function initialize(win) {
|
||||
} catch (error) {
|
||||
log.warn(`Invalid body data`);
|
||||
res.writeHead(400);
|
||||
res.write('Invalid body data');
|
||||
res.end();
|
||||
res.end(JSON.stringify({
|
||||
success: false,
|
||||
msg: 'Invalid body data'
|
||||
}));
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (data) {
|
||||
window.webContents.executeJavaScript(`document.saveFile("${data.filename}", "${data.code}")`).then((result) => {
|
||||
res.write(result);
|
||||
res.end();
|
||||
});
|
||||
let result;
|
||||
switch(req.method) {
|
||||
// Request files
|
||||
case "GET":
|
||||
result = await window.webContents.executeJavaScript(`document.getFiles()`);
|
||||
break;
|
||||
|
||||
// Create or update files
|
||||
// Support POST for VScode implementation
|
||||
case "POST":
|
||||
case "PUT":
|
||||
if (!data) {
|
||||
log.warn(`Invalid script update request - No data`);
|
||||
res.writeHead(400);
|
||||
res.end(JSON.stringify({
|
||||
success: false,
|
||||
msg: 'Invalid script update request - No data'
|
||||
}));
|
||||
return;
|
||||
}
|
||||
|
||||
result = await window.webContents.executeJavaScript(`document.saveFile("${data.filename}", "${data.code}")`);
|
||||
break;
|
||||
|
||||
// Delete files
|
||||
case "DELETE":
|
||||
result = await window.webContents.executeJavaScript(`document.deleteFile("${data.filename}")`);
|
||||
break;
|
||||
}
|
||||
|
||||
if (!result.res) {
|
||||
//We've encountered an error
|
||||
res.writeHead(400);
|
||||
log.warn(`Api Server Error`, result.msg);
|
||||
}
|
||||
|
||||
res.end(JSON.stringify({
|
||||
success: result.res,
|
||||
msg: result.msg,
|
||||
data: result.data
|
||||
}));
|
||||
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -8,20 +8,42 @@ const api = require("./api-server");
|
||||
const cp = require("child_process");
|
||||
const path = require("path");
|
||||
const fs = require("fs");
|
||||
const { windowTracker } = require("./windowTracker");
|
||||
const { fileURLToPath } = require("url");
|
||||
|
||||
const debug = process.argv.includes("--debug");
|
||||
|
||||
async function createWindow(killall) {
|
||||
const setStopProcessHandler = global.app_handlers.stopProcess;
|
||||
|
||||
let icon;
|
||||
if (process.platform == 'linux') {
|
||||
icon = path.join(__dirname, 'icon.png');
|
||||
}
|
||||
|
||||
const tracker = windowTracker('main');
|
||||
const window = new BrowserWindow({
|
||||
icon,
|
||||
show: false,
|
||||
backgroundThrottling: false,
|
||||
backgroundColor: "#000000",
|
||||
title: 'Bitburner',
|
||||
x: tracker.state.x,
|
||||
y: tracker.state.y,
|
||||
width: tracker.state.width,
|
||||
height: tracker.state.height,
|
||||
minWidth: 600,
|
||||
minHeight: 400,
|
||||
webPreferences: {
|
||||
nativeWindowOpen: true,
|
||||
preload: path.join(__dirname, 'preload.js'),
|
||||
},
|
||||
});
|
||||
|
||||
setTimeout(() => tracker.track(window), 1000);
|
||||
if (tracker.state.isMaximized) window.maximize();
|
||||
|
||||
window.removeMenu();
|
||||
window.maximize();
|
||||
noScripts = killall ? { query: { noScripts: killall } } : {};
|
||||
window.loadFile("index.html", noScripts);
|
||||
window.show();
|
||||
|
137
electron/main.js
@ -1,12 +1,19 @@
|
||||
/* eslint-disable no-process-exit */
|
||||
/* eslint-disable @typescript-eslint/no-var-requires */
|
||||
const { app, dialog, BrowserWindow } = require("electron");
|
||||
const { app, dialog, BrowserWindow, ipcMain } = require("electron");
|
||||
const log = require("electron-log");
|
||||
const greenworks = require("./greenworks");
|
||||
const api = require("./api-server");
|
||||
const gameWindow = require("./gameWindow");
|
||||
const achievements = require("./achievements");
|
||||
const utils = require("./utils");
|
||||
const storage = require("./storage");
|
||||
const debounce = require("lodash/debounce");
|
||||
const Config = require("electron-config");
|
||||
const config = new Config();
|
||||
|
||||
log.transports.file.level = config.get("file-log-level", "info");
|
||||
log.transports.console.level = config.get("console-log-level", "debug");
|
||||
|
||||
log.catchErrors();
|
||||
log.info(`Started app: ${JSON.stringify(process.argv)}`);
|
||||
@ -30,6 +37,8 @@ try {
|
||||
global.greenworksError = ex.message;
|
||||
}
|
||||
|
||||
let isRestoreDisabled = false;
|
||||
|
||||
function setStopProcessHandler(app, window, enabled) {
|
||||
const closingWindowHandler = async (e) => {
|
||||
// We need to prevent the default closing event to add custom logic
|
||||
@ -41,6 +50,18 @@ function setStopProcessHandler(app, window, enabled) {
|
||||
// Shutdown the http server
|
||||
api.disable();
|
||||
|
||||
// Trigger debounced saves right now before closing
|
||||
try {
|
||||
await saveToDisk.flush();
|
||||
} catch (error) {
|
||||
log.error(error);
|
||||
}
|
||||
try {
|
||||
await saveToCloud.flush();
|
||||
} catch (error) {
|
||||
log.error(error);
|
||||
}
|
||||
|
||||
// Because of a steam limitation, if the player has launched an external browser,
|
||||
// steam will keep displaying the game as "Running" in their UI as long as the browser is up.
|
||||
// So we'll alert the player to close their browser.
|
||||
@ -87,21 +108,106 @@ function setStopProcessHandler(app, window, enabled) {
|
||||
process.exit(0);
|
||||
};
|
||||
|
||||
const receivedGameReadyHandler = async (event, arg) => {
|
||||
if (!window) {
|
||||
log.warn("Window was undefined in game info handler");
|
||||
return;
|
||||
}
|
||||
|
||||
log.debug("Received game information", arg);
|
||||
window.gameInfo = { ...arg };
|
||||
await storage.prepareSaveFolders(window);
|
||||
|
||||
const restoreNewest = config.get("onload-restore-newest", true);
|
||||
if (restoreNewest && !isRestoreDisabled) {
|
||||
try {
|
||||
await storage.restoreIfNewerExists(window)
|
||||
} catch (error) {
|
||||
log.error("Could not restore newer file", error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const receivedDisableRestoreHandler = async (event, arg) => {
|
||||
if (!window) {
|
||||
log.warn("Window was undefined in disable import handler");
|
||||
return;
|
||||
}
|
||||
|
||||
log.debug(`Disabling auto-restore for ${arg.duration}ms.`);
|
||||
isRestoreDisabled = true;
|
||||
setTimeout(() => {
|
||||
isRestoreDisabled = false;
|
||||
log.debug("Re-enabling auto-restore");
|
||||
}, arg.duration);
|
||||
}
|
||||
|
||||
const receivedGameSavedHandler = async (event, arg) => {
|
||||
if (!window) {
|
||||
log.warn("Window was undefined in game saved handler");
|
||||
return;
|
||||
}
|
||||
|
||||
const { save, ...other } = arg;
|
||||
log.silly("Received game saved info", {...other, save: `${save.length} bytes`});
|
||||
|
||||
if (storage.isAutosaveEnabled()) {
|
||||
saveToDisk(save, arg.fileName);
|
||||
}
|
||||
if (storage.isCloudEnabled()) {
|
||||
const minimumPlaytime = 1000 * 60 * 15;
|
||||
const playtime = window.gameInfo.player.playtime;
|
||||
log.silly(window.gameInfo);
|
||||
if (playtime > minimumPlaytime) {
|
||||
saveToCloud(save);
|
||||
} else {
|
||||
log.debug(`Auto-save to cloud disabled for save game under ${minimumPlaytime}ms (${playtime}ms)`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const saveToCloud = debounce(async (save) => {
|
||||
log.debug("Saving to Steam Cloud ...")
|
||||
try {
|
||||
const playerId = window.gameInfo.player.identifier;
|
||||
await storage.pushGameSaveToSteamCloud(save, playerId);
|
||||
log.silly("Saved Game to Steam Cloud");
|
||||
} catch (error) {
|
||||
log.error(error);
|
||||
utils.writeToast(window, "Could not save to Steam Cloud.", "error", 5000);
|
||||
}
|
||||
}, config.get("cloud-save-min-time", 1000 * 60 * 15), { leading: true });
|
||||
|
||||
const saveToDisk = debounce(async (save, fileName) => {
|
||||
log.debug("Saving to Disk ...")
|
||||
try {
|
||||
const file = await storage.saveGameToDisk(window, { save, fileName });
|
||||
log.silly(`Saved Game to '${file.replaceAll('\\', '\\\\')}'`);
|
||||
} catch (error) {
|
||||
log.error(error);
|
||||
utils.writeToast(window, "Could not save to disk", "error", 5000);
|
||||
}
|
||||
}, config.get("disk-save-min-time", 1000 * 60 * 5), { leading: true });
|
||||
|
||||
if (enabled) {
|
||||
log.debug('Adding closing handlers');
|
||||
log.debug("Adding closing handlers");
|
||||
ipcMain.on("push-game-ready", receivedGameReadyHandler);
|
||||
ipcMain.on("push-game-saved", receivedGameSavedHandler);
|
||||
ipcMain.on("push-disable-restore", receivedDisableRestoreHandler)
|
||||
window.on("closed", clearWindowHandler);
|
||||
window.on("close", closingWindowHandler)
|
||||
app.on("window-all-closed", stopProcessHandler);
|
||||
} else {
|
||||
log.debug('Removing closing handlers');
|
||||
log.debug("Removing closing handlers");
|
||||
ipcMain.removeAllListeners();
|
||||
window.removeListener("closed", clearWindowHandler);
|
||||
window.removeListener("close", closingWindowHandler);
|
||||
app.removeListener("window-all-closed", stopProcessHandler);
|
||||
}
|
||||
}
|
||||
|
||||
function startWindow(noScript) {
|
||||
gameWindow.createWindow(noScript);
|
||||
async function startWindow(noScript) {
|
||||
return gameWindow.createWindow(noScript);
|
||||
}
|
||||
|
||||
global.app_handlers = {
|
||||
@ -110,7 +216,7 @@ global.app_handlers = {
|
||||
}
|
||||
|
||||
app.whenReady().then(async () => {
|
||||
log.info('Application is ready!');
|
||||
log.info("Application is ready!");
|
||||
|
||||
if (process.argv.includes("--export-save")) {
|
||||
const window = new BrowserWindow({ show: false });
|
||||
@ -119,15 +225,14 @@ app.whenReady().then(async () => {
|
||||
setStopProcessHandler(app, window, true);
|
||||
await utils.exportSave(window);
|
||||
} else {
|
||||
startWindow(process.argv.includes("--no-scripts"));
|
||||
}
|
||||
|
||||
if (global.greenworksError) {
|
||||
dialog.showMessageBox({
|
||||
title: 'Bitburner',
|
||||
message: 'Could not connect to Steam',
|
||||
detail: `${global.greenworksError}\n\nYou won't be able to receive achievements until this is resolved and you restart the game.`,
|
||||
type: 'warning', buttons: ['OK']
|
||||
});
|
||||
const window = await startWindow(process.argv.includes("--no-scripts"));
|
||||
if (global.greenworksError) {
|
||||
await dialog.showMessageBox(window, {
|
||||
title: "Bitburner",
|
||||
message: "Could not connect to Steam",
|
||||
detail: `${global.greenworksError}\n\nYou won't be able to receive achievements until this is resolved and you restart the game.`,
|
||||
type: 'warning', buttons: ['OK']
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
|
171
electron/menu.js
@ -1,11 +1,169 @@
|
||||
/* eslint-disable @typescript-eslint/no-var-requires */
|
||||
const { Menu, clipboard, dialog } = require("electron");
|
||||
const { app, Menu, clipboard, dialog, shell } = require("electron");
|
||||
const log = require("electron-log");
|
||||
const Config = require("electron-config");
|
||||
const api = require("./api-server");
|
||||
const utils = require("./utils");
|
||||
const storage = require("./storage");
|
||||
const config = new Config();
|
||||
|
||||
function getMenu(window) {
|
||||
return Menu.buildFromTemplate([
|
||||
{
|
||||
label: "File",
|
||||
submenu: [
|
||||
{
|
||||
label: "Save Game",
|
||||
click: () => window.webContents.send("trigger-save"),
|
||||
},
|
||||
{
|
||||
label: "Export Save",
|
||||
click: () => window.webContents.send("trigger-game-export"),
|
||||
},
|
||||
{
|
||||
label: "Export Scripts",
|
||||
click: async () => window.webContents.send("trigger-scripts-export"),
|
||||
},
|
||||
{
|
||||
type: "separator",
|
||||
},
|
||||
{
|
||||
label: "Load Last Save",
|
||||
click: async () => {
|
||||
try {
|
||||
const saveGame = await storage.loadLastFromDisk(window);
|
||||
window.webContents.send("push-save-request", { save: saveGame });
|
||||
} catch (error) {
|
||||
log.error(error);
|
||||
utils.writeToast(window, "Could not load last save from disk", "error", 5000);
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
label: "Load From File",
|
||||
click: async () => {
|
||||
const defaultPath = await storage.getSaveFolder(window);
|
||||
const result = await dialog.showOpenDialog(window, {
|
||||
title: "Load From File",
|
||||
defaultPath: defaultPath,
|
||||
buttonLabel: "Load",
|
||||
filters: [
|
||||
{ name: "Game Saves", extensions: ["json", "json.gz", "txt"] },
|
||||
{ name: "All", extensions: ["*"] },
|
||||
],
|
||||
properties: [
|
||||
"openFile", "dontAddToRecent",
|
||||
]
|
||||
});
|
||||
if (result.canceled) return;
|
||||
const file = result.filePaths[0];
|
||||
|
||||
try {
|
||||
const saveGame = await storage.loadFileFromDisk(file);
|
||||
window.webContents.send("push-save-request", { save: saveGame });
|
||||
} catch (error) {
|
||||
log.error(error);
|
||||
utils.writeToast(window, "Could not load save from disk", "error", 5000);
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
label: "Load From Steam Cloud",
|
||||
enabled: storage.isCloudEnabled(),
|
||||
click: async () => {
|
||||
try {
|
||||
const saveGame = await storage.getSteamCloudSaveString();
|
||||
await storage.pushSaveGameForImport(window, saveGame, false);
|
||||
} catch (error) {
|
||||
log.error(error);
|
||||
utils.writeToast(window, "Could not load from Steam Cloud", "error", 5000);
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
type: "separator",
|
||||
},
|
||||
{
|
||||
label: "Compress Disk Saves (.gz)",
|
||||
type: "checkbox",
|
||||
checked: storage.isSaveCompressionEnabled(),
|
||||
click: (menuItem) => {
|
||||
storage.setSaveCompressionConfig(menuItem.checked);
|
||||
utils.writeToast(window,
|
||||
`${menuItem.checked ? "Enabled" : "Disabled"} Save Compression`, "info", 5000);
|
||||
refreshMenu(window);
|
||||
},
|
||||
},
|
||||
{
|
||||
label: "Auto-Save to Disk",
|
||||
type: "checkbox",
|
||||
checked: storage.isAutosaveEnabled(),
|
||||
click: (menuItem) => {
|
||||
storage.setAutosaveConfig(menuItem.checked);
|
||||
utils.writeToast(window,
|
||||
`${menuItem.checked ? "Enabled" : "Disabled"} Auto-Save to Disk`, "info", 5000);
|
||||
refreshMenu(window);
|
||||
},
|
||||
},
|
||||
{
|
||||
label: "Auto-Save to Steam Cloud",
|
||||
type: "checkbox",
|
||||
enabled: !global.greenworksError,
|
||||
checked: storage.isCloudEnabled(),
|
||||
click: (menuItem) => {
|
||||
storage.setCloudEnabledConfig(menuItem.checked);
|
||||
utils.writeToast(window,
|
||||
`${menuItem.checked ? "Enabled" : "Disabled"} Auto-Save to Steam Cloud`, "info", 5000);
|
||||
refreshMenu(window);
|
||||
},
|
||||
},
|
||||
{
|
||||
label: "Restore Newest on Load",
|
||||
type: "checkbox",
|
||||
checked: config.get("onload-restore-newest", true),
|
||||
click: (menuItem) => {
|
||||
config.set("onload-restore-newest", menuItem.checked);
|
||||
utils.writeToast(window,
|
||||
`${menuItem.checked ? "Enabled" : "Disabled"} Restore Newest on Load`, "info", 5000);
|
||||
refreshMenu(window);
|
||||
},
|
||||
},
|
||||
{
|
||||
type: "separator",
|
||||
},
|
||||
{
|
||||
label: "Open Directory",
|
||||
submenu: [
|
||||
{
|
||||
label: "Open Game Directory",
|
||||
click: () => shell.openPath(app.getAppPath()),
|
||||
},
|
||||
{
|
||||
label: "Open Saves Directory",
|
||||
click: async () => {
|
||||
const path = await storage.getSaveFolder(window);
|
||||
shell.openPath(path);
|
||||
},
|
||||
},
|
||||
{
|
||||
label: "Open Logs Directory",
|
||||
click: () => shell.openPath(app.getPath("logs")),
|
||||
},
|
||||
{
|
||||
label: "Open Data Directory",
|
||||
click: () => shell.openPath(app.getPath("userData")),
|
||||
},
|
||||
]
|
||||
},
|
||||
{
|
||||
type: "separator",
|
||||
},
|
||||
{
|
||||
label: "Quit",
|
||||
click: () => app.quit(),
|
||||
},
|
||||
]
|
||||
},
|
||||
{
|
||||
label: "Edit",
|
||||
submenu: [
|
||||
@ -163,6 +321,17 @@ function getMenu(window) {
|
||||
label: "Activate",
|
||||
click: () => window.webContents.openDevTools(),
|
||||
},
|
||||
{
|
||||
label: "Delete Steam Cloud Data",
|
||||
enabled: !global.greenworksError,
|
||||
click: async () => {
|
||||
try {
|
||||
await storage.deleteCloudFile();
|
||||
} catch (error) {
|
||||
log.error(error);
|
||||
}
|
||||
}
|
||||
}
|
||||
],
|
||||
},
|
||||
]);
|
||||
|
13
electron/package-lock.json
generated
@ -9,7 +9,8 @@
|
||||
"version": "1.0.0",
|
||||
"dependencies": {
|
||||
"electron-config": "^2.0.0",
|
||||
"electron-log": "^4.4.4"
|
||||
"electron-log": "^4.4.4",
|
||||
"lodash": "^4.17.21"
|
||||
}
|
||||
},
|
||||
"node_modules/conf": {
|
||||
@ -104,6 +105,11 @@
|
||||
"node": ">=4"
|
||||
}
|
||||
},
|
||||
"node_modules/lodash": {
|
||||
"version": "4.17.21",
|
||||
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz",
|
||||
"integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg=="
|
||||
},
|
||||
"node_modules/make-dir": {
|
||||
"version": "1.3.0",
|
||||
"resolved": "https://registry.npmjs.org/make-dir/-/make-dir-1.3.0.tgz",
|
||||
@ -259,6 +265,11 @@
|
||||
"path-exists": "^3.0.0"
|
||||
}
|
||||
},
|
||||
"lodash": {
|
||||
"version": "4.17.21",
|
||||
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz",
|
||||
"integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg=="
|
||||
},
|
||||
"make-dir": {
|
||||
"version": "1.3.0",
|
||||
"resolved": "https://registry.npmjs.org/make-dir/-/make-dir-1.3.0.tgz",
|
||||
|
@ -16,6 +16,8 @@
|
||||
"./dist/**/*",
|
||||
"./node_modules/**/*",
|
||||
"./public/**/*",
|
||||
"./src/**",
|
||||
"./lib/**,",
|
||||
"*.js"
|
||||
],
|
||||
"directories": {
|
||||
@ -23,6 +25,7 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"electron-config": "^2.0.0",
|
||||
"electron-log": "^4.4.4"
|
||||
"electron-log": "^4.4.4",
|
||||
"lodash": "^4.17.21"
|
||||
}
|
||||
}
|
||||
|
38
electron/preload.js
Normal file
@ -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
@ -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,
|
||||
};
|
68
electron/windowTracker.js
Normal file
@ -0,0 +1,68 @@
|
||||
/* 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 (!window || window.isDestroyed()) {
|
||||
log.silly(`Saving window state failed because window is not available`);
|
||||
return;
|
||||
}
|
||||
|
||||
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 };
|
34502
input/bitburner.api.json
@ -8,4 +8,8 @@ module.exports = {
|
||||
'.cypress', 'node_modules', 'dist',
|
||||
],
|
||||
testEnvironment: "jsdom",
|
||||
moduleNameMapper: {
|
||||
"\\.(jpg|jpeg|png|gif|eot|otf|webp|svg|ttf|woff|woff2|mp4|webm|wav|mp3|m4a|aac|oga)$": "<rootDir>/test/__mocks__/fileMock.js",
|
||||
"\\.(css|less)$": "<rootDir>/test/__mocks__/styleMock.js"
|
||||
}
|
||||
};
|
||||
|
24
markdown/bitburner.corporation.buybackshares.md
Normal file
@ -0,0 +1,24 @@
|
||||
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
|
||||
|
||||
[Home](./index.md) > [bitburner](./bitburner.md) > [Corporation](./bitburner.corporation.md) > [buyBackShares](./bitburner.corporation.buybackshares.md)
|
||||
|
||||
## Corporation.buyBackShares() method
|
||||
|
||||
Buyback Shares
|
||||
|
||||
<b>Signature:</b>
|
||||
|
||||
```typescript
|
||||
buyBackShares(amount: number): void;
|
||||
```
|
||||
|
||||
## Parameters
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --- | --- | --- |
|
||||
| amount | number | Amount of shares to buy back. |
|
||||
|
||||
<b>Returns:</b>
|
||||
|
||||
void
|
||||
|
@ -19,6 +19,7 @@ export interface Corporation extends WarehouseAPI, OfficeAPI
|
||||
| --- | --- |
|
||||
| [acceptInvestmentOffer()](./bitburner.corporation.acceptinvestmentoffer.md) | Accept investment based on you companies current valuation |
|
||||
| [bribe(factionName, amountCash, amountShares)](./bitburner.corporation.bribe.md) | Bribe a faction |
|
||||
| [buyBackShares(amount)](./bitburner.corporation.buybackshares.md) | Buyback Shares |
|
||||
| [createCorporation(corporationName, selfFund)](./bitburner.corporation.createcorporation.md) | Create a Corporation |
|
||||
| [expandCity(divisionName, cityName)](./bitburner.corporation.expandcity.md) | Expand to a new city |
|
||||
| [expandIndustry(industryType, divisionName)](./bitburner.corporation.expandindustry.md) | Expand to a new industry |
|
||||
@ -34,5 +35,6 @@ export interface Corporation extends WarehouseAPI, OfficeAPI
|
||||
| [hasUnlockUpgrade(upgradeName)](./bitburner.corporation.hasunlockupgrade.md) | Check if you have a one time unlockable upgrade |
|
||||
| [issueDividends(percent)](./bitburner.corporation.issuedividends.md) | Issue dividends |
|
||||
| [levelUpgrade(upgradeName)](./bitburner.corporation.levelupgrade.md) | Level an upgrade. |
|
||||
| [sellShares(amount)](./bitburner.corporation.sellshares.md) | Sell Shares |
|
||||
| [unlockUpgrade(upgradeName)](./bitburner.corporation.unlockupgrade.md) | Unlock an upgrade |
|
||||
|
||||
|
24
markdown/bitburner.corporation.sellshares.md
Normal file
@ -0,0 +1,24 @@
|
||||
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
|
||||
|
||||
[Home](./index.md) > [bitburner](./bitburner.md) > [Corporation](./bitburner.corporation.md) > [sellShares](./bitburner.corporation.sellshares.md)
|
||||
|
||||
## Corporation.sellShares() method
|
||||
|
||||
Sell Shares
|
||||
|
||||
<b>Signature:</b>
|
||||
|
||||
```typescript
|
||||
sellShares(amount: number): void;
|
||||
```
|
||||
|
||||
## Parameters
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --- | --- | --- |
|
||||
| amount | number | Amount of shares to sell. |
|
||||
|
||||
<b>Returns:</b>
|
||||
|
||||
void
|
||||
|
27
markdown/bitburner.warehouseapi.bulkpurchase.md
Normal file
@ -0,0 +1,27 @@
|
||||
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
|
||||
|
||||
[Home](./index.md) > [bitburner](./bitburner.md) > [WarehouseAPI](./bitburner.warehouseapi.md) > [bulkPurchase](./bitburner.warehouseapi.bulkpurchase.md)
|
||||
|
||||
## WarehouseAPI.bulkPurchase() method
|
||||
|
||||
Set material to bulk buy
|
||||
|
||||
<b>Signature:</b>
|
||||
|
||||
```typescript
|
||||
bulkPurchase(divisionName: string, cityName: string, materialName: string, amt: number): void;
|
||||
```
|
||||
|
||||
## Parameters
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --- | --- | --- |
|
||||
| divisionName | string | Name of the division |
|
||||
| cityName | string | Name of the city |
|
||||
| materialName | string | Name of the material |
|
||||
| amt | number | Amount of material to buy |
|
||||
|
||||
<b>Returns:</b>
|
||||
|
||||
void
|
||||
|
1924
package-lock.json
generated
32
package.json
@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "bitburner",
|
||||
"license": "SEE LICENSE IN license.txt",
|
||||
"version": "1.3.0",
|
||||
"version": "1.5.0",
|
||||
"main": "electron-main.js",
|
||||
"author": {
|
||||
"name": "Daniel Xie & Olivier Gagnon"
|
||||
@ -13,19 +13,10 @@
|
||||
"@emotion/react": "^11.4.1",
|
||||
"@emotion/styled": "^11.3.0",
|
||||
"@material-ui/core": "^4.12.3",
|
||||
"@microsoft/api-documenter": "^7.13.65",
|
||||
"@microsoft/api-extractor": "^7.18.17",
|
||||
"@monaco-editor/react": "^4.2.2",
|
||||
"@mui/icons-material": "^5.0.3",
|
||||
"@mui/material": "^5.0.3",
|
||||
"@mui/styles": "^5.0.1",
|
||||
"@types/acorn": "^4.0.6",
|
||||
"@types/escodegen": "^0.0.7",
|
||||
"@types/numeral": "0.0.25",
|
||||
"@types/react": "^17.0.21",
|
||||
"@types/react-beautiful-dnd": "^13.1.2",
|
||||
"@types/react-dom": "^17.0.9",
|
||||
"@types/react-resizable": "^1.7.3",
|
||||
"acorn": "^8.4.1",
|
||||
"acorn-walk": "^8.1.1",
|
||||
"arg": "^5.0.0",
|
||||
@ -45,7 +36,6 @@
|
||||
"notistack": "^2.0.2",
|
||||
"numeral": "2.0.6",
|
||||
"prop-types": "^15.8.0",
|
||||
"raw-loader": "^4.0.2",
|
||||
"react": "^17.0.2",
|
||||
"react-beautiful-dnd": "^13.1.0",
|
||||
"react-dom": "^17.0.2",
|
||||
@ -59,10 +49,19 @@
|
||||
"@babel/preset-env": "^7.15.0",
|
||||
"@babel/preset-react": "^7.0.0",
|
||||
"@babel/preset-typescript": "^7.15.0",
|
||||
"@microsoft/api-documenter": "^7.13.65",
|
||||
"@microsoft/api-extractor": "^7.18.17",
|
||||
"@pmmmwh/react-refresh-webpack-plugin": "^0.5.1",
|
||||
"@testing-library/cypress": "^8.0.1",
|
||||
"@types/acorn": "^4.0.6",
|
||||
"@types/escodegen": "^0.0.7",
|
||||
"@types/file-saver": "^2.0.3",
|
||||
"@types/lodash": "^4.14.168",
|
||||
"@types/numeral": "0.0.25",
|
||||
"@types/react": "^17.0.21",
|
||||
"@types/react-beautiful-dnd": "^13.1.2",
|
||||
"@types/react-dom": "^17.0.9",
|
||||
"@types/react-resizable": "^1.7.3",
|
||||
"@typescript-eslint/eslint-plugin": "^4.22.0",
|
||||
"@typescript-eslint/parser": "^4.22.0",
|
||||
"babel-jest": "^27.0.6",
|
||||
@ -71,6 +70,7 @@
|
||||
"electron": "^14.0.2",
|
||||
"electron-packager": "^15.4.0",
|
||||
"eslint": "^7.24.0",
|
||||
"file-loader": "^6.2.0",
|
||||
"fork-ts-checker-webpack-plugin": "^6.3.3",
|
||||
"html-webpack-plugin": "^3.2.0",
|
||||
"http-server": "^13.0.1",
|
||||
@ -79,6 +79,7 @@
|
||||
"lodash": "^4.17.21",
|
||||
"mini-css-extract-plugin": "^0.4.1",
|
||||
"prettier": "^2.3.2",
|
||||
"raw-loader": "^4.0.2",
|
||||
"react-refresh": "^0.10.0",
|
||||
"regenerator-runtime": "^0.13.9",
|
||||
"source-map": "^0.7.3",
|
||||
@ -102,7 +103,7 @@
|
||||
"cy:dev": "start-server-and-test start:dev http://localhost:8000 cy:open",
|
||||
"cy:open": "cypress open",
|
||||
"cy:run": "cypress run",
|
||||
"doc": "npx api-extractor run && npx api-documenter markdown",
|
||||
"doc": "npx api-extractor run && npx api-documenter markdown && rm input/bitburner.api.json && rm -r input",
|
||||
"format": "prettier --write .",
|
||||
"start": "http-server -p 8000",
|
||||
"start:dev": "webpack-dev-server --progress --env.devServer --mode development",
|
||||
@ -112,13 +113,18 @@
|
||||
"build:dev": "webpack --mode development",
|
||||
"lint": "eslint --fix . --ext js,jsx,ts,tsx",
|
||||
"lint:report": "eslint --ext js,jsx,ts,tsx .",
|
||||
"preinstall": "node ./scripts/engines-check.js",
|
||||
"preinstall": "node ./tools/engines-check/engines-check.js",
|
||||
"postinstall": "cd electron && npm install",
|
||||
"test": "jest",
|
||||
"test:watch": "jest --watch",
|
||||
"watch": "webpack --watch --mode production",
|
||||
"watch:dev": "webpack --watch --mode development",
|
||||
"electron": "sh ./package.sh",
|
||||
"electron:packager": "electron-packager .package bitburner --all --out .build --overwrite --icon .package/icon.png --no-prune",
|
||||
"electron:packager-all": "electron-packager .package bitburner --all --out .build --overwrite --icon .package/icon.png",
|
||||
"electron:packager-win": "electron-packager .package bitburner --platform win32 --arch x64 --out .build --overwrite --icon .package/icon.png",
|
||||
"electron:packager-mac": "electron-packager .package bitburner --platform darwin --arch x64 --out .build --overwrite --icon .package/icon.png",
|
||||
"electron:packager-linux": "electron-packager .package bitburner --platform linux --arch x64 --out .build --overwrite --icon .package/icon.png",
|
||||
"allbuild": "npm run build && npm run electron && git add --all && git commit --amend --no-edit && git push -f -u origin dev"
|
||||
}
|
||||
}
|
||||
|
@ -1,5 +1,8 @@
|
||||
#!/bin/sh
|
||||
|
||||
# Clear out any files remaining from old builds
|
||||
rm -rf .package
|
||||
|
||||
mkdir -p .package/dist/src/ThirdParty || true
|
||||
mkdir -p .package/src/ThirdParty || true
|
||||
mkdir -p .package/node_modules || true
|
||||
@ -8,6 +11,7 @@ cp index.html .package
|
||||
cp -r electron/* .package
|
||||
cp -r dist/ext .package/dist
|
||||
cp -r dist/icons .package/dist
|
||||
cp -r dist/images .package/dist
|
||||
|
||||
# The css files
|
||||
cp dist/vendor.css .package/dist
|
||||
@ -26,5 +30,6 @@ cd electron
|
||||
npm install
|
||||
cd ..
|
||||
|
||||
BUILD_PLATFORM="${1:-"all"}"
|
||||
# And finally build the app.
|
||||
npm run electron:packager
|
||||
npm run electron:packager-$BUILD_PLATFORM
|
6
src/@types/global.d.ts
vendored
@ -1,2 +1,8 @@
|
||||
// Defined by webpack on startup or compilation
|
||||
declare let __COMMIT_HASH__: string;
|
||||
|
||||
// When using file-loader, we'll get a path to the resource
|
||||
declare module "*.png" {
|
||||
const value: string;
|
||||
export default value;
|
||||
}
|
||||
|
@ -2050,7 +2050,7 @@ function initAugmentations(): void {
|
||||
info:
|
||||
"A brain implant carefully assembled around the synapses, which " +
|
||||
"micromanages the activity and levels of various neuroreceptor " +
|
||||
"chemicals and modulates electrical acvitiy to optimize concentration, " +
|
||||
"chemicals and modulates electrical activity to optimize concentration, " +
|
||||
"allowing the user to multitask much more effectively.",
|
||||
stats: (
|
||||
<>
|
||||
|
@ -110,7 +110,7 @@ export function AugmentationsRoot(props: IProps): React.ReactElement {
|
||||
<br />
|
||||
<br />
|
||||
It is recommended to install several Augmentations at once. Preferably everything from any faction of your
|
||||
chosing.
|
||||
choosing.
|
||||
</>
|
||||
}
|
||||
/>
|
||||
|
@ -26,7 +26,7 @@ export function InstalledAugmentations(): React.ReactElement {
|
||||
|
||||
if (Settings.OwnedAugmentationsOrder === OwnedAugmentationsOrderSetting.Alphabetically) {
|
||||
sourceAugs.sort((aug1, aug2) => {
|
||||
return aug1.name <= aug2.name ? -1 : 1;
|
||||
return aug1.name.localeCompare(aug2.name);
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -670,7 +670,6 @@ export function initBitNodeMultipliers(p: IPlayer): void {
|
||||
BitNodeMultipliers.InfiltrationMoney = 0.75;
|
||||
BitNodeMultipliers.CorporationValuation = 0.2;
|
||||
BitNodeMultipliers.HacknetNodeMoney = 0.2;
|
||||
BitNodeMultipliers.FactionPassiveRepGain = 0;
|
||||
BitNodeMultipliers.HackExpGain = 0.25;
|
||||
BitNodeMultipliers.DaedalusAugsRequirement = 1.166; // Results in 35 Augs needed
|
||||
BitNodeMultipliers.PurchasedServerSoftcap = 2;
|
||||
@ -694,7 +693,6 @@ export function initBitNodeMultipliers(p: IPlayer): void {
|
||||
BitNodeMultipliers.InfiltrationMoney = 0.75;
|
||||
BitNodeMultipliers.CorporationValuation = 0.2;
|
||||
BitNodeMultipliers.HacknetNodeMoney = 0.2;
|
||||
BitNodeMultipliers.FactionPassiveRepGain = 0;
|
||||
BitNodeMultipliers.HackExpGain = 0.25;
|
||||
BitNodeMultipliers.FourSigmaMarketDataCost = 2;
|
||||
BitNodeMultipliers.FourSigmaMarketDataApiCost = 2;
|
||||
|
@ -8,38 +8,37 @@ import { CinematicText } from "../../ui/React/CinematicText";
|
||||
import { use } from "../../ui/Context";
|
||||
import makeStyles from "@mui/styles/makeStyles";
|
||||
import createStyles from "@mui/styles/createStyles";
|
||||
import IconButton from "@mui/material/IconButton";
|
||||
import Typography from "@mui/material/Typography";
|
||||
import Tooltip from "@mui/material/Tooltip";
|
||||
import { Settings } from "../../Settings/Settings";
|
||||
import Button from "@mui/material/Button";
|
||||
|
||||
|
||||
const useStyles = makeStyles(() =>
|
||||
createStyles({
|
||||
level0: {
|
||||
color: "red",
|
||||
portal: {
|
||||
cursor: "pointer",
|
||||
fontFamily: "inherit",
|
||||
fontSize: "1rem",
|
||||
fontWeight: "bold",
|
||||
lineHeight: 1,
|
||||
padding: 0,
|
||||
"&:hover": {
|
||||
color: "#fff",
|
||||
},
|
||||
},
|
||||
level0: {
|
||||
color: "red",
|
||||
},
|
||||
level1: {
|
||||
color: "yellow",
|
||||
cursor: "pointer",
|
||||
"&:hover": {
|
||||
color: "#fff",
|
||||
},
|
||||
},
|
||||
level2: {
|
||||
color: "#48d1cc",
|
||||
cursor: "pointer",
|
||||
"&:hover": {
|
||||
color: "#fff",
|
||||
},
|
||||
},
|
||||
level3: {
|
||||
color: "blue",
|
||||
cursor: "pointer",
|
||||
"&:hover": {
|
||||
color: "#fff",
|
||||
},
|
||||
},
|
||||
}),
|
||||
);
|
||||
@ -71,6 +70,7 @@ function BitNodePortal(props: IPortalProps): React.ReactElement {
|
||||
if (props.level === 2) {
|
||||
cssClass = classes.level2;
|
||||
}
|
||||
cssClass = `${classes.portal} ${cssClass}`
|
||||
|
||||
return (
|
||||
<>
|
||||
@ -85,9 +85,24 @@ function BitNodePortal(props: IPortalProps): React.ReactElement {
|
||||
</Typography>
|
||||
}
|
||||
>
|
||||
<span onClick={() => setPortalOpen(true)} className={cssClass}>
|
||||
<b>O</b>
|
||||
</span>
|
||||
{Settings.DisableASCIIArt ? (
|
||||
<Button
|
||||
onClick={() => setPortalOpen(true)}
|
||||
sx={{ m: 2 }}
|
||||
aria-description={bitNode.desc}
|
||||
>
|
||||
<Typography>BitNode-{bitNode.number.toString()}: {bitNode.name}</Typography>
|
||||
</Button>
|
||||
) : (
|
||||
<IconButton
|
||||
onClick={() => setPortalOpen(true)}
|
||||
className={cssClass}
|
||||
aria-label={`BitNode-${bitNode.number.toString()}: ${bitNode.name}`}
|
||||
aria-description={bitNode.desc}
|
||||
>
|
||||
O
|
||||
</IconButton>
|
||||
)}
|
||||
</Tooltip>
|
||||
<PortalModal
|
||||
open={portalOpen}
|
||||
@ -98,6 +113,10 @@ function BitNodePortal(props: IPortalProps): React.ReactElement {
|
||||
destroyedBitNode={props.destroyedBitNode}
|
||||
flume={props.flume}
|
||||
/>
|
||||
|
||||
{Settings.DisableASCIIArt && (
|
||||
<br/>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
@ -151,63 +170,104 @@ export function BitverseRoot(props: IProps): React.ReactElement {
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
if (Settings.DisableASCIIArt) {
|
||||
return (
|
||||
<>
|
||||
{Object.values(BitNodes).filter((node) => {
|
||||
console.log(node.desc);
|
||||
return node.desc !== 'COMING SOON';
|
||||
}).map((node) => {
|
||||
return (
|
||||
<BitNodePortal key={node.number} n={node.number} level={nextSourceFileFlags[node.number]} enter={enter} flume={props.flume} destroyedBitNode={destroyed} />
|
||||
)
|
||||
})}
|
||||
<br />
|
||||
<br />
|
||||
<br />
|
||||
<br />
|
||||
<CinematicText lines={[
|
||||
"> Many decades ago, a humanoid extraterrestrial species which we call the Enders descended on the Earth...violently",
|
||||
"> Our species fought back, but it was futile. The Enders had technology far beyond our own...",
|
||||
"> Instead of killing every last one of us, the human race was enslaved...",
|
||||
"> We were shackled in a digital world, chained into a prison for our minds...",
|
||||
"> Using their advanced technology, the Enders created complex simulations of a virtual reality...",
|
||||
"> Simulations designed to keep us content...ignorant of the truth.",
|
||||
"> Simulations used to trap and suppress our consciousness, to keep us under control...",
|
||||
"> Why did they do this? Why didn't they just end our entire race? We don't know, not yet.",
|
||||
"> Humanity's only hope is to destroy these simulations, destroy the only realities we've ever known...",
|
||||
"> Only then can we begin to fight back...",
|
||||
"> By hacking the daemon that generated your reality, you've just destroyed one simulation, called a BitNode...",
|
||||
"> But there is still a long way to go...",
|
||||
"> The technology the Enders used to enslave the human race wasn't just a single complex simulation...",
|
||||
"> There are tens if not hundreds of BitNodes out there...",
|
||||
"> Each with their own simulations of a reality...",
|
||||
"> Each creating their own universes...a universe of universes",
|
||||
"> And all of which must be destroyed...",
|
||||
"> .......................................",
|
||||
"> Welcome to the Bitverse...",
|
||||
"> ",
|
||||
"> (Enter a new BitNode using the image above)",
|
||||
]} />
|
||||
</>
|
||||
)
|
||||
} else {
|
||||
return (
|
||||
// prettier-ignore
|
||||
<>
|
||||
<Typography sx={{lineHeight: '1em',whiteSpace: 'pre'}}> O </Typography>
|
||||
<Typography sx={{lineHeight: '1em',whiteSpace: 'pre'}}> | O O | O O | </Typography>
|
||||
<Typography sx={{lineHeight: '1em',whiteSpace: 'pre'}}> O | | / __| \ | | O </Typography>
|
||||
<Typography sx={{lineHeight: '1em',whiteSpace: 'pre'}}> O | O | | O / | O | | O | O </Typography>
|
||||
<Typography sx={{lineHeight: '1em',whiteSpace: 'pre'}}> | | | | |_/ |/ | \_ \_| | | | | </Typography>
|
||||
<Typography sx={{lineHeight: '1em',whiteSpace: 'pre'}}> O | | | O | | O__/ | / \__ | | O | | | O </Typography>
|
||||
<Typography sx={{lineHeight: '1em',whiteSpace: 'pre'}}> | | | | | | | / /| O / \| | | | | | | </Typography>
|
||||
<Typography sx={{lineHeight: '1em',whiteSpace: 'pre'}}>O | | | \| | O / _/ | / O | |/ | | | O</Typography>
|
||||
<Typography sx={{lineHeight: '1em',whiteSpace: 'pre'}}>| | | |O / | | O / | O O | | \ O| | | |</Typography>
|
||||
<Typography sx={{lineHeight: '1em',whiteSpace: 'pre'}}>| | |/ \/ / __| | |/ \ | \ | |__ \ \/ \| | |</Typography>
|
||||
<Typography sx={{lineHeight: '1em',whiteSpace: 'pre'}}> \| O | |_/ |\| \ <BitNodePortal n={13} level={nextSourceFileFlags[13]} enter={enter} flume={props.flume} destroyedBitNode={destroyed} /> \__| \_| | O |/ </Typography>
|
||||
<Typography sx={{lineHeight: '1em',whiteSpace: 'pre'}}> | | |_/ | | \| / | \_| | | </Typography>
|
||||
<Typography sx={{lineHeight: '1em',whiteSpace: 'pre'}}> \| / \| | / / \ |/ </Typography>
|
||||
<Typography sx={{lineHeight: '1em',whiteSpace: 'pre'}}> | <BitNodePortal n={10} level={nextSourceFileFlags[10]} enter={enter} flume={props.flume} destroyedBitNode={destroyed} /> | | / | <BitNodePortal n={11} level={nextSourceFileFlags[11]} enter={enter} flume={props.flume} destroyedBitNode={destroyed} /> | </Typography>
|
||||
<Typography sx={{lineHeight: '1em',whiteSpace: 'pre'}}> <BitNodePortal n={9} level={nextSourceFileFlags[9]} enter={enter} flume={props.flume} destroyedBitNode={destroyed} /> | | | | | | | <BitNodePortal n={12} level={nextSourceFileFlags[12]} enter={enter} flume={props.flume} destroyedBitNode={destroyed} /> </Typography>
|
||||
<Typography sx={{lineHeight: '1em',whiteSpace: 'pre'}}> | | | / / \ \ | | | </Typography>
|
||||
<Typography sx={{lineHeight: '1em',whiteSpace: 'pre'}}> \| | / <BitNodePortal n={7} level={nextSourceFileFlags[7]} enter={enter} flume={props.flume} destroyedBitNode={destroyed} /> / \ <BitNodePortal n={8} level={nextSourceFileFlags[8]} enter={enter} flume={props.flume} destroyedBitNode={destroyed} /> \ | |/ </Typography>
|
||||
<Typography sx={{lineHeight: '1em',whiteSpace: 'pre'}}> \ | / / | | \ \ | / </Typography>
|
||||
<Typography sx={{lineHeight: '1em',whiteSpace: 'pre'}}> \ \JUMP <BitNodePortal n={5} level={nextSourceFileFlags[5]} enter={enter} flume={props.flume} destroyedBitNode={destroyed} />3R | | | | | | R3<BitNodePortal n={6} level={nextSourceFileFlags[6]} enter={enter} flume={props.flume} destroyedBitNode={destroyed} /> PMUJ/ / </Typography>
|
||||
<Typography sx={{lineHeight: '1em',whiteSpace: 'pre'}}> \|| | | | | | | | | ||/ </Typography>
|
||||
<Typography sx={{lineHeight: '1em',whiteSpace: 'pre'}}> \| \_ | | | | | | _/ |/ </Typography>
|
||||
<Typography sx={{lineHeight: '1em',whiteSpace: 'pre'}}> \ \| / \ / \ |/ / </Typography>
|
||||
<Typography sx={{lineHeight: '1em',whiteSpace: 'pre'}}> <BitNodePortal n={1} level={nextSourceFileFlags[1]} enter={enter} flume={props.flume} destroyedBitNode={destroyed} /> |/ <BitNodePortal n={2} level={nextSourceFileFlags[2]} enter={enter} flume={props.flume} destroyedBitNode={destroyed} /> | | <BitNodePortal n={3} level={nextSourceFileFlags[3]} enter={enter} flume={props.flume} destroyedBitNode={destroyed} /> \| <BitNodePortal n={4} level={nextSourceFileFlags[4]} enter={enter} flume={props.flume} destroyedBitNode={destroyed} /> </Typography>
|
||||
<Typography sx={{lineHeight: '1em',whiteSpace: 'pre'}}> | | | | | | | | </Typography>
|
||||
<Typography sx={{lineHeight: '1em',whiteSpace: 'pre'}}> \JUMP3R|JUMP|3R| |R3|PMUJ|R3PMUJ/ </Typography>
|
||||
<br />
|
||||
<br />
|
||||
<br />
|
||||
<br />
|
||||
<CinematicText lines={[
|
||||
"> Many decades ago, a humanoid extraterrestrial species which we call the Enders descended on the Earth...violently",
|
||||
"> Our species fought back, but it was futile. The Enders had technology far beyond our own...",
|
||||
"> Instead of killing every last one of us, the human race was enslaved...",
|
||||
"> We were shackled in a digital world, chained into a prison for our minds...",
|
||||
"> Using their advanced technology, the Enders created complex simulations of a virtual reality...",
|
||||
"> Simulations designed to keep us content...ignorant of the truth.",
|
||||
"> Simulations used to trap and suppress our consciousness, to keep us under control...",
|
||||
"> Why did they do this? Why didn't they just end our entire race? We don't know, not yet.",
|
||||
"> Humanity's only hope is to destroy these simulations, destroy the only realities we've ever known...",
|
||||
"> Only then can we begin to fight back...",
|
||||
"> By hacking the daemon that generated your reality, you've just destroyed one simulation, called a BitNode...",
|
||||
"> But there is still a long way to go...",
|
||||
"> The technology the Enders used to enslave the human race wasn't just a single complex simulation...",
|
||||
"> There are tens if not hundreds of BitNodes out there...",
|
||||
"> Each with their own simulations of a reality...",
|
||||
"> Each creating their own universes...a universe of universes",
|
||||
"> And all of which must be destroyed...",
|
||||
"> .......................................",
|
||||
"> Welcome to the Bitverse...",
|
||||
"> ",
|
||||
"> (Enter a new BitNode using the image above)",
|
||||
]} />
|
||||
</>
|
||||
);
|
||||
|
||||
<>
|
||||
<Typography sx={{lineHeight: '1em',whiteSpace: 'pre'}}> O </Typography>
|
||||
<Typography sx={{lineHeight: '1em',whiteSpace: 'pre'}}> | O O | O O | </Typography>
|
||||
<Typography sx={{lineHeight: '1em',whiteSpace: 'pre'}}> O | | / __| \ | | O </Typography>
|
||||
<Typography sx={{lineHeight: '1em',whiteSpace: 'pre'}}> O | O | | O / | O | | O | O </Typography>
|
||||
<Typography sx={{lineHeight: '1em',whiteSpace: 'pre'}}> | | | | |_/ |/ | \_ \_| | | | | </Typography>
|
||||
<Typography sx={{lineHeight: '1em',whiteSpace: 'pre'}}> O | | | O | | O__/ | / \__ | | O | | | O </Typography>
|
||||
<Typography sx={{lineHeight: '1em',whiteSpace: 'pre'}}> | | | | | | | / /| O / \| | | | | | | </Typography>
|
||||
<Typography sx={{lineHeight: '1em',whiteSpace: 'pre'}}>O | | | \| | O / _/ | / O | |/ | | | O</Typography>
|
||||
<Typography sx={{lineHeight: '1em',whiteSpace: 'pre'}}>| | | |O / | | O / | O O | | \ O| | | |</Typography>
|
||||
<Typography sx={{lineHeight: '1em',whiteSpace: 'pre'}}>| | |/ \/ / __| | |/ \ | \ | |__ \ \/ \| | |</Typography>
|
||||
<Typography sx={{lineHeight: '1em',whiteSpace: 'pre'}}> \| O | |_/ |\| \ <BitNodePortal n={13} level={nextSourceFileFlags[13]} enter={enter} flume={props.flume} destroyedBitNode={destroyed} /> \__| \_| | O |/ </Typography>
|
||||
<Typography sx={{lineHeight: '1em',whiteSpace: 'pre'}}> | | |_/ | | \| / | \_| | | </Typography>
|
||||
<Typography sx={{lineHeight: '1em',whiteSpace: 'pre'}}> \| / \| | / / \ |/ </Typography>
|
||||
<Typography sx={{lineHeight: '1em',whiteSpace: 'pre'}}> | <BitNodePortal n={10} level={nextSourceFileFlags[10]} enter={enter} flume={props.flume} destroyedBitNode={destroyed} /> | | / | <BitNodePortal n={11} level={nextSourceFileFlags[11]} enter={enter} flume={props.flume} destroyedBitNode={destroyed} /> | </Typography>
|
||||
<Typography sx={{lineHeight: '1em',whiteSpace: 'pre'}}> <BitNodePortal n={9} level={nextSourceFileFlags[9]} enter={enter} flume={props.flume} destroyedBitNode={destroyed} /> | | | | | | | <BitNodePortal n={12} level={nextSourceFileFlags[12]} enter={enter} flume={props.flume} destroyedBitNode={destroyed} /> </Typography>
|
||||
<Typography sx={{lineHeight: '1em',whiteSpace: 'pre'}}> | | | / / \ \ | | | </Typography>
|
||||
<Typography sx={{lineHeight: '1em',whiteSpace: 'pre'}}> \| | / <BitNodePortal n={7} level={nextSourceFileFlags[7]} enter={enter} flume={props.flume} destroyedBitNode={destroyed} /> / \ <BitNodePortal n={8} level={nextSourceFileFlags[8]} enter={enter} flume={props.flume} destroyedBitNode={destroyed} /> \ | |/ </Typography>
|
||||
<Typography sx={{lineHeight: '1em',whiteSpace: 'pre'}}> \ | / / | | \ \ | / </Typography>
|
||||
<Typography sx={{lineHeight: '1em',whiteSpace: 'pre'}}> \ \JUMP <BitNodePortal n={5} level={nextSourceFileFlags[5]} enter={enter} flume={props.flume} destroyedBitNode={destroyed} />3R | | | | | | R3<BitNodePortal n={6} level={nextSourceFileFlags[6]} enter={enter} flume={props.flume} destroyedBitNode={destroyed} /> PMUJ/ / </Typography>
|
||||
<Typography sx={{lineHeight: '1em',whiteSpace: 'pre'}}> \|| | | | | | | | | ||/ </Typography>
|
||||
<Typography sx={{lineHeight: '1em',whiteSpace: 'pre'}}> \| \_ | | | | | | _/ |/ </Typography>
|
||||
<Typography sx={{lineHeight: '1em',whiteSpace: 'pre'}}> \ \| / \ / \ |/ / </Typography>
|
||||
<Typography sx={{lineHeight: '1em',whiteSpace: 'pre'}}> <BitNodePortal n={1} level={nextSourceFileFlags[1]} enter={enter} flume={props.flume} destroyedBitNode={destroyed} /> |/ <BitNodePortal n={2} level={nextSourceFileFlags[2]} enter={enter} flume={props.flume} destroyedBitNode={destroyed} /> | | <BitNodePortal n={3} level={nextSourceFileFlags[3]} enter={enter} flume={props.flume} destroyedBitNode={destroyed} /> \| <BitNodePortal n={4} level={nextSourceFileFlags[4]} enter={enter} flume={props.flume} destroyedBitNode={destroyed} /> </Typography>
|
||||
<Typography sx={{lineHeight: '1em',whiteSpace: 'pre'}}> | | | | | | | | </Typography>
|
||||
<Typography sx={{lineHeight: '1em',whiteSpace: 'pre'}}> \JUMP3R|JUMP|3R| |R3|PMUJ|R3PMUJ/ </Typography>
|
||||
<br />
|
||||
<br />
|
||||
<br />
|
||||
<br />
|
||||
<CinematicText lines={[
|
||||
"> Many decades ago, a humanoid extraterrestrial species which we call the Enders descended on the Earth...violently",
|
||||
"> Our species fought back, but it was futile. The Enders had technology far beyond our own...",
|
||||
"> Instead of killing every last one of us, the human race was enslaved...",
|
||||
"> We were shackled in a digital world, chained into a prison for our minds...",
|
||||
"> Using their advanced technology, the Enders created complex simulations of a virtual reality...",
|
||||
"> Simulations designed to keep us content...ignorant of the truth.",
|
||||
"> Simulations used to trap and suppress our consciousness, to keep us under control...",
|
||||
"> Why did they do this? Why didn't they just end our entire race? We don't know, not yet.",
|
||||
"> Humanity's only hope is to destroy these simulations, destroy the only realities we've ever known...",
|
||||
"> Only then can we begin to fight back...",
|
||||
"> By hacking the daemon that generated your reality, you've just destroyed one simulation, called a BitNode...",
|
||||
"> But there is still a long way to go...",
|
||||
"> The technology the Enders used to enslave the human race wasn't just a single complex simulation...",
|
||||
"> There are tens if not hundreds of BitNodes out there...",
|
||||
"> Each with their own simulations of a reality...",
|
||||
"> Each creating their own universes...a universe of universes",
|
||||
"> And all of which must be destroyed...",
|
||||
"> .......................................",
|
||||
"> Welcome to the Bitverse...",
|
||||
"> ",
|
||||
"> (Enter a new BitNode using the image above)",
|
||||
]} />
|
||||
</>
|
||||
);
|
||||
}
|
||||
return <></>;
|
||||
}
|
||||
|
@ -43,6 +43,8 @@ export function PortalModal(props: IProps): React.ReactElement {
|
||||
<br />
|
||||
<br />
|
||||
<Button
|
||||
aria-label={`enter-bitnode-${bitNode.number.toString()}`}
|
||||
autoFocus={true}
|
||||
onClick={() => {
|
||||
props.enter(router, props.flume, props.destroyedBitNode, props.n);
|
||||
props.onClose();
|
||||
|
@ -2083,7 +2083,7 @@ export class Bladeburner implements IBladeburner {
|
||||
this.startAction(player, actionId);
|
||||
workerScript.log(
|
||||
"bladeburner.startAction",
|
||||
() => `Starting bladeburner action with type '${type}' and name ${name}"`,
|
||||
() => `Starting bladeburner action with type '${type}' and name '${name}'`,
|
||||
);
|
||||
return true;
|
||||
} catch (e: any) {
|
||||
|
@ -14,6 +14,7 @@ import Typography from "@mui/material/Typography";
|
||||
import TextField from "@mui/material/TextField";
|
||||
|
||||
const MAX_BET = 100e6;
|
||||
export const DECK_COUNT = 5; // 5-deck multideck
|
||||
|
||||
enum Result {
|
||||
Pending = "",
|
||||
@ -45,7 +46,7 @@ export class Blackjack extends Game<Props, State> {
|
||||
constructor(props: Props) {
|
||||
super(props);
|
||||
|
||||
this.deck = new Deck(5); // 5-deck multideck
|
||||
this.deck = new Deck(DECK_COUNT);
|
||||
|
||||
const initialBet = 1e6;
|
||||
|
||||
|
@ -40,13 +40,13 @@ const strategies: {
|
||||
} = {
|
||||
Red: {
|
||||
match: (n: number): boolean => {
|
||||
if (n === 0) return false;
|
||||
return redNumbers.includes(n);
|
||||
},
|
||||
payout: 1,
|
||||
},
|
||||
Black: {
|
||||
match: (n: number): boolean => {
|
||||
if (n === 0) return false;
|
||||
return !redNumbers.includes(n);
|
||||
},
|
||||
payout: 1,
|
||||
@ -118,12 +118,6 @@ export function Roulette(props: IProps): React.ReactElement {
|
||||
const [status, setStatus] = useState<string | JSX.Element>("waiting");
|
||||
const [n, setN] = useState(0);
|
||||
const [lock, setLock] = useState(true);
|
||||
const [strategy, setStrategy] = useState<Strategy>({
|
||||
payout: 0,
|
||||
match: (): boolean => {
|
||||
return false;
|
||||
},
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
const i = window.setInterval(step, 50);
|
||||
@ -156,13 +150,12 @@ export function Roulette(props: IProps): React.ReactElement {
|
||||
return `${n}${color}`;
|
||||
}
|
||||
|
||||
function play(s: Strategy): void {
|
||||
function play(strategy: Strategy): void {
|
||||
if (reachedLimit(props.p)) return;
|
||||
|
||||
setCanPlay(false);
|
||||
setLock(false);
|
||||
setStatus("playing");
|
||||
setStrategy(s);
|
||||
|
||||
setTimeout(() => {
|
||||
let n = Math.floor(rng.random() * 37);
|
||||
|
@ -159,18 +159,18 @@ export function SlotMachine(props: IProps): React.ReactElement {
|
||||
const copy = index.slice();
|
||||
for (let i = 0; i < copy.length; i++) {
|
||||
if (copy[i] === locks[i] && !stoppedOne) continue;
|
||||
copy[i] = (copy[i] + 1) % symbols.length;
|
||||
copy[i] = (copy[i] - 1 >= 0) ? copy[i] - 1 : symbols.length - 1;
|
||||
stoppedOne = true;
|
||||
}
|
||||
|
||||
setIndex(copy);
|
||||
|
||||
if (stoppedOne && copy.every((e, i) => e === locks[i])) {
|
||||
checkWinnings();
|
||||
checkWinnings(getTable(copy, symbols));
|
||||
}
|
||||
}
|
||||
|
||||
function getTable(): string[][] {
|
||||
function getTable(index:number[], symbols:string[]): string[][] {
|
||||
return [
|
||||
[
|
||||
symbols[(index[0] + symbols.length - 1) % symbols.length],
|
||||
@ -209,8 +209,7 @@ export function SlotMachine(props: IProps): React.ReactElement {
|
||||
]);
|
||||
}
|
||||
|
||||
function checkWinnings(): void {
|
||||
const t = getTable();
|
||||
function checkWinnings(t:string[][]): void {
|
||||
const getPaylineData = function (payline: number[][]): string[] {
|
||||
const data = [];
|
||||
for (const point of payline) {
|
||||
@ -267,7 +266,7 @@ export function SlotMachine(props: IProps): React.ReactElement {
|
||||
setInvestment(investment);
|
||||
}
|
||||
|
||||
const t = getTable();
|
||||
const t = getTable(index, symbols);
|
||||
// prettier-ignore
|
||||
return (
|
||||
<>
|
||||
@ -288,7 +287,7 @@ export function SlotMachine(props: IProps): React.ReactElement {
|
||||
disabled={!canPlay}
|
||||
>Spin!</Button>)}}
|
||||
/>
|
||||
|
||||
|
||||
<Typography variant="h4">{status}</Typography>
|
||||
<Typography>Pay lines</Typography>
|
||||
|
||||
|
@ -273,22 +273,64 @@ export const CONSTANTS: {
|
||||
TotalNumBitNodes: 24,
|
||||
|
||||
LatestUpdate: `
|
||||
v1.4.0 - 2022-01-18 Sharing is caring
|
||||
-------------------------------------
|
||||
v1.5.0 - Steam Cloud integration
|
||||
--------------------------------
|
||||
|
||||
** Computer sharing **
|
||||
** Steam Cloud Saving **
|
||||
|
||||
* A new mechanic has been added, it's is invoked by calling the new function 'share'.
|
||||
This mechanic helps you farm reputation faster.
|
||||
* Added (@MartinFournier)
|
||||
|
||||
** gang **
|
||||
** UI **
|
||||
|
||||
* Installing augs means losing a little bit of ascension multipliers.
|
||||
* background now matches game primary color (@nickofolas)
|
||||
* page title contains version (@MartinFourier)
|
||||
* Major text editor improvements (@nickofolas)
|
||||
* Display bonus time on sleeve page (@MartinFourier)
|
||||
* Several UI improvements (@nickofolas, @smolgumball, @DrCuriosity, @phyzical)
|
||||
* Fix aug display in alpha (@Dominik Winter)
|
||||
* Fix display of corporation product equation (@SagePtr)
|
||||
* Make Bitverse more accessible (@ChrissiQ)
|
||||
* Make corporation warehouse more accessible (@ChrissiQ)
|
||||
* Make tab style more consistent (@nikfolas)
|
||||
|
||||
** There's more but I'm going to write it later. **
|
||||
** Netscript **
|
||||
|
||||
** Misc. **
|
||||
* Fix bug with async.
|
||||
* Add 'printf' ns function (@Ninetailed)
|
||||
* Remove blob caching.
|
||||
* Fix formulas access check (@Ornedan)
|
||||
* Fix bug in exp calculation (@qcorradi)
|
||||
* Fix NaN comparison (@qcorradi)
|
||||
* Fix travelToCity with bad argument (@SlyCedix)
|
||||
* Fix bug where augs could not be purchased via sing (@reacocard)
|
||||
* Fix rounding error in donateToFaction (@Risenafis)
|
||||
* Fix bug with weakenAnalyze (@rhobes)
|
||||
* Prevent exploit with atExit (@Ornedan)
|
||||
* Double 'share' power
|
||||
|
||||
* Nerf noodle bar.
|
||||
** Corporations **
|
||||
|
||||
* Fix bugs with corp API (@pigalot)
|
||||
* Add smart supply func to corp API (@pd)
|
||||
|
||||
** Misc. **
|
||||
|
||||
* The file API now allows GET and DELETE (@lordducky)
|
||||
* Force achievement calculation on BN completion (@SagePtr)
|
||||
* Cleanup in repository (@MartinFourier)
|
||||
* Several improvements to the electron version (@MartinFourier)
|
||||
* Fix bug with casino roulette (@jamie-mac)
|
||||
* Terminal history persists in savefile (@MartinFourier)
|
||||
* Fix tests (@jamie-mac)
|
||||
* Fix crash with electron windows tracker (@smolgumball)
|
||||
* Fix BN6/7 passive reputation gain (@BrianLDev)
|
||||
* Fix Sleeve not resetting on install (@waffleattack)
|
||||
* Sort joined factions (@jjayeon)
|
||||
* Update documentation / typo (@lethern, @Meowdoleon, @JohnnyUrosevic, @JosephDavidTalbot,
|
||||
@pd, @lethern, @lordducky, @zeddrak, @fearnlj01, @reasonablytall, @MatthewTh0,
|
||||
@SagePtr, @manniL, @Jedimaster4559, @loganville, @Arrow2thekn33, @wdpk, @fwolfst,
|
||||
@fschoenfeldt, @Waladil, @AdamTReineke, @citrusmunch, @factubsio, @ashtongreen,
|
||||
@ChrissiQ, @DJ-Laser, @waffleattack, @ApamNapat, @CrafterKolyan, @DSteve595)
|
||||
* Nerf noodle bar.
|
||||
`,
|
||||
};
|
||||
|
@ -1,3 +1,5 @@
|
||||
import { IPlayer } from 'src/PersonObjects/IPlayer';
|
||||
import { MaterialSizes } from './MaterialSizes';
|
||||
import { ICorporation } from "./ICorporation";
|
||||
import { IIndustry } from "./IIndustry";
|
||||
import { IndustryStartingCosts, IndustryResearchTrees } from "./IndustryData";
|
||||
@ -14,12 +16,12 @@ import { EmployeePositions } from "./EmployeePositions";
|
||||
import { Employee } from "./Employee";
|
||||
import { IndustryUpgrades } from "./IndustryUpgrades";
|
||||
import { ResearchMap } from "./ResearchMap";
|
||||
import { isRelevantMaterial } from "./ui/Helpers";
|
||||
|
||||
export function NewIndustry(corporation: ICorporation, industry: string, name: string): void {
|
||||
for (let i = 0; i < corporation.divisions.length; ++i) {
|
||||
if (corporation.divisions[i].name === name) {
|
||||
throw new Error("This division name is already in use!");
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
@ -245,6 +247,57 @@ export function BuyMaterial(material: Material, amt: number): void {
|
||||
material.buy = amt;
|
||||
}
|
||||
|
||||
export function BulkPurchase(corp: ICorporation, warehouse: Warehouse, material: Material, amt: number): void {
|
||||
const matSize = MaterialSizes[material.name];
|
||||
const maxAmount = (warehouse.size - warehouse.sizeUsed) / matSize;
|
||||
if (isNaN(amt) || amt < 0) {
|
||||
throw new Error(`Invalid input amount`);
|
||||
}
|
||||
if (amt * matSize > maxAmount) {
|
||||
throw new Error(`You do not have enough warehouse size to fit this purchase`);
|
||||
}
|
||||
const cost = amt * material.bCost;
|
||||
if (corp.funds >= cost) {
|
||||
corp.funds = corp.funds - cost;
|
||||
material.qty += amt;
|
||||
} else {
|
||||
throw new Error(`You cannot afford this purchase.`);
|
||||
}
|
||||
}
|
||||
|
||||
export function SellShares(corporation: ICorporation, player: IPlayer, numShares: number): number {
|
||||
if (isNaN(numShares)) throw new Error("Invalid value for number of shares");
|
||||
if (numShares < 0) throw new Error("Invalid value for number of shares");
|
||||
if (numShares > corporation.numShares) throw new Error("You don't have that many shares to sell!");
|
||||
if (!corporation.public) throw new Error("You haven't gone public!");
|
||||
if (corporation.shareSaleCooldown) throw new Error("Share sale on cooldown!");
|
||||
const stockSaleResults = corporation.calculateShareSale(numShares);
|
||||
const profit = stockSaleResults[0];
|
||||
const newSharePrice = stockSaleResults[1];
|
||||
const newSharesUntilUpdate = stockSaleResults[2];
|
||||
|
||||
corporation.numShares -= numShares;
|
||||
corporation.issuedShares += numShares;
|
||||
corporation.sharePrice = newSharePrice;
|
||||
corporation.shareSalesUntilPriceUpdate = newSharesUntilUpdate;
|
||||
corporation.shareSaleCooldown = CorporationConstants.SellSharesCooldown;
|
||||
player.gainMoney(profit, "corporation");
|
||||
return profit;
|
||||
}
|
||||
|
||||
export function BuyBackShares(corporation: ICorporation, player: IPlayer, numShares: number): boolean {
|
||||
if (isNaN(numShares)) throw new Error("Invalid value for number of shares");
|
||||
if (numShares < 0) throw new Error("Invalid value for number of shares");
|
||||
if (numShares > corporation.issuedShares) throw new Error("You don't have that many shares to buy!");
|
||||
if (!corporation.public) throw new Error("You haven't gone public!");
|
||||
const buybackPrice = corporation.sharePrice * 1.1;
|
||||
if (corporation.funds < (numShares * buybackPrice)) throw new Error("You cant afford that many shares!");
|
||||
corporation.numShares += numShares;
|
||||
corporation.issuedShares -= numShares;
|
||||
player.loseMoney(numShares * buybackPrice, "corporation");
|
||||
return true;
|
||||
}
|
||||
|
||||
export function AssignJob(employee: Employee, job: string): void {
|
||||
if (!Object.values(EmployeePositions).includes(job)) throw new Error(`'${job}' is not a valid job.`);
|
||||
employee.pos = job;
|
||||
@ -290,6 +343,7 @@ export function PurchaseWarehouse(corp: ICorporation, division: IIndustry, city:
|
||||
|
||||
export function UpgradeWarehouse(corp: ICorporation, division: IIndustry, warehouse: Warehouse): void {
|
||||
const sizeUpgradeCost = CorporationConstants.WarehouseUpgradeBaseCost * Math.pow(1.07, warehouse.level + 1);
|
||||
if (corp.funds < sizeUpgradeCost) return;
|
||||
++warehouse.level;
|
||||
warehouse.updateSize(corp, division);
|
||||
corp.funds = corp.funds - sizeUpgradeCost;
|
||||
@ -334,6 +388,9 @@ export function MakeProduct(
|
||||
if (productName == null || productName === "") {
|
||||
throw new Error("You must specify a name for your product!");
|
||||
}
|
||||
if (!division.makesProducts) {
|
||||
throw new Error("You cannot create products for this industry!");
|
||||
}
|
||||
if (isNaN(designInvest)) {
|
||||
throw new Error("Invalid value for design investment");
|
||||
}
|
||||
@ -343,17 +400,29 @@ export function MakeProduct(
|
||||
if (corp.funds < designInvest + marketingInvest) {
|
||||
throw new Error("You don't have enough company funds to make this large of an investment");
|
||||
}
|
||||
let maxProducts = 3
|
||||
if (division.hasResearch("uPgrade: Capacity.II")) {
|
||||
maxProducts = 5
|
||||
} else if (division.hasResearch("uPgrade: Capacity.I")) {
|
||||
maxProducts = 4
|
||||
}
|
||||
const products = division.products
|
||||
if (Object.keys(products).length >= maxProducts) {
|
||||
throw new Error(`You are already at the max products (${maxProducts}) for division: ${division.name}!`);
|
||||
}
|
||||
|
||||
const product = new Product({
|
||||
name: productName.replace(/[<>]/g, ""), //Sanitize for HTMl elements
|
||||
createCity: city,
|
||||
designCost: designInvest,
|
||||
advCost: marketingInvest,
|
||||
});
|
||||
if (division.products[product.name] instanceof Product) {
|
||||
if (products[product.name] instanceof Product) {
|
||||
throw new Error(`You already have a product with this name!`);
|
||||
}
|
||||
|
||||
corp.funds = corp.funds - (designInvest + marketingInvest);
|
||||
division.products[product.name] = product;
|
||||
products[product.name] = product;
|
||||
}
|
||||
|
||||
export function Research(division: IIndustry, researchName: string): void {
|
||||
@ -372,7 +441,7 @@ export function Research(division: IIndustry, researchName: string): void {
|
||||
division.researched[researchName] = true;
|
||||
}
|
||||
|
||||
export function ExportMaterial(divisionName: string, cityName: string, material: Material, amt: string): void {
|
||||
export function ExportMaterial(divisionName: string, cityName: string, material: Material, amt: string, division?: Industry): void {
|
||||
// Sanitize amt
|
||||
let sanitizedAmt = amt.replace(/\s+/g, "").toUpperCase();
|
||||
sanitizedAmt = sanitizedAmt.replace(/[^-()\d/*+.MAX]/g, "");
|
||||
@ -388,6 +457,11 @@ export function ExportMaterial(divisionName: string, cityName: string, material:
|
||||
if (n == null || isNaN(n) || n < 0) {
|
||||
throw new Error("Invalid amount entered for export");
|
||||
}
|
||||
|
||||
if (!division || !isRelevantMaterial(material.name, division)) {
|
||||
throw new Error(`You cannot export material: ${material.name} to division: ${divisionName}!`);
|
||||
}
|
||||
|
||||
const exportObj = { ind: divisionName, city: cityName, amt: sanitizedAmt };
|
||||
material.exp.push(exportObj);
|
||||
}
|
||||
|
@ -174,33 +174,16 @@ export class OfficeSpace {
|
||||
}
|
||||
|
||||
setEmployeeToJob(job: string, amount: number): boolean {
|
||||
let unassignedCount = 0;
|
||||
let jobCount = 0;
|
||||
for (let i = 0; i < this.employees.length; ++i) {
|
||||
if (this.employees[i].pos === EmployeePositions.Unassigned) {
|
||||
unassignedCount++;
|
||||
} else if (this.employees[i].pos === job) {
|
||||
let jobCount = this.employees.reduce((acc, employee) => (employee.pos === job ? acc + 1 : acc), 0);
|
||||
|
||||
for (const employee of this.employees) {
|
||||
if (jobCount == amount) return true
|
||||
if (employee.pos === EmployeePositions.Unassigned && jobCount <= amount) {
|
||||
employee.pos = job;
|
||||
jobCount++;
|
||||
}
|
||||
}
|
||||
|
||||
if ((jobCount + unassignedCount) < amount) return false;
|
||||
|
||||
for (let i = 0; i < this.employees.length; ++i) {
|
||||
if (this.employees[i].pos === EmployeePositions.Unassigned) {
|
||||
if (jobCount <= amount) {
|
||||
this.employees[i].pos = job;
|
||||
jobCount++;
|
||||
unassignedCount--;
|
||||
}
|
||||
if (jobCount === amount) break;
|
||||
} else if (this.employees[i].pos === job) {
|
||||
if (jobCount >= amount) {
|
||||
this.employees[i].pos = EmployeePositions.Unassigned;
|
||||
jobCount--;
|
||||
unassignedCount++;
|
||||
}
|
||||
if (jobCount === amount) break;
|
||||
} else if (employee.pos === job && jobCount >= amount) {
|
||||
employee.pos = EmployeePositions.Unassigned;
|
||||
jobCount--;
|
||||
}
|
||||
}
|
||||
if (jobCount !== amount) return false;
|
||||
|
@ -6,6 +6,8 @@ import { useCorporation } from "./Context";
|
||||
import Typography from "@mui/material/Typography";
|
||||
import Button from "@mui/material/Button";
|
||||
import TextField from "@mui/material/TextField";
|
||||
import { BuyBackShares } from '../Actions';
|
||||
import { dialogBoxCreate } from '../../ui/React/DialogBox';
|
||||
|
||||
interface IProps {
|
||||
open: boolean;
|
||||
@ -36,20 +38,12 @@ export function BuybackSharesModal(props: IProps): React.ReactElement {
|
||||
|
||||
function buy(): void {
|
||||
if (disabled) return;
|
||||
if (shares === null) return;
|
||||
corp.numShares += shares;
|
||||
if (isNaN(corp.issuedShares)) {
|
||||
console.warn("Corporation issuedShares is NaN: " + corp.issuedShares);
|
||||
console.warn("Converting to number now");
|
||||
const res = corp.issuedShares;
|
||||
if (isNaN(res)) {
|
||||
corp.issuedShares = 0;
|
||||
} else {
|
||||
corp.issuedShares = res;
|
||||
}
|
||||
try {
|
||||
BuyBackShares(corp, player, shares)
|
||||
}
|
||||
catch (err) {
|
||||
dialogBoxCreate(err + "");
|
||||
}
|
||||
corp.issuedShares -= shares;
|
||||
player.loseMoney(shares * buybackPrice, "corporation");
|
||||
props.onClose();
|
||||
props.rerender();
|
||||
}
|
||||
|
@ -30,7 +30,7 @@ export function CityTabs(props: IProps): React.ReactElement {
|
||||
}
|
||||
return (
|
||||
<>
|
||||
<Tabs variant="fullWidth" value={city} onChange={handleChange}>
|
||||
<Tabs variant="fullWidth" value={city} onChange={handleChange} sx={{ maxWidth: '65%' }}>
|
||||
{Object.values(division.offices).map(
|
||||
(office: OfficeSpace | 0) => office !== 0 && <Tab key={office.loc} label={office.loc} value={office.loc} />,
|
||||
)}
|
||||
|
@ -38,7 +38,7 @@ export function CorporationRoot(): React.ReactElement {
|
||||
|
||||
return (
|
||||
<Context.Corporation.Provider value={corporation}>
|
||||
<Tabs variant="fullWidth" value={divisionName} onChange={handleChange}>
|
||||
<Tabs variant="scrollable" value={divisionName} onChange={handleChange} sx={{ maxWidth: '65%' }} scrollButtons>
|
||||
<Tab label={corporation.name} value={"Overview"} />
|
||||
{corporation.divisions.map((div) => (
|
||||
<Tab key={div.name} label={div.name} value={div.name} />
|
||||
|
@ -50,7 +50,7 @@ export function ExportModal(props: IProps): React.ReactElement {
|
||||
|
||||
function exportMaterial(): void {
|
||||
try {
|
||||
ExportMaterial(industry, city, props.mat, amt);
|
||||
ExportMaterial(industry, city, props.mat, amt, currentDivision);
|
||||
} catch (err) {
|
||||
dialogBoxCreate(err + "");
|
||||
}
|
||||
|
@ -26,6 +26,7 @@ import Table from "@mui/material/Table";
|
||||
import TableBody from "@mui/material/TableBody";
|
||||
import TableRow from "@mui/material/TableRow";
|
||||
import { TableCell } from "../../ui/React/Table";
|
||||
import { Box } from "@mui/material";
|
||||
|
||||
interface IProps {
|
||||
office: OfficeSpace;
|
||||
@ -430,51 +431,46 @@ export function IndustryOffice(props: IProps): React.ReactElement {
|
||||
<Typography>
|
||||
Size: {props.office.employees.length} / {props.office.size} employees
|
||||
</Typography>
|
||||
<Tooltip title={<Typography>Automatically hires an employee and gives him/her a random name</Typography>}>
|
||||
<span>
|
||||
<Button disabled={props.office.atCapacity()} onClick={autohireEmployeeButtonOnClick}>
|
||||
Hire Employee
|
||||
</Button>
|
||||
</span>
|
||||
</Tooltip>
|
||||
<br />
|
||||
<Tooltip title={<Typography>Upgrade the office's size so that it can hold more employees!</Typography>}>
|
||||
<span>
|
||||
<Button disabled={corp.funds < 0} onClick={() => setUpgradeOfficeSizeOpen(true)}>
|
||||
Upgrade size
|
||||
</Button>
|
||||
</span>
|
||||
</Tooltip>
|
||||
<UpgradeOfficeSizeModal
|
||||
rerender={props.rerender}
|
||||
office={props.office}
|
||||
open={upgradeOfficeSizeOpen}
|
||||
onClose={() => setUpgradeOfficeSizeOpen(false)}
|
||||
/>
|
||||
|
||||
{!division.hasResearch("AutoPartyManager") && (
|
||||
<>
|
||||
<Tooltip
|
||||
title={<Typography>Throw an office party to increase your employee's morale and happiness</Typography>}
|
||||
>
|
||||
<span>
|
||||
<Button disabled={corp.funds < 0} onClick={() => setThrowPartyOpen(true)}>
|
||||
Throw Party
|
||||
</Button>
|
||||
</span>
|
||||
<Box sx={{ display: 'grid', gridTemplateColumns: '1fr', width: 'fit-content' }}>
|
||||
<Box sx={{ gridTemplateColumns: 'repeat(3, 1fr)' }}>
|
||||
<Tooltip title={<Typography>Automatically hires an employee and gives him/her a random name</Typography>}>
|
||||
<Button disabled={props.office.atCapacity()} onClick={autohireEmployeeButtonOnClick}>
|
||||
Hire Employee
|
||||
</Button>
|
||||
</Tooltip>
|
||||
<ThrowPartyModal
|
||||
<Tooltip title={<Typography>Upgrade the office's size so that it can hold more employees!</Typography>}>
|
||||
<Button disabled={corp.funds < 0} onClick={() => setUpgradeOfficeSizeOpen(true)}>
|
||||
Upgrade size
|
||||
</Button>
|
||||
</Tooltip>
|
||||
<UpgradeOfficeSizeModal
|
||||
rerender={props.rerender}
|
||||
office={props.office}
|
||||
open={throwPartyOpen}
|
||||
onClose={() => setThrowPartyOpen(false)}
|
||||
open={upgradeOfficeSizeOpen}
|
||||
onClose={() => setUpgradeOfficeSizeOpen(false)}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
|
||||
<br />
|
||||
{!division.hasResearch("AutoPartyManager") && (
|
||||
<>
|
||||
<Tooltip
|
||||
title={<Typography>Throw an office party to increase your employee's morale and happiness</Typography>}
|
||||
>
|
||||
<Button disabled={corp.funds < 0} onClick={() => setThrowPartyOpen(true)}>
|
||||
Throw Party
|
||||
</Button>
|
||||
</Tooltip>
|
||||
<ThrowPartyModal
|
||||
rerender={props.rerender}
|
||||
office={props.office}
|
||||
open={throwPartyOpen}
|
||||
onClose={() => setThrowPartyOpen(false)}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
|
||||
<SwitchButton manualMode={employeeManualAssignMode} switchMode={setEmployeeManualAssignMode} />
|
||||
</Box>
|
||||
<SwitchButton manualMode={employeeManualAssignMode} switchMode={setEmployeeManualAssignMode} />
|
||||
</Box>
|
||||
{employeeManualAssignMode ? (
|
||||
<ManualManagement rerender={props.rerender} office={props.office} />
|
||||
) : (
|
||||
|
@ -13,12 +13,12 @@ export function IndustryProductEquation(props: IProps): React.ReactElement {
|
||||
if (reqAmt === undefined) continue;
|
||||
reqs.push(String.raw`${reqAmt}\text{ }${reqMat}`);
|
||||
}
|
||||
const prod = props.division.prodMats.slice();
|
||||
const prod = props.division.prodMats.map((p) => `1\\text{ }${p}`);
|
||||
if (props.division.makesProducts) {
|
||||
prod.push(props.division.type);
|
||||
prod.push("Products");
|
||||
}
|
||||
|
||||
return (
|
||||
<MathJaxWrapper>{"\\(" + reqs.join("+") + `\\Rightarrow` + prod.map((p) => `1 \\text{${p}}`).join("+") + "\\)"}</MathJaxWrapper>
|
||||
<MathJaxWrapper>{"\\(" + reqs.join("+") + `\\Rightarrow ` + prod.join("+") + "\\)"}</MathJaxWrapper>
|
||||
);
|
||||
}
|
||||
|
@ -27,6 +27,8 @@ import Tooltip from "@mui/material/Tooltip";
|
||||
import Paper from "@mui/material/Paper";
|
||||
import Button from "@mui/material/Button";
|
||||
import Box from "@mui/material/Box";
|
||||
import makeStyles from "@mui/styles/makeStyles";
|
||||
import createStyles from "@mui/styles/createStyles";
|
||||
|
||||
interface IProps {
|
||||
corp: ICorporation;
|
||||
@ -37,6 +39,14 @@ interface IProps {
|
||||
rerender: () => void;
|
||||
}
|
||||
|
||||
const useStyles = makeStyles(() =>
|
||||
createStyles({
|
||||
retainHeight: {
|
||||
minHeight: '3em',
|
||||
},
|
||||
})
|
||||
);
|
||||
|
||||
function WarehouseRoot(props: IProps): React.ReactElement {
|
||||
const corp = useCorporation();
|
||||
const division = useDivision();
|
||||
@ -56,6 +66,8 @@ function WarehouseRoot(props: IProps): React.ReactElement {
|
||||
props.rerender();
|
||||
}
|
||||
|
||||
const classes = useStyles();
|
||||
|
||||
// Current State:
|
||||
let stateText;
|
||||
switch (division.state) {
|
||||
@ -139,13 +151,13 @@ function WarehouseRoot(props: IProps): React.ReactElement {
|
||||
{numeralWrapper.formatBigNumber(props.warehouse.size)}
|
||||
</Typography>
|
||||
</Tooltip>
|
||||
|
||||
<Button disabled={!canAffordUpgrade} onClick={upgradeWarehouseOnClick}>
|
||||
Upgrade Warehouse Size -
|
||||
<MoneyCost money={sizeUpgradeCost} corp={corp} />
|
||||
</Button>
|
||||
</Box>
|
||||
|
||||
<Button disabled={!canAffordUpgrade} onClick={upgradeWarehouseOnClick}>
|
||||
Upgrade Warehouse Size -
|
||||
<MoneyCost money={sizeUpgradeCost} corp={corp} />
|
||||
</Button>
|
||||
|
||||
<Typography>This industry uses the following equation for its production: </Typography>
|
||||
<br />
|
||||
<Typography>
|
||||
@ -158,7 +170,7 @@ function WarehouseRoot(props: IProps): React.ReactElement {
|
||||
</Typography>
|
||||
<br />
|
||||
|
||||
<Typography>{stateText}</Typography>
|
||||
<Typography className={classes.retainHeight}>{stateText}</Typography>
|
||||
|
||||
{corp.unlockUpgrades[1] && (
|
||||
<>
|
||||
|
@ -36,7 +36,7 @@ export function LimitProductProductionModal(props: IProps): React.ReactElement {
|
||||
return (
|
||||
<Modal open={props.open} onClose={props.onClose}>
|
||||
<Typography>
|
||||
Enter a limit to the amount of this product you would like to product per second. Leave the box empty to set no
|
||||
Enter a limit to the amount of this product you would like to produce per second. Leave the box empty to set no
|
||||
limit.
|
||||
</Typography>
|
||||
<TextField autoFocus={true} placeholder="Limit" type="number" onChange={onChange} onKeyDown={onKeyDown} />
|
||||
|
@ -112,7 +112,7 @@ export function MaterialElem(props: IMaterialProps): React.ReactElement {
|
||||
|
||||
return (
|
||||
<Paper>
|
||||
<Box display="flex">
|
||||
<Box sx={{ display: 'grid', gridTemplateColumns: '2fr 1fr', m: '5px' }}>
|
||||
<Box>
|
||||
<Tooltip
|
||||
title={
|
||||
@ -149,11 +149,10 @@ export function MaterialElem(props: IMaterialProps): React.ReactElement {
|
||||
</Tooltip>
|
||||
</Box>
|
||||
|
||||
<Box>
|
||||
<Box sx={{ "& button": { width: '100%' } }}>
|
||||
<Tooltip
|
||||
title={tutorial ? <Typography>Purchase your required materials to get production started!</Typography> : ""}
|
||||
>
|
||||
<span>
|
||||
<Button
|
||||
color={tutorial ? "error" : "primary"}
|
||||
onClick={() => setPurchaseMaterialOpen(true)}
|
||||
@ -161,7 +160,6 @@ export function MaterialElem(props: IMaterialProps): React.ReactElement {
|
||||
>
|
||||
{purchaseButtonText}
|
||||
</Button>
|
||||
</span>
|
||||
</Tooltip>
|
||||
<PurchaseMaterialModal
|
||||
mat={mat}
|
||||
@ -177,7 +175,6 @@ export function MaterialElem(props: IMaterialProps): React.ReactElement {
|
||||
<ExportModal mat={mat} open={exportOpen} onClose={() => setExportOpen(false)} />
|
||||
</>
|
||||
)}
|
||||
<br />
|
||||
|
||||
<Button
|
||||
color={division.prodMats.includes(props.mat.name) && !mat.sllman[0] ? "error" : "primary"}
|
||||
|
@ -89,19 +89,21 @@ export function Overview({ rerender }: IProps): React.ReactElement {
|
||||
<StatsTable rows={multRows} />
|
||||
<br />
|
||||
<BonusTime />
|
||||
<Tooltip
|
||||
title={
|
||||
<Typography>
|
||||
Get a copy of and read 'The Complete Handbook for Creating a Successful Corporation.' This is a .lit file
|
||||
that guides you through the beginning of setting up a Corporation and provides some tips/pointers for
|
||||
helping you get started with managing it.
|
||||
</Typography>
|
||||
}
|
||||
>
|
||||
<Button onClick={() => corp.getStarterGuide(player)}>Getting Started Guide</Button>
|
||||
</Tooltip>
|
||||
{corp.public ? <PublicButtons rerender={rerender} /> : <PrivateButtons rerender={rerender} />}
|
||||
<BribeButton />
|
||||
<Box sx={{ display: 'grid', gridTemplateColumns: 'repeat(3, 1fr)', width: 'fit-content' }}>
|
||||
<Tooltip
|
||||
title={
|
||||
<Typography>
|
||||
Get a copy of and read 'The Complete Handbook for Creating a Successful Corporation.' This is a .lit file
|
||||
that guides you through the beginning of setting up a Corporation and provides some tips/pointers for
|
||||
helping you get started with managing it.
|
||||
</Typography>
|
||||
}
|
||||
>
|
||||
<Button onClick={() => corp.getStarterGuide(player)}>Getting Started Guide</Button>
|
||||
</Tooltip>
|
||||
{corp.public ? <PublicButtons rerender={rerender} /> : <PrivateButtons rerender={rerender} />}
|
||||
<BribeButton />
|
||||
</Box>
|
||||
<br />
|
||||
<Upgrades rerender={rerender} />
|
||||
</>
|
||||
@ -125,11 +127,9 @@ function PrivateButtons({ rerender }: IPrivateButtonsProps): React.ReactElement
|
||||
return (
|
||||
<>
|
||||
<Tooltip title={<Typography>{findInvestorsTooltip}</Typography>}>
|
||||
<span>
|
||||
<Button disabled={!fundingAvailable} onClick={() => setFindInvestorsopen(true)}>
|
||||
Find Investors
|
||||
</Button>
|
||||
</span>
|
||||
<Button disabled={!fundingAvailable} onClick={() => setFindInvestorsopen(true)}>
|
||||
Find Investors
|
||||
</Button>
|
||||
</Tooltip>
|
||||
<Tooltip
|
||||
title={
|
||||
@ -143,7 +143,6 @@ function PrivateButtons({ rerender }: IPrivateButtonsProps): React.ReactElement
|
||||
</Tooltip>
|
||||
<FindInvestorsModal open={findInvestorsopen} onClose={() => setFindInvestorsopen(false)} rerender={rerender} />
|
||||
<GoPublicModal open={goPublicopen} onClose={() => setGoPublicopen(false)} rerender={rerender} />
|
||||
<br />
|
||||
</>
|
||||
);
|
||||
}
|
||||
@ -201,8 +200,8 @@ function PublicButtons({ rerender }: IPublicButtonsProps): React.ReactElement {
|
||||
const sellSharesTooltip = sellSharesOnCd
|
||||
? "Cannot sell shares for " + corp.convertCooldownToString(corp.shareSaleCooldown)
|
||||
: "Sell your shares in the company. The money earned from selling your " +
|
||||
"shares goes into your personal account, not the Corporation's. " +
|
||||
"This is one of the only ways to profit from your business venture.";
|
||||
"shares goes into your personal account, not the Corporation's. " +
|
||||
"This is one of the only ways to profit from your business venture.";
|
||||
|
||||
const issueNewSharesOnCd = corp.issueNewSharesCooldown > 0;
|
||||
const issueNewSharesTooltip = issueNewSharesOnCd
|
||||
@ -212,28 +211,21 @@ function PublicButtons({ rerender }: IPublicButtonsProps): React.ReactElement {
|
||||
return (
|
||||
<>
|
||||
<Tooltip title={<Typography>{sellSharesTooltip}</Typography>}>
|
||||
<span>
|
||||
<Button disabled={sellSharesOnCd} onClick={() => setSellSharesOpen(true)}>
|
||||
Sell Shares
|
||||
</Button>
|
||||
</span>
|
||||
<Button disabled={sellSharesOnCd} onClick={() => setSellSharesOpen(true)}>
|
||||
Sell Shares
|
||||
</Button>
|
||||
</Tooltip>
|
||||
<SellSharesModal open={sellSharesOpen} onClose={() => setSellSharesOpen(false)} rerender={rerender} />
|
||||
<Tooltip title={<Typography>Buy back shares you that previously issued or sold at market price.</Typography>}>
|
||||
<span>
|
||||
<Button disabled={corp.issuedShares < 1} onClick={() => setBuybackSharesOpen(true)}>
|
||||
Buyback shares
|
||||
</Button>
|
||||
</span>
|
||||
<Button disabled={corp.issuedShares < 1} onClick={() => setBuybackSharesOpen(true)}>
|
||||
Buyback shares
|
||||
</Button>
|
||||
</Tooltip>
|
||||
<BuybackSharesModal open={buybackSharesOpen} onClose={() => setBuybackSharesOpen(false)} rerender={rerender} />
|
||||
<br />
|
||||
<Tooltip title={<Typography>{issueNewSharesTooltip}</Typography>}>
|
||||
<span>
|
||||
<Button disabled={issueNewSharesOnCd} onClick={() => setIssueNewSharesOpen(true)}>
|
||||
Issue New Shares
|
||||
</Button>
|
||||
</span>
|
||||
<Button disabled={issueNewSharesOnCd} onClick={() => setIssueNewSharesOpen(true)}>
|
||||
Issue New Shares
|
||||
</Button>
|
||||
</Tooltip>
|
||||
<IssueNewSharesModal open={issueNewSharesOpen} onClose={() => setIssueNewSharesOpen(false)} />
|
||||
<Tooltip
|
||||
@ -242,7 +234,6 @@ function PublicButtons({ rerender }: IPublicButtonsProps): React.ReactElement {
|
||||
<Button onClick={() => setIssueDividendsOpen(true)}>Issue Dividends</Button>
|
||||
</Tooltip>
|
||||
<IssueDividendsModal open={issueDividendsOpen} onClose={() => setIssueDividendsOpen(false)} />
|
||||
<br />
|
||||
</>
|
||||
);
|
||||
}
|
||||
@ -269,11 +260,9 @@ function BribeButton(): React.ReactElement {
|
||||
: "Your Corporation is not powerful enough to bribe Faction leaders"
|
||||
}
|
||||
>
|
||||
<span>
|
||||
<Button disabled={!canBribe} onClick={openBribe}>
|
||||
Bribe Factions
|
||||
</Button>
|
||||
</span>
|
||||
<Button disabled={!canBribe} onClick={openBribe}>
|
||||
Bribe Factions
|
||||
</Button>
|
||||
</Tooltip>
|
||||
<BribeFactionModal open={open} onClose={() => setOpen(false)} />
|
||||
</>
|
||||
|
@ -81,7 +81,7 @@ export function ProductElem(props: IProductProps): React.ReactElement {
|
||||
);
|
||||
} else if (product.sCost) {
|
||||
if (isString(product.sCost)) {
|
||||
const sCost = (product.sCost as string).replace(/MP/g, product.pCost + "");
|
||||
const sCost = (product.sCost as string).replace(/MP/g, product.pCost + product.rat / product.mku + "");
|
||||
sellButtonText = (
|
||||
<>
|
||||
{sellButtonText} @ <Money money={eval(sCost)} />
|
||||
|
@ -4,7 +4,7 @@ import { MaterialSizes } from "../MaterialSizes";
|
||||
import { Warehouse } from "../Warehouse";
|
||||
import { Material } from "../Material";
|
||||
import { numeralWrapper } from "../../ui/numeralFormat";
|
||||
import { BuyMaterial } from "../Actions";
|
||||
import { BulkPurchase, BuyMaterial } from "../Actions";
|
||||
import { Modal } from "../../ui/React/Modal";
|
||||
import { useCorporation, useDivision } from "./Context";
|
||||
import Typography from "@mui/material/Typography";
|
||||
@ -54,33 +54,17 @@ interface IBPProps {
|
||||
warehouse: Warehouse;
|
||||
}
|
||||
|
||||
function BulkPurchase(props: IBPProps): React.ReactElement {
|
||||
function BulkPurchaseSection(props: IBPProps): React.ReactElement {
|
||||
const corp = useCorporation();
|
||||
const [buyAmt, setBuyAmt] = useState("");
|
||||
|
||||
function bulkPurchase(): void {
|
||||
const amount = parseFloat(buyAmt);
|
||||
|
||||
const matSize = MaterialSizes[props.mat.name];
|
||||
const maxAmount = (props.warehouse.size - props.warehouse.sizeUsed) / matSize;
|
||||
if (amount * matSize > maxAmount) {
|
||||
dialogBoxCreate(`You do not have enough warehouse size to fit this purchase`);
|
||||
return;
|
||||
}
|
||||
|
||||
if (isNaN(amount) || amount < 0) {
|
||||
dialogBoxCreate("Invalid input amount");
|
||||
} else {
|
||||
const cost = amount * props.mat.bCost;
|
||||
if (corp.funds >= cost) {
|
||||
corp.funds = corp.funds - cost;
|
||||
props.mat.qty += amount;
|
||||
} else {
|
||||
dialogBoxCreate(`You cannot afford this purchase.`);
|
||||
return;
|
||||
}
|
||||
props.onClose();
|
||||
try {
|
||||
BulkPurchase(corp, props.warehouse, props.mat, parseFloat(buyAmt));
|
||||
} catch (err) {
|
||||
dialogBoxCreate(err + "");
|
||||
}
|
||||
props.onClose();
|
||||
}
|
||||
|
||||
function onKeyDown(event: React.KeyboardEvent<HTMLInputElement>): void {
|
||||
@ -164,7 +148,7 @@ export function PurchaseMaterialModal(props: IProps): React.ReactElement {
|
||||
<Button onClick={purchaseMaterial}>Confirm</Button>
|
||||
<Button onClick={clearPurchase}>Clear Purchase</Button>
|
||||
{division.hasResearch("Bulk Purchasing") && (
|
||||
<BulkPurchase onClose={props.onClose} mat={props.mat} warehouse={props.warehouse} />
|
||||
<BulkPurchaseSection onClose={props.onClose} mat={props.mat} warehouse={props.warehouse} />
|
||||
)}
|
||||
</>
|
||||
</Modal>
|
||||
|
@ -6,17 +6,18 @@ import { IIndustry } from "../IIndustry";
|
||||
import { Research } from "../Actions";
|
||||
import { Node } from "../ResearchTree";
|
||||
import { ResearchMap } from "../ResearchMap";
|
||||
import { Settings } from "../../Settings/Settings";
|
||||
import { dialogBoxCreate } from "../../ui/React/DialogBox";
|
||||
import Typography from "@mui/material/Typography";
|
||||
import Tooltip from "@mui/material/Tooltip";
|
||||
import Button from "@mui/material/Button";
|
||||
import Box from "@mui/material/Box";
|
||||
|
||||
import ListItemButton from "@mui/material/ListItemButton";
|
||||
import ListItemText from "@mui/material/ListItemText";
|
||||
import Collapse from "@mui/material/Collapse";
|
||||
import ExpandMore from "@mui/icons-material/ExpandMore";
|
||||
import ExpandLess from "@mui/icons-material/ExpandLess";
|
||||
import CheckIcon from '@mui/icons-material/Check';
|
||||
|
||||
interface INodeProps {
|
||||
n: Node | null;
|
||||
division: IIndustry;
|
||||
@ -42,8 +43,8 @@ function Upgrade({ n, division }: INodeProps): React.ReactElement {
|
||||
|
||||
dialogBoxCreate(
|
||||
`Researched ${n.text}. It may take a market cycle ` +
|
||||
`(~${CorporationConstants.SecsPerMarketCycle} seconds) before the effects of ` +
|
||||
`the Research apply.`,
|
||||
`(~${CorporationConstants.SecsPerMarketCycle} seconds) before the effects of ` +
|
||||
`the Research apply.`,
|
||||
);
|
||||
}
|
||||
|
||||
@ -52,8 +53,8 @@ function Upgrade({ n, division }: INodeProps): React.ReactElement {
|
||||
color = "info";
|
||||
}
|
||||
|
||||
const but = (
|
||||
<Box>
|
||||
const wrapInTooltip = (ele: React.ReactElement): React.ReactElement => {
|
||||
return (
|
||||
<Tooltip
|
||||
title={
|
||||
<Typography>
|
||||
@ -63,12 +64,22 @@ function Upgrade({ n, division }: INodeProps): React.ReactElement {
|
||||
</Typography>
|
||||
}
|
||||
>
|
||||
{ele}
|
||||
</Tooltip>
|
||||
)
|
||||
}
|
||||
|
||||
const but = (
|
||||
<Box>
|
||||
{wrapInTooltip(
|
||||
<span>
|
||||
<Button color={color} disabled={disabled && !n.researched} onClick={research}>
|
||||
{n.text}
|
||||
<Button color={color} disabled={disabled && !n.researched} onClick={research}
|
||||
style={{ width: '100%', textAlign: 'left', justifyContent: 'unset' }}
|
||||
>
|
||||
{n.researched && (<CheckIcon sx={{ mr: 1 }} />)}{n.text}
|
||||
</Button>
|
||||
</span>
|
||||
</Tooltip>
|
||||
)}
|
||||
</Box>
|
||||
);
|
||||
|
||||
@ -76,15 +87,25 @@ function Upgrade({ n, division }: INodeProps): React.ReactElement {
|
||||
|
||||
return (
|
||||
<Box>
|
||||
<Box display="flex">
|
||||
{but}
|
||||
<ListItemButton onClick={() => setOpen((old) => !old)}>
|
||||
<ListItemText />
|
||||
<Box display="flex" sx={{ border: '1px solid ' + Settings.theme.well }}>
|
||||
{wrapInTooltip(
|
||||
<span style={{ width: '100%' }}>
|
||||
<Button color={color} disabled={disabled && !n.researched} onClick={research} sx={{
|
||||
width: '100%',
|
||||
textAlign: 'left',
|
||||
justifyContent: 'unset',
|
||||
borderColor: Settings.theme.button
|
||||
}}>
|
||||
{n.researched && (<CheckIcon sx={{ mr: 1 }} />)}{n.text}
|
||||
</Button>
|
||||
</span>
|
||||
)}
|
||||
<Button onClick={() => setOpen((old) => !old)} sx={{ borderColor: Settings.theme.button, minWidth: 'fit-content' }}>
|
||||
{open ? <ExpandLess color="primary" /> : <ExpandMore color="primary" />}
|
||||
</ListItemButton>
|
||||
</Button>
|
||||
</Box>
|
||||
<Collapse in={open} unmountOnExit>
|
||||
<Box m={4}>
|
||||
<Box m={1}>
|
||||
{n.children.map((m) => (
|
||||
<Upgrade key={m.text} division={division} n={m} />
|
||||
))}
|
||||
@ -108,7 +129,7 @@ export function ResearchModal(props: IProps): React.ReactElement {
|
||||
return (
|
||||
<Modal open={props.open} onClose={props.onClose}>
|
||||
<Upgrade division={props.industry} n={researchTree.root} />
|
||||
<Typography>
|
||||
<Typography sx={{ mt: 1 }}>
|
||||
Research points: {props.industry.sciResearch.qty.toFixed(3)}
|
||||
<br />
|
||||
Multipliers from research:
|
||||
|
@ -4,12 +4,12 @@ import { dialogBoxCreate } from "../../ui/React/DialogBox";
|
||||
import { Modal } from "../../ui/React/Modal";
|
||||
import { use } from "../../ui/Context";
|
||||
import { useCorporation } from "./Context";
|
||||
import { CorporationConstants } from "../data/Constants";
|
||||
import { ICorporation } from "../ICorporation";
|
||||
import Typography from "@mui/material/Typography";
|
||||
import TextField from "@mui/material/TextField";
|
||||
import Button from "@mui/material/Button";
|
||||
import { Money } from "../../ui/React/Money";
|
||||
import { SellShares } from "../Actions";
|
||||
interface IProps {
|
||||
open: boolean;
|
||||
onClose: () => void;
|
||||
@ -48,38 +48,23 @@ export function SellSharesModal(props: IProps): React.ReactElement {
|
||||
}
|
||||
|
||||
function sell(): void {
|
||||
if (shares === null) return;
|
||||
if (disabled) return;
|
||||
const stockSaleResults = corp.calculateShareSale(shares);
|
||||
const profit = stockSaleResults[0];
|
||||
const newSharePrice = stockSaleResults[1];
|
||||
const newSharesUntilUpdate = stockSaleResults[2];
|
||||
try {
|
||||
const profit = SellShares(corp, player, shares)
|
||||
props.onClose();
|
||||
dialogBoxCreate(
|
||||
<>
|
||||
Sold {numeralWrapper.formatMoney(shares)} shares for
|
||||
<Money money={profit} />. The corporation's stock price fell to <Money money={corp.sharePrice} />
|
||||
as a result of dilution.
|
||||
</>,
|
||||
);
|
||||
|
||||
corp.numShares -= shares;
|
||||
if (isNaN(corp.issuedShares)) {
|
||||
console.error(`Corporation issuedShares is NaN: ${corp.issuedShares}`);
|
||||
const res = corp.issuedShares;
|
||||
if (isNaN(res)) {
|
||||
corp.issuedShares = 0;
|
||||
} else {
|
||||
corp.issuedShares = res;
|
||||
}
|
||||
props.rerender();
|
||||
} catch (err) {
|
||||
dialogBoxCreate(err + "");
|
||||
}
|
||||
corp.issuedShares += shares;
|
||||
corp.sharePrice = newSharePrice;
|
||||
corp.shareSalesUntilPriceUpdate = newSharesUntilUpdate;
|
||||
corp.shareSaleCooldown = CorporationConstants.SellSharesCooldown;
|
||||
player.gainMoney(profit, "corporation");
|
||||
props.onClose();
|
||||
dialogBoxCreate(
|
||||
<>
|
||||
Sold {numeralWrapper.formatMoney(shares)} shares for
|
||||
<Money money={profit} />. The corporation's stock price fell to <Money money={corp.sharePrice} />
|
||||
as a result of dilution.
|
||||
</>,
|
||||
);
|
||||
|
||||
props.rerender();
|
||||
}
|
||||
|
||||
function onKeyDown(event: React.KeyboardEvent<HTMLInputElement>): void {
|
||||
|
@ -23,7 +23,7 @@ export function StaneksGiftRoot({ staneksGift }: IProps): React.ReactElement {
|
||||
The gift is a grid on which you can place upgrades called fragments. The main type of fragment increases a stat,
|
||||
like your hacking skill or agility exp. Once a stat fragment is placed it then needs to be charged via scripts
|
||||
in order to become useful. The other kind of fragments are called booster fragments. They increase the
|
||||
efficiency of neighboring fragments them (no diagonal). Q/E to rotate fragments.
|
||||
efficiency of the neighboring fragments (not diagonally). Use Q/E to rotate fragments.
|
||||
</Typography>
|
||||
{staneksGift.storedCycles > 5 && (
|
||||
<Typography>
|
||||
|
@ -44,7 +44,7 @@ export function findCrime(roughName: string): Crime | null {
|
||||
return Crimes.DealDrugs;
|
||||
} else if (roughName.includes("bond") && roughName.includes("forge")) {
|
||||
return Crimes.BondForgery;
|
||||
} else if (roughName.includes("traffick") && roughName.includes("arms")) {
|
||||
} else if ((roughName.includes("traffic") || roughName.includes("illegal")) && roughName.includes("arms")) {
|
||||
return Crimes.TraffickArms;
|
||||
} else if (roughName.includes("homicide")) {
|
||||
return Crimes.Homicide;
|
||||
@ -52,7 +52,7 @@ export function findCrime(roughName: string): Crime | null {
|
||||
return Crimes.GrandTheftAuto;
|
||||
} else if (roughName.includes("kidnap")) {
|
||||
return Crimes.Kidnap;
|
||||
} else if (roughName.includes("assassinate") || roughName.includes("assassination")) {
|
||||
} else if (roughName.includes("assassin")) {
|
||||
return Crimes.Assassination;
|
||||
} else if (roughName.includes("heist")) {
|
||||
return Crimes.Heist;
|
||||
|
@ -19,10 +19,18 @@ interface IProps {
|
||||
export function Corporation(props: IProps): React.ReactElement {
|
||||
function addTonsCorporationFunds(): void {
|
||||
if (props.player.corporation) {
|
||||
props.player.corporation.funds = props.player.corporation.funds + 1e99;
|
||||
props.player.corporation.funds = props.player.corporation.funds + bigNumber;
|
||||
}
|
||||
}
|
||||
|
||||
function modifyCorporationFunds(modify: number): (x: number) => void {
|
||||
return function (funds: number): void {
|
||||
if (props.player.corporation) {
|
||||
props.player.corporation.funds += funds * modify;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
function resetCorporationFunds(): void {
|
||||
if (props.player.corporation) {
|
||||
props.player.corporation.funds = props.player.corporation.funds - props.player.corporation.funds;
|
||||
@ -77,8 +85,17 @@ export function Corporation(props: IProps): React.ReactElement {
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>
|
||||
<Button onClick={addTonsCorporationFunds}>Tons of funds</Button>
|
||||
<Button onClick={resetCorporationFunds}>Reset funds</Button>
|
||||
<Typography>Funds:</Typography>
|
||||
</td>
|
||||
<td>
|
||||
<Adjuster
|
||||
label="set funds"
|
||||
placeholder="amt"
|
||||
tons={addTonsCorporationFunds}
|
||||
add={modifyCorporationFunds(1)}
|
||||
subtract={modifyCorporationFunds(-1)}
|
||||
reset={resetCorporationFunds}
|
||||
/>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
|
@ -8,6 +8,7 @@ import ExpandMoreIcon from "@mui/icons-material/ExpandMore";
|
||||
import Button from "@mui/material/Button";
|
||||
import Typography from "@mui/material/Typography";
|
||||
import { IPlayer } from "../../PersonObjects/IPlayer";
|
||||
import { Adjuster } from "./Adjuster";
|
||||
|
||||
interface IProps {
|
||||
player: IPlayer;
|
||||
@ -38,6 +39,12 @@ export function Sleeves(props: IProps): React.ReactElement {
|
||||
}
|
||||
}
|
||||
|
||||
function sleeveSetStoredCycles(cycles: number): void {
|
||||
for (let i = 0; i < props.player.sleeves.length; ++i) {
|
||||
props.player.sleeves[i].storedCycles = cycles;
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<Accordion TransitionProps={{ unmountOnExit: true }}>
|
||||
<AccordionSummary expandIcon={<ExpandMoreIcon />}>
|
||||
@ -68,6 +75,18 @@ export function Sleeves(props: IProps): React.ReactElement {
|
||||
<Button onClick={sleeveSyncClearAll}>Clear all</Button>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td colSpan={3}>
|
||||
<Adjuster
|
||||
label="Stored Cycles"
|
||||
placeholder="cycles"
|
||||
tons={() => sleeveSetStoredCycles(10000000)}
|
||||
add={sleeveSetStoredCycles}
|
||||
subtract={sleeveSetStoredCycles}
|
||||
reset={() => sleeveSetStoredCycles(0)}
|
||||
/>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</AccordionDetails>
|
||||
|
212
src/Electron.tsx
@ -1,11 +1,15 @@
|
||||
import { Player } from "./Player";
|
||||
import { isScriptFilename } from "./Script/isScriptFilename";
|
||||
import { Script } from "./Script/Script";
|
||||
import { Router } from "./ui/GameRoot";
|
||||
import { removeLeadingSlash } from "./Terminal/DirectoryHelpers";
|
||||
import { Terminal } from "./Terminal";
|
||||
import { SnackbarEvents } from "./ui/React/Snackbar";
|
||||
import { IMap } from "./types";
|
||||
import { IMap, IReturnStatus } from "./types";
|
||||
import { GetServer } from "./Server/AllServers";
|
||||
import { ImportPlayerData, SaveData, saveObject } from "./SaveObject";
|
||||
import { Settings } from "./Settings/Settings";
|
||||
import { exportScripts } from "./Terminal/commands/download";
|
||||
import { CONSTANTS } from "./Constants";
|
||||
import { hash } from "./hash/hash";
|
||||
|
||||
export function initElectron(): void {
|
||||
const userAgent = navigator.userAgent.toLowerCase();
|
||||
@ -14,36 +18,81 @@ export function initElectron(): void {
|
||||
(document as any).achievements = [];
|
||||
initWebserver();
|
||||
initAppNotifier();
|
||||
initSaveFunctions();
|
||||
initElectronBridge();
|
||||
}
|
||||
}
|
||||
|
||||
function initWebserver(): void {
|
||||
(document as any).saveFile = function (filename: string, code: string): string {
|
||||
interface IReturnWebStatus extends IReturnStatus {
|
||||
data?: {
|
||||
[propName: string]: any;
|
||||
};
|
||||
}
|
||||
function normalizeFileName(filename: string): string {
|
||||
filename = filename.replace(/\/\/+/g, "/");
|
||||
filename = removeLeadingSlash(filename);
|
||||
if (filename.includes("/")) {
|
||||
filename = "/" + removeLeadingSlash(filename);
|
||||
}
|
||||
return filename;
|
||||
}
|
||||
|
||||
(document as any).getFiles = function (): IReturnWebStatus {
|
||||
const home = GetServer("home");
|
||||
if (home === null) {
|
||||
return {
|
||||
res: false,
|
||||
msg: "Home server does not exist.",
|
||||
};
|
||||
}
|
||||
return {
|
||||
res: true,
|
||||
data: {
|
||||
files: home.scripts.map((script) => ({
|
||||
filename: script.filename,
|
||||
code: script.code,
|
||||
ramUsage: script.ramUsage,
|
||||
})),
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
(document as any).deleteFile = function (filename: string): IReturnWebStatus {
|
||||
filename = normalizeFileName(filename);
|
||||
const home = GetServer("home");
|
||||
if (home === null) {
|
||||
return {
|
||||
res: false,
|
||||
msg: "Home server does not exist.",
|
||||
};
|
||||
}
|
||||
return home.removeFile(filename);
|
||||
};
|
||||
|
||||
(document as any).saveFile = function (filename: string, code: string): IReturnWebStatus {
|
||||
filename = normalizeFileName(filename);
|
||||
|
||||
code = Buffer.from(code, "base64").toString();
|
||||
const home = GetServer("home");
|
||||
if (home === null) return "'home' server not found.";
|
||||
if (isScriptFilename(filename)) {
|
||||
//If the current script already exists on the server, overwrite it
|
||||
for (let i = 0; i < home.scripts.length; i++) {
|
||||
if (filename == home.scripts[i].filename) {
|
||||
home.scripts[i].saveScript(Player, filename, code, "home", home.scripts);
|
||||
return "written";
|
||||
}
|
||||
}
|
||||
|
||||
//If the current script does NOT exist, create a new one
|
||||
const script = new Script();
|
||||
script.saveScript(Player, filename, code, "home", home.scripts);
|
||||
home.scripts.push(script);
|
||||
return "written";
|
||||
if (home === null) {
|
||||
return {
|
||||
res: false,
|
||||
msg: "Home server does not exist.",
|
||||
};
|
||||
}
|
||||
|
||||
return "not a script file";
|
||||
const { success, overwritten } = home.writeToScriptFile(Player, filename, code);
|
||||
let script;
|
||||
if (success) {
|
||||
script = home.getScript(filename);
|
||||
}
|
||||
return {
|
||||
res: success,
|
||||
data: {
|
||||
overwritten,
|
||||
ramUsage: script?.ramUsage,
|
||||
},
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
@ -67,6 +116,123 @@ function initAppNotifier(): void {
|
||||
};
|
||||
|
||||
// Will be consumud by the electron wrapper.
|
||||
// @ts-ignore
|
||||
window.appNotifier = funcs;
|
||||
(window as any).appNotifier = funcs;
|
||||
}
|
||||
|
||||
function initSaveFunctions(): void {
|
||||
const funcs = {
|
||||
triggerSave: (): Promise<void> => saveObject.saveGame(true),
|
||||
triggerGameExport: (): void => {
|
||||
try {
|
||||
saveObject.exportGame();
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
SnackbarEvents.emit("Could not export game.", "error", 2000);
|
||||
}
|
||||
},
|
||||
triggerScriptsExport: (): void => exportScripts("*", Player.getHomeComputer()),
|
||||
getSaveData: (): { save: string; fileName: string } => {
|
||||
return {
|
||||
save: saveObject.getSaveString(Settings.ExcludeRunningScriptsFromSave),
|
||||
fileName: saveObject.getSaveFileName(),
|
||||
};
|
||||
},
|
||||
getSaveInfo: async (base64save: string): Promise<ImportPlayerData | undefined> => {
|
||||
try {
|
||||
const data = await saveObject.getImportDataFromString(base64save);
|
||||
return data.playerData;
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
return;
|
||||
}
|
||||
},
|
||||
pushSaveData: (base64save: string, automatic = false): void => Router.toImportSave(base64save, automatic),
|
||||
};
|
||||
|
||||
// Will be consumud by the electron wrapper.
|
||||
(window as any).appSaveFns = funcs;
|
||||
}
|
||||
|
||||
function initElectronBridge(): void {
|
||||
const bridge = (window as any).electronBridge as any;
|
||||
if (!bridge) return;
|
||||
|
||||
bridge.receive("get-save-data-request", () => {
|
||||
const data = (window as any).appSaveFns.getSaveData();
|
||||
bridge.send("get-save-data-response", data);
|
||||
});
|
||||
bridge.receive("get-save-info-request", async (save: string) => {
|
||||
const data = await (window as any).appSaveFns.getSaveInfo(save);
|
||||
bridge.send("get-save-info-response", data);
|
||||
});
|
||||
bridge.receive("push-save-request", ({ save, automatic = false }: { save: string; automatic: boolean }) => {
|
||||
(window as any).appSaveFns.pushSaveData(save, automatic);
|
||||
});
|
||||
bridge.receive("trigger-save", () => {
|
||||
return (window as any).appSaveFns
|
||||
.triggerSave()
|
||||
.then(() => {
|
||||
bridge.send("save-completed");
|
||||
})
|
||||
.catch((error: any) => {
|
||||
console.log(error);
|
||||
SnackbarEvents.emit("Could not save game.", "error", 2000);
|
||||
});
|
||||
});
|
||||
bridge.receive("trigger-game-export", () => {
|
||||
try {
|
||||
(window as any).appSaveFns.triggerGameExport();
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
SnackbarEvents.emit("Could not export game.", "error", 2000);
|
||||
}
|
||||
});
|
||||
bridge.receive("trigger-scripts-export", () => {
|
||||
try {
|
||||
(window as any).appSaveFns.triggerScriptsExport();
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
SnackbarEvents.emit("Could not export scripts.", "error", 2000);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
export function pushGameSaved(data: SaveData): void {
|
||||
const bridge = (window as any).electronBridge as any;
|
||||
if (!bridge) return;
|
||||
|
||||
bridge.send("push-game-saved", data);
|
||||
}
|
||||
|
||||
export function pushGameReady(): void {
|
||||
const bridge = (window as any).electronBridge as any;
|
||||
if (!bridge) return;
|
||||
|
||||
// Send basic information to the electron wrapper
|
||||
bridge.send("push-game-ready", {
|
||||
player: {
|
||||
identifier: Player.identifier,
|
||||
playtime: Player.totalPlaytime,
|
||||
lastSave: Player.lastSave,
|
||||
},
|
||||
game: {
|
||||
version: CONSTANTS.VersionString,
|
||||
hash: hash(),
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
export function pushImportResult(wasImported: boolean): void {
|
||||
const bridge = (window as any).electronBridge as any;
|
||||
if (!bridge) return;
|
||||
|
||||
bridge.send("push-import-result", { wasImported });
|
||||
pushDisableRestore();
|
||||
}
|
||||
|
||||
export function pushDisableRestore(): void {
|
||||
const bridge = (window as any).electronBridge as any;
|
||||
if (!bridge) return;
|
||||
|
||||
bridge.send("push-disable-restore", { duration: 1000 * 60 });
|
||||
}
|
||||
|
@ -19,6 +19,42 @@ import { SourceFileFlags } from "../SourceFile/SourceFileFlags";
|
||||
import { dialogBoxCreate } from "../ui/React/DialogBox";
|
||||
import { InvitationEvent } from "./ui/InvitationModal";
|
||||
|
||||
const factionOrder = [
|
||||
"CyberSec",
|
||||
"Tian Di Hui",
|
||||
"Netburners",
|
||||
"Sector-12",
|
||||
"Chongqing",
|
||||
"New Tokyo",
|
||||
"Ishima",
|
||||
"Aevum",
|
||||
"Volhaven",
|
||||
"NiteSec",
|
||||
"The Black Hand",
|
||||
"BitRunners",
|
||||
"ECorp",
|
||||
"MegaCorp",
|
||||
"KuaiGong International",
|
||||
"Four Sigma",
|
||||
"NWO",
|
||||
"Blade Industries",
|
||||
"OmniTek Incorporated",
|
||||
"Bachman & Associates",
|
||||
"Clarke Incorporated",
|
||||
"Fulcrum Secret Technologies",
|
||||
"Slum Snakes",
|
||||
"Tetrads",
|
||||
"Silhouette",
|
||||
"Speakers for the Dead",
|
||||
"The Dark Army",
|
||||
"The Syndicate",
|
||||
"The Covenant",
|
||||
"Daedalus",
|
||||
"Illuminati",
|
||||
"Bladeburners",
|
||||
"Church of the Machine God",
|
||||
]
|
||||
|
||||
export function inviteToFaction(faction: Faction): void {
|
||||
Player.receiveInvite(faction.name);
|
||||
faction.alreadyInvited = true;
|
||||
@ -31,6 +67,8 @@ export function joinFaction(faction: Faction): void {
|
||||
if (faction.isMember) return;
|
||||
faction.isMember = true;
|
||||
Player.factions.push(faction.name);
|
||||
Player.factions.sort((a, b) =>
|
||||
factionOrder.indexOf(a) - factionOrder.indexOf(b));
|
||||
const factionInfo = faction.getInfo();
|
||||
|
||||
//Determine what factions you are banned from now that you have joined this faction
|
||||
@ -132,19 +170,19 @@ export function purchaseAugmentation(aug: Augmentation, fac: Faction, sing = fal
|
||||
if (!Settings.SuppressBuyAugmentationConfirmation) {
|
||||
dialogBoxCreate(
|
||||
"You purchased " +
|
||||
aug.name +
|
||||
". Its enhancements will not take " +
|
||||
"effect until they are installed. To install your augmentations, go to the " +
|
||||
"'Augmentations' tab on the left-hand navigation menu. Purchasing additional " +
|
||||
"augmentations will now be more expensive.",
|
||||
aug.name +
|
||||
". Its enhancements will not take " +
|
||||
"effect until they are installed. To install your augmentations, go to the " +
|
||||
"'Augmentations' tab on the left-hand navigation menu. Purchasing additional " +
|
||||
"augmentations will now be more expensive.",
|
||||
);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
dialogBoxCreate(
|
||||
"Hmm, something went wrong when trying to purchase an Augmentation. " +
|
||||
"Please report this to the game developer with an explanation of how to " +
|
||||
"reproduce this.",
|
||||
"Please report this to the game developer with an explanation of how to " +
|
||||
"reproduce this.",
|
||||
);
|
||||
}
|
||||
return "";
|
||||
|
@ -157,7 +157,7 @@ export const FactionInfos: IMap<FactionInfo> = {
|
||||
(
|
||||
<>
|
||||
MegaCorp does what no other dares to do. We imagine. We create. We invent. We create what others have never even
|
||||
dreamed of. Our work fills the world's needs for food, water, power, and transportation on an unprecendented
|
||||
dreamed of. Our work fills the world's needs for food, water, power, and transportation on an unprecedented
|
||||
scale, in ways that no other company can.
|
||||
<br />
|
||||
<br />
|
||||
|