@ -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
After Width: | Height: | Size: 15 KiB |
12
cypress.json
@ -1,10 +1,14 @@
|
|||||||
{
|
{
|
||||||
"baseUrl": "http://localhost:8000",
|
"baseUrl": "http://localhost:8000",
|
||||||
"fixturesFolder": false,
|
|
||||||
"trashAssetsBeforeRuns": true,
|
"trashAssetsBeforeRuns": true,
|
||||||
"screenshotsFolder": ".cypress/screenshots",
|
|
||||||
"videosFolder": ".cypress/videos",
|
|
||||||
"videoUploadOnPasses": false,
|
"videoUploadOnPasses": false,
|
||||||
"viewportWidth": 1980,
|
"viewportWidth": 1980,
|
||||||
"viewportHeight": 1080
|
"viewportHeight": 1080,
|
||||||
|
"fixturesFolder": "test/cypress/fixtures",
|
||||||
|
"integrationFolder": "test/cypress/integration",
|
||||||
|
"pluginsFile": "test/cypress/plugins/index.js",
|
||||||
|
"supportFile": "test/cypress/support/index.js",
|
||||||
|
"screenshotsFolder": ".cypress/screenshots",
|
||||||
|
"videosFolder": ".cypress/videos",
|
||||||
|
"downloadsFolder": ".cypress/downloads"
|
||||||
}
|
}
|
||||||
|
@ -1,9 +0,0 @@
|
|||||||
export {};
|
|
||||||
|
|
||||||
beforeEach(() => {
|
|
||||||
cy.visit("/");
|
|
||||||
cy.clearLocalStorage();
|
|
||||||
cy.window().then((win) => {
|
|
||||||
win.indexedDB.deleteDatabase("bitburnerSave");
|
|
||||||
});
|
|
||||||
});
|
|
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
BIN
dist/favicon.ico
vendored
Normal file
After Width: | Height: | Size: 15 KiB |
BIN
dist/images/297df8c0e47764ea113951318b2acf55.png
vendored
Normal file
After Width: | Height: | Size: 118 KiB |
BIN
dist/images/447bc31e61f55e7eff875be3e9a81f1a.png
vendored
Normal file
After Width: | Height: | Size: 120 KiB |
BIN
dist/images/4e0e750f2f09de58219773edd46cbbf5.png
vendored
Normal file
After Width: | Height: | Size: 118 KiB |
BIN
dist/images/5aa87b7de67a77c914088783b055e1cf.png
vendored
Normal file
After Width: | Height: | Size: 119 KiB |
BIN
dist/images/66f4b86d86164fc117bd6d648e4eaa6f.png
vendored
Normal file
After Width: | Height: | Size: 116 KiB |
BIN
dist/images/6caf35202b10b52e1fc2743f674c33e8.png
vendored
Normal file
After Width: | Height: | Size: 111 KiB |
BIN
dist/images/83b2443ab7e7d346766c8f6bc5afc7a7.png
vendored
Normal file
After Width: | Height: | Size: 112 KiB |
BIN
dist/images/85a7b2896acb62be76f3ea7100fe9012.png
vendored
Normal file
After Width: | Height: | Size: 121 KiB |
BIN
dist/images/9f96a5084f4e5f1a6c0041b41b34d62d.png
vendored
Normal file
After Width: | Height: | Size: 118 KiB |
BIN
dist/images/a1110d6c8d16a14c4570411750248399.png
vendored
Normal file
After Width: | Height: | Size: 120 KiB |
BIN
dist/images/c7164b072d62c91c27c6d607b5207e7b.png
vendored
Normal file
After Width: | Height: | Size: 121 KiB |
BIN
dist/images/cb88977ea837bccb9cceb727adc78302.png
vendored
Normal file
After Width: | Height: | Size: 118 KiB |
BIN
dist/images/e66b0c327f97d08e4253f52234d659eb.png
vendored
Normal file
After Width: | Height: | Size: 120 KiB |
BIN
dist/images/e97de4daa946331c7e99dee9c05d629c.png
vendored
Normal file
After Width: | Height: | Size: 117 KiB |
20
dist/main.bundle.js
vendored
Normal file
1
dist/main.bundle.js.map
vendored
Normal file
36
dist/vendor.bundle.js
vendored
1
dist/vendor.bundle.js.map
vendored
Normal file
@ -182,7 +182,7 @@ To contribute to and view your changes to the BitBurner documentation on [Read T
|
|||||||
Docs](http://bitburner.readthedocs.io/), you will
|
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();
|
||||||
|
129
electron/main.js
@ -1,12 +1,19 @@
|
|||||||
/* eslint-disable no-process-exit */
|
/* eslint-disable no-process-exit */
|
||||||
/* eslint-disable @typescript-eslint/no-var-requires */
|
/* eslint-disable @typescript-eslint/no-var-requires */
|
||||||
const { app, dialog, BrowserWindow } = require("electron");
|
const { app, dialog, BrowserWindow, ipcMain } = require("electron");
|
||||||
const log = require("electron-log");
|
const log = require("electron-log");
|
||||||
const greenworks = require("./greenworks");
|
const greenworks = require("./greenworks");
|
||||||
const api = require("./api-server");
|
const api = require("./api-server");
|
||||||
const gameWindow = require("./gameWindow");
|
const gameWindow = require("./gameWindow");
|
||||||
const achievements = require("./achievements");
|
const achievements = require("./achievements");
|
||||||
const utils = require("./utils");
|
const utils = require("./utils");
|
||||||
|
const storage = require("./storage");
|
||||||
|
const debounce = require("lodash/debounce");
|
||||||
|
const Config = require("electron-config");
|
||||||
|
const config = new Config();
|
||||||
|
|
||||||
|
log.transports.file.level = config.get("file-log-level", "info");
|
||||||
|
log.transports.console.level = config.get("console-log-level", "debug");
|
||||||
|
|
||||||
log.catchErrors();
|
log.catchErrors();
|
||||||
log.info(`Started app: ${JSON.stringify(process.argv)}`);
|
log.info(`Started app: ${JSON.stringify(process.argv)}`);
|
||||||
@ -30,6 +37,8 @@ try {
|
|||||||
global.greenworksError = ex.message;
|
global.greenworksError = ex.message;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let isRestoreDisabled = false;
|
||||||
|
|
||||||
function setStopProcessHandler(app, window, enabled) {
|
function setStopProcessHandler(app, window, enabled) {
|
||||||
const closingWindowHandler = async (e) => {
|
const closingWindowHandler = async (e) => {
|
||||||
// We need to prevent the default closing event to add custom logic
|
// We need to prevent the default closing event to add custom logic
|
||||||
@ -41,6 +50,18 @@ function setStopProcessHandler(app, window, enabled) {
|
|||||||
// Shutdown the http server
|
// Shutdown the http server
|
||||||
api.disable();
|
api.disable();
|
||||||
|
|
||||||
|
// Trigger debounced saves right now before closing
|
||||||
|
try {
|
||||||
|
await saveToDisk.flush();
|
||||||
|
} catch (error) {
|
||||||
|
log.error(error);
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
await saveToCloud.flush();
|
||||||
|
} catch (error) {
|
||||||
|
log.error(error);
|
||||||
|
}
|
||||||
|
|
||||||
// Because of a steam limitation, if the player has launched an external browser,
|
// Because of a steam limitation, if the player has launched an external browser,
|
||||||
// steam will keep displaying the game as "Running" in their UI as long as the browser is up.
|
// steam will keep displaying the game as "Running" in their UI as long as the browser is up.
|
||||||
// So we'll alert the player to close their browser.
|
// So we'll alert the player to close their browser.
|
||||||
@ -87,21 +108,106 @@ function setStopProcessHandler(app, window, enabled) {
|
|||||||
process.exit(0);
|
process.exit(0);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const receivedGameReadyHandler = async (event, arg) => {
|
||||||
|
if (!window) {
|
||||||
|
log.warn("Window was undefined in game info handler");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
log.debug("Received game information", arg);
|
||||||
|
window.gameInfo = { ...arg };
|
||||||
|
await storage.prepareSaveFolders(window);
|
||||||
|
|
||||||
|
const restoreNewest = config.get("onload-restore-newest", true);
|
||||||
|
if (restoreNewest && !isRestoreDisabled) {
|
||||||
|
try {
|
||||||
|
await storage.restoreIfNewerExists(window)
|
||||||
|
} catch (error) {
|
||||||
|
log.error("Could not restore newer file", error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const receivedDisableRestoreHandler = async (event, arg) => {
|
||||||
|
if (!window) {
|
||||||
|
log.warn("Window was undefined in disable import handler");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
log.debug(`Disabling auto-restore for ${arg.duration}ms.`);
|
||||||
|
isRestoreDisabled = true;
|
||||||
|
setTimeout(() => {
|
||||||
|
isRestoreDisabled = false;
|
||||||
|
log.debug("Re-enabling auto-restore");
|
||||||
|
}, arg.duration);
|
||||||
|
}
|
||||||
|
|
||||||
|
const receivedGameSavedHandler = async (event, arg) => {
|
||||||
|
if (!window) {
|
||||||
|
log.warn("Window was undefined in game saved handler");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { save, ...other } = arg;
|
||||||
|
log.silly("Received game saved info", {...other, save: `${save.length} bytes`});
|
||||||
|
|
||||||
|
if (storage.isAutosaveEnabled()) {
|
||||||
|
saveToDisk(save, arg.fileName);
|
||||||
|
}
|
||||||
|
if (storage.isCloudEnabled()) {
|
||||||
|
const minimumPlaytime = 1000 * 60 * 15;
|
||||||
|
const playtime = window.gameInfo.player.playtime;
|
||||||
|
log.silly(window.gameInfo);
|
||||||
|
if (playtime > minimumPlaytime) {
|
||||||
|
saveToCloud(save);
|
||||||
|
} else {
|
||||||
|
log.debug(`Auto-save to cloud disabled for save game under ${minimumPlaytime}ms (${playtime}ms)`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const saveToCloud = debounce(async (save) => {
|
||||||
|
log.debug("Saving to Steam Cloud ...")
|
||||||
|
try {
|
||||||
|
const playerId = window.gameInfo.player.identifier;
|
||||||
|
await storage.pushGameSaveToSteamCloud(save, playerId);
|
||||||
|
log.silly("Saved Game to Steam Cloud");
|
||||||
|
} catch (error) {
|
||||||
|
log.error(error);
|
||||||
|
utils.writeToast(window, "Could not save to Steam Cloud.", "error", 5000);
|
||||||
|
}
|
||||||
|
}, config.get("cloud-save-min-time", 1000 * 60 * 15), { leading: true });
|
||||||
|
|
||||||
|
const saveToDisk = debounce(async (save, fileName) => {
|
||||||
|
log.debug("Saving to Disk ...")
|
||||||
|
try {
|
||||||
|
const file = await storage.saveGameToDisk(window, { save, fileName });
|
||||||
|
log.silly(`Saved Game to '${file.replaceAll('\\', '\\\\')}'`);
|
||||||
|
} catch (error) {
|
||||||
|
log.error(error);
|
||||||
|
utils.writeToast(window, "Could not save to disk", "error", 5000);
|
||||||
|
}
|
||||||
|
}, config.get("disk-save-min-time", 1000 * 60 * 5), { leading: true });
|
||||||
|
|
||||||
if (enabled) {
|
if (enabled) {
|
||||||
log.debug('Adding closing handlers');
|
log.debug("Adding closing handlers");
|
||||||
|
ipcMain.on("push-game-ready", receivedGameReadyHandler);
|
||||||
|
ipcMain.on("push-game-saved", receivedGameSavedHandler);
|
||||||
|
ipcMain.on("push-disable-restore", receivedDisableRestoreHandler)
|
||||||
window.on("closed", clearWindowHandler);
|
window.on("closed", clearWindowHandler);
|
||||||
window.on("close", closingWindowHandler)
|
window.on("close", closingWindowHandler)
|
||||||
app.on("window-all-closed", stopProcessHandler);
|
app.on("window-all-closed", stopProcessHandler);
|
||||||
} else {
|
} else {
|
||||||
log.debug('Removing closing handlers');
|
log.debug("Removing closing handlers");
|
||||||
|
ipcMain.removeAllListeners();
|
||||||
window.removeListener("closed", clearWindowHandler);
|
window.removeListener("closed", clearWindowHandler);
|
||||||
window.removeListener("close", closingWindowHandler);
|
window.removeListener("close", closingWindowHandler);
|
||||||
app.removeListener("window-all-closed", stopProcessHandler);
|
app.removeListener("window-all-closed", stopProcessHandler);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function startWindow(noScript) {
|
async function startWindow(noScript) {
|
||||||
gameWindow.createWindow(noScript);
|
return gameWindow.createWindow(noScript);
|
||||||
}
|
}
|
||||||
|
|
||||||
global.app_handlers = {
|
global.app_handlers = {
|
||||||
@ -110,7 +216,7 @@ global.app_handlers = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
app.whenReady().then(async () => {
|
app.whenReady().then(async () => {
|
||||||
log.info('Application is ready!');
|
log.info("Application is ready!");
|
||||||
|
|
||||||
if (process.argv.includes("--export-save")) {
|
if (process.argv.includes("--export-save")) {
|
||||||
const window = new BrowserWindow({ show: false });
|
const window = new BrowserWindow({ show: false });
|
||||||
@ -119,15 +225,14 @@ app.whenReady().then(async () => {
|
|||||||
setStopProcessHandler(app, window, true);
|
setStopProcessHandler(app, window, true);
|
||||||
await utils.exportSave(window);
|
await utils.exportSave(window);
|
||||||
} else {
|
} else {
|
||||||
startWindow(process.argv.includes("--no-scripts"));
|
const window = await startWindow(process.argv.includes("--no-scripts"));
|
||||||
}
|
|
||||||
|
|
||||||
if (global.greenworksError) {
|
if (global.greenworksError) {
|
||||||
dialog.showMessageBox({
|
await dialog.showMessageBox(window, {
|
||||||
title: 'Bitburner',
|
title: "Bitburner",
|
||||||
message: 'Could not connect to Steam',
|
message: "Could not connect to Steam",
|
||||||
detail: `${global.greenworksError}\n\nYou won't be able to receive achievements until this is resolved and you restart the game.`,
|
detail: `${global.greenworksError}\n\nYou won't be able to receive achievements until this is resolved and you restart the game.`,
|
||||||
type: 'warning', buttons: ['OK']
|
type: 'warning', buttons: ['OK']
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
171
electron/menu.js
@ -1,11 +1,169 @@
|
|||||||
/* eslint-disable @typescript-eslint/no-var-requires */
|
/* eslint-disable @typescript-eslint/no-var-requires */
|
||||||
const { Menu, clipboard, dialog } = require("electron");
|
const { app, Menu, clipboard, dialog, shell } = require("electron");
|
||||||
const log = require("electron-log");
|
const log = require("electron-log");
|
||||||
|
const Config = require("electron-config");
|
||||||
const api = require("./api-server");
|
const api = require("./api-server");
|
||||||
const utils = require("./utils");
|
const utils = require("./utils");
|
||||||
|
const storage = require("./storage");
|
||||||
|
const config = new Config();
|
||||||
|
|
||||||
function getMenu(window) {
|
function getMenu(window) {
|
||||||
return Menu.buildFromTemplate([
|
return Menu.buildFromTemplate([
|
||||||
|
{
|
||||||
|
label: "File",
|
||||||
|
submenu: [
|
||||||
|
{
|
||||||
|
label: "Save Game",
|
||||||
|
click: () => window.webContents.send("trigger-save"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "Export Save",
|
||||||
|
click: () => window.webContents.send("trigger-game-export"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "Export Scripts",
|
||||||
|
click: async () => window.webContents.send("trigger-scripts-export"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: "separator",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "Load Last Save",
|
||||||
|
click: async () => {
|
||||||
|
try {
|
||||||
|
const saveGame = await storage.loadLastFromDisk(window);
|
||||||
|
window.webContents.send("push-save-request", { save: saveGame });
|
||||||
|
} catch (error) {
|
||||||
|
log.error(error);
|
||||||
|
utils.writeToast(window, "Could not load last save from disk", "error", 5000);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "Load From File",
|
||||||
|
click: async () => {
|
||||||
|
const defaultPath = await storage.getSaveFolder(window);
|
||||||
|
const result = await dialog.showOpenDialog(window, {
|
||||||
|
title: "Load From File",
|
||||||
|
defaultPath: defaultPath,
|
||||||
|
buttonLabel: "Load",
|
||||||
|
filters: [
|
||||||
|
{ name: "Game Saves", extensions: ["json", "json.gz", "txt"] },
|
||||||
|
{ name: "All", extensions: ["*"] },
|
||||||
|
],
|
||||||
|
properties: [
|
||||||
|
"openFile", "dontAddToRecent",
|
||||||
|
]
|
||||||
|
});
|
||||||
|
if (result.canceled) return;
|
||||||
|
const file = result.filePaths[0];
|
||||||
|
|
||||||
|
try {
|
||||||
|
const saveGame = await storage.loadFileFromDisk(file);
|
||||||
|
window.webContents.send("push-save-request", { save: saveGame });
|
||||||
|
} catch (error) {
|
||||||
|
log.error(error);
|
||||||
|
utils.writeToast(window, "Could not load save from disk", "error", 5000);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "Load From Steam Cloud",
|
||||||
|
enabled: storage.isCloudEnabled(),
|
||||||
|
click: async () => {
|
||||||
|
try {
|
||||||
|
const saveGame = await storage.getSteamCloudSaveString();
|
||||||
|
await storage.pushSaveGameForImport(window, saveGame, false);
|
||||||
|
} catch (error) {
|
||||||
|
log.error(error);
|
||||||
|
utils.writeToast(window, "Could not load from Steam Cloud", "error", 5000);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: "separator",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "Compress Disk Saves (.gz)",
|
||||||
|
type: "checkbox",
|
||||||
|
checked: storage.isSaveCompressionEnabled(),
|
||||||
|
click: (menuItem) => {
|
||||||
|
storage.setSaveCompressionConfig(menuItem.checked);
|
||||||
|
utils.writeToast(window,
|
||||||
|
`${menuItem.checked ? "Enabled" : "Disabled"} Save Compression`, "info", 5000);
|
||||||
|
refreshMenu(window);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "Auto-Save to Disk",
|
||||||
|
type: "checkbox",
|
||||||
|
checked: storage.isAutosaveEnabled(),
|
||||||
|
click: (menuItem) => {
|
||||||
|
storage.setAutosaveConfig(menuItem.checked);
|
||||||
|
utils.writeToast(window,
|
||||||
|
`${menuItem.checked ? "Enabled" : "Disabled"} Auto-Save to Disk`, "info", 5000);
|
||||||
|
refreshMenu(window);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "Auto-Save to Steam Cloud",
|
||||||
|
type: "checkbox",
|
||||||
|
enabled: !global.greenworksError,
|
||||||
|
checked: storage.isCloudEnabled(),
|
||||||
|
click: (menuItem) => {
|
||||||
|
storage.setCloudEnabledConfig(menuItem.checked);
|
||||||
|
utils.writeToast(window,
|
||||||
|
`${menuItem.checked ? "Enabled" : "Disabled"} Auto-Save to Steam Cloud`, "info", 5000);
|
||||||
|
refreshMenu(window);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "Restore Newest on Load",
|
||||||
|
type: "checkbox",
|
||||||
|
checked: config.get("onload-restore-newest", true),
|
||||||
|
click: (menuItem) => {
|
||||||
|
config.set("onload-restore-newest", menuItem.checked);
|
||||||
|
utils.writeToast(window,
|
||||||
|
`${menuItem.checked ? "Enabled" : "Disabled"} Restore Newest on Load`, "info", 5000);
|
||||||
|
refreshMenu(window);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: "separator",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "Open Directory",
|
||||||
|
submenu: [
|
||||||
|
{
|
||||||
|
label: "Open Game Directory",
|
||||||
|
click: () => shell.openPath(app.getAppPath()),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "Open Saves Directory",
|
||||||
|
click: async () => {
|
||||||
|
const path = await storage.getSaveFolder(window);
|
||||||
|
shell.openPath(path);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "Open Logs Directory",
|
||||||
|
click: () => shell.openPath(app.getPath("logs")),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "Open Data Directory",
|
||||||
|
click: () => shell.openPath(app.getPath("userData")),
|
||||||
|
},
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: "separator",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "Quit",
|
||||||
|
click: () => app.quit(),
|
||||||
|
},
|
||||||
|
]
|
||||||
|
},
|
||||||
{
|
{
|
||||||
label: "Edit",
|
label: "Edit",
|
||||||
submenu: [
|
submenu: [
|
||||||
@ -163,6 +321,17 @@ function getMenu(window) {
|
|||||||
label: "Activate",
|
label: "Activate",
|
||||||
click: () => window.webContents.openDevTools(),
|
click: () => window.webContents.openDevTools(),
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
label: "Delete Steam Cloud Data",
|
||||||
|
enabled: !global.greenworksError,
|
||||||
|
click: async () => {
|
||||||
|
try {
|
||||||
|
await storage.deleteCloudFile();
|
||||||
|
} catch (error) {
|
||||||
|
log.error(error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
]);
|
]);
|
||||||
|
13
electron/package-lock.json
generated
@ -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 };
|
34502
input/bitburner.api.json
@ -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"
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
24
markdown/bitburner.corporation.buybackshares.md
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
|
||||||
|
|
||||||
|
[Home](./index.md) > [bitburner](./bitburner.md) > [Corporation](./bitburner.corporation.md) > [buyBackShares](./bitburner.corporation.buybackshares.md)
|
||||||
|
|
||||||
|
## Corporation.buyBackShares() method
|
||||||
|
|
||||||
|
Buyback Shares
|
||||||
|
|
||||||
|
<b>Signature:</b>
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
buyBackShares(amount: number): void;
|
||||||
|
```
|
||||||
|
|
||||||
|
## Parameters
|
||||||
|
|
||||||
|
| Parameter | Type | Description |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| amount | number | Amount of shares to buy back. |
|
||||||
|
|
||||||
|
<b>Returns:</b>
|
||||||
|
|
||||||
|
void
|
||||||
|
|
@ -19,6 +19,7 @@ export interface Corporation extends WarehouseAPI, OfficeAPI
|
|||||||
| --- | --- |
|
| --- | --- |
|
||||||
| [acceptInvestmentOffer()](./bitburner.corporation.acceptinvestmentoffer.md) | Accept investment based on you companies current valuation |
|
| [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 |
|
||||||
|
|
||||||
|
24
markdown/bitburner.corporation.sellshares.md
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
|
||||||
|
|
||||||
|
[Home](./index.md) > [bitburner](./bitburner.md) > [Corporation](./bitburner.corporation.md) > [sellShares](./bitburner.corporation.sellshares.md)
|
||||||
|
|
||||||
|
## Corporation.sellShares() method
|
||||||
|
|
||||||
|
Sell Shares
|
||||||
|
|
||||||
|
<b>Signature:</b>
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
sellShares(amount: number): void;
|
||||||
|
```
|
||||||
|
|
||||||
|
## Parameters
|
||||||
|
|
||||||
|
| Parameter | Type | Description |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| amount | number | Amount of shares to sell. |
|
||||||
|
|
||||||
|
<b>Returns:</b>
|
||||||
|
|
||||||
|
void
|
||||||
|
|
27
markdown/bitburner.warehouseapi.bulkpurchase.md
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
|
||||||
|
|
||||||
|
[Home](./index.md) > [bitburner](./bitburner.md) > [WarehouseAPI](./bitburner.warehouseapi.md) > [bulkPurchase](./bitburner.warehouseapi.bulkpurchase.md)
|
||||||
|
|
||||||
|
## WarehouseAPI.bulkPurchase() method
|
||||||
|
|
||||||
|
Set material to bulk buy
|
||||||
|
|
||||||
|
<b>Signature:</b>
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
bulkPurchase(divisionName: string, cityName: string, materialName: string, amt: number): void;
|
||||||
|
```
|
||||||
|
|
||||||
|
## Parameters
|
||||||
|
|
||||||
|
| Parameter | Type | Description |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| divisionName | string | Name of the division |
|
||||||
|
| cityName | string | Name of the city |
|
||||||
|
| materialName | string | Name of the material |
|
||||||
|
| amt | number | Amount of material to buy |
|
||||||
|
|
||||||
|
<b>Returns:</b>
|
||||||
|
|
||||||
|
void
|
||||||
|
|
1924
package-lock.json
generated
32
package.json
@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "bitburner",
|
"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
|
6
src/@types/global.d.ts
vendored
@ -1,2 +1,8 @@
|
|||||||
// Defined by webpack on startup or compilation
|
// Defined by webpack on startup or compilation
|
||||||
declare let __COMMIT_HASH__: string;
|
declare let __COMMIT_HASH__: string;
|
||||||
|
|
||||||
|
// When using file-loader, we'll get a path to the resource
|
||||||
|
declare module "*.png" {
|
||||||
|
const value: string;
|
||||||
|
export default value;
|
||||||
|
}
|
||||||
|
@ -2050,7 +2050,7 @@ function initAugmentations(): void {
|
|||||||
info:
|
info:
|
||||||
"A brain implant carefully assembled around the synapses, which " +
|
"A brain implant carefully assembled around the synapses, which " +
|
||||||
"micromanages the activity and levels of various neuroreceptor " +
|
"micromanages the activity and levels of various neuroreceptor " +
|
||||||
"chemicals and modulates electrical acvitiy to optimize concentration, " +
|
"chemicals and modulates electrical activity to optimize concentration, " +
|
||||||
"allowing the user to multitask much more effectively.",
|
"allowing the user to multitask much more effectively.",
|
||||||
stats: (
|
stats: (
|
||||||
<>
|
<>
|
||||||
|
@ -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,6 +170,47 @@ export function BitverseRoot(props: IProps): React.ReactElement {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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 (
|
return (
|
||||||
// prettier-ignore
|
// prettier-ignore
|
||||||
<>
|
<>
|
||||||
@ -208,6 +268,6 @@ export function BitverseRoot(props: IProps): React.ReactElement {
|
|||||||
]} />
|
]} />
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
}
|
||||||
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 (
|
||||||
<>
|
<>
|
||||||
|
@ -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 **
|
||||||
|
|
||||||
|
* 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. **
|
** 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.
|
* 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) {
|
|
||||||
if (this.employees[i].pos === EmployeePositions.Unassigned) {
|
|
||||||
unassignedCount++;
|
|
||||||
} else if (this.employees[i].pos === job) {
|
|
||||||
jobCount++;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if ((jobCount + unassignedCount) < amount) return false;
|
for (const employee of this.employees) {
|
||||||
|
if (jobCount == amount) return true
|
||||||
for (let i = 0; i < this.employees.length; ++i) {
|
if (employee.pos === EmployeePositions.Unassigned && jobCount <= amount) {
|
||||||
if (this.employees[i].pos === EmployeePositions.Unassigned) {
|
employee.pos = job;
|
||||||
if (jobCount <= amount) {
|
|
||||||
this.employees[i].pos = job;
|
|
||||||
jobCount++;
|
jobCount++;
|
||||||
unassignedCount--;
|
} else if (employee.pos === job && jobCount >= amount) {
|
||||||
}
|
employee.pos = EmployeePositions.Unassigned;
|
||||||
if (jobCount === amount) break;
|
|
||||||
} else if (this.employees[i].pos === job) {
|
|
||||||
if (jobCount >= amount) {
|
|
||||||
this.employees[i].pos = EmployeePositions.Unassigned;
|
|
||||||
jobCount--;
|
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);
|
|
||||||
console.warn("Converting to number now");
|
|
||||||
const res = corp.issuedShares;
|
|
||||||
if (isNaN(res)) {
|
|
||||||
corp.issuedShares = 0;
|
|
||||||
} else {
|
|
||||||
corp.issuedShares = res;
|
|
||||||
}
|
}
|
||||||
|
catch (err) {
|
||||||
|
dialogBoxCreate(err + "");
|
||||||
}
|
}
|
||||||
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,20 +431,17 @@ 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>
|
||||||
|
<Box sx={{ display: 'grid', gridTemplateColumns: '1fr', width: 'fit-content' }}>
|
||||||
|
<Box sx={{ gridTemplateColumns: 'repeat(3, 1fr)' }}>
|
||||||
<Tooltip title={<Typography>Automatically hires an employee and gives him/her a random name</Typography>}>
|
<Tooltip title={<Typography>Automatically hires an employee and gives him/her a random name</Typography>}>
|
||||||
<span>
|
|
||||||
<Button disabled={props.office.atCapacity()} onClick={autohireEmployeeButtonOnClick}>
|
<Button disabled={props.office.atCapacity()} onClick={autohireEmployeeButtonOnClick}>
|
||||||
Hire Employee
|
Hire Employee
|
||||||
</Button>
|
</Button>
|
||||||
</span>
|
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
<br />
|
|
||||||
<Tooltip title={<Typography>Upgrade the office's size so that it can hold more employees!</Typography>}>
|
<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)}>
|
<Button disabled={corp.funds < 0} onClick={() => setUpgradeOfficeSizeOpen(true)}>
|
||||||
Upgrade size
|
Upgrade size
|
||||||
</Button>
|
</Button>
|
||||||
</span>
|
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
<UpgradeOfficeSizeModal
|
<UpgradeOfficeSizeModal
|
||||||
rerender={props.rerender}
|
rerender={props.rerender}
|
||||||
@ -457,11 +455,9 @@ export function IndustryOffice(props: IProps): React.ReactElement {
|
|||||||
<Tooltip
|
<Tooltip
|
||||||
title={<Typography>Throw an office party to increase your employee's morale and happiness</Typography>}
|
title={<Typography>Throw an office party to increase your employee's morale and happiness</Typography>}
|
||||||
>
|
>
|
||||||
<span>
|
|
||||||
<Button disabled={corp.funds < 0} onClick={() => setThrowPartyOpen(true)}>
|
<Button disabled={corp.funds < 0} onClick={() => setThrowPartyOpen(true)}>
|
||||||
Throw Party
|
Throw Party
|
||||||
</Button>
|
</Button>
|
||||||
</span>
|
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
<ThrowPartyModal
|
<ThrowPartyModal
|
||||||
rerender={props.rerender}
|
rerender={props.rerender}
|
||||||
@ -472,9 +468,9 @@ export function IndustryOffice(props: IProps): React.ReactElement {
|
|||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<br />
|
</Box>
|
||||||
|
|
||||||
<SwitchButton manualMode={employeeManualAssignMode} switchMode={setEmployeeManualAssignMode} />
|
<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,12 +151,12 @@ function WarehouseRoot(props: IProps): React.ReactElement {
|
|||||||
{numeralWrapper.formatBigNumber(props.warehouse.size)}
|
{numeralWrapper.formatBigNumber(props.warehouse.size)}
|
||||||
</Typography>
|
</Typography>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
|
</Box>
|
||||||
|
|
||||||
<Button disabled={!canAffordUpgrade} onClick={upgradeWarehouseOnClick}>
|
<Button disabled={!canAffordUpgrade} onClick={upgradeWarehouseOnClick}>
|
||||||
Upgrade Warehouse Size -
|
Upgrade Warehouse Size -
|
||||||
<MoneyCost money={sizeUpgradeCost} corp={corp} />
|
<MoneyCost money={sizeUpgradeCost} corp={corp} />
|
||||||
</Button>
|
</Button>
|
||||||
</Box>
|
|
||||||
|
|
||||||
<Typography>This industry uses the following equation for its production: </Typography>
|
<Typography>This industry uses the following equation for its production: </Typography>
|
||||||
<br />
|
<br />
|
||||||
@ -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,6 +89,7 @@ export function Overview({ rerender }: IProps): React.ReactElement {
|
|||||||
<StatsTable rows={multRows} />
|
<StatsTable rows={multRows} />
|
||||||
<br />
|
<br />
|
||||||
<BonusTime />
|
<BonusTime />
|
||||||
|
<Box sx={{ display: 'grid', gridTemplateColumns: 'repeat(3, 1fr)', width: 'fit-content' }}>
|
||||||
<Tooltip
|
<Tooltip
|
||||||
title={
|
title={
|
||||||
<Typography>
|
<Typography>
|
||||||
@ -102,6 +103,7 @@ export function Overview({ rerender }: IProps): React.ReactElement {
|
|||||||
</Tooltip>
|
</Tooltip>
|
||||||
{corp.public ? <PublicButtons rerender={rerender} /> : <PrivateButtons rerender={rerender} />}
|
{corp.public ? <PublicButtons rerender={rerender} /> : <PrivateButtons rerender={rerender} />}
|
||||||
<BribeButton />
|
<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 />
|
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -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,34 +54,18 @@ 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 {
|
||||||
if (event.keyCode === 13) bulkPurchase();
|
if (event.keyCode === 13) bulkPurchase();
|
||||||
@ -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;
|
||||||
@ -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,28 +48,9 @@ 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];
|
|
||||||
const newSharesUntilUpdate = stockSaleResults[2];
|
|
||||||
|
|
||||||
corp.numShares -= shares;
|
|
||||||
if (isNaN(corp.issuedShares)) {
|
|
||||||
console.error(`Corporation issuedShares is NaN: ${corp.issuedShares}`);
|
|
||||||
const res = corp.issuedShares;
|
|
||||||
if (isNaN(res)) {
|
|
||||||
corp.issuedShares = 0;
|
|
||||||
} else {
|
|
||||||
corp.issuedShares = res;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
corp.issuedShares += shares;
|
|
||||||
corp.sharePrice = newSharePrice;
|
|
||||||
corp.shareSalesUntilPriceUpdate = newSharesUntilUpdate;
|
|
||||||
corp.shareSaleCooldown = CorporationConstants.SellSharesCooldown;
|
|
||||||
player.gainMoney(profit, "corporation");
|
|
||||||
props.onClose();
|
props.onClose();
|
||||||
dialogBoxCreate(
|
dialogBoxCreate(
|
||||||
<>
|
<>
|
||||||
@ -80,6 +61,10 @@ export function SellSharesModal(props: IProps): React.ReactElement {
|
|||||||
);
|
);
|
||||||
|
|
||||||
props.rerender();
|
props.rerender();
|
||||||
|
} catch (err) {
|
||||||
|
dialogBoxCreate(err + "");
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
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>
|
||||||
|
210
src/Electron.tsx
@ -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";
|
|
||||||
}
|
}
|
||||||
|
const { success, overwritten } = home.writeToScriptFile(Player, filename, code);
|
||||||
|
let script;
|
||||||
|
if (success) {
|
||||||
|
script = home.getScript(filename);
|
||||||
}
|
}
|
||||||
|
return {
|
||||||
//If the current script does NOT exist, create a new one
|
res: success,
|
||||||
const script = new Script();
|
data: {
|
||||||
script.saveScript(Player, filename, code, "home", home.scripts);
|
overwritten,
|
||||||
home.scripts.push(script);
|
ramUsage: script?.ramUsage,
|
||||||
return "written";
|
},
|
||||||
}
|
};
|
||||||
|
|
||||||
return "not a script file";
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -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
|
||||||
|
@ -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 />
|
||||||
|