Merge pull request #3088 from danielyxie/dev

v1.5.0
This commit is contained in:
hydroflame 2022-03-07 17:51:40 -05:00 committed by GitHub
commit ef167046eb
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
245 changed files with 7632 additions and 36940 deletions

@ -1,27 +1,26 @@
node_modules/ node_modules/
dist/
input/
.dist .dist
.tmp .tmp
.package .package
.build
assets/
css/
.cypress/ .cypress/
cypress/
dist/
input/
assets/
doc/ doc/
markdown/ markdown/
netscript_tests/
scripts/
test/netscript/
electron/lib electron/lib
electron/greenworks.js electron/greenworks.js
src/ThirdParty/* src/ThirdParty/*
src/JSInterpreter.js src/JSInterpreter.js
tools/engines-check/
test/*.bundle.*
editor.main.js editor.main.js
main.bundle.js main.bundle.js
webpack.config.js webpack.config.js
webpack.config-test.js

100
.github/workflows/bump-version.yml vendored Normal file

@ -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 /doc/build
/node_modules /node_modules
/electron/node_modules /electron/node_modules
/dist/*.map
/test/*.map /test/*.map
/test/*.bundle.* /test/*.bundle.*
/test/*.css /test/*.css
/input/bitburner.api.json
.cypress .cypress
# tmp folder for electron # tmp folder for electron

@ -6,7 +6,7 @@ Bitburner is a programming-based [incremental game](https://en.wikipedia.org/wik
that revolves around hacking and cyberpunk themes. that revolves around hacking and cyberpunk themes.
The game can be played at https://danielyxie.github.io/bitburner or installed through [Steam](https://store.steampowered.com/app/1812820/Bitburner/). The game can be played at https://danielyxie.github.io/bitburner or installed through [Steam](https://store.steampowered.com/app/1812820/Bitburner/).
See the [frequently asked questions](./FAQ.md) for more information . To discuss the game or get help, join the [official discord server](https://discord.gg/TFc3hKD) See the [frequently asked questions](./doc/FAQ.md) for more information . To discuss the game or get help, join the [official discord server](https://discord.gg/TFc3hKD)
# Documentation # Documentation
@ -18,13 +18,13 @@ The [in-game documentation](./markdown/bitburner.md) is generated from the [Type
Anyone is welcome to contribute to the documentation by editing the [source Anyone is welcome to contribute to the documentation by editing the [source
files](/doc/source) and then making a pull request with your contributions. files](/doc/source) and then making a pull request with your contributions.
For further guidance, please refer to the "As A Documentor" section of For further guidance, please refer to the "As A Documentor" section of
[CONTRIBUTING](CONTRIBUTING.md). [CONTRIBUTING](./doc/CONTRIBUTING.md).
# Contribution # Contribution
There are many ways to contribute to the game. It can be as simple as fixing There are many ways to contribute to the game. It can be as simple as fixing
a typo, correcting a bug, or improving the UI. For guidance on doing so, a typo, correcting a bug, or improving the UI. For guidance on doing so,
please refer to the [CONTRIBUTING](CONTRIBUTING.md) document. please refer to the [CONTRIBUTING](./doc/CONTRIBUTING.md) document.
You will retain all ownership of the Copyright of any contributions you make, You will retain all ownership of the Copyright of any contributions you make,
and will have the same rights to use or license your contributions. By and will have the same rights to use or license your contributions. By

BIN
assets/favicon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

@ -1,10 +1,14 @@
{ {
"baseUrl": "http://localhost:8000", "baseUrl": "http://localhost:8000",
"fixturesFolder": false,
"trashAssetsBeforeRuns": true, "trashAssetsBeforeRuns": true,
"screenshotsFolder": ".cypress/screenshots",
"videosFolder": ".cypress/videos",
"videoUploadOnPasses": false, "videoUploadOnPasses": false,
"viewportWidth": 1980, "viewportWidth": 1980,
"viewportHeight": 1080 "viewportHeight": 1080,
"fixturesFolder": "test/cypress/fixtures",
"integrationFolder": "test/cypress/integration",
"pluginsFile": "test/cypress/plugins/index.js",
"supportFile": "test/cypress/support/index.js",
"screenshotsFolder": ".cypress/screenshots",
"videosFolder": ".cypress/videos",
"downloadsFolder": ".cypress/downloads"
} }

@ -1,9 +0,0 @@
export {};
beforeEach(() => {
cy.visit("/");
cy.clearLocalStorage();
cy.window().then((win) => {
win.indexedDB.deleteDatabase("bitburnerSave");
});
});

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. * @param percent - Percent of profit to issue as dividends.
*/ */
issueDividends(percent: number): void; 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 * @remarks
* RAM cost: 0.3 GB * 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. * 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. * @param filename - Optional. Filename or PID of the script.
@ -4807,7 +4817,7 @@ export declare interface Server {
/** IP Address. Must be unique */ /** IP Address. Must be unique */
ip: string; ip: string;
/** Flag indicating whether player is curently connected to this server */ /** Flag indicating whether player is currently connected to this server */
isConnectedTo: boolean; isConnectedTo: boolean;
/** RAM (GB) available on this server */ /** RAM (GB) available on this server */
@ -6635,6 +6645,14 @@ export declare interface WarehouseAPI {
* @param amt - Amount of material to buy * @param amt - Amount of material to buy
*/ */
buyMaterial(divisionName: string, cityName: string, materialName: string, amt: number): void; 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 * Get warehouse data
* @param divisionName - Name of the division * @param divisionName - Name of the division

20
dist/engine.bundle.js vendored

File diff suppressed because one or more lines are too long

BIN
dist/favicon.ico vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 118 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 120 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 118 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 119 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 116 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 111 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 112 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 121 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 118 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 120 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 121 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 118 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 120 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 117 KiB

20
dist/main.bundle.js vendored Normal file

File diff suppressed because one or more lines are too long

1
dist/main.bundle.js.map vendored Normal file

File diff suppressed because one or more lines are too long

36
dist/vendor.bundle.js vendored

File diff suppressed because one or more lines are too long

1
dist/vendor.bundle.js.map vendored Normal file

File diff suppressed because one or more lines are too long

@ -182,7 +182,7 @@ To contribute to and view your changes to the BitBurner documentation on [Read T
Docs](http://bitburner.readthedocs.io/), you will Docs](http://bitburner.readthedocs.io/), you will
need to have Python installed, along with [Sphinx](http://www.sphinx-doc.org). need to have Python installed, along with [Sphinx](http://www.sphinx-doc.org).
To make change to the [in-game documentation](./markdown/bitburner.md), you will need to modify the [TypeScript definitions](./src/ScriptEditor/NetscriptDefinitions.d.ts), not the markdown files. To make change to the [in-game documentation](../markdown/bitburner.md), you will need to modify the [TypeScript definitions](../src/ScriptEditor/NetscriptDefinitions.d.ts), not the markdown files.
We are using [API Extractor](https://api-extractor.com/pages/tsdoc/doc_comment_syntax/) (tsdoc hints) to generate the markdown doc. Make your changes to the TypeScript definitions and then run `npm run doc`. We are using [API Extractor](https://api-extractor.com/pages/tsdoc/doc_comment_syntax/) (tsdoc hints) to generate the markdown doc. Make your changes to the TypeScript definitions and then run `npm run doc`.

@ -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. | | | | 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: Numbers in an expression cannot have leading 0's |
| | | NOTE: The order of evaluation expects script operator precedence |
| | | | | | | |
| | | Examples: | | | | Examples: |
| | | Input: digits = "123", target = 6 | | | | 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. 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 Information about all Companies
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
TODO TODO

@ -33,6 +33,11 @@ from faction to faction.
List of Factions and their Requirements List of Factions and their Requirements
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
.. raw:: html
<details><summary><a>Early Game Factions</a></summary>
.. _gameplay_factions::
+---------------------+----------------+-----------------------------------------+-------------------------------+ +---------------------+----------------+-----------------------------------------+-------------------------------+
| Early Game | Faction Name | Requirements | Joining this Faction prevents | | Early Game | Faction Name | Requirements | Joining this Faction prevents |
@ -49,7 +54,18 @@ List of Factions and their Requirements
| | | * Total Hacknet RAM of 8 | | | | | * Total Hacknet RAM of 8 | |
| | | * Total Hacknet Cores of 4 | | | | | * Total Hacknet Cores of 4 | |
+---------------------+----------------+-----------------------------------------+-------------------------------+ +---------------------+----------------+-----------------------------------------+-------------------------------+
| City Factions | Sector-12 | * Be in Sector-12 | * Chongqing | .. raw:: html
</details>
<details><summary><a>City Factions</a></summary>
.. _gameplay_factions::
+---------------------+----------------+-----------------------------------------+-------------------------------+
| City Factions | Faction Name | Requirements | Joining this Faction prevents |
| | | | you from joining: |
+ +----------------+-----------------------------------------+-------------------------------+
| | Sector-12 | * Be in Sector-12 | * Chongqing |
| | | * $15m | * New Tokyo | | | | * $15m | * New Tokyo |
| | | | * Ishima | | | | | * Ishima |
| | | | * Volhaven | | | | | * Volhaven |
@ -77,8 +93,19 @@ List of Factions and their Requirements
| | | | * New Tokyo | | | | | * New Tokyo |
| | | | * Ishima | | | | | * Ishima |
+---------------------+----------------+-----------------------------------------+-------------------------------+ +---------------------+----------------+-----------------------------------------+-------------------------------+
| Hacking | NiteSec | * Install a backdoor on the avmnite-02h | | .. raw:: html
| Groups | | server | |
</details>
<details><summary><a>Hacking Groups</a></summary>
.. _gameplay_factions::
+---------------------+----------------+-----------------------------------------+-------------------------------+
| Hacking | Faction Name | Requirements | Joining this Faction prevents |
| Groups | | | you from joining: |
+ +----------------+-----------------------------------------+-------------------------------+
| | NiteSec | * Install a backdoor on the avmnite-02h | |
| | | server | |
| | | | | | | | | |
+ +----------------+-----------------------------------------+-------------------------------+ + +----------------+-----------------------------------------+-------------------------------+
| | The Black Hand | * Install a backdoor on the I.I.I.I | | | | The Black Hand | * Install a backdoor on the I.I.I.I | |
@ -89,7 +116,18 @@ List of Factions and their Requirements
| | | server | | | | | server | |
| | | | | | | | | |
+---------------------+----------------+-----------------------------------------+-------------------------------+ +---------------------+----------------+-----------------------------------------+-------------------------------+
| Megacorporations | ECorp | * Have 200k reputation with | | .. raw:: html
</details>
<details><summary><a>Megacorporations</a></summary>
.. _gameplay_factions::
+---------------------+----------------+-----------------------------------------+-------------------------------+
| Megacorporations | Faction Name | Requirements | Joining this Faction prevents |
| | | | you from joining: |
+ +----------------+-----------------------------------------+-------------------------------+
| | ECorp | * Have 200k reputation with | |
| | | the Corporation | | | | | the Corporation | |
+ +----------------+-----------------------------------------+-------------------------------+ + +----------------+-----------------------------------------+-------------------------------+
| | MegaCorp | * Have 200k reputation with | | | | MegaCorp | * Have 200k reputation with | |
@ -121,8 +159,19 @@ List of Factions and their Requirements
| | | * Install a backdoor on the | | | | | * Install a backdoor on the | |
| | | fulcrumassets server | | | | | fulcrumassets server | |
+---------------------+----------------+-----------------------------------------+-------------------------------+ +---------------------+----------------+-----------------------------------------+-------------------------------+
| Criminal | Slum Snakes | * All Combat Stats of 30 | | .. raw:: html
| Organizations | | * -9 Karma | |
</details>
<details><summary><a>Criminal Organizations</a></summary>
.. _gameplay_factions::
+---------------------+----------------+-----------------------------------------+-------------------------------+
| Criminal | Faction Name | Requirements | Joining this Faction prevents |
| Organizations | | | you from joining: |
+ +----------------+-----------------------------------------+-------------------------------+
| | Slum Snakes | * All Combat Stats of 30 | |
| | | * -9 Karma | |
| | | * $1m | | | | | * $1m | |
+ +----------------+-----------------------------------------+-------------------------------+ + +----------------+-----------------------------------------+-------------------------------+
| | Tetrads | * Be in Chongqing, New Tokyo, or Ishima | | | | Tetrads | * Be in Chongqing, New Tokyo, or Ishima | |
@ -153,8 +202,19 @@ List of Factions and their Requirements
| | | * -90 Karma | | | | | * -90 Karma | |
| | | * Not working for CIA or NSA | | | | | * Not working for CIA or NSA | |
+---------------------+----------------+-----------------------------------------+-------------------------------+ +---------------------+----------------+-----------------------------------------+-------------------------------+
| Endgame | The Covenant | * 20 Augmentations | | .. raw:: html
| Factions | | * $75b | |
</details>
<details><summary><a>Endgame Factions</a></summary>
.. _gameplay_factions::
+---------------------+----------------+-----------------------------------------+-------------------------------+
| Endgame | Faction Name | Requirements | Joining this Faction prevents |
| Factions | | | you from joining: |
+ +----------------+-----------------------------------------+-------------------------------+
| | The Covenant | * 20 Augmentations | |
| | | * $75b | |
| | | * Hacking Level of 850 | | | | | * Hacking Level of 850 | |
| | | * All Combat Stats of 850 | | | | | * All Combat Stats of 850 | |
+ +----------------+-----------------------------------------+-------------------------------+ + +----------------+-----------------------------------------+-------------------------------+
@ -168,3 +228,6 @@ List of Factions and their Requirements
| | | * Hacking Level of 1500 | | | | | * Hacking Level of 1500 | |
| | | * All Combat Stats of 1200 | | | | | * All Combat Stats of 1200 | |
+---------------------+----------------+-----------------------------------------+-------------------------------+ +---------------------+----------------+-----------------------------------------+-------------------------------+
.. raw:: html
</details><br>

@ -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 This means that a server's security level will not fall below this
value if you are trying to weaken() it. 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 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 v1.3.0 - 2022-01-04 Cleaning up
------------------------------- -------------------------------

@ -64,9 +64,9 @@ documentation_title = '{0} Documentation'.format(project)
# built documents. # built documents.
# #
# The short X.Y version. # The short X.Y version.
version = '1.3' version = '1.5'
# The full version, including alpha/beta/rc tags. # 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 # The language for content autogenerated by Sphinx. Refer to documentation
# for a list of supported languages. # 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 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 :Max Level: 3
This Source-File lets you access and use the Singularity Functions in other BitNodes. 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: Difficulty:
Depending on what Source-Files you have unlocked before attempting this BitNode, 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 // This is backward but the game fills in an array called `document.achievements` and we retrieve it from
// here. Hey if it works it works. // here. Hey if it works it works.
const steamAchievements = greenworks.getAchievementNames(); const steamAchievements = greenworks.getAchievementNames();
log.debug(`All Steam achievements ${JSON.stringify(steamAchievements)}`); log.silly(`All Steam achievements ${JSON.stringify(steamAchievements)}`);
const playerAchieved = (await Promise.all(steamAchievements.map(checkSteamAchievement))).filter(name => !!name); const playerAchieved = (await Promise.all(steamAchievements.map(checkSteamAchievement))).filter(name => !!name);
log.debug(`Player has Steam achievements ${JSON.stringify(playerAchieved)}`); log.debug(`Player has Steam achievements ${JSON.stringify(playerAchieved)}`);
const intervalID = setInterval(async () => { const intervalID = setInterval(async () => {

@ -12,11 +12,13 @@ async function initialize(win) {
window = win; window = win;
server = http.createServer(async function (req, res) { server = http.createServer(async function (req, res) {
let body = ""; let body = "";
res.setHeader('Content-Type', 'application/json');
req.on("data", (chunk) => { req.on("data", (chunk) => {
body += chunk.toString(); // convert Buffer to string body += chunk.toString(); // convert Buffer to string
}); });
req.on("end", () => {
req.on("end", async () => {
const providedToken = req.headers?.authorization?.replace('Bearer ', '') ?? ''; const providedToken = req.headers?.authorization?.replace('Bearer ', '') ?? '';
const isValid = providedToken === getAuthenticationToken(); const isValid = providedToken === getAuthenticationToken();
if (isValid) { if (isValid) {
@ -24,8 +26,11 @@ async function initialize(win) {
} else { } else {
log.log('Invalid authentication token'); log.log('Invalid authentication token');
res.writeHead(401); res.writeHead(401);
res.write('Invalid authentication token');
res.end(); res.end(JSON.stringify({
success: false,
msg: 'Invalid authentication token'
}));
return; return;
} }
@ -35,17 +40,56 @@ async function initialize(win) {
} catch (error) { } catch (error) {
log.warn(`Invalid body data`); log.warn(`Invalid body data`);
res.writeHead(400); res.writeHead(400);
res.write('Invalid body data'); res.end(JSON.stringify({
res.end(); success: false,
msg: 'Invalid body data'
}));
return; return;
} }
if (data) { let result;
window.webContents.executeJavaScript(`document.saveFile("${data.filename}", "${data.code}")`).then((result) => { switch(req.method) {
res.write(result); // Request files
res.end(); case "GET":
}); result = await window.webContents.executeJavaScript(`document.getFiles()`);
break;
// Create or update files
// Support POST for VScode implementation
case "POST":
case "PUT":
if (!data) {
log.warn(`Invalid script update request - No data`);
res.writeHead(400);
res.end(JSON.stringify({
success: false,
msg: 'Invalid script update request - No data'
}));
return;
}
result = await window.webContents.executeJavaScript(`document.saveFile("${data.filename}", "${data.code}")`);
break;
// Delete files
case "DELETE":
result = await window.webContents.executeJavaScript(`document.deleteFile("${data.filename}")`);
break;
} }
if (!result.res) {
//We've encountered an error
res.writeHead(400);
log.warn(`Api Server Error`, result.msg);
}
res.end(JSON.stringify({
success: result.res,
msg: result.msg,
data: result.data
}));
}); });
}); });

@ -8,20 +8,42 @@ const api = require("./api-server");
const cp = require("child_process"); const cp = require("child_process");
const path = require("path"); const path = require("path");
const fs = require("fs"); const fs = require("fs");
const { windowTracker } = require("./windowTracker");
const { fileURLToPath } = require("url"); const { fileURLToPath } = require("url");
const debug = process.argv.includes("--debug"); const debug = process.argv.includes("--debug");
async function createWindow(killall) { async function createWindow(killall) {
const setStopProcessHandler = global.app_handlers.stopProcess; const setStopProcessHandler = global.app_handlers.stopProcess;
let icon;
if (process.platform == 'linux') {
icon = path.join(__dirname, 'icon.png');
}
const tracker = windowTracker('main');
const window = new BrowserWindow({ const window = new BrowserWindow({
icon,
show: false, show: false,
backgroundThrottling: false, backgroundThrottling: false,
backgroundColor: "#000000", backgroundColor: "#000000",
title: 'Bitburner',
x: tracker.state.x,
y: tracker.state.y,
width: tracker.state.width,
height: tracker.state.height,
minWidth: 600,
minHeight: 400,
webPreferences: {
nativeWindowOpen: true,
preload: path.join(__dirname, 'preload.js'),
},
}); });
setTimeout(() => tracker.track(window), 1000);
if (tracker.state.isMaximized) window.maximize();
window.removeMenu(); window.removeMenu();
window.maximize();
noScripts = killall ? { query: { noScripts: killall } } : {}; noScripts = killall ? { query: { noScripts: killall } } : {};
window.loadFile("index.html", noScripts); window.loadFile("index.html", noScripts);
window.show(); window.show();

@ -1,12 +1,19 @@
/* eslint-disable no-process-exit */ /* eslint-disable no-process-exit */
/* eslint-disable @typescript-eslint/no-var-requires */ /* eslint-disable @typescript-eslint/no-var-requires */
const { app, dialog, BrowserWindow } = require("electron"); const { app, dialog, BrowserWindow, ipcMain } = require("electron");
const log = require("electron-log"); const log = require("electron-log");
const greenworks = require("./greenworks"); const greenworks = require("./greenworks");
const api = require("./api-server"); const api = require("./api-server");
const gameWindow = require("./gameWindow"); const gameWindow = require("./gameWindow");
const achievements = require("./achievements"); const achievements = require("./achievements");
const utils = require("./utils"); const utils = require("./utils");
const storage = require("./storage");
const debounce = require("lodash/debounce");
const Config = require("electron-config");
const config = new Config();
log.transports.file.level = config.get("file-log-level", "info");
log.transports.console.level = config.get("console-log-level", "debug");
log.catchErrors(); log.catchErrors();
log.info(`Started app: ${JSON.stringify(process.argv)}`); log.info(`Started app: ${JSON.stringify(process.argv)}`);
@ -30,6 +37,8 @@ try {
global.greenworksError = ex.message; global.greenworksError = ex.message;
} }
let isRestoreDisabled = false;
function setStopProcessHandler(app, window, enabled) { function setStopProcessHandler(app, window, enabled) {
const closingWindowHandler = async (e) => { const closingWindowHandler = async (e) => {
// We need to prevent the default closing event to add custom logic // We need to prevent the default closing event to add custom logic
@ -41,6 +50,18 @@ function setStopProcessHandler(app, window, enabled) {
// Shutdown the http server // Shutdown the http server
api.disable(); api.disable();
// Trigger debounced saves right now before closing
try {
await saveToDisk.flush();
} catch (error) {
log.error(error);
}
try {
await saveToCloud.flush();
} catch (error) {
log.error(error);
}
// Because of a steam limitation, if the player has launched an external browser, // Because of a steam limitation, if the player has launched an external browser,
// steam will keep displaying the game as "Running" in their UI as long as the browser is up. // steam will keep displaying the game as "Running" in their UI as long as the browser is up.
// So we'll alert the player to close their browser. // So we'll alert the player to close their browser.
@ -87,21 +108,106 @@ function setStopProcessHandler(app, window, enabled) {
process.exit(0); process.exit(0);
}; };
const receivedGameReadyHandler = async (event, arg) => {
if (!window) {
log.warn("Window was undefined in game info handler");
return;
}
log.debug("Received game information", arg);
window.gameInfo = { ...arg };
await storage.prepareSaveFolders(window);
const restoreNewest = config.get("onload-restore-newest", true);
if (restoreNewest && !isRestoreDisabled) {
try {
await storage.restoreIfNewerExists(window)
} catch (error) {
log.error("Could not restore newer file", error);
}
}
}
const receivedDisableRestoreHandler = async (event, arg) => {
if (!window) {
log.warn("Window was undefined in disable import handler");
return;
}
log.debug(`Disabling auto-restore for ${arg.duration}ms.`);
isRestoreDisabled = true;
setTimeout(() => {
isRestoreDisabled = false;
log.debug("Re-enabling auto-restore");
}, arg.duration);
}
const receivedGameSavedHandler = async (event, arg) => {
if (!window) {
log.warn("Window was undefined in game saved handler");
return;
}
const { save, ...other } = arg;
log.silly("Received game saved info", {...other, save: `${save.length} bytes`});
if (storage.isAutosaveEnabled()) {
saveToDisk(save, arg.fileName);
}
if (storage.isCloudEnabled()) {
const minimumPlaytime = 1000 * 60 * 15;
const playtime = window.gameInfo.player.playtime;
log.silly(window.gameInfo);
if (playtime > minimumPlaytime) {
saveToCloud(save);
} else {
log.debug(`Auto-save to cloud disabled for save game under ${minimumPlaytime}ms (${playtime}ms)`);
}
}
}
const saveToCloud = debounce(async (save) => {
log.debug("Saving to Steam Cloud ...")
try {
const playerId = window.gameInfo.player.identifier;
await storage.pushGameSaveToSteamCloud(save, playerId);
log.silly("Saved Game to Steam Cloud");
} catch (error) {
log.error(error);
utils.writeToast(window, "Could not save to Steam Cloud.", "error", 5000);
}
}, config.get("cloud-save-min-time", 1000 * 60 * 15), { leading: true });
const saveToDisk = debounce(async (save, fileName) => {
log.debug("Saving to Disk ...")
try {
const file = await storage.saveGameToDisk(window, { save, fileName });
log.silly(`Saved Game to '${file.replaceAll('\\', '\\\\')}'`);
} catch (error) {
log.error(error);
utils.writeToast(window, "Could not save to disk", "error", 5000);
}
}, config.get("disk-save-min-time", 1000 * 60 * 5), { leading: true });
if (enabled) { if (enabled) {
log.debug('Adding closing handlers'); log.debug("Adding closing handlers");
ipcMain.on("push-game-ready", receivedGameReadyHandler);
ipcMain.on("push-game-saved", receivedGameSavedHandler);
ipcMain.on("push-disable-restore", receivedDisableRestoreHandler)
window.on("closed", clearWindowHandler); window.on("closed", clearWindowHandler);
window.on("close", closingWindowHandler) window.on("close", closingWindowHandler)
app.on("window-all-closed", stopProcessHandler); app.on("window-all-closed", stopProcessHandler);
} else { } else {
log.debug('Removing closing handlers'); log.debug("Removing closing handlers");
ipcMain.removeAllListeners();
window.removeListener("closed", clearWindowHandler); window.removeListener("closed", clearWindowHandler);
window.removeListener("close", closingWindowHandler); window.removeListener("close", closingWindowHandler);
app.removeListener("window-all-closed", stopProcessHandler); app.removeListener("window-all-closed", stopProcessHandler);
} }
} }
function startWindow(noScript) { async function startWindow(noScript) {
gameWindow.createWindow(noScript); return gameWindow.createWindow(noScript);
} }
global.app_handlers = { global.app_handlers = {
@ -110,7 +216,7 @@ global.app_handlers = {
} }
app.whenReady().then(async () => { app.whenReady().then(async () => {
log.info('Application is ready!'); log.info("Application is ready!");
if (process.argv.includes("--export-save")) { if (process.argv.includes("--export-save")) {
const window = new BrowserWindow({ show: false }); const window = new BrowserWindow({ show: false });
@ -119,15 +225,14 @@ app.whenReady().then(async () => {
setStopProcessHandler(app, window, true); setStopProcessHandler(app, window, true);
await utils.exportSave(window); await utils.exportSave(window);
} else { } else {
startWindow(process.argv.includes("--no-scripts")); const window = await startWindow(process.argv.includes("--no-scripts"));
} if (global.greenworksError) {
await dialog.showMessageBox(window, {
if (global.greenworksError) { title: "Bitburner",
dialog.showMessageBox({ message: "Could not connect to Steam",
title: 'Bitburner', detail: `${global.greenworksError}\n\nYou won't be able to receive achievements until this is resolved and you restart the game.`,
message: 'Could not connect to Steam', type: 'warning', buttons: ['OK']
detail: `${global.greenworksError}\n\nYou won't be able to receive achievements until this is resolved and you restart the game.`, });
type: 'warning', buttons: ['OK'] }
});
} }
}); });

@ -1,11 +1,169 @@
/* eslint-disable @typescript-eslint/no-var-requires */ /* eslint-disable @typescript-eslint/no-var-requires */
const { Menu, clipboard, dialog } = require("electron"); const { app, Menu, clipboard, dialog, shell } = require("electron");
const log = require("electron-log"); const log = require("electron-log");
const Config = require("electron-config");
const api = require("./api-server"); const api = require("./api-server");
const utils = require("./utils"); const utils = require("./utils");
const storage = require("./storage");
const config = new Config();
function getMenu(window) { function getMenu(window) {
return Menu.buildFromTemplate([ return Menu.buildFromTemplate([
{
label: "File",
submenu: [
{
label: "Save Game",
click: () => window.webContents.send("trigger-save"),
},
{
label: "Export Save",
click: () => window.webContents.send("trigger-game-export"),
},
{
label: "Export Scripts",
click: async () => window.webContents.send("trigger-scripts-export"),
},
{
type: "separator",
},
{
label: "Load Last Save",
click: async () => {
try {
const saveGame = await storage.loadLastFromDisk(window);
window.webContents.send("push-save-request", { save: saveGame });
} catch (error) {
log.error(error);
utils.writeToast(window, "Could not load last save from disk", "error", 5000);
}
}
},
{
label: "Load From File",
click: async () => {
const defaultPath = await storage.getSaveFolder(window);
const result = await dialog.showOpenDialog(window, {
title: "Load From File",
defaultPath: defaultPath,
buttonLabel: "Load",
filters: [
{ name: "Game Saves", extensions: ["json", "json.gz", "txt"] },
{ name: "All", extensions: ["*"] },
],
properties: [
"openFile", "dontAddToRecent",
]
});
if (result.canceled) return;
const file = result.filePaths[0];
try {
const saveGame = await storage.loadFileFromDisk(file);
window.webContents.send("push-save-request", { save: saveGame });
} catch (error) {
log.error(error);
utils.writeToast(window, "Could not load save from disk", "error", 5000);
}
}
},
{
label: "Load From Steam Cloud",
enabled: storage.isCloudEnabled(),
click: async () => {
try {
const saveGame = await storage.getSteamCloudSaveString();
await storage.pushSaveGameForImport(window, saveGame, false);
} catch (error) {
log.error(error);
utils.writeToast(window, "Could not load from Steam Cloud", "error", 5000);
}
}
},
{
type: "separator",
},
{
label: "Compress Disk Saves (.gz)",
type: "checkbox",
checked: storage.isSaveCompressionEnabled(),
click: (menuItem) => {
storage.setSaveCompressionConfig(menuItem.checked);
utils.writeToast(window,
`${menuItem.checked ? "Enabled" : "Disabled"} Save Compression`, "info", 5000);
refreshMenu(window);
},
},
{
label: "Auto-Save to Disk",
type: "checkbox",
checked: storage.isAutosaveEnabled(),
click: (menuItem) => {
storage.setAutosaveConfig(menuItem.checked);
utils.writeToast(window,
`${menuItem.checked ? "Enabled" : "Disabled"} Auto-Save to Disk`, "info", 5000);
refreshMenu(window);
},
},
{
label: "Auto-Save to Steam Cloud",
type: "checkbox",
enabled: !global.greenworksError,
checked: storage.isCloudEnabled(),
click: (menuItem) => {
storage.setCloudEnabledConfig(menuItem.checked);
utils.writeToast(window,
`${menuItem.checked ? "Enabled" : "Disabled"} Auto-Save to Steam Cloud`, "info", 5000);
refreshMenu(window);
},
},
{
label: "Restore Newest on Load",
type: "checkbox",
checked: config.get("onload-restore-newest", true),
click: (menuItem) => {
config.set("onload-restore-newest", menuItem.checked);
utils.writeToast(window,
`${menuItem.checked ? "Enabled" : "Disabled"} Restore Newest on Load`, "info", 5000);
refreshMenu(window);
},
},
{
type: "separator",
},
{
label: "Open Directory",
submenu: [
{
label: "Open Game Directory",
click: () => shell.openPath(app.getAppPath()),
},
{
label: "Open Saves Directory",
click: async () => {
const path = await storage.getSaveFolder(window);
shell.openPath(path);
},
},
{
label: "Open Logs Directory",
click: () => shell.openPath(app.getPath("logs")),
},
{
label: "Open Data Directory",
click: () => shell.openPath(app.getPath("userData")),
},
]
},
{
type: "separator",
},
{
label: "Quit",
click: () => app.quit(),
},
]
},
{ {
label: "Edit", label: "Edit",
submenu: [ submenu: [
@ -163,6 +321,17 @@ function getMenu(window) {
label: "Activate", label: "Activate",
click: () => window.webContents.openDevTools(), click: () => window.webContents.openDevTools(),
}, },
{
label: "Delete Steam Cloud Data",
enabled: !global.greenworksError,
click: async () => {
try {
await storage.deleteCloudFile();
} catch (error) {
log.error(error);
}
}
}
], ],
}, },
]); ]);

@ -9,7 +9,8 @@
"version": "1.0.0", "version": "1.0.0",
"dependencies": { "dependencies": {
"electron-config": "^2.0.0", "electron-config": "^2.0.0",
"electron-log": "^4.4.4" "electron-log": "^4.4.4",
"lodash": "^4.17.21"
} }
}, },
"node_modules/conf": { "node_modules/conf": {
@ -104,6 +105,11 @@
"node": ">=4" "node": ">=4"
} }
}, },
"node_modules/lodash": {
"version": "4.17.21",
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz",
"integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg=="
},
"node_modules/make-dir": { "node_modules/make-dir": {
"version": "1.3.0", "version": "1.3.0",
"resolved": "https://registry.npmjs.org/make-dir/-/make-dir-1.3.0.tgz", "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-1.3.0.tgz",
@ -259,6 +265,11 @@
"path-exists": "^3.0.0" "path-exists": "^3.0.0"
} }
}, },
"lodash": {
"version": "4.17.21",
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz",
"integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg=="
},
"make-dir": { "make-dir": {
"version": "1.3.0", "version": "1.3.0",
"resolved": "https://registry.npmjs.org/make-dir/-/make-dir-1.3.0.tgz", "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-1.3.0.tgz",

@ -16,6 +16,8 @@
"./dist/**/*", "./dist/**/*",
"./node_modules/**/*", "./node_modules/**/*",
"./public/**/*", "./public/**/*",
"./src/**",
"./lib/**,",
"*.js" "*.js"
], ],
"directories": { "directories": {
@ -23,6 +25,7 @@
}, },
"dependencies": { "dependencies": {
"electron-config": "^2.0.0", "electron-config": "^2.0.0",
"electron-log": "^4.4.4" "electron-log": "^4.4.4",
"lodash": "^4.17.21"
} }
} }

38
electron/preload.js Normal file

@ -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 };

File diff suppressed because it is too large Load Diff

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

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

@ -0,0 +1,24 @@
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
[Home](./index.md) &gt; [bitburner](./bitburner.md) &gt; [Corporation](./bitburner.corporation.md) &gt; [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 | | [acceptInvestmentOffer()](./bitburner.corporation.acceptinvestmentoffer.md) | Accept investment based on you companies current valuation |
| [bribe(factionName, amountCash, amountShares)](./bitburner.corporation.bribe.md) | Bribe a faction | | [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 | | [createCorporation(corporationName, selfFund)](./bitburner.corporation.createcorporation.md) | Create a Corporation |
| [expandCity(divisionName, cityName)](./bitburner.corporation.expandcity.md) | Expand to a new city | | [expandCity(divisionName, cityName)](./bitburner.corporation.expandcity.md) | Expand to a new city |
| [expandIndustry(industryType, divisionName)](./bitburner.corporation.expandindustry.md) | Expand to a new industry | | [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 | | [hasUnlockUpgrade(upgradeName)](./bitburner.corporation.hasunlockupgrade.md) | Check if you have a one time unlockable upgrade |
| [issueDividends(percent)](./bitburner.corporation.issuedividends.md) | Issue dividends | | [issueDividends(percent)](./bitburner.corporation.issuedividends.md) | Issue dividends |
| [levelUpgrade(upgradeName)](./bitburner.corporation.levelupgrade.md) | Level an upgrade. | | [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 | | [unlockUpgrade(upgradeName)](./bitburner.corporation.unlockupgrade.md) | Unlock an upgrade |

@ -0,0 +1,24 @@
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
[Home](./index.md) &gt; [bitburner](./bitburner.md) &gt; [Corporation](./bitburner.corporation.md) &gt; [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

@ -0,0 +1,27 @@
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
[Home](./index.md) &gt; [bitburner](./bitburner.md) &gt; [WarehouseAPI](./bitburner.warehouseapi.md) &gt; [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

File diff suppressed because it is too large Load Diff

@ -1,7 +1,7 @@
{ {
"name": "bitburner", "name": "bitburner",
"license": "SEE LICENSE IN license.txt", "license": "SEE LICENSE IN license.txt",
"version": "1.3.0", "version": "1.5.0",
"main": "electron-main.js", "main": "electron-main.js",
"author": { "author": {
"name": "Daniel Xie & Olivier Gagnon" "name": "Daniel Xie & Olivier Gagnon"
@ -13,19 +13,10 @@
"@emotion/react": "^11.4.1", "@emotion/react": "^11.4.1",
"@emotion/styled": "^11.3.0", "@emotion/styled": "^11.3.0",
"@material-ui/core": "^4.12.3", "@material-ui/core": "^4.12.3",
"@microsoft/api-documenter": "^7.13.65",
"@microsoft/api-extractor": "^7.18.17",
"@monaco-editor/react": "^4.2.2", "@monaco-editor/react": "^4.2.2",
"@mui/icons-material": "^5.0.3", "@mui/icons-material": "^5.0.3",
"@mui/material": "^5.0.3", "@mui/material": "^5.0.3",
"@mui/styles": "^5.0.1", "@mui/styles": "^5.0.1",
"@types/acorn": "^4.0.6",
"@types/escodegen": "^0.0.7",
"@types/numeral": "0.0.25",
"@types/react": "^17.0.21",
"@types/react-beautiful-dnd": "^13.1.2",
"@types/react-dom": "^17.0.9",
"@types/react-resizable": "^1.7.3",
"acorn": "^8.4.1", "acorn": "^8.4.1",
"acorn-walk": "^8.1.1", "acorn-walk": "^8.1.1",
"arg": "^5.0.0", "arg": "^5.0.0",
@ -45,7 +36,6 @@
"notistack": "^2.0.2", "notistack": "^2.0.2",
"numeral": "2.0.6", "numeral": "2.0.6",
"prop-types": "^15.8.0", "prop-types": "^15.8.0",
"raw-loader": "^4.0.2",
"react": "^17.0.2", "react": "^17.0.2",
"react-beautiful-dnd": "^13.1.0", "react-beautiful-dnd": "^13.1.0",
"react-dom": "^17.0.2", "react-dom": "^17.0.2",
@ -59,10 +49,19 @@
"@babel/preset-env": "^7.15.0", "@babel/preset-env": "^7.15.0",
"@babel/preset-react": "^7.0.0", "@babel/preset-react": "^7.0.0",
"@babel/preset-typescript": "^7.15.0", "@babel/preset-typescript": "^7.15.0",
"@microsoft/api-documenter": "^7.13.65",
"@microsoft/api-extractor": "^7.18.17",
"@pmmmwh/react-refresh-webpack-plugin": "^0.5.1", "@pmmmwh/react-refresh-webpack-plugin": "^0.5.1",
"@testing-library/cypress": "^8.0.1", "@testing-library/cypress": "^8.0.1",
"@types/acorn": "^4.0.6",
"@types/escodegen": "^0.0.7",
"@types/file-saver": "^2.0.3", "@types/file-saver": "^2.0.3",
"@types/lodash": "^4.14.168", "@types/lodash": "^4.14.168",
"@types/numeral": "0.0.25",
"@types/react": "^17.0.21",
"@types/react-beautiful-dnd": "^13.1.2",
"@types/react-dom": "^17.0.9",
"@types/react-resizable": "^1.7.3",
"@typescript-eslint/eslint-plugin": "^4.22.0", "@typescript-eslint/eslint-plugin": "^4.22.0",
"@typescript-eslint/parser": "^4.22.0", "@typescript-eslint/parser": "^4.22.0",
"babel-jest": "^27.0.6", "babel-jest": "^27.0.6",
@ -71,6 +70,7 @@
"electron": "^14.0.2", "electron": "^14.0.2",
"electron-packager": "^15.4.0", "electron-packager": "^15.4.0",
"eslint": "^7.24.0", "eslint": "^7.24.0",
"file-loader": "^6.2.0",
"fork-ts-checker-webpack-plugin": "^6.3.3", "fork-ts-checker-webpack-plugin": "^6.3.3",
"html-webpack-plugin": "^3.2.0", "html-webpack-plugin": "^3.2.0",
"http-server": "^13.0.1", "http-server": "^13.0.1",
@ -79,6 +79,7 @@
"lodash": "^4.17.21", "lodash": "^4.17.21",
"mini-css-extract-plugin": "^0.4.1", "mini-css-extract-plugin": "^0.4.1",
"prettier": "^2.3.2", "prettier": "^2.3.2",
"raw-loader": "^4.0.2",
"react-refresh": "^0.10.0", "react-refresh": "^0.10.0",
"regenerator-runtime": "^0.13.9", "regenerator-runtime": "^0.13.9",
"source-map": "^0.7.3", "source-map": "^0.7.3",
@ -102,7 +103,7 @@
"cy:dev": "start-server-and-test start:dev http://localhost:8000 cy:open", "cy:dev": "start-server-and-test start:dev http://localhost:8000 cy:open",
"cy:open": "cypress open", "cy:open": "cypress open",
"cy:run": "cypress run", "cy:run": "cypress run",
"doc": "npx api-extractor run && npx api-documenter markdown", "doc": "npx api-extractor run && npx api-documenter markdown && rm input/bitburner.api.json && rm -r input",
"format": "prettier --write .", "format": "prettier --write .",
"start": "http-server -p 8000", "start": "http-server -p 8000",
"start:dev": "webpack-dev-server --progress --env.devServer --mode development", "start:dev": "webpack-dev-server --progress --env.devServer --mode development",
@ -112,13 +113,18 @@
"build:dev": "webpack --mode development", "build:dev": "webpack --mode development",
"lint": "eslint --fix . --ext js,jsx,ts,tsx", "lint": "eslint --fix . --ext js,jsx,ts,tsx",
"lint:report": "eslint --ext js,jsx,ts,tsx .", "lint:report": "eslint --ext js,jsx,ts,tsx .",
"preinstall": "node ./scripts/engines-check.js", "preinstall": "node ./tools/engines-check/engines-check.js",
"postinstall": "cd electron && npm install",
"test": "jest", "test": "jest",
"test:watch": "jest --watch", "test:watch": "jest --watch",
"watch": "webpack --watch --mode production", "watch": "webpack --watch --mode production",
"watch:dev": "webpack --watch --mode development", "watch:dev": "webpack --watch --mode development",
"electron": "sh ./package.sh", "electron": "sh ./package.sh",
"electron:packager": "electron-packager .package bitburner --all --out .build --overwrite --icon .package/icon.png --no-prune", "electron:packager": "electron-packager .package bitburner --all --out .build --overwrite --icon .package/icon.png --no-prune",
"electron:packager-all": "electron-packager .package bitburner --all --out .build --overwrite --icon .package/icon.png",
"electron:packager-win": "electron-packager .package bitburner --platform win32 --arch x64 --out .build --overwrite --icon .package/icon.png",
"electron:packager-mac": "electron-packager .package bitburner --platform darwin --arch x64 --out .build --overwrite --icon .package/icon.png",
"electron:packager-linux": "electron-packager .package bitburner --platform linux --arch x64 --out .build --overwrite --icon .package/icon.png",
"allbuild": "npm run build && npm run electron && git add --all && git commit --amend --no-edit && git push -f -u origin dev" "allbuild": "npm run build && npm run electron && git add --all && git commit --amend --no-edit && git push -f -u origin dev"
} }
} }

@ -1,5 +1,8 @@
#!/bin/sh #!/bin/sh
# Clear out any files remaining from old builds
rm -rf .package
mkdir -p .package/dist/src/ThirdParty || true mkdir -p .package/dist/src/ThirdParty || true
mkdir -p .package/src/ThirdParty || true mkdir -p .package/src/ThirdParty || true
mkdir -p .package/node_modules || true mkdir -p .package/node_modules || true
@ -8,6 +11,7 @@ cp index.html .package
cp -r electron/* .package cp -r electron/* .package
cp -r dist/ext .package/dist cp -r dist/ext .package/dist
cp -r dist/icons .package/dist cp -r dist/icons .package/dist
cp -r dist/images .package/dist
# The css files # The css files
cp dist/vendor.css .package/dist cp dist/vendor.css .package/dist
@ -26,5 +30,6 @@ cd electron
npm install npm install
cd .. cd ..
BUILD_PLATFORM="${1:-"all"}"
# And finally build the app. # And finally build the app.
npm run electron:packager npm run electron:packager-$BUILD_PLATFORM

@ -1,2 +1,8 @@
// Defined by webpack on startup or compilation // Defined by webpack on startup or compilation
declare let __COMMIT_HASH__: string; declare let __COMMIT_HASH__: string;
// When using file-loader, we'll get a path to the resource
declare module "*.png" {
const value: string;
export default value;
}

@ -2050,7 +2050,7 @@ function initAugmentations(): void {
info: info:
"A brain implant carefully assembled around the synapses, which " + "A brain implant carefully assembled around the synapses, which " +
"micromanages the activity and levels of various neuroreceptor " + "micromanages the activity and levels of various neuroreceptor " +
"chemicals and modulates electrical acvitiy to optimize concentration, " + "chemicals and modulates electrical activity to optimize concentration, " +
"allowing the user to multitask much more effectively.", "allowing the user to multitask much more effectively.",
stats: ( stats: (
<> <>

@ -110,7 +110,7 @@ export function AugmentationsRoot(props: IProps): React.ReactElement {
<br /> <br />
<br /> <br />
It is recommended to install several Augmentations at once. Preferably everything from any faction of your 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) { if (Settings.OwnedAugmentationsOrder === OwnedAugmentationsOrderSetting.Alphabetically) {
sourceAugs.sort((aug1, aug2) => { sourceAugs.sort((aug1, aug2) => {
return aug1.name <= aug2.name ? -1 : 1; return aug1.name.localeCompare(aug2.name);
}); });
} }

@ -670,7 +670,6 @@ export function initBitNodeMultipliers(p: IPlayer): void {
BitNodeMultipliers.InfiltrationMoney = 0.75; BitNodeMultipliers.InfiltrationMoney = 0.75;
BitNodeMultipliers.CorporationValuation = 0.2; BitNodeMultipliers.CorporationValuation = 0.2;
BitNodeMultipliers.HacknetNodeMoney = 0.2; BitNodeMultipliers.HacknetNodeMoney = 0.2;
BitNodeMultipliers.FactionPassiveRepGain = 0;
BitNodeMultipliers.HackExpGain = 0.25; BitNodeMultipliers.HackExpGain = 0.25;
BitNodeMultipliers.DaedalusAugsRequirement = 1.166; // Results in 35 Augs needed BitNodeMultipliers.DaedalusAugsRequirement = 1.166; // Results in 35 Augs needed
BitNodeMultipliers.PurchasedServerSoftcap = 2; BitNodeMultipliers.PurchasedServerSoftcap = 2;
@ -694,7 +693,6 @@ export function initBitNodeMultipliers(p: IPlayer): void {
BitNodeMultipliers.InfiltrationMoney = 0.75; BitNodeMultipliers.InfiltrationMoney = 0.75;
BitNodeMultipliers.CorporationValuation = 0.2; BitNodeMultipliers.CorporationValuation = 0.2;
BitNodeMultipliers.HacknetNodeMoney = 0.2; BitNodeMultipliers.HacknetNodeMoney = 0.2;
BitNodeMultipliers.FactionPassiveRepGain = 0;
BitNodeMultipliers.HackExpGain = 0.25; BitNodeMultipliers.HackExpGain = 0.25;
BitNodeMultipliers.FourSigmaMarketDataCost = 2; BitNodeMultipliers.FourSigmaMarketDataCost = 2;
BitNodeMultipliers.FourSigmaMarketDataApiCost = 2; BitNodeMultipliers.FourSigmaMarketDataApiCost = 2;

@ -8,38 +8,37 @@ import { CinematicText } from "../../ui/React/CinematicText";
import { use } from "../../ui/Context"; import { use } from "../../ui/Context";
import makeStyles from "@mui/styles/makeStyles"; import makeStyles from "@mui/styles/makeStyles";
import createStyles from "@mui/styles/createStyles"; import createStyles from "@mui/styles/createStyles";
import IconButton from "@mui/material/IconButton";
import Typography from "@mui/material/Typography"; import Typography from "@mui/material/Typography";
import Tooltip from "@mui/material/Tooltip"; import Tooltip from "@mui/material/Tooltip";
import { Settings } from "../../Settings/Settings";
import Button from "@mui/material/Button";
const useStyles = makeStyles(() => const useStyles = makeStyles(() =>
createStyles({ createStyles({
level0: { portal: {
color: "red",
cursor: "pointer", cursor: "pointer",
fontFamily: "inherit",
fontSize: "1rem",
fontWeight: "bold",
lineHeight: 1,
padding: 0,
"&:hover": { "&:hover": {
color: "#fff", color: "#fff",
}, },
}, },
level0: {
color: "red",
},
level1: { level1: {
color: "yellow", color: "yellow",
cursor: "pointer",
"&:hover": {
color: "#fff",
},
}, },
level2: { level2: {
color: "#48d1cc", color: "#48d1cc",
cursor: "pointer",
"&:hover": {
color: "#fff",
},
}, },
level3: { level3: {
color: "blue", color: "blue",
cursor: "pointer",
"&:hover": {
color: "#fff",
},
}, },
}), }),
); );
@ -71,6 +70,7 @@ function BitNodePortal(props: IPortalProps): React.ReactElement {
if (props.level === 2) { if (props.level === 2) {
cssClass = classes.level2; cssClass = classes.level2;
} }
cssClass = `${classes.portal} ${cssClass}`
return ( return (
<> <>
@ -85,9 +85,24 @@ function BitNodePortal(props: IPortalProps): React.ReactElement {
</Typography> </Typography>
} }
> >
<span onClick={() => setPortalOpen(true)} className={cssClass}> {Settings.DisableASCIIArt ? (
<b>O</b> <Button
</span> 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> </Tooltip>
<PortalModal <PortalModal
open={portalOpen} open={portalOpen}
@ -98,6 +113,10 @@ function BitNodePortal(props: IPortalProps): React.ReactElement {
destroyedBitNode={props.destroyedBitNode} destroyedBitNode={props.destroyedBitNode}
flume={props.flume} 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 // prettier-ignore
<> <>
<Typography sx={{lineHeight: '1em',whiteSpace: 'pre'}}> 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 | </Typography>
<Typography sx={{lineHeight: '1em',whiteSpace: 'pre'}}> 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'}}> O | O | | O / | O | | O | O </Typography>
<Typography sx={{lineHeight: '1em',whiteSpace: 'pre'}}> | | | | |_/ |/ | \_ \_| | | | | </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 | | | O | | O__/ | / \__ | | O | | | O </Typography>
<Typography sx={{lineHeight: '1em',whiteSpace: 'pre'}}> | | | | | | | / /| 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</Typography>
<Typography sx={{lineHeight: '1em',whiteSpace: 'pre'}}>| | | |O / | | 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'}}>| | |/ \/ / __| | |/ \ | \ | |__ \ \/ \| | |</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'}}> \| 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'}}> \| / \| | / / \ |/ </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={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'}}> <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'}}> | | | / / \ \ | | | </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'}}> \| | / <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'}}> \ | / / | | \ \ | / </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'}}> \ \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'}}> \| \_ | | | | | | _/ |/ </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'}}> <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'}}> | | | | | | | | </Typography>
<Typography sx={{lineHeight: '1em',whiteSpace: 'pre'}}> \JUMP3R|JUMP|3R| |R3|PMUJ|R3PMUJ/ </Typography> <Typography sx={{lineHeight: '1em',whiteSpace: 'pre'}}> \JUMP3R|JUMP|3R| |R3|PMUJ|R3PMUJ/ </Typography>
<br /> <br />
<br /> <br />
<br /> <br />
<br /> <br />
<CinematicText lines={[ <CinematicText lines={[
"> Many decades ago, a humanoid extraterrestrial species which we call the Enders descended on the Earth...violently", "> 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...", "> 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...", "> 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...", "> 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...", "> Using their advanced technology, the Enders created complex simulations of a virtual reality...",
"> Simulations designed to keep us content...ignorant of the truth.", "> Simulations designed to keep us content...ignorant of the truth.",
"> Simulations used to trap and suppress our consciousness, to keep us under control...", "> 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.", "> 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...", "> 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...", "> 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...", "> 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...", "> 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...", "> 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...", "> There are tens if not hundreds of BitNodes out there...",
"> Each with their own simulations of a reality...", "> Each with their own simulations of a reality...",
"> Each creating their own universes...a universe of universes", "> Each creating their own universes...a universe of universes",
"> And all of which must be destroyed...", "> And all of which must be destroyed...",
"> .......................................", "> .......................................",
"> Welcome to the Bitverse...", "> Welcome to the Bitverse...",
"> ", "> ",
"> (Enter a new BitNode using the image above)", "> (Enter a new BitNode using the image above)",
]} /> ]} />
</> </>
); );
}
return <></>; return <></>;
} }

@ -43,6 +43,8 @@ export function PortalModal(props: IProps): React.ReactElement {
<br /> <br />
<br /> <br />
<Button <Button
aria-label={`enter-bitnode-${bitNode.number.toString()}`}
autoFocus={true}
onClick={() => { onClick={() => {
props.enter(router, props.flume, props.destroyedBitNode, props.n); props.enter(router, props.flume, props.destroyedBitNode, props.n);
props.onClose(); props.onClose();

@ -2083,7 +2083,7 @@ export class Bladeburner implements IBladeburner {
this.startAction(player, actionId); this.startAction(player, actionId);
workerScript.log( workerScript.log(
"bladeburner.startAction", "bladeburner.startAction",
() => `Starting bladeburner action with type '${type}' and name ${name}"`, () => `Starting bladeburner action with type '${type}' and name '${name}'`,
); );
return true; return true;
} catch (e: any) { } catch (e: any) {

@ -14,6 +14,7 @@ import Typography from "@mui/material/Typography";
import TextField from "@mui/material/TextField"; import TextField from "@mui/material/TextField";
const MAX_BET = 100e6; const MAX_BET = 100e6;
export const DECK_COUNT = 5; // 5-deck multideck
enum Result { enum Result {
Pending = "", Pending = "",
@ -45,7 +46,7 @@ export class Blackjack extends Game<Props, State> {
constructor(props: Props) { constructor(props: Props) {
super(props); super(props);
this.deck = new Deck(5); // 5-deck multideck this.deck = new Deck(DECK_COUNT);
const initialBet = 1e6; const initialBet = 1e6;

@ -40,13 +40,13 @@ const strategies: {
} = { } = {
Red: { Red: {
match: (n: number): boolean => { match: (n: number): boolean => {
if (n === 0) return false;
return redNumbers.includes(n); return redNumbers.includes(n);
}, },
payout: 1, payout: 1,
}, },
Black: { Black: {
match: (n: number): boolean => { match: (n: number): boolean => {
if (n === 0) return false;
return !redNumbers.includes(n); return !redNumbers.includes(n);
}, },
payout: 1, payout: 1,
@ -118,12 +118,6 @@ export function Roulette(props: IProps): React.ReactElement {
const [status, setStatus] = useState<string | JSX.Element>("waiting"); const [status, setStatus] = useState<string | JSX.Element>("waiting");
const [n, setN] = useState(0); const [n, setN] = useState(0);
const [lock, setLock] = useState(true); const [lock, setLock] = useState(true);
const [strategy, setStrategy] = useState<Strategy>({
payout: 0,
match: (): boolean => {
return false;
},
});
useEffect(() => { useEffect(() => {
const i = window.setInterval(step, 50); const i = window.setInterval(step, 50);
@ -156,13 +150,12 @@ export function Roulette(props: IProps): React.ReactElement {
return `${n}${color}`; return `${n}${color}`;
} }
function play(s: Strategy): void { function play(strategy: Strategy): void {
if (reachedLimit(props.p)) return; if (reachedLimit(props.p)) return;
setCanPlay(false); setCanPlay(false);
setLock(false); setLock(false);
setStatus("playing"); setStatus("playing");
setStrategy(s);
setTimeout(() => { setTimeout(() => {
let n = Math.floor(rng.random() * 37); let n = Math.floor(rng.random() * 37);

@ -159,18 +159,18 @@ export function SlotMachine(props: IProps): React.ReactElement {
const copy = index.slice(); const copy = index.slice();
for (let i = 0; i < copy.length; i++) { for (let i = 0; i < copy.length; i++) {
if (copy[i] === locks[i] && !stoppedOne) continue; if (copy[i] === locks[i] && !stoppedOne) continue;
copy[i] = (copy[i] + 1) % symbols.length; copy[i] = (copy[i] - 1 >= 0) ? copy[i] - 1 : symbols.length - 1;
stoppedOne = true; stoppedOne = true;
} }
setIndex(copy); setIndex(copy);
if (stoppedOne && copy.every((e, i) => e === locks[i])) { if (stoppedOne && copy.every((e, i) => e === locks[i])) {
checkWinnings(); checkWinnings(getTable(copy, symbols));
} }
} }
function getTable(): string[][] { function getTable(index:number[], symbols:string[]): string[][] {
return [ return [
[ [
symbols[(index[0] + symbols.length - 1) % symbols.length], symbols[(index[0] + symbols.length - 1) % symbols.length],
@ -209,8 +209,7 @@ export function SlotMachine(props: IProps): React.ReactElement {
]); ]);
} }
function checkWinnings(): void { function checkWinnings(t:string[][]): void {
const t = getTable();
const getPaylineData = function (payline: number[][]): string[] { const getPaylineData = function (payline: number[][]): string[] {
const data = []; const data = [];
for (const point of payline) { for (const point of payline) {
@ -267,7 +266,7 @@ export function SlotMachine(props: IProps): React.ReactElement {
setInvestment(investment); setInvestment(investment);
} }
const t = getTable(); const t = getTable(index, symbols);
// prettier-ignore // prettier-ignore
return ( return (
<> <>
@ -288,7 +287,7 @@ export function SlotMachine(props: IProps): React.ReactElement {
disabled={!canPlay} disabled={!canPlay}
>Spin!</Button>)}} >Spin!</Button>)}}
/> />
<Typography variant="h4">{status}</Typography> <Typography variant="h4">{status}</Typography>
<Typography>Pay lines</Typography> <Typography>Pay lines</Typography>

@ -273,22 +273,64 @@ export const CONSTANTS: {
TotalNumBitNodes: 24, TotalNumBitNodes: 24,
LatestUpdate: ` 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'. * Added (@MartinFournier)
This mechanic helps you farm reputation faster.
** 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 { ICorporation } from "./ICorporation";
import { IIndustry } from "./IIndustry"; import { IIndustry } from "./IIndustry";
import { IndustryStartingCosts, IndustryResearchTrees } from "./IndustryData"; import { IndustryStartingCosts, IndustryResearchTrees } from "./IndustryData";
@ -14,12 +16,12 @@ import { EmployeePositions } from "./EmployeePositions";
import { Employee } from "./Employee"; import { Employee } from "./Employee";
import { IndustryUpgrades } from "./IndustryUpgrades"; import { IndustryUpgrades } from "./IndustryUpgrades";
import { ResearchMap } from "./ResearchMap"; import { ResearchMap } from "./ResearchMap";
import { isRelevantMaterial } from "./ui/Helpers";
export function NewIndustry(corporation: ICorporation, industry: string, name: string): void { export function NewIndustry(corporation: ICorporation, industry: string, name: string): void {
for (let i = 0; i < corporation.divisions.length; ++i) { for (let i = 0; i < corporation.divisions.length; ++i) {
if (corporation.divisions[i].name === name) { if (corporation.divisions[i].name === name) {
throw new Error("This division name is already in use!"); 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; 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 { export function AssignJob(employee: Employee, job: string): void {
if (!Object.values(EmployeePositions).includes(job)) throw new Error(`'${job}' is not a valid job.`); if (!Object.values(EmployeePositions).includes(job)) throw new Error(`'${job}' is not a valid job.`);
employee.pos = 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 { export function UpgradeWarehouse(corp: ICorporation, division: IIndustry, warehouse: Warehouse): void {
const sizeUpgradeCost = CorporationConstants.WarehouseUpgradeBaseCost * Math.pow(1.07, warehouse.level + 1); const sizeUpgradeCost = CorporationConstants.WarehouseUpgradeBaseCost * Math.pow(1.07, warehouse.level + 1);
if (corp.funds < sizeUpgradeCost) return;
++warehouse.level; ++warehouse.level;
warehouse.updateSize(corp, division); warehouse.updateSize(corp, division);
corp.funds = corp.funds - sizeUpgradeCost; corp.funds = corp.funds - sizeUpgradeCost;
@ -334,6 +388,9 @@ export function MakeProduct(
if (productName == null || productName === "") { if (productName == null || productName === "") {
throw new Error("You must specify a name for your product!"); 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)) { if (isNaN(designInvest)) {
throw new Error("Invalid value for design investment"); throw new Error("Invalid value for design investment");
} }
@ -343,17 +400,29 @@ export function MakeProduct(
if (corp.funds < designInvest + marketingInvest) { if (corp.funds < designInvest + marketingInvest) {
throw new Error("You don't have enough company funds to make this large of an investment"); 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({ const product = new Product({
name: productName.replace(/[<>]/g, ""), //Sanitize for HTMl elements name: productName.replace(/[<>]/g, ""), //Sanitize for HTMl elements
createCity: city, createCity: city,
designCost: designInvest, designCost: designInvest,
advCost: marketingInvest, 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!`); throw new Error(`You already have a product with this name!`);
} }
corp.funds = corp.funds - (designInvest + marketingInvest); corp.funds = corp.funds - (designInvest + marketingInvest);
division.products[product.name] = product; products[product.name] = product;
} }
export function Research(division: IIndustry, researchName: string): void { export function Research(division: IIndustry, researchName: string): void {
@ -372,7 +441,7 @@ export function Research(division: IIndustry, researchName: string): void {
division.researched[researchName] = true; 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 // Sanitize amt
let sanitizedAmt = amt.replace(/\s+/g, "").toUpperCase(); let sanitizedAmt = amt.replace(/\s+/g, "").toUpperCase();
sanitizedAmt = sanitizedAmt.replace(/[^-()\d/*+.MAX]/g, ""); 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) { if (n == null || isNaN(n) || n < 0) {
throw new Error("Invalid amount entered for export"); 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 }; const exportObj = { ind: divisionName, city: cityName, amt: sanitizedAmt };
material.exp.push(exportObj); material.exp.push(exportObj);
} }

@ -174,33 +174,16 @@ export class OfficeSpace {
} }
setEmployeeToJob(job: string, amount: number): boolean { setEmployeeToJob(job: string, amount: number): boolean {
let unassignedCount = 0; let jobCount = this.employees.reduce((acc, employee) => (employee.pos === job ? acc + 1 : acc), 0);
let jobCount = 0;
for (let i = 0; i < this.employees.length; ++i) { for (const employee of this.employees) {
if (this.employees[i].pos === EmployeePositions.Unassigned) { if (jobCount == amount) return true
unassignedCount++; if (employee.pos === EmployeePositions.Unassigned && jobCount <= amount) {
} else if (this.employees[i].pos === job) { employee.pos = job;
jobCount++; jobCount++;
} } else if (employee.pos === job && jobCount >= amount) {
} employee.pos = EmployeePositions.Unassigned;
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;
} }
} }
if (jobCount !== amount) return false; if (jobCount !== amount) return false;

@ -6,6 +6,8 @@ import { useCorporation } from "./Context";
import Typography from "@mui/material/Typography"; import Typography from "@mui/material/Typography";
import Button from "@mui/material/Button"; import Button from "@mui/material/Button";
import TextField from "@mui/material/TextField"; import TextField from "@mui/material/TextField";
import { BuyBackShares } from '../Actions';
import { dialogBoxCreate } from '../../ui/React/DialogBox';
interface IProps { interface IProps {
open: boolean; open: boolean;
@ -36,20 +38,12 @@ export function BuybackSharesModal(props: IProps): React.ReactElement {
function buy(): void { function buy(): void {
if (disabled) return; if (disabled) return;
if (shares === null) return; try {
corp.numShares += shares; BuyBackShares(corp, player, shares)
if (isNaN(corp.issuedShares)) { }
console.warn("Corporation issuedShares is NaN: " + corp.issuedShares); catch (err) {
console.warn("Converting to number now"); dialogBoxCreate(err + "");
const res = corp.issuedShares;
if (isNaN(res)) {
corp.issuedShares = 0;
} else {
corp.issuedShares = res;
}
} }
corp.issuedShares -= shares;
player.loseMoney(shares * buybackPrice, "corporation");
props.onClose(); props.onClose();
props.rerender(); props.rerender();
} }

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

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

@ -50,7 +50,7 @@ export function ExportModal(props: IProps): React.ReactElement {
function exportMaterial(): void { function exportMaterial(): void {
try { try {
ExportMaterial(industry, city, props.mat, amt); ExportMaterial(industry, city, props.mat, amt, currentDivision);
} catch (err) { } catch (err) {
dialogBoxCreate(err + ""); dialogBoxCreate(err + "");
} }

@ -26,6 +26,7 @@ import Table from "@mui/material/Table";
import TableBody from "@mui/material/TableBody"; import TableBody from "@mui/material/TableBody";
import TableRow from "@mui/material/TableRow"; import TableRow from "@mui/material/TableRow";
import { TableCell } from "../../ui/React/Table"; import { TableCell } from "../../ui/React/Table";
import { Box } from "@mui/material";
interface IProps { interface IProps {
office: OfficeSpace; office: OfficeSpace;
@ -430,51 +431,46 @@ export function IndustryOffice(props: IProps): React.ReactElement {
<Typography> <Typography>
Size: {props.office.employees.length} / {props.office.size} employees Size: {props.office.employees.length} / {props.office.size} employees
</Typography> </Typography>
<Tooltip title={<Typography>Automatically hires an employee and gives him/her a random name</Typography>}> <Box sx={{ display: 'grid', gridTemplateColumns: '1fr', width: 'fit-content' }}>
<span> <Box sx={{ gridTemplateColumns: 'repeat(3, 1fr)' }}>
<Button disabled={props.office.atCapacity()} onClick={autohireEmployeeButtonOnClick}> <Tooltip title={<Typography>Automatically hires an employee and gives him/her a random name</Typography>}>
Hire Employee <Button disabled={props.office.atCapacity()} onClick={autohireEmployeeButtonOnClick}>
</Button> Hire Employee
</span> </Button>
</Tooltip>
<br />
<Tooltip title={<Typography>Upgrade the office's size so that it can hold more employees!</Typography>}>
<span>
<Button disabled={corp.funds < 0} onClick={() => setUpgradeOfficeSizeOpen(true)}>
Upgrade size
</Button>
</span>
</Tooltip>
<UpgradeOfficeSizeModal
rerender={props.rerender}
office={props.office}
open={upgradeOfficeSizeOpen}
onClose={() => setUpgradeOfficeSizeOpen(false)}
/>
{!division.hasResearch("AutoPartyManager") && (
<>
<Tooltip
title={<Typography>Throw an office party to increase your employee's morale and happiness</Typography>}
>
<span>
<Button disabled={corp.funds < 0} onClick={() => setThrowPartyOpen(true)}>
Throw Party
</Button>
</span>
</Tooltip> </Tooltip>
<ThrowPartyModal <Tooltip title={<Typography>Upgrade the office's size so that it can hold more employees!</Typography>}>
<Button disabled={corp.funds < 0} onClick={() => setUpgradeOfficeSizeOpen(true)}>
Upgrade size
</Button>
</Tooltip>
<UpgradeOfficeSizeModal
rerender={props.rerender} rerender={props.rerender}
office={props.office} office={props.office}
open={throwPartyOpen} open={upgradeOfficeSizeOpen}
onClose={() => setThrowPartyOpen(false)} onClose={() => setUpgradeOfficeSizeOpen(false)}
/> />
</>
)}
<br /> {!division.hasResearch("AutoPartyManager") && (
<>
<Tooltip
title={<Typography>Throw an office party to increase your employee's morale and happiness</Typography>}
>
<Button disabled={corp.funds < 0} onClick={() => setThrowPartyOpen(true)}>
Throw Party
</Button>
</Tooltip>
<ThrowPartyModal
rerender={props.rerender}
office={props.office}
open={throwPartyOpen}
onClose={() => setThrowPartyOpen(false)}
/>
</>
)}
<SwitchButton manualMode={employeeManualAssignMode} switchMode={setEmployeeManualAssignMode} /> </Box>
<SwitchButton manualMode={employeeManualAssignMode} switchMode={setEmployeeManualAssignMode} />
</Box>
{employeeManualAssignMode ? ( {employeeManualAssignMode ? (
<ManualManagement rerender={props.rerender} office={props.office} /> <ManualManagement rerender={props.rerender} office={props.office} />
) : ( ) : (

@ -13,12 +13,12 @@ export function IndustryProductEquation(props: IProps): React.ReactElement {
if (reqAmt === undefined) continue; if (reqAmt === undefined) continue;
reqs.push(String.raw`${reqAmt}\text{ }${reqMat}`); 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) { if (props.division.makesProducts) {
prod.push(props.division.type); prod.push("Products");
} }
return ( 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 Paper from "@mui/material/Paper";
import Button from "@mui/material/Button"; import Button from "@mui/material/Button";
import Box from "@mui/material/Box"; import Box from "@mui/material/Box";
import makeStyles from "@mui/styles/makeStyles";
import createStyles from "@mui/styles/createStyles";
interface IProps { interface IProps {
corp: ICorporation; corp: ICorporation;
@ -37,6 +39,14 @@ interface IProps {
rerender: () => void; rerender: () => void;
} }
const useStyles = makeStyles(() =>
createStyles({
retainHeight: {
minHeight: '3em',
},
})
);
function WarehouseRoot(props: IProps): React.ReactElement { function WarehouseRoot(props: IProps): React.ReactElement {
const corp = useCorporation(); const corp = useCorporation();
const division = useDivision(); const division = useDivision();
@ -56,6 +66,8 @@ function WarehouseRoot(props: IProps): React.ReactElement {
props.rerender(); props.rerender();
} }
const classes = useStyles();
// Current State: // Current State:
let stateText; let stateText;
switch (division.state) { switch (division.state) {
@ -139,13 +151,13 @@ function WarehouseRoot(props: IProps): React.ReactElement {
{numeralWrapper.formatBigNumber(props.warehouse.size)} {numeralWrapper.formatBigNumber(props.warehouse.size)}
</Typography> </Typography>
</Tooltip> </Tooltip>
<Button disabled={!canAffordUpgrade} onClick={upgradeWarehouseOnClick}>
Upgrade Warehouse Size -&nbsp;
<MoneyCost money={sizeUpgradeCost} corp={corp} />
</Button>
</Box> </Box>
<Button disabled={!canAffordUpgrade} onClick={upgradeWarehouseOnClick}>
Upgrade Warehouse Size -&nbsp;
<MoneyCost money={sizeUpgradeCost} corp={corp} />
</Button>
<Typography>This industry uses the following equation for its production: </Typography> <Typography>This industry uses the following equation for its production: </Typography>
<br /> <br />
<Typography> <Typography>
@ -158,7 +170,7 @@ function WarehouseRoot(props: IProps): React.ReactElement {
</Typography> </Typography>
<br /> <br />
<Typography>{stateText}</Typography> <Typography className={classes.retainHeight}>{stateText}</Typography>
{corp.unlockUpgrades[1] && ( {corp.unlockUpgrades[1] && (
<> <>

@ -36,7 +36,7 @@ export function LimitProductProductionModal(props: IProps): React.ReactElement {
return ( return (
<Modal open={props.open} onClose={props.onClose}> <Modal open={props.open} onClose={props.onClose}>
<Typography> <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. limit.
</Typography> </Typography>
<TextField autoFocus={true} placeholder="Limit" type="number" onChange={onChange} onKeyDown={onKeyDown} /> <TextField autoFocus={true} placeholder="Limit" type="number" onChange={onChange} onKeyDown={onKeyDown} />

@ -112,7 +112,7 @@ export function MaterialElem(props: IMaterialProps): React.ReactElement {
return ( return (
<Paper> <Paper>
<Box display="flex"> <Box sx={{ display: 'grid', gridTemplateColumns: '2fr 1fr', m: '5px' }}>
<Box> <Box>
<Tooltip <Tooltip
title={ title={
@ -149,11 +149,10 @@ export function MaterialElem(props: IMaterialProps): React.ReactElement {
</Tooltip> </Tooltip>
</Box> </Box>
<Box> <Box sx={{ "& button": { width: '100%' } }}>
<Tooltip <Tooltip
title={tutorial ? <Typography>Purchase your required materials to get production started!</Typography> : ""} title={tutorial ? <Typography>Purchase your required materials to get production started!</Typography> : ""}
> >
<span>
<Button <Button
color={tutorial ? "error" : "primary"} color={tutorial ? "error" : "primary"}
onClick={() => setPurchaseMaterialOpen(true)} onClick={() => setPurchaseMaterialOpen(true)}
@ -161,7 +160,6 @@ export function MaterialElem(props: IMaterialProps): React.ReactElement {
> >
{purchaseButtonText} {purchaseButtonText}
</Button> </Button>
</span>
</Tooltip> </Tooltip>
<PurchaseMaterialModal <PurchaseMaterialModal
mat={mat} mat={mat}
@ -177,7 +175,6 @@ export function MaterialElem(props: IMaterialProps): React.ReactElement {
<ExportModal mat={mat} open={exportOpen} onClose={() => setExportOpen(false)} /> <ExportModal mat={mat} open={exportOpen} onClose={() => setExportOpen(false)} />
</> </>
)} )}
<br />
<Button <Button
color={division.prodMats.includes(props.mat.name) && !mat.sllman[0] ? "error" : "primary"} color={division.prodMats.includes(props.mat.name) && !mat.sllman[0] ? "error" : "primary"}

@ -89,19 +89,21 @@ export function Overview({ rerender }: IProps): React.ReactElement {
<StatsTable rows={multRows} /> <StatsTable rows={multRows} />
<br /> <br />
<BonusTime /> <BonusTime />
<Tooltip <Box sx={{ display: 'grid', gridTemplateColumns: 'repeat(3, 1fr)', width: 'fit-content' }}>
title={ <Tooltip
<Typography> title={
Get a copy of and read 'The Complete Handbook for Creating a Successful Corporation.' This is a .lit file <Typography>
that guides you through the beginning of setting up a Corporation and provides some tips/pointers for Get a copy of and read 'The Complete Handbook for Creating a Successful Corporation.' This is a .lit file
helping you get started with managing it. that guides you through the beginning of setting up a Corporation and provides some tips/pointers for
</Typography> helping you get started with managing it.
} </Typography>
> }
<Button onClick={() => corp.getStarterGuide(player)}>Getting Started Guide</Button> >
</Tooltip> <Button onClick={() => corp.getStarterGuide(player)}>Getting Started Guide</Button>
{corp.public ? <PublicButtons rerender={rerender} /> : <PrivateButtons rerender={rerender} />} </Tooltip>
<BribeButton /> {corp.public ? <PublicButtons rerender={rerender} /> : <PrivateButtons rerender={rerender} />}
<BribeButton />
</Box>
<br /> <br />
<Upgrades rerender={rerender} /> <Upgrades rerender={rerender} />
</> </>
@ -125,11 +127,9 @@ function PrivateButtons({ rerender }: IPrivateButtonsProps): React.ReactElement
return ( return (
<> <>
<Tooltip title={<Typography>{findInvestorsTooltip}</Typography>}> <Tooltip title={<Typography>{findInvestorsTooltip}</Typography>}>
<span> <Button disabled={!fundingAvailable} onClick={() => setFindInvestorsopen(true)}>
<Button disabled={!fundingAvailable} onClick={() => setFindInvestorsopen(true)}> Find Investors
Find Investors </Button>
</Button>
</span>
</Tooltip> </Tooltip>
<Tooltip <Tooltip
title={ title={
@ -143,7 +143,6 @@ function PrivateButtons({ rerender }: IPrivateButtonsProps): React.ReactElement
</Tooltip> </Tooltip>
<FindInvestorsModal open={findInvestorsopen} onClose={() => setFindInvestorsopen(false)} rerender={rerender} /> <FindInvestorsModal open={findInvestorsopen} onClose={() => setFindInvestorsopen(false)} rerender={rerender} />
<GoPublicModal open={goPublicopen} onClose={() => setGoPublicopen(false)} rerender={rerender} /> <GoPublicModal open={goPublicopen} onClose={() => setGoPublicopen(false)} rerender={rerender} />
<br />
</> </>
); );
} }
@ -201,8 +200,8 @@ function PublicButtons({ rerender }: IPublicButtonsProps): React.ReactElement {
const sellSharesTooltip = sellSharesOnCd const sellSharesTooltip = sellSharesOnCd
? "Cannot sell shares for " + corp.convertCooldownToString(corp.shareSaleCooldown) ? "Cannot sell shares for " + corp.convertCooldownToString(corp.shareSaleCooldown)
: "Sell your shares in the company. The money earned from selling your " + : "Sell your shares in the company. The money earned from selling your " +
"shares goes into your personal account, not the Corporation's. " + "shares goes into your personal account, not the Corporation's. " +
"This is one of the only ways to profit from your business venture."; "This is one of the only ways to profit from your business venture.";
const issueNewSharesOnCd = corp.issueNewSharesCooldown > 0; const issueNewSharesOnCd = corp.issueNewSharesCooldown > 0;
const issueNewSharesTooltip = issueNewSharesOnCd const issueNewSharesTooltip = issueNewSharesOnCd
@ -212,28 +211,21 @@ function PublicButtons({ rerender }: IPublicButtonsProps): React.ReactElement {
return ( return (
<> <>
<Tooltip title={<Typography>{sellSharesTooltip}</Typography>}> <Tooltip title={<Typography>{sellSharesTooltip}</Typography>}>
<span> <Button disabled={sellSharesOnCd} onClick={() => setSellSharesOpen(true)}>
<Button disabled={sellSharesOnCd} onClick={() => setSellSharesOpen(true)}> Sell Shares
Sell Shares </Button>
</Button>
</span>
</Tooltip> </Tooltip>
<SellSharesModal open={sellSharesOpen} onClose={() => setSellSharesOpen(false)} rerender={rerender} /> <SellSharesModal open={sellSharesOpen} onClose={() => setSellSharesOpen(false)} rerender={rerender} />
<Tooltip title={<Typography>Buy back shares you that previously issued or sold at market price.</Typography>}> <Tooltip title={<Typography>Buy back shares you that previously issued or sold at market price.</Typography>}>
<span> <Button disabled={corp.issuedShares < 1} onClick={() => setBuybackSharesOpen(true)}>
<Button disabled={corp.issuedShares < 1} onClick={() => setBuybackSharesOpen(true)}> Buyback shares
Buyback shares </Button>
</Button>
</span>
</Tooltip> </Tooltip>
<BuybackSharesModal open={buybackSharesOpen} onClose={() => setBuybackSharesOpen(false)} rerender={rerender} /> <BuybackSharesModal open={buybackSharesOpen} onClose={() => setBuybackSharesOpen(false)} rerender={rerender} />
<br />
<Tooltip title={<Typography>{issueNewSharesTooltip}</Typography>}> <Tooltip title={<Typography>{issueNewSharesTooltip}</Typography>}>
<span> <Button disabled={issueNewSharesOnCd} onClick={() => setIssueNewSharesOpen(true)}>
<Button disabled={issueNewSharesOnCd} onClick={() => setIssueNewSharesOpen(true)}> Issue New Shares
Issue New Shares </Button>
</Button>
</span>
</Tooltip> </Tooltip>
<IssueNewSharesModal open={issueNewSharesOpen} onClose={() => setIssueNewSharesOpen(false)} /> <IssueNewSharesModal open={issueNewSharesOpen} onClose={() => setIssueNewSharesOpen(false)} />
<Tooltip <Tooltip
@ -242,7 +234,6 @@ function PublicButtons({ rerender }: IPublicButtonsProps): React.ReactElement {
<Button onClick={() => setIssueDividendsOpen(true)}>Issue Dividends</Button> <Button onClick={() => setIssueDividendsOpen(true)}>Issue Dividends</Button>
</Tooltip> </Tooltip>
<IssueDividendsModal open={issueDividendsOpen} onClose={() => setIssueDividendsOpen(false)} /> <IssueDividendsModal open={issueDividendsOpen} onClose={() => setIssueDividendsOpen(false)} />
<br />
</> </>
); );
} }
@ -269,11 +260,9 @@ function BribeButton(): React.ReactElement {
: "Your Corporation is not powerful enough to bribe Faction leaders" : "Your Corporation is not powerful enough to bribe Faction leaders"
} }
> >
<span> <Button disabled={!canBribe} onClick={openBribe}>
<Button disabled={!canBribe} onClick={openBribe}> Bribe Factions
Bribe Factions </Button>
</Button>
</span>
</Tooltip> </Tooltip>
<BribeFactionModal open={open} onClose={() => setOpen(false)} /> <BribeFactionModal open={open} onClose={() => setOpen(false)} />
</> </>

@ -81,7 +81,7 @@ export function ProductElem(props: IProductProps): React.ReactElement {
); );
} else if (product.sCost) { } else if (product.sCost) {
if (isString(product.sCost)) { if (isString(product.sCost)) {
const sCost = (product.sCost as string).replace(/MP/g, product.pCost + ""); const sCost = (product.sCost as string).replace(/MP/g, product.pCost + product.rat / product.mku + "");
sellButtonText = ( sellButtonText = (
<> <>
{sellButtonText} @ <Money money={eval(sCost)} /> {sellButtonText} @ <Money money={eval(sCost)} />

@ -4,7 +4,7 @@ import { MaterialSizes } from "../MaterialSizes";
import { Warehouse } from "../Warehouse"; import { Warehouse } from "../Warehouse";
import { Material } from "../Material"; import { Material } from "../Material";
import { numeralWrapper } from "../../ui/numeralFormat"; import { numeralWrapper } from "../../ui/numeralFormat";
import { BuyMaterial } from "../Actions"; import { BulkPurchase, BuyMaterial } from "../Actions";
import { Modal } from "../../ui/React/Modal"; import { Modal } from "../../ui/React/Modal";
import { useCorporation, useDivision } from "./Context"; import { useCorporation, useDivision } from "./Context";
import Typography from "@mui/material/Typography"; import Typography from "@mui/material/Typography";
@ -54,33 +54,17 @@ interface IBPProps {
warehouse: Warehouse; warehouse: Warehouse;
} }
function BulkPurchase(props: IBPProps): React.ReactElement { function BulkPurchaseSection(props: IBPProps): React.ReactElement {
const corp = useCorporation(); const corp = useCorporation();
const [buyAmt, setBuyAmt] = useState(""); const [buyAmt, setBuyAmt] = useState("");
function bulkPurchase(): void { function bulkPurchase(): void {
const amount = parseFloat(buyAmt); try {
BulkPurchase(corp, props.warehouse, props.mat, parseFloat(buyAmt));
const matSize = MaterialSizes[props.mat.name]; } catch (err) {
const maxAmount = (props.warehouse.size - props.warehouse.sizeUsed) / matSize; dialogBoxCreate(err + "");
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();
} }
props.onClose();
} }
function onKeyDown(event: React.KeyboardEvent<HTMLInputElement>): void { 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={purchaseMaterial}>Confirm</Button>
<Button onClick={clearPurchase}>Clear Purchase</Button> <Button onClick={clearPurchase}>Clear Purchase</Button>
{division.hasResearch("Bulk Purchasing") && ( {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> </Modal>

@ -6,17 +6,18 @@ import { IIndustry } from "../IIndustry";
import { Research } from "../Actions"; import { Research } from "../Actions";
import { Node } from "../ResearchTree"; import { Node } from "../ResearchTree";
import { ResearchMap } from "../ResearchMap"; import { ResearchMap } from "../ResearchMap";
import { Settings } from "../../Settings/Settings";
import { dialogBoxCreate } from "../../ui/React/DialogBox"; import { dialogBoxCreate } from "../../ui/React/DialogBox";
import Typography from "@mui/material/Typography"; import Typography from "@mui/material/Typography";
import Tooltip from "@mui/material/Tooltip"; import Tooltip from "@mui/material/Tooltip";
import Button from "@mui/material/Button"; import Button from "@mui/material/Button";
import Box from "@mui/material/Box"; import Box from "@mui/material/Box";
import ListItemButton from "@mui/material/ListItemButton";
import ListItemText from "@mui/material/ListItemText";
import Collapse from "@mui/material/Collapse"; import Collapse from "@mui/material/Collapse";
import ExpandMore from "@mui/icons-material/ExpandMore"; import ExpandMore from "@mui/icons-material/ExpandMore";
import ExpandLess from "@mui/icons-material/ExpandLess"; import ExpandLess from "@mui/icons-material/ExpandLess";
import CheckIcon from '@mui/icons-material/Check';
interface INodeProps { interface INodeProps {
n: Node | null; n: Node | null;
division: IIndustry; division: IIndustry;
@ -42,8 +43,8 @@ function Upgrade({ n, division }: INodeProps): React.ReactElement {
dialogBoxCreate( dialogBoxCreate(
`Researched ${n.text}. It may take a market cycle ` + `Researched ${n.text}. It may take a market cycle ` +
`(~${CorporationConstants.SecsPerMarketCycle} seconds) before the effects of ` + `(~${CorporationConstants.SecsPerMarketCycle} seconds) before the effects of ` +
`the Research apply.`, `the Research apply.`,
); );
} }
@ -52,8 +53,8 @@ function Upgrade({ n, division }: INodeProps): React.ReactElement {
color = "info"; color = "info";
} }
const but = ( const wrapInTooltip = (ele: React.ReactElement): React.ReactElement => {
<Box> return (
<Tooltip <Tooltip
title={ title={
<Typography> <Typography>
@ -63,12 +64,22 @@ function Upgrade({ n, division }: INodeProps): React.ReactElement {
</Typography> </Typography>
} }
> >
{ele}
</Tooltip>
)
}
const but = (
<Box>
{wrapInTooltip(
<span> <span>
<Button color={color} disabled={disabled && !n.researched} onClick={research}> <Button color={color} disabled={disabled && !n.researched} onClick={research}
{n.text} style={{ width: '100%', textAlign: 'left', justifyContent: 'unset' }}
>
{n.researched && (<CheckIcon sx={{ mr: 1 }} />)}{n.text}
</Button> </Button>
</span> </span>
</Tooltip> )}
</Box> </Box>
); );
@ -76,15 +87,25 @@ function Upgrade({ n, division }: INodeProps): React.ReactElement {
return ( return (
<Box> <Box>
<Box display="flex"> <Box display="flex" sx={{ border: '1px solid ' + Settings.theme.well }}>
{but} {wrapInTooltip(
<ListItemButton onClick={() => setOpen((old) => !old)}> <span style={{ width: '100%' }}>
<ListItemText /> <Button color={color} disabled={disabled && !n.researched} onClick={research} sx={{
width: '100%',
textAlign: 'left',
justifyContent: 'unset',
borderColor: Settings.theme.button
}}>
{n.researched && (<CheckIcon sx={{ mr: 1 }} />)}{n.text}
</Button>
</span>
)}
<Button onClick={() => setOpen((old) => !old)} sx={{ borderColor: Settings.theme.button, minWidth: 'fit-content' }}>
{open ? <ExpandLess color="primary" /> : <ExpandMore color="primary" />} {open ? <ExpandLess color="primary" /> : <ExpandMore color="primary" />}
</ListItemButton> </Button>
</Box> </Box>
<Collapse in={open} unmountOnExit> <Collapse in={open} unmountOnExit>
<Box m={4}> <Box m={1}>
{n.children.map((m) => ( {n.children.map((m) => (
<Upgrade key={m.text} division={division} n={m} /> <Upgrade key={m.text} division={division} n={m} />
))} ))}
@ -108,7 +129,7 @@ export function ResearchModal(props: IProps): React.ReactElement {
return ( return (
<Modal open={props.open} onClose={props.onClose}> <Modal open={props.open} onClose={props.onClose}>
<Upgrade division={props.industry} n={researchTree.root} /> <Upgrade division={props.industry} n={researchTree.root} />
<Typography> <Typography sx={{ mt: 1 }}>
Research points: {props.industry.sciResearch.qty.toFixed(3)} Research points: {props.industry.sciResearch.qty.toFixed(3)}
<br /> <br />
Multipliers from research: Multipliers from research:

@ -4,12 +4,12 @@ import { dialogBoxCreate } from "../../ui/React/DialogBox";
import { Modal } from "../../ui/React/Modal"; import { Modal } from "../../ui/React/Modal";
import { use } from "../../ui/Context"; import { use } from "../../ui/Context";
import { useCorporation } from "./Context"; import { useCorporation } from "./Context";
import { CorporationConstants } from "../data/Constants";
import { ICorporation } from "../ICorporation"; import { ICorporation } from "../ICorporation";
import Typography from "@mui/material/Typography"; import Typography from "@mui/material/Typography";
import TextField from "@mui/material/TextField"; import TextField from "@mui/material/TextField";
import Button from "@mui/material/Button"; import Button from "@mui/material/Button";
import { Money } from "../../ui/React/Money"; import { Money } from "../../ui/React/Money";
import { SellShares } from "../Actions";
interface IProps { interface IProps {
open: boolean; open: boolean;
onClose: () => void; onClose: () => void;
@ -48,38 +48,23 @@ export function SellSharesModal(props: IProps): React.ReactElement {
} }
function sell(): void { function sell(): void {
if (shares === null) return;
if (disabled) return; if (disabled) return;
const stockSaleResults = corp.calculateShareSale(shares); try {
const profit = stockSaleResults[0]; const profit = SellShares(corp, player, shares)
const newSharePrice = stockSaleResults[1]; props.onClose();
const newSharesUntilUpdate = stockSaleResults[2]; dialogBoxCreate(
<>
Sold {numeralWrapper.formatMoney(shares)} shares for
<Money money={profit} />. The corporation's stock price fell to&nbsp; <Money money={corp.sharePrice} />
as a result of dilution.
</>,
);
corp.numShares -= shares; props.rerender();
if (isNaN(corp.issuedShares)) { } catch (err) {
console.error(`Corporation issuedShares is NaN: ${corp.issuedShares}`); dialogBoxCreate(err + "");
const res = corp.issuedShares;
if (isNaN(res)) {
corp.issuedShares = 0;
} else {
corp.issuedShares = res;
}
} }
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&nbsp; <Money money={corp.sharePrice} />
as a result of dilution.
</>,
);
props.rerender();
} }
function onKeyDown(event: React.KeyboardEvent<HTMLInputElement>): void { 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, 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 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 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> </Typography>
{staneksGift.storedCycles > 5 && ( {staneksGift.storedCycles > 5 && (
<Typography> <Typography>

@ -44,7 +44,7 @@ export function findCrime(roughName: string): Crime | null {
return Crimes.DealDrugs; return Crimes.DealDrugs;
} else if (roughName.includes("bond") && roughName.includes("forge")) { } else if (roughName.includes("bond") && roughName.includes("forge")) {
return Crimes.BondForgery; 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; return Crimes.TraffickArms;
} else if (roughName.includes("homicide")) { } else if (roughName.includes("homicide")) {
return Crimes.Homicide; return Crimes.Homicide;
@ -52,7 +52,7 @@ export function findCrime(roughName: string): Crime | null {
return Crimes.GrandTheftAuto; return Crimes.GrandTheftAuto;
} else if (roughName.includes("kidnap")) { } else if (roughName.includes("kidnap")) {
return Crimes.Kidnap; return Crimes.Kidnap;
} else if (roughName.includes("assassinate") || roughName.includes("assassination")) { } else if (roughName.includes("assassin")) {
return Crimes.Assassination; return Crimes.Assassination;
} else if (roughName.includes("heist")) { } else if (roughName.includes("heist")) {
return Crimes.Heist; return Crimes.Heist;

@ -19,10 +19,18 @@ interface IProps {
export function Corporation(props: IProps): React.ReactElement { export function Corporation(props: IProps): React.ReactElement {
function addTonsCorporationFunds(): void { function addTonsCorporationFunds(): void {
if (props.player.corporation) { 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 { function resetCorporationFunds(): void {
if (props.player.corporation) { if (props.player.corporation) {
props.player.corporation.funds = props.player.corporation.funds - props.player.corporation.funds; props.player.corporation.funds = props.player.corporation.funds - props.player.corporation.funds;
@ -77,8 +85,17 @@ export function Corporation(props: IProps): React.ReactElement {
<tbody> <tbody>
<tr> <tr>
<td> <td>
<Button onClick={addTonsCorporationFunds}>Tons of funds</Button> <Typography>Funds:</Typography>
<Button onClick={resetCorporationFunds}>Reset funds</Button> </td>
<td>
<Adjuster
label="set funds"
placeholder="amt"
tons={addTonsCorporationFunds}
add={modifyCorporationFunds(1)}
subtract={modifyCorporationFunds(-1)}
reset={resetCorporationFunds}
/>
</td> </td>
</tr> </tr>
<tr> <tr>

@ -8,6 +8,7 @@ import ExpandMoreIcon from "@mui/icons-material/ExpandMore";
import Button from "@mui/material/Button"; import Button from "@mui/material/Button";
import Typography from "@mui/material/Typography"; import Typography from "@mui/material/Typography";
import { IPlayer } from "../../PersonObjects/IPlayer"; import { IPlayer } from "../../PersonObjects/IPlayer";
import { Adjuster } from "./Adjuster";
interface IProps { interface IProps {
player: IPlayer; player: IPlayer;
@ -38,6 +39,12 @@ export function Sleeves(props: IProps): React.ReactElement {
} }
} }
function sleeveSetStoredCycles(cycles: number): void {
for (let i = 0; i < props.player.sleeves.length; ++i) {
props.player.sleeves[i].storedCycles = cycles;
}
}
return ( return (
<Accordion TransitionProps={{ unmountOnExit: true }}> <Accordion TransitionProps={{ unmountOnExit: true }}>
<AccordionSummary expandIcon={<ExpandMoreIcon />}> <AccordionSummary expandIcon={<ExpandMoreIcon />}>
@ -68,6 +75,18 @@ export function Sleeves(props: IProps): React.ReactElement {
<Button onClick={sleeveSyncClearAll}>Clear all</Button> <Button onClick={sleeveSyncClearAll}>Clear all</Button>
</td> </td>
</tr> </tr>
<tr>
<td colSpan={3}>
<Adjuster
label="Stored Cycles"
placeholder="cycles"
tons={() => sleeveSetStoredCycles(10000000)}
add={sleeveSetStoredCycles}
subtract={sleeveSetStoredCycles}
reset={() => sleeveSetStoredCycles(0)}
/>
</td>
</tr>
</tbody> </tbody>
</table> </table>
</AccordionDetails> </AccordionDetails>

@ -1,11 +1,15 @@
import { Player } from "./Player"; import { Player } from "./Player";
import { isScriptFilename } from "./Script/isScriptFilename"; import { Router } from "./ui/GameRoot";
import { Script } from "./Script/Script";
import { removeLeadingSlash } from "./Terminal/DirectoryHelpers"; import { removeLeadingSlash } from "./Terminal/DirectoryHelpers";
import { Terminal } from "./Terminal"; import { Terminal } from "./Terminal";
import { SnackbarEvents } from "./ui/React/Snackbar"; import { SnackbarEvents } from "./ui/React/Snackbar";
import { IMap } from "./types"; import { IMap, IReturnStatus } from "./types";
import { GetServer } from "./Server/AllServers"; import { GetServer } from "./Server/AllServers";
import { ImportPlayerData, SaveData, saveObject } from "./SaveObject";
import { Settings } from "./Settings/Settings";
import { exportScripts } from "./Terminal/commands/download";
import { CONSTANTS } from "./Constants";
import { hash } from "./hash/hash";
export function initElectron(): void { export function initElectron(): void {
const userAgent = navigator.userAgent.toLowerCase(); const userAgent = navigator.userAgent.toLowerCase();
@ -14,36 +18,81 @@ export function initElectron(): void {
(document as any).achievements = []; (document as any).achievements = [];
initWebserver(); initWebserver();
initAppNotifier(); initAppNotifier();
initSaveFunctions();
initElectronBridge();
} }
} }
function initWebserver(): void { function initWebserver(): void {
(document as any).saveFile = function (filename: string, code: string): string { interface IReturnWebStatus extends IReturnStatus {
data?: {
[propName: string]: any;
};
}
function normalizeFileName(filename: string): string {
filename = filename.replace(/\/\/+/g, "/"); filename = filename.replace(/\/\/+/g, "/");
filename = removeLeadingSlash(filename); filename = removeLeadingSlash(filename);
if (filename.includes("/")) { if (filename.includes("/")) {
filename = "/" + removeLeadingSlash(filename); filename = "/" + removeLeadingSlash(filename);
} }
return filename;
}
(document as any).getFiles = function (): IReturnWebStatus {
const home = GetServer("home");
if (home === null) {
return {
res: false,
msg: "Home server does not exist.",
};
}
return {
res: true,
data: {
files: home.scripts.map((script) => ({
filename: script.filename,
code: script.code,
ramUsage: script.ramUsage,
})),
},
};
};
(document as any).deleteFile = function (filename: string): IReturnWebStatus {
filename = normalizeFileName(filename);
const home = GetServer("home");
if (home === null) {
return {
res: false,
msg: "Home server does not exist.",
};
}
return home.removeFile(filename);
};
(document as any).saveFile = function (filename: string, code: string): IReturnWebStatus {
filename = normalizeFileName(filename);
code = Buffer.from(code, "base64").toString(); code = Buffer.from(code, "base64").toString();
const home = GetServer("home"); const home = GetServer("home");
if (home === null) return "'home' server not found."; if (home === null) {
if (isScriptFilename(filename)) { return {
//If the current script already exists on the server, overwrite it res: false,
for (let i = 0; i < home.scripts.length; i++) { msg: "Home server does not exist.",
if (filename == home.scripts[i].filename) { };
home.scripts[i].saveScript(Player, filename, code, "home", home.scripts);
return "written";
}
}
//If the current script does NOT exist, create a new one
const script = new Script();
script.saveScript(Player, filename, code, "home", home.scripts);
home.scripts.push(script);
return "written";
} }
const { success, overwritten } = home.writeToScriptFile(Player, filename, code);
return "not a script file"; let script;
if (success) {
script = home.getScript(filename);
}
return {
res: success,
data: {
overwritten,
ramUsage: script?.ramUsage,
},
};
}; };
} }
@ -67,6 +116,123 @@ function initAppNotifier(): void {
}; };
// Will be consumud by the electron wrapper. // Will be consumud by the electron wrapper.
// @ts-ignore (window as any).appNotifier = funcs;
window.appNotifier = funcs; }
function initSaveFunctions(): void {
const funcs = {
triggerSave: (): Promise<void> => saveObject.saveGame(true),
triggerGameExport: (): void => {
try {
saveObject.exportGame();
} catch (error) {
console.log(error);
SnackbarEvents.emit("Could not export game.", "error", 2000);
}
},
triggerScriptsExport: (): void => exportScripts("*", Player.getHomeComputer()),
getSaveData: (): { save: string; fileName: string } => {
return {
save: saveObject.getSaveString(Settings.ExcludeRunningScriptsFromSave),
fileName: saveObject.getSaveFileName(),
};
},
getSaveInfo: async (base64save: string): Promise<ImportPlayerData | undefined> => {
try {
const data = await saveObject.getImportDataFromString(base64save);
return data.playerData;
} catch (error) {
console.error(error);
return;
}
},
pushSaveData: (base64save: string, automatic = false): void => Router.toImportSave(base64save, automatic),
};
// Will be consumud by the electron wrapper.
(window as any).appSaveFns = funcs;
}
function initElectronBridge(): void {
const bridge = (window as any).electronBridge as any;
if (!bridge) return;
bridge.receive("get-save-data-request", () => {
const data = (window as any).appSaveFns.getSaveData();
bridge.send("get-save-data-response", data);
});
bridge.receive("get-save-info-request", async (save: string) => {
const data = await (window as any).appSaveFns.getSaveInfo(save);
bridge.send("get-save-info-response", data);
});
bridge.receive("push-save-request", ({ save, automatic = false }: { save: string; automatic: boolean }) => {
(window as any).appSaveFns.pushSaveData(save, automatic);
});
bridge.receive("trigger-save", () => {
return (window as any).appSaveFns
.triggerSave()
.then(() => {
bridge.send("save-completed");
})
.catch((error: any) => {
console.log(error);
SnackbarEvents.emit("Could not save game.", "error", 2000);
});
});
bridge.receive("trigger-game-export", () => {
try {
(window as any).appSaveFns.triggerGameExport();
} catch (error) {
console.log(error);
SnackbarEvents.emit("Could not export game.", "error", 2000);
}
});
bridge.receive("trigger-scripts-export", () => {
try {
(window as any).appSaveFns.triggerScriptsExport();
} catch (error) {
console.log(error);
SnackbarEvents.emit("Could not export scripts.", "error", 2000);
}
});
}
export function pushGameSaved(data: SaveData): void {
const bridge = (window as any).electronBridge as any;
if (!bridge) return;
bridge.send("push-game-saved", data);
}
export function pushGameReady(): void {
const bridge = (window as any).electronBridge as any;
if (!bridge) return;
// Send basic information to the electron wrapper
bridge.send("push-game-ready", {
player: {
identifier: Player.identifier,
playtime: Player.totalPlaytime,
lastSave: Player.lastSave,
},
game: {
version: CONSTANTS.VersionString,
hash: hash(),
},
});
}
export function pushImportResult(wasImported: boolean): void {
const bridge = (window as any).electronBridge as any;
if (!bridge) return;
bridge.send("push-import-result", { wasImported });
pushDisableRestore();
}
export function pushDisableRestore(): void {
const bridge = (window as any).electronBridge as any;
if (!bridge) return;
bridge.send("push-disable-restore", { duration: 1000 * 60 });
} }

@ -19,6 +19,42 @@ import { SourceFileFlags } from "../SourceFile/SourceFileFlags";
import { dialogBoxCreate } from "../ui/React/DialogBox"; import { dialogBoxCreate } from "../ui/React/DialogBox";
import { InvitationEvent } from "./ui/InvitationModal"; 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 { export function inviteToFaction(faction: Faction): void {
Player.receiveInvite(faction.name); Player.receiveInvite(faction.name);
faction.alreadyInvited = true; faction.alreadyInvited = true;
@ -31,6 +67,8 @@ export function joinFaction(faction: Faction): void {
if (faction.isMember) return; if (faction.isMember) return;
faction.isMember = true; faction.isMember = true;
Player.factions.push(faction.name); Player.factions.push(faction.name);
Player.factions.sort((a, b) =>
factionOrder.indexOf(a) - factionOrder.indexOf(b));
const factionInfo = faction.getInfo(); const factionInfo = faction.getInfo();
//Determine what factions you are banned from now that you have joined this faction //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) { if (!Settings.SuppressBuyAugmentationConfirmation) {
dialogBoxCreate( dialogBoxCreate(
"You purchased " + "You purchased " +
aug.name + aug.name +
". Its enhancements will not take " + ". Its enhancements will not take " +
"effect until they are installed. To install your augmentations, go to the " + "effect until they are installed. To install your augmentations, go to the " +
"'Augmentations' tab on the left-hand navigation menu. Purchasing additional " + "'Augmentations' tab on the left-hand navigation menu. Purchasing additional " +
"augmentations will now be more expensive.", "augmentations will now be more expensive.",
); );
} }
} }
} else { } else {
dialogBoxCreate( dialogBoxCreate(
"Hmm, something went wrong when trying to purchase an Augmentation. " + "Hmm, something went wrong when trying to purchase an Augmentation. " +
"Please report this to the game developer with an explanation of how to " + "Please report this to the game developer with an explanation of how to " +
"reproduce this.", "reproduce this.",
); );
} }
return ""; 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 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. scale, in ways that no other company can.
<br /> <br />
<br /> <br />

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