merge latest dev
@ -1,27 +1,26 @@
|
||||
node_modules/
|
||||
dist/
|
||||
input/
|
||||
|
||||
.dist
|
||||
.tmp
|
||||
.package
|
||||
|
||||
assets/
|
||||
css/
|
||||
.build
|
||||
.cypress/
|
||||
cypress/
|
||||
|
||||
dist/
|
||||
input/
|
||||
assets/
|
||||
doc/
|
||||
markdown/
|
||||
netscript_tests/
|
||||
scripts/
|
||||
|
||||
|
||||
test/netscript/
|
||||
|
||||
electron/lib
|
||||
electron/greenworks.js
|
||||
src/ThirdParty/*
|
||||
src/JSInterpreter.js
|
||||
tools/engines-check/
|
||||
|
||||
test/*.bundle.*
|
||||
editor.main.js
|
||||
main.bundle.js
|
||||
webpack.config.js
|
||||
webpack.config-test.js
|
||||
|
100
.github/workflows/bump-version.yml
vendored
Normal file
@ -0,0 +1,100 @@
|
||||
name: Bump BitBurner Version
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
version:
|
||||
description: 'Version (format: x.y.z)'
|
||||
required: true
|
||||
versionNumber:
|
||||
description: 'Version Number (for saves migration)'
|
||||
required: true
|
||||
changelog:
|
||||
description: 'Changelog (url that points to RAW markdown)'
|
||||
default: ''
|
||||
buildApp:
|
||||
description: 'Include Application Build'
|
||||
type: boolean
|
||||
default: 'true'
|
||||
required: true
|
||||
buildDoc:
|
||||
description: 'Include Documentation Build'
|
||||
type: boolean
|
||||
default: 'true'
|
||||
required: true
|
||||
prepareRelease:
|
||||
description: 'Prepare Draft Release'
|
||||
type: boolean
|
||||
default: 'true'
|
||||
required: true
|
||||
|
||||
jobs:
|
||||
bumpVersion:
|
||||
name: Bump Version
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: write
|
||||
pull-requests: write
|
||||
steps:
|
||||
- name: Install pandoc dependency
|
||||
run: sudo apt-get install -y pandoc
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v2
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- name: Use Node.js 16.13.1
|
||||
uses: actions/setup-node@v2
|
||||
with:
|
||||
node-version: 16.13.1
|
||||
cache: 'npm'
|
||||
- name: Install NPM dependencies for version updater
|
||||
working-directory: ./tools/bump-version
|
||||
run: npm ci
|
||||
- name: Bump version & update changelogs
|
||||
working-directory: ./tools/bump-version
|
||||
run: |
|
||||
curl ${{ github.event.inputs.changelog }} > changes.md
|
||||
node index.js --version=${{ github.event.inputs.version }} --versionNumber=${{ github.event.inputs.versionNumber }} < changes.md
|
||||
- name: Install NPM dependencies for app
|
||||
if: ${{ github.event.inputs.buildApp == 'true' || github.event.inputs.buildDoc == 'true' }}
|
||||
run: npm ci
|
||||
- name: Build Production App
|
||||
if: ${{ github.event.inputs.buildApp == 'true' }}
|
||||
run: npm run build
|
||||
- name: Build Documentation
|
||||
if: ${{ github.event.inputs.buildDoc == 'true' }}
|
||||
run: npm run doc
|
||||
- name: Commit Files
|
||||
run: |
|
||||
git config --global user.name "GitHub"
|
||||
git config --global user.email "noreply@github.com"
|
||||
git checkout -b bump/v${{ github.event.inputs.version }}
|
||||
git add -A
|
||||
echo "Bump version to v${{ github.event.inputs.version }}" > commitmessage.txt
|
||||
echo "" >> commitmessage.txt
|
||||
cat ./tools/bump-version/changes.md >> commitmessage.txt
|
||||
git commit -F commitmessage.txt
|
||||
git push -u origin bump/v${{ github.event.inputs.version }}
|
||||
- name: Create Pull Request
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
run: |
|
||||
gh pr create \
|
||||
--base "${{ github.ref_name }}" \
|
||||
--head "bump/v${{ github.event.inputs.version }}" \
|
||||
--title "Bump version to v${{ github.event.inputs.version }}" \
|
||||
--body-file ./tools/bump-version/changes.md
|
||||
- name: Prepare release
|
||||
if: ${{ github.event.inputs.prepareRelease == 'true' }}
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
run: |
|
||||
RELEASE_TITLE="$(head -n 1 ./tools/bump-version/changes.md | sed 's/## //')"
|
||||
RELEASE_TITLE="${RELEASE_TITLE:-v${{ github.event.inputs.version }}}"
|
||||
gh release create \
|
||||
v${{ github.event.inputs.version }} \
|
||||
--target dev \
|
||||
--title "$RELEASE_TITLE" \
|
||||
--notes-file ./tools/bump-version/changes.md \
|
||||
--generate-notes \
|
||||
--draft
|
2
.github/workflows/ci.yml
vendored
@ -51,5 +51,5 @@ jobs:
|
||||
cache: 'npm'
|
||||
- name: Install npm dependencies
|
||||
run: npm ci
|
||||
- name: Run linter
|
||||
- name: Run tests
|
||||
run: npm run test
|
||||
|
44
.github/workflows/fetch-changes.yml
vendored
Normal file
@ -0,0 +1,44 @@
|
||||
name: Fetch Merged Changes
|
||||
on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
fromCommit:
|
||||
description: 'From Commit SHA (full-length)'
|
||||
required: true
|
||||
toCommit:
|
||||
description: 'To Commit SHA (full-length, if omitted will use latest)'
|
||||
|
||||
jobs:
|
||||
fetchChangelog:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- name: Use Node v16.13.1
|
||||
uses: actions/setup-node@v2
|
||||
with:
|
||||
node-version: 16.13.1
|
||||
cache: 'npm'
|
||||
- name: Install NPM dependencies
|
||||
working-directory: ./tools/fetch-changelog
|
||||
run: npm ci
|
||||
- name: Fetch Changes from GitHub API
|
||||
working-directory: ./tools/fetch-changelog
|
||||
env:
|
||||
GITHUB_API_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
run: |
|
||||
node index.js \
|
||||
--from=${{ github.event.inputs.fromCommit }} \
|
||||
--to=${{ github.event.inputs.toCommit }} > changes.md
|
||||
echo
|
||||
echo "============================================================"
|
||||
echo
|
||||
cat changes.md
|
||||
echo
|
||||
echo "============================================================"
|
||||
echo
|
||||
echo "You may want to go to https://gist.github.com/ to upload the final changelog"
|
||||
echo "The next step will require an url because we can't easily pass multiline strings to actions"
|
||||
- uses: actions/upload-artifact@v2
|
||||
with:
|
||||
name: bitburner_changelog___DRAFT.md
|
||||
path: ./tools/fetch-changelog/changes.md
|
2
.gitignore
vendored
@ -5,10 +5,10 @@ Netburner.txt
|
||||
/doc/build
|
||||
/node_modules
|
||||
/electron/node_modules
|
||||
/dist/*.map
|
||||
/test/*.map
|
||||
/test/*.bundle.*
|
||||
/test/*.css
|
||||
/input/bitburner.api.json
|
||||
.cypress
|
||||
|
||||
# tmp folder for electron
|
||||
|
@ -6,7 +6,7 @@ Bitburner is a programming-based [incremental game](https://en.wikipedia.org/wik
|
||||
that revolves around hacking and cyberpunk themes.
|
||||
The game can be played at https://danielyxie.github.io/bitburner or installed through [Steam](https://store.steampowered.com/app/1812820/Bitburner/).
|
||||
|
||||
See the [frequently asked questions](./FAQ.md) for more information . To discuss the game or get help, join the [official discord server](https://discord.gg/TFc3hKD)
|
||||
See the [frequently asked questions](./doc/FAQ.md) for more information . To discuss the game or get help, join the [official discord server](https://discord.gg/TFc3hKD)
|
||||
|
||||
# Documentation
|
||||
|
||||
@ -18,13 +18,13 @@ The [in-game documentation](./markdown/bitburner.md) is generated from the [Type
|
||||
Anyone is welcome to contribute to the documentation by editing the [source
|
||||
files](/doc/source) and then making a pull request with your contributions.
|
||||
For further guidance, please refer to the "As A Documentor" section of
|
||||
[CONTRIBUTING](CONTRIBUTING.md).
|
||||
[CONTRIBUTING](./doc/CONTRIBUTING.md).
|
||||
|
||||
# Contribution
|
||||
|
||||
There are many ways to contribute to the game. It can be as simple as fixing
|
||||
a typo, correcting a bug, or improving the UI. For guidance on doing so,
|
||||
please refer to the [CONTRIBUTING](CONTRIBUTING.md) document.
|
||||
please refer to the [CONTRIBUTING](./doc/CONTRIBUTING.md) document.
|
||||
|
||||
You will retain all ownership of the Copyright of any contributions you make,
|
||||
and will have the same rights to use or license your contributions. By
|
||||
|
BIN
assets/favicon.ico
Normal file
After Width: | Height: | Size: 15 KiB |
12
cypress.json
@ -1,10 +1,14 @@
|
||||
{
|
||||
"baseUrl": "http://localhost:8000",
|
||||
"fixturesFolder": false,
|
||||
"trashAssetsBeforeRuns": true,
|
||||
"screenshotsFolder": ".cypress/screenshots",
|
||||
"videosFolder": ".cypress/videos",
|
||||
"videoUploadOnPasses": false,
|
||||
"viewportWidth": 1980,
|
||||
"viewportHeight": 1080
|
||||
"viewportHeight": 1080,
|
||||
"fixturesFolder": "test/cypress/fixtures",
|
||||
"integrationFolder": "test/cypress/integration",
|
||||
"pluginsFile": "test/cypress/plugins/index.js",
|
||||
"supportFile": "test/cypress/support/index.js",
|
||||
"screenshotsFolder": ".cypress/screenshots",
|
||||
"videosFolder": ".cypress/videos",
|
||||
"downloadsFolder": ".cypress/downloads"
|
||||
}
|
||||
|
@ -1,9 +0,0 @@
|
||||
export {};
|
||||
|
||||
beforeEach(() => {
|
||||
cy.visit("/");
|
||||
cy.clearLocalStorage();
|
||||
cy.window().then((win) => {
|
||||
win.indexedDB.deleteDatabase("bitburnerSave");
|
||||
});
|
||||
});
|
66
dist/bitburner.d.ts
vendored
@ -1173,6 +1173,16 @@ export declare interface Fragment {
|
||||
limit: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Game Information
|
||||
* @internal
|
||||
*/
|
||||
export declare interface GameInfo {
|
||||
version: string;
|
||||
commit: string;
|
||||
platform: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gang API
|
||||
* @remarks
|
||||
@ -3539,9 +3549,9 @@ export declare interface NS extends Singularity {
|
||||
* @remarks
|
||||
* RAM cost: 0.1 GB
|
||||
*
|
||||
* Returns the server’s instrinsic “growth parameter”. This growth
|
||||
* parameter is a number between 0 and 100 that represents how
|
||||
* quickly the server’s money grows. This parameter affects the
|
||||
* Returns the server’s intrinsic “growth parameter”. This growth
|
||||
* parameter is a number typically between 0 and 100 that represents
|
||||
* how quickly the server’s money grows. This parameter affects the
|
||||
* percentage by which the server’s money is increased when using the
|
||||
* grow function. A higher growth parameter will result in a
|
||||
* higher percentage increase from grow.
|
||||
@ -4430,6 +4440,23 @@ export declare interface NS extends Singularity {
|
||||
* ```
|
||||
*/
|
||||
flags(schema: [string, string | number | boolean | string[]][]): any;
|
||||
|
||||
/**
|
||||
* Share your computer with your factions.
|
||||
* @remarks
|
||||
* RAM cost: 2.4 GB
|
||||
*
|
||||
* Increases your rep gain of hacking contracts while share is called.
|
||||
* Scales with thread count.
|
||||
*/
|
||||
share(): Promise<void>;
|
||||
|
||||
/**
|
||||
* Calculate your share power. Based on all the active share calls.
|
||||
* @remarks
|
||||
* RAM cost: 0.2 GB
|
||||
*/
|
||||
getSharePower(): number;
|
||||
}
|
||||
|
||||
/**
|
||||
@ -5974,7 +6001,7 @@ export declare interface Stanek {
|
||||
/**
|
||||
* List possible fragments.
|
||||
* @remarks
|
||||
* RAM cost: cost: 0 GB
|
||||
* RAM cost: 0 GB
|
||||
*
|
||||
* @returns List of possible fragments.
|
||||
*/
|
||||
@ -5983,7 +6010,7 @@ export declare interface Stanek {
|
||||
/**
|
||||
* List of fragments in Stanek's Gift.
|
||||
* @remarks
|
||||
* RAM cost: cost: 5 GB
|
||||
* RAM cost: 5 GB
|
||||
*
|
||||
* @returns List of active fragments placed on Stanek's Gift.
|
||||
*/
|
||||
@ -5992,14 +6019,14 @@ export declare interface Stanek {
|
||||
/**
|
||||
* Clear the board of all fragments.
|
||||
* @remarks
|
||||
* RAM cost: cost: 0 GB
|
||||
* RAM cost: 0 GB
|
||||
*/
|
||||
clear(): void;
|
||||
|
||||
/**
|
||||
* Check if fragment can be placed at specified location.
|
||||
* @remarks
|
||||
* RAM cost: cost: 0.5 GB
|
||||
* RAM cost: 0.5 GB
|
||||
*
|
||||
* @param rootX - rootX Root X against which to align the top left of the fragment.
|
||||
* @param rootY - rootY Root Y against which to align the top left of the fragment.
|
||||
@ -6011,7 +6038,7 @@ export declare interface Stanek {
|
||||
/**
|
||||
* Place fragment on Stanek's Gift.
|
||||
* @remarks
|
||||
* RAM cost: cost: 5 GB
|
||||
* RAM cost: 5 GB
|
||||
*
|
||||
* @param rootX - X against which to align the top left of the fragment.
|
||||
* @param rootY - Y against which to align the top left of the fragment.
|
||||
@ -6023,7 +6050,7 @@ export declare interface Stanek {
|
||||
/**
|
||||
* Get placed fragment at location.
|
||||
* @remarks
|
||||
* RAM cost: cost: 5 GB
|
||||
* RAM cost: 5 GB
|
||||
*
|
||||
* @param rootX - X against which to align the top left of the fragment.
|
||||
* @param rootY - Y against which to align the top left of the fragment.
|
||||
@ -6034,7 +6061,7 @@ export declare interface Stanek {
|
||||
/**
|
||||
* Remove fragment at location.
|
||||
* @remarks
|
||||
* RAM cost: cost: 0.15 GB
|
||||
* RAM cost: 0.15 GB
|
||||
*
|
||||
* @param rootX - X against which to align the top left of the fragment.
|
||||
* @param rootY - Y against which to align the top left of the fragment.
|
||||
@ -6439,7 +6466,7 @@ export declare interface UserInterface {
|
||||
/**
|
||||
* Get the current theme
|
||||
* @remarks
|
||||
* RAM cost: cost: 0 GB
|
||||
* RAM cost: 0 GB
|
||||
*
|
||||
* @returns An object containing the theme's colors
|
||||
*/
|
||||
@ -6448,7 +6475,7 @@ export declare interface UserInterface {
|
||||
/**
|
||||
* Sets the current theme
|
||||
* @remarks
|
||||
* RAM cost: cost: 0 GB
|
||||
* RAM cost: 0 GB
|
||||
* @example
|
||||
* Usage example (NS2)
|
||||
* ```ts
|
||||
@ -6462,14 +6489,14 @@ export declare interface UserInterface {
|
||||
/**
|
||||
* Resets the player's theme to the default values
|
||||
* @remarks
|
||||
* RAM cost: cost: 0 GB
|
||||
* RAM cost: 0 GB
|
||||
*/
|
||||
resetTheme(): void;
|
||||
|
||||
/**
|
||||
* Get the current styles
|
||||
* @remarks
|
||||
* RAM cost: cost: 0 GB
|
||||
* RAM cost: 0 GB
|
||||
*
|
||||
* @returns An object containing the player's styles
|
||||
*/
|
||||
@ -6478,7 +6505,7 @@ export declare interface UserInterface {
|
||||
/**
|
||||
* Sets the current styles
|
||||
* @remarks
|
||||
* RAM cost: cost: 0 GB
|
||||
* RAM cost: 0 GB
|
||||
* @example
|
||||
* Usage example (NS2)
|
||||
* ```ts
|
||||
@ -6492,9 +6519,16 @@ export declare interface UserInterface {
|
||||
/**
|
||||
* Resets the player's styles to the default values
|
||||
* @remarks
|
||||
* RAM cost: cost: 0 GB
|
||||
* RAM cost: 0 GB
|
||||
*/
|
||||
resetStyles(): void;
|
||||
|
||||
/**
|
||||
* Gets the current game information (version, commit, ...)
|
||||
* @remarks
|
||||
* RAM cost: 0 GB
|
||||
*/
|
||||
getGameInfo(): GameInfo;
|
||||
}
|
||||
|
||||
/**
|
||||
|
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
44
dist/vendor.bundle.js
vendored
1
dist/vendor.bundle.js.map
vendored
Normal file
@ -182,7 +182,7 @@ To contribute to and view your changes to the BitBurner documentation on [Read T
|
||||
Docs](http://bitburner.readthedocs.io/), you will
|
||||
need to have Python installed, along with [Sphinx](http://www.sphinx-doc.org).
|
||||
|
||||
To make change to the [in-game documentation](./markdown/bitburner.md), you will need to modify the [TypeScript definitions](./src/ScriptEditor/NetscriptDefinitions.d.ts), not the markdown files.
|
||||
To make change to the [in-game documentation](../markdown/bitburner.md), you will need to modify the [TypeScript definitions](../src/ScriptEditor/NetscriptDefinitions.d.ts), not the markdown files.
|
||||
|
||||
We are using [API Extractor](https://api-extractor.com/pages/tsdoc/doc_comment_syntax/) (tsdoc hints) to generate the markdown doc. Make your changes to the TypeScript definitions and then run `npm run doc`.
|
||||
|
@ -28,7 +28,7 @@ List of all Source-Files
|
||||
|| || * Increases the player's charisma and company salary multipliers by 8%/12%/14%. |
|
||||
+-------------------------------------+--------------------------------------------------------------------------------------------------------------------------------------------------------------------------+
|
||||
|| BitNode-4: The Singularity || * Let the player access and use Netscript Singularity Functions in other BitNodes. |
|
||||
|| || * Each level of this Source-File reduces the RAM cost of singularity functions. |
|
||||
|| || * Each level of this Source-File reduces the RAM cost of singularity functions. |
|
||||
+-------------------------------------+--------------------------------------------------------------------------------------------------------------------------------------------------------------------------+
|
||||
|| BitNode-5: Artificial Intelligence || * Unlocks :ref:`gameplay_intelligence`. |
|
||||
|| || * Unlocks :js:func:`getBitNodeMultipliers` and start with Formulas.exe. |
|
||||
|
@ -113,7 +113,7 @@ The list contains the name of (i.e. the value returned by
|
||||
| | | to any position from i to i+n. |
|
||||
| | | |
|
||||
| | | Assuming you are initially positioned at the start of the array, determine |
|
||||
| | | whether you are able to reach the last index of the array. |
|
||||
| | | whether you are able to reach the last index of the array. |
|
||||
+------------------------------------+------------------------------------------------------------------------------------------+
|
||||
| Merge Overlapping Intervals | | Given an array of intervals, merge all overlapping intervals. An interval |
|
||||
| | | is an array with two numbers, where the first number is always less than |
|
||||
|
@ -30,6 +30,11 @@ from faction to faction.
|
||||
|
||||
List of Factions and their Requirements
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
.. raw:: html
|
||||
|
||||
<details><summary><a>Early Game Factions</a></summary>
|
||||
|
||||
.. _gameplay_factions::
|
||||
|
||||
+---------------------+----------------+-----------------------------------------+-------------------------------+
|
||||
| Early Game | Faction Name | Requirements | Joining this Faction prevents |
|
||||
@ -46,7 +51,18 @@ List of Factions and their Requirements
|
||||
| | | * Total Hacknet RAM of 8 | |
|
||||
| | | * Total Hacknet Cores of 4 | |
|
||||
+---------------------+----------------+-----------------------------------------+-------------------------------+
|
||||
| City Factions | Sector-12 | * Be in Sector-12 | * Chongqing |
|
||||
.. raw:: html
|
||||
|
||||
</details>
|
||||
<details><summary><a>City Factions</a></summary>
|
||||
|
||||
.. _gameplay_factions::
|
||||
|
||||
+---------------------+----------------+-----------------------------------------+-------------------------------+
|
||||
| City Factions | Faction Name | Requirements | Joining this Faction prevents |
|
||||
| | | | you from joining: |
|
||||
+ +----------------+-----------------------------------------+-------------------------------+
|
||||
| | Sector-12 | * Be in Sector-12 | * Chongqing |
|
||||
| | | * $15m | * New Tokyo |
|
||||
| | | | * Ishima |
|
||||
| | | | * Volhaven |
|
||||
@ -74,8 +90,19 @@ List of Factions and their Requirements
|
||||
| | | | * New Tokyo |
|
||||
| | | | * Ishima |
|
||||
+---------------------+----------------+-----------------------------------------+-------------------------------+
|
||||
| Hacking | NiteSec | * Install a backdoor on the avmnite-02h | |
|
||||
| Groups | | server | |
|
||||
.. raw:: html
|
||||
|
||||
</details>
|
||||
<details><summary><a>Hacking Groups</a></summary>
|
||||
|
||||
.. _gameplay_factions::
|
||||
|
||||
+---------------------+----------------+-----------------------------------------+-------------------------------+
|
||||
| Hacking | Faction Name | Requirements | Joining this Faction prevents |
|
||||
| Groups | | | you from joining: |
|
||||
+ +----------------+-----------------------------------------+-------------------------------+
|
||||
| | NiteSec | * Install a backdoor on the avmnite-02h | |
|
||||
| | | server | |
|
||||
| | | | |
|
||||
+ +----------------+-----------------------------------------+-------------------------------+
|
||||
| | The Black Hand | * Install a backdoor on the I.I.I.I | |
|
||||
@ -86,7 +113,18 @@ List of Factions and their Requirements
|
||||
| | | server | |
|
||||
| | | | |
|
||||
+---------------------+----------------+-----------------------------------------+-------------------------------+
|
||||
| Megacorporations | ECorp | * Have 200k reputation with | |
|
||||
.. raw:: html
|
||||
|
||||
</details>
|
||||
<details><summary><a>Megacorporations</a></summary>
|
||||
|
||||
.. _gameplay_factions::
|
||||
|
||||
+---------------------+----------------+-----------------------------------------+-------------------------------+
|
||||
| Megacorporations | Faction Name | Requirements | Joining this Faction prevents |
|
||||
| | | | you from joining: |
|
||||
+ +----------------+-----------------------------------------+-------------------------------+
|
||||
| | ECorp | * Have 200k reputation with | |
|
||||
| | | the Corporation | |
|
||||
+ +----------------+-----------------------------------------+-------------------------------+
|
||||
| | MegaCorp | * Have 200k reputation with | |
|
||||
@ -118,8 +156,19 @@ List of Factions and their Requirements
|
||||
| | | * Install a backdoor on the | |
|
||||
| | | fulcrumassets server | |
|
||||
+---------------------+----------------+-----------------------------------------+-------------------------------+
|
||||
| Criminal | Slum Snakes | * All Combat Stats of 30 | |
|
||||
| Organizations | | * -9 Karma | |
|
||||
.. raw:: html
|
||||
|
||||
</details>
|
||||
<details><summary><a>Criminal Organizations</a></summary>
|
||||
|
||||
.. _gameplay_factions::
|
||||
|
||||
+---------------------+----------------+-----------------------------------------+-------------------------------+
|
||||
| Criminal | Faction Name | Requirements | Joining this Faction prevents |
|
||||
| Organizations | | | you from joining: |
|
||||
+ +----------------+-----------------------------------------+-------------------------------+
|
||||
| | Slum Snakes | * All Combat Stats of 30 | |
|
||||
| | | * -9 Karma | |
|
||||
| | | * $1m | |
|
||||
+ +----------------+-----------------------------------------+-------------------------------+
|
||||
| | Tetrads | * Be in Chongqing, New Tokyo, or Ishima | |
|
||||
@ -150,8 +199,19 @@ List of Factions and their Requirements
|
||||
| | | * -90 Karma | |
|
||||
| | | * Not working for CIA or NSA | |
|
||||
+---------------------+----------------+-----------------------------------------+-------------------------------+
|
||||
| Endgame | The Covenant | * 20 Augmentations | |
|
||||
| Factions | | * $75b | |
|
||||
.. raw:: html
|
||||
|
||||
</details>
|
||||
<details><summary><a>Endgame Factions</a></summary>
|
||||
|
||||
.. _gameplay_factions::
|
||||
|
||||
+---------------------+----------------+-----------------------------------------+-------------------------------+
|
||||
| Endgame | Faction Name | Requirements | Joining this Faction prevents |
|
||||
| Factions | | | you from joining: |
|
||||
+ +----------------+-----------------------------------------+-------------------------------+
|
||||
| | The Covenant | * 20 Augmentations | |
|
||||
| | | * $75b | |
|
||||
| | | * Hacking Level of 850 | |
|
||||
| | | * All Combat Stats of 850 | |
|
||||
+ +----------------+-----------------------------------------+-------------------------------+
|
||||
@ -165,3 +225,6 @@ List of Factions and their Requirements
|
||||
| | | * Hacking Level of 1500 | |
|
||||
| | | * All Combat Stats of 1200 | |
|
||||
+---------------------+----------------+-----------------------------------------+-------------------------------+
|
||||
.. raw:: html
|
||||
|
||||
</details><br>
|
||||
|
@ -23,4 +23,4 @@ Bug
|
||||
---
|
||||
|
||||
Otherwise, the game is probably frozen/stuck due to a bug. To report a bug, follow
|
||||
the guidelines `here <https://github.com/danielyxie/bitburner/blob/master/CONTRIBUTING.md#reporting-bugs>`_.
|
||||
the guidelines `here <https://github.com/danielyxie/bitburner/blob/master/doc/CONTRIBUTING.md#reporting-bugs>`_.
|
||||
|
@ -9,7 +9,7 @@ async function enableAchievementsInterval(window) {
|
||||
// This is backward but the game fills in an array called `document.achievements` and we retrieve it from
|
||||
// here. Hey if it works it works.
|
||||
const steamAchievements = greenworks.getAchievementNames();
|
||||
log.debug(`All Steam achievements ${JSON.stringify(steamAchievements)}`);
|
||||
log.silly(`All Steam achievements ${JSON.stringify(steamAchievements)}`);
|
||||
const playerAchieved = (await Promise.all(steamAchievements.map(checkSteamAchievement))).filter(name => !!name);
|
||||
log.debug(`Player has Steam achievements ${JSON.stringify(playerAchieved)}`);
|
||||
const intervalID = setInterval(async () => {
|
||||
|
@ -12,11 +12,13 @@ async function initialize(win) {
|
||||
window = win;
|
||||
server = http.createServer(async function (req, res) {
|
||||
let body = "";
|
||||
res.setHeader('Content-Type', 'application/json');
|
||||
|
||||
req.on("data", (chunk) => {
|
||||
body += chunk.toString(); // convert Buffer to string
|
||||
});
|
||||
req.on("end", () => {
|
||||
|
||||
req.on("end", async () => {
|
||||
const providedToken = req.headers?.authorization?.replace('Bearer ', '') ?? '';
|
||||
const isValid = providedToken === getAuthenticationToken();
|
||||
if (isValid) {
|
||||
@ -24,8 +26,11 @@ async function initialize(win) {
|
||||
} else {
|
||||
log.log('Invalid authentication token');
|
||||
res.writeHead(401);
|
||||
res.write('Invalid authentication token');
|
||||
res.end();
|
||||
|
||||
res.end(JSON.stringify({
|
||||
success: false,
|
||||
msg: 'Invalid authentication token'
|
||||
}));
|
||||
return;
|
||||
}
|
||||
|
||||
@ -35,17 +40,56 @@ async function initialize(win) {
|
||||
} catch (error) {
|
||||
log.warn(`Invalid body data`);
|
||||
res.writeHead(400);
|
||||
res.write('Invalid body data');
|
||||
res.end();
|
||||
res.end(JSON.stringify({
|
||||
success: false,
|
||||
msg: 'Invalid body data'
|
||||
}));
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (data) {
|
||||
window.webContents.executeJavaScript(`document.saveFile("${data.filename}", "${data.code}")`).then((result) => {
|
||||
res.write(result);
|
||||
res.end();
|
||||
});
|
||||
let result;
|
||||
switch(req.method) {
|
||||
// Request files
|
||||
case "GET":
|
||||
result = await window.webContents.executeJavaScript(`document.getFiles()`);
|
||||
break;
|
||||
|
||||
// Create or update files
|
||||
// Support POST for VScode implementation
|
||||
case "POST":
|
||||
case "PUT":
|
||||
if (!data) {
|
||||
log.warn(`Invalid script update request - No data`);
|
||||
res.writeHead(400);
|
||||
res.end(JSON.stringify({
|
||||
success: false,
|
||||
msg: 'Invalid script update request - No data'
|
||||
}));
|
||||
return;
|
||||
}
|
||||
|
||||
result = await window.webContents.executeJavaScript(`document.saveFile("${data.filename}", "${data.code}")`);
|
||||
break;
|
||||
|
||||
// Delete files
|
||||
case "DELETE":
|
||||
result = await window.webContents.executeJavaScript(`document.deleteFile("${data.filename}")`);
|
||||
break;
|
||||
}
|
||||
|
||||
if (!result.res) {
|
||||
//We've encountered an error
|
||||
res.writeHead(400);
|
||||
log.warn(`Api Server Error`, result.msg);
|
||||
}
|
||||
|
||||
res.end(JSON.stringify({
|
||||
success: result.res,
|
||||
msg: result.msg,
|
||||
data: result.data
|
||||
}));
|
||||
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -8,20 +8,42 @@ const api = require("./api-server");
|
||||
const cp = require("child_process");
|
||||
const path = require("path");
|
||||
const fs = require("fs");
|
||||
const { windowTracker } = require("./windowTracker");
|
||||
const { fileURLToPath } = require("url");
|
||||
|
||||
const debug = process.argv.includes("--debug");
|
||||
|
||||
async function createWindow(killall) {
|
||||
const setStopProcessHandler = global.app_handlers.stopProcess;
|
||||
|
||||
let icon;
|
||||
if (process.platform == 'linux') {
|
||||
icon = path.join(__dirname, 'icon.png');
|
||||
}
|
||||
|
||||
const tracker = windowTracker('main');
|
||||
const window = new BrowserWindow({
|
||||
icon,
|
||||
show: false,
|
||||
backgroundThrottling: false,
|
||||
backgroundColor: "#000000",
|
||||
title: 'Bitburner',
|
||||
x: tracker.state.x,
|
||||
y: tracker.state.y,
|
||||
width: tracker.state.width,
|
||||
height: tracker.state.height,
|
||||
minWidth: 600,
|
||||
minHeight: 400,
|
||||
webPreferences: {
|
||||
nativeWindowOpen: true,
|
||||
preload: path.join(__dirname, 'preload.js'),
|
||||
},
|
||||
});
|
||||
|
||||
setTimeout(() => tracker.track(window), 1000);
|
||||
if (tracker.state.isMaximized) window.maximize();
|
||||
|
||||
window.removeMenu();
|
||||
window.maximize();
|
||||
noScripts = killall ? { query: { noScripts: killall } } : {};
|
||||
window.loadFile("index.html", noScripts);
|
||||
window.show();
|
||||
|
137
electron/main.js
@ -1,12 +1,19 @@
|
||||
/* eslint-disable no-process-exit */
|
||||
/* eslint-disable @typescript-eslint/no-var-requires */
|
||||
const { app, dialog, BrowserWindow } = require("electron");
|
||||
const { app, dialog, BrowserWindow, ipcMain } = require("electron");
|
||||
const log = require("electron-log");
|
||||
const greenworks = require("./greenworks");
|
||||
const api = require("./api-server");
|
||||
const gameWindow = require("./gameWindow");
|
||||
const achievements = require("./achievements");
|
||||
const utils = require("./utils");
|
||||
const storage = require("./storage");
|
||||
const debounce = require("lodash/debounce");
|
||||
const Config = require("electron-config");
|
||||
const config = new Config();
|
||||
|
||||
log.transports.file.level = config.get("file-log-level", "info");
|
||||
log.transports.console.level = config.get("console-log-level", "debug");
|
||||
|
||||
log.catchErrors();
|
||||
log.info(`Started app: ${JSON.stringify(process.argv)}`);
|
||||
@ -30,6 +37,8 @@ try {
|
||||
global.greenworksError = ex.message;
|
||||
}
|
||||
|
||||
let isRestoreDisabled = false;
|
||||
|
||||
function setStopProcessHandler(app, window, enabled) {
|
||||
const closingWindowHandler = async (e) => {
|
||||
// We need to prevent the default closing event to add custom logic
|
||||
@ -41,6 +50,18 @@ function setStopProcessHandler(app, window, enabled) {
|
||||
// Shutdown the http server
|
||||
api.disable();
|
||||
|
||||
// Trigger debounced saves right now before closing
|
||||
try {
|
||||
await saveToDisk.flush();
|
||||
} catch (error) {
|
||||
log.error(error);
|
||||
}
|
||||
try {
|
||||
await saveToCloud.flush();
|
||||
} catch (error) {
|
||||
log.error(error);
|
||||
}
|
||||
|
||||
// Because of a steam limitation, if the player has launched an external browser,
|
||||
// steam will keep displaying the game as "Running" in their UI as long as the browser is up.
|
||||
// So we'll alert the player to close their browser.
|
||||
@ -87,21 +108,106 @@ function setStopProcessHandler(app, window, enabled) {
|
||||
process.exit(0);
|
||||
};
|
||||
|
||||
const receivedGameReadyHandler = async (event, arg) => {
|
||||
if (!window) {
|
||||
log.warn("Window was undefined in game info handler");
|
||||
return;
|
||||
}
|
||||
|
||||
log.debug("Received game information", arg);
|
||||
window.gameInfo = { ...arg };
|
||||
await storage.prepareSaveFolders(window);
|
||||
|
||||
const restoreNewest = config.get("onload-restore-newest", true);
|
||||
if (restoreNewest && !isRestoreDisabled) {
|
||||
try {
|
||||
await storage.restoreIfNewerExists(window)
|
||||
} catch (error) {
|
||||
log.error("Could not restore newer file", error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const receivedDisableRestoreHandler = async (event, arg) => {
|
||||
if (!window) {
|
||||
log.warn("Window was undefined in disable import handler");
|
||||
return;
|
||||
}
|
||||
|
||||
log.debug(`Disabling auto-restore for ${arg.duration}ms.`);
|
||||
isRestoreDisabled = true;
|
||||
setTimeout(() => {
|
||||
isRestoreDisabled = false;
|
||||
log.debug("Re-enabling auto-restore");
|
||||
}, arg.duration);
|
||||
}
|
||||
|
||||
const receivedGameSavedHandler = async (event, arg) => {
|
||||
if (!window) {
|
||||
log.warn("Window was undefined in game saved handler");
|
||||
return;
|
||||
}
|
||||
|
||||
const { save, ...other } = arg;
|
||||
log.silly("Received game saved info", {...other, save: `${save.length} bytes`});
|
||||
|
||||
if (storage.isAutosaveEnabled()) {
|
||||
saveToDisk(save, arg.fileName);
|
||||
}
|
||||
if (storage.isCloudEnabled()) {
|
||||
const minimumPlaytime = 1000 * 60 * 15;
|
||||
const playtime = window.gameInfo.player.playtime;
|
||||
log.silly(window.gameInfo);
|
||||
if (playtime > minimumPlaytime) {
|
||||
saveToCloud(save);
|
||||
} else {
|
||||
log.debug(`Auto-save to cloud disabled for save game under ${minimumPlaytime}ms (${playtime}ms)`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const saveToCloud = debounce(async (save) => {
|
||||
log.debug("Saving to Steam Cloud ...")
|
||||
try {
|
||||
const playerId = window.gameInfo.player.identifier;
|
||||
await storage.pushGameSaveToSteamCloud(save, playerId);
|
||||
log.silly("Saved Game to Steam Cloud");
|
||||
} catch (error) {
|
||||
log.error(error);
|
||||
utils.writeToast(window, "Could not save to Steam Cloud.", "error", 5000);
|
||||
}
|
||||
}, config.get("cloud-save-min-time", 1000 * 60 * 15), { leading: true });
|
||||
|
||||
const saveToDisk = debounce(async (save, fileName) => {
|
||||
log.debug("Saving to Disk ...")
|
||||
try {
|
||||
const file = await storage.saveGameToDisk(window, { save, fileName });
|
||||
log.silly(`Saved Game to '${file.replaceAll('\\', '\\\\')}'`);
|
||||
} catch (error) {
|
||||
log.error(error);
|
||||
utils.writeToast(window, "Could not save to disk", "error", 5000);
|
||||
}
|
||||
}, config.get("disk-save-min-time", 1000 * 60 * 5), { leading: true });
|
||||
|
||||
if (enabled) {
|
||||
log.debug('Adding closing handlers');
|
||||
log.debug("Adding closing handlers");
|
||||
ipcMain.on("push-game-ready", receivedGameReadyHandler);
|
||||
ipcMain.on("push-game-saved", receivedGameSavedHandler);
|
||||
ipcMain.on("push-disable-restore", receivedDisableRestoreHandler)
|
||||
window.on("closed", clearWindowHandler);
|
||||
window.on("close", closingWindowHandler)
|
||||
app.on("window-all-closed", stopProcessHandler);
|
||||
} else {
|
||||
log.debug('Removing closing handlers');
|
||||
log.debug("Removing closing handlers");
|
||||
ipcMain.removeAllListeners();
|
||||
window.removeListener("closed", clearWindowHandler);
|
||||
window.removeListener("close", closingWindowHandler);
|
||||
app.removeListener("window-all-closed", stopProcessHandler);
|
||||
}
|
||||
}
|
||||
|
||||
function startWindow(noScript) {
|
||||
gameWindow.createWindow(noScript);
|
||||
async function startWindow(noScript) {
|
||||
return gameWindow.createWindow(noScript);
|
||||
}
|
||||
|
||||
global.app_handlers = {
|
||||
@ -110,7 +216,7 @@ global.app_handlers = {
|
||||
}
|
||||
|
||||
app.whenReady().then(async () => {
|
||||
log.info('Application is ready!');
|
||||
log.info("Application is ready!");
|
||||
|
||||
if (process.argv.includes("--export-save")) {
|
||||
const window = new BrowserWindow({ show: false });
|
||||
@ -119,15 +225,14 @@ app.whenReady().then(async () => {
|
||||
setStopProcessHandler(app, window, true);
|
||||
await utils.exportSave(window);
|
||||
} else {
|
||||
startWindow(process.argv.includes("--no-scripts"));
|
||||
}
|
||||
|
||||
if (global.greenworksError) {
|
||||
dialog.showMessageBox({
|
||||
title: 'Bitburner',
|
||||
message: 'Could not connect to Steam',
|
||||
detail: `${global.greenworksError}\n\nYou won't be able to receive achievements until this is resolved and you restart the game.`,
|
||||
type: 'warning', buttons: ['OK']
|
||||
});
|
||||
const window = await startWindow(process.argv.includes("--no-scripts"));
|
||||
if (global.greenworksError) {
|
||||
await dialog.showMessageBox(window, {
|
||||
title: "Bitburner",
|
||||
message: "Could not connect to Steam",
|
||||
detail: `${global.greenworksError}\n\nYou won't be able to receive achievements until this is resolved and you restart the game.`,
|
||||
type: 'warning', buttons: ['OK']
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
|
171
electron/menu.js
@ -1,11 +1,169 @@
|
||||
/* eslint-disable @typescript-eslint/no-var-requires */
|
||||
const { Menu, clipboard, dialog } = require("electron");
|
||||
const { app, Menu, clipboard, dialog, shell } = require("electron");
|
||||
const log = require("electron-log");
|
||||
const Config = require("electron-config");
|
||||
const api = require("./api-server");
|
||||
const utils = require("./utils");
|
||||
const storage = require("./storage");
|
||||
const config = new Config();
|
||||
|
||||
function getMenu(window) {
|
||||
return Menu.buildFromTemplate([
|
||||
{
|
||||
label: "File",
|
||||
submenu: [
|
||||
{
|
||||
label: "Save Game",
|
||||
click: () => window.webContents.send("trigger-save"),
|
||||
},
|
||||
{
|
||||
label: "Export Save",
|
||||
click: () => window.webContents.send("trigger-game-export"),
|
||||
},
|
||||
{
|
||||
label: "Export Scripts",
|
||||
click: async () => window.webContents.send("trigger-scripts-export"),
|
||||
},
|
||||
{
|
||||
type: "separator",
|
||||
},
|
||||
{
|
||||
label: "Load Last Save",
|
||||
click: async () => {
|
||||
try {
|
||||
const saveGame = await storage.loadLastFromDisk(window);
|
||||
window.webContents.send("push-save-request", { save: saveGame });
|
||||
} catch (error) {
|
||||
log.error(error);
|
||||
utils.writeToast(window, "Could not load last save from disk", "error", 5000);
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
label: "Load From File",
|
||||
click: async () => {
|
||||
const defaultPath = await storage.getSaveFolder(window);
|
||||
const result = await dialog.showOpenDialog(window, {
|
||||
title: "Load From File",
|
||||
defaultPath: defaultPath,
|
||||
buttonLabel: "Load",
|
||||
filters: [
|
||||
{ name: "Game Saves", extensions: ["json", "json.gz", "txt"] },
|
||||
{ name: "All", extensions: ["*"] },
|
||||
],
|
||||
properties: [
|
||||
"openFile", "dontAddToRecent",
|
||||
]
|
||||
});
|
||||
if (result.canceled) return;
|
||||
const file = result.filePaths[0];
|
||||
|
||||
try {
|
||||
const saveGame = await storage.loadFileFromDisk(file);
|
||||
window.webContents.send("push-save-request", { save: saveGame });
|
||||
} catch (error) {
|
||||
log.error(error);
|
||||
utils.writeToast(window, "Could not load save from disk", "error", 5000);
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
label: "Load From Steam Cloud",
|
||||
enabled: storage.isCloudEnabled(),
|
||||
click: async () => {
|
||||
try {
|
||||
const saveGame = await storage.getSteamCloudSaveString();
|
||||
await storage.pushSaveGameForImport(window, saveGame, false);
|
||||
} catch (error) {
|
||||
log.error(error);
|
||||
utils.writeToast(window, "Could not load from Steam Cloud", "error", 5000);
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
type: "separator",
|
||||
},
|
||||
{
|
||||
label: "Compress Disk Saves (.gz)",
|
||||
type: "checkbox",
|
||||
checked: storage.isSaveCompressionEnabled(),
|
||||
click: (menuItem) => {
|
||||
storage.setSaveCompressionConfig(menuItem.checked);
|
||||
utils.writeToast(window,
|
||||
`${menuItem.checked ? "Enabled" : "Disabled"} Save Compression`, "info", 5000);
|
||||
refreshMenu(window);
|
||||
},
|
||||
},
|
||||
{
|
||||
label: "Auto-Save to Disk",
|
||||
type: "checkbox",
|
||||
checked: storage.isAutosaveEnabled(),
|
||||
click: (menuItem) => {
|
||||
storage.setAutosaveConfig(menuItem.checked);
|
||||
utils.writeToast(window,
|
||||
`${menuItem.checked ? "Enabled" : "Disabled"} Auto-Save to Disk`, "info", 5000);
|
||||
refreshMenu(window);
|
||||
},
|
||||
},
|
||||
{
|
||||
label: "Auto-Save to Steam Cloud",
|
||||
type: "checkbox",
|
||||
enabled: !global.greenworksError,
|
||||
checked: storage.isCloudEnabled(),
|
||||
click: (menuItem) => {
|
||||
storage.setCloudEnabledConfig(menuItem.checked);
|
||||
utils.writeToast(window,
|
||||
`${menuItem.checked ? "Enabled" : "Disabled"} Auto-Save to Steam Cloud`, "info", 5000);
|
||||
refreshMenu(window);
|
||||
},
|
||||
},
|
||||
{
|
||||
label: "Restore Newest on Load",
|
||||
type: "checkbox",
|
||||
checked: config.get("onload-restore-newest", true),
|
||||
click: (menuItem) => {
|
||||
config.set("onload-restore-newest", menuItem.checked);
|
||||
utils.writeToast(window,
|
||||
`${menuItem.checked ? "Enabled" : "Disabled"} Restore Newest on Load`, "info", 5000);
|
||||
refreshMenu(window);
|
||||
},
|
||||
},
|
||||
{
|
||||
type: "separator",
|
||||
},
|
||||
{
|
||||
label: "Open Directory",
|
||||
submenu: [
|
||||
{
|
||||
label: "Open Game Directory",
|
||||
click: () => shell.openPath(app.getAppPath()),
|
||||
},
|
||||
{
|
||||
label: "Open Saves Directory",
|
||||
click: async () => {
|
||||
const path = await storage.getSaveFolder(window);
|
||||
shell.openPath(path);
|
||||
},
|
||||
},
|
||||
{
|
||||
label: "Open Logs Directory",
|
||||
click: () => shell.openPath(app.getPath("logs")),
|
||||
},
|
||||
{
|
||||
label: "Open Data Directory",
|
||||
click: () => shell.openPath(app.getPath("userData")),
|
||||
},
|
||||
]
|
||||
},
|
||||
{
|
||||
type: "separator",
|
||||
},
|
||||
{
|
||||
label: "Quit",
|
||||
click: () => app.quit(),
|
||||
},
|
||||
]
|
||||
},
|
||||
{
|
||||
label: "Edit",
|
||||
submenu: [
|
||||
@ -163,6 +321,17 @@ function getMenu(window) {
|
||||
label: "Activate",
|
||||
click: () => window.webContents.openDevTools(),
|
||||
},
|
||||
{
|
||||
label: "Delete Steam Cloud Data",
|
||||
enabled: !global.greenworksError,
|
||||
click: async () => {
|
||||
try {
|
||||
await storage.deleteCloudFile();
|
||||
} catch (error) {
|
||||
log.error(error);
|
||||
}
|
||||
}
|
||||
}
|
||||
],
|
||||
},
|
||||
]);
|
||||
|
13
electron/package-lock.json
generated
@ -9,7 +9,8 @@
|
||||
"version": "1.0.0",
|
||||
"dependencies": {
|
||||
"electron-config": "^2.0.0",
|
||||
"electron-log": "^4.4.4"
|
||||
"electron-log": "^4.4.4",
|
||||
"lodash": "^4.17.21"
|
||||
}
|
||||
},
|
||||
"node_modules/conf": {
|
||||
@ -104,6 +105,11 @@
|
||||
"node": ">=4"
|
||||
}
|
||||
},
|
||||
"node_modules/lodash": {
|
||||
"version": "4.17.21",
|
||||
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz",
|
||||
"integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg=="
|
||||
},
|
||||
"node_modules/make-dir": {
|
||||
"version": "1.3.0",
|
||||
"resolved": "https://registry.npmjs.org/make-dir/-/make-dir-1.3.0.tgz",
|
||||
@ -259,6 +265,11 @@
|
||||
"path-exists": "^3.0.0"
|
||||
}
|
||||
},
|
||||
"lodash": {
|
||||
"version": "4.17.21",
|
||||
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz",
|
||||
"integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg=="
|
||||
},
|
||||
"make-dir": {
|
||||
"version": "1.3.0",
|
||||
"resolved": "https://registry.npmjs.org/make-dir/-/make-dir-1.3.0.tgz",
|
||||
|
@ -16,6 +16,8 @@
|
||||
"./dist/**/*",
|
||||
"./node_modules/**/*",
|
||||
"./public/**/*",
|
||||
"./src/**",
|
||||
"./lib/**,",
|
||||
"*.js"
|
||||
],
|
||||
"directories": {
|
||||
@ -23,6 +25,7 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"electron-config": "^2.0.0",
|
||||
"electron-log": "^4.4.4"
|
||||
"electron-log": "^4.4.4",
|
||||
"lodash": "^4.17.21"
|
||||
}
|
||||
}
|
||||
|
38
electron/preload.js
Normal file
@ -0,0 +1,38 @@
|
||||
/* eslint-disable @typescript-eslint/no-var-requires */
|
||||
const { ipcRenderer, contextBridge } = require('electron')
|
||||
const log = require("electron-log");
|
||||
|
||||
contextBridge.exposeInMainWorld(
|
||||
"electronBridge", {
|
||||
send: (channel, data) => {
|
||||
log.log("Send on channel " + channel)
|
||||
// whitelist channels
|
||||
let validChannels = [
|
||||
"get-save-data-response",
|
||||
"get-save-info-response",
|
||||
"push-game-saved",
|
||||
"push-game-ready",
|
||||
"push-import-result",
|
||||
"push-disable-restore",
|
||||
];
|
||||
if (validChannels.includes(channel)) {
|
||||
ipcRenderer.send(channel, data);
|
||||
}
|
||||
},
|
||||
receive: (channel, func) => {
|
||||
log.log("Receive on channel " + channel)
|
||||
let validChannels = [
|
||||
"get-save-data-request",
|
||||
"get-save-info-request",
|
||||
"push-save-request",
|
||||
"trigger-save",
|
||||
"trigger-game-export",
|
||||
"trigger-scripts-export",
|
||||
];
|
||||
if (validChannels.includes(channel)) {
|
||||
// Deliberately strip event as it includes `sender`
|
||||
ipcRenderer.on(channel, (event, ...args) => func(...args));
|
||||
}
|
||||
}
|
||||
}
|
||||
);
|
386
electron/storage.js
Normal file
@ -0,0 +1,386 @@
|
||||
/* eslint-disable @typescript-eslint/no-var-requires */
|
||||
const { app, ipcMain } = require("electron");
|
||||
const zlib = require("zlib");
|
||||
const path = require("path");
|
||||
const fs = require("fs/promises");
|
||||
const { promisify } = require("util");
|
||||
const gzip = promisify(zlib.gzip);
|
||||
const gunzip = promisify(zlib.gunzip);
|
||||
|
||||
const greenworks = require("./greenworks");
|
||||
const log = require("electron-log");
|
||||
const flatten = require("lodash/flatten");
|
||||
const Config = require("electron-config");
|
||||
const config = new Config();
|
||||
|
||||
// https://stackoverflow.com/a/69418940
|
||||
const dirSize = async (directory) => {
|
||||
const files = await fs.readdir(directory);
|
||||
const stats = files.map(file => fs.stat(path.join(directory, file)));
|
||||
return (await Promise.all(stats)).reduce((accumulator, { size }) => accumulator + size, 0);
|
||||
}
|
||||
|
||||
const getDirFileStats = async (directory) => {
|
||||
const files = await fs.readdir(directory);
|
||||
const stats = files.map((f) => {
|
||||
const file = path.join(directory, f);
|
||||
return fs.stat(file).then((stat) => ({ file, stat }));
|
||||
});
|
||||
const data = (await Promise.all(stats));
|
||||
return data;
|
||||
};
|
||||
|
||||
const getNewestFile = async (directory) => {
|
||||
const data = await getDirFileStats(directory)
|
||||
return data.sort((a, b) => b.stat.mtime.getTime() - a.stat.mtime.getTime())[0];
|
||||
};
|
||||
|
||||
const getAllSaves = async (window) => {
|
||||
const rootDirectory = await getSaveFolder(window, true);
|
||||
const data = await fs.readdir(rootDirectory, { withFileTypes: true});
|
||||
const savesPromises = data.filter((e) => e.isDirectory()).
|
||||
map((dir) => path.join(rootDirectory, dir.name)).
|
||||
map((dir) => getDirFileStats(dir));
|
||||
const saves = await Promise.all(savesPromises);
|
||||
const flat = flatten(saves);
|
||||
return flat;
|
||||
}
|
||||
|
||||
async function prepareSaveFolders(window) {
|
||||
const rootFolder = await getSaveFolder(window, true);
|
||||
const currentFolder = await getSaveFolder(window);
|
||||
const backupsFolder = path.join(rootFolder, "/_backups")
|
||||
await prepareFolders(rootFolder, currentFolder, backupsFolder);
|
||||
}
|
||||
|
||||
async function prepareFolders(...folders) {
|
||||
for (const folder of folders) {
|
||||
try {
|
||||
// Making sure the folder exists
|
||||
// eslint-disable-next-line no-await-in-loop
|
||||
await fs.stat(folder);
|
||||
} catch (error) {
|
||||
if (error.code === 'ENOENT') {
|
||||
log.warn(`'${folder}' not found, creating it...`);
|
||||
// eslint-disable-next-line no-await-in-loop
|
||||
await fs.mkdir(folder);
|
||||
} else {
|
||||
log.error(error);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function getFolderSizeInBytes(saveFolder) {
|
||||
try {
|
||||
return await dirSize(saveFolder);
|
||||
} catch (error) {
|
||||
log.error(error);
|
||||
}
|
||||
}
|
||||
|
||||
function setAutosaveConfig(value) {
|
||||
config.set("autosave-enabled", value);
|
||||
}
|
||||
|
||||
function isAutosaveEnabled() {
|
||||
return config.get("autosave-enabled", true);
|
||||
}
|
||||
|
||||
function setSaveCompressionConfig(value) {
|
||||
config.set("save-compression-enabled", value);
|
||||
}
|
||||
|
||||
function isSaveCompressionEnabled() {
|
||||
return config.get("save-compression-enabled", true);
|
||||
}
|
||||
|
||||
function setCloudEnabledConfig(value) {
|
||||
config.set("cloud-enabled", value);
|
||||
}
|
||||
|
||||
async function getSaveFolder(window, root = false) {
|
||||
if (root) return path.join(app.getPath("userData"), "/saves");
|
||||
const identifier = window.gameInfo?.player?.identifier ?? "";
|
||||
return path.join(app.getPath("userData"), "/saves", `/${identifier}`);
|
||||
}
|
||||
|
||||
function isCloudEnabled() {
|
||||
// If the Steam API could not be initialized on game start, we'll abort this.
|
||||
if (global.greenworksError) return false;
|
||||
|
||||
// If the user disables it in Steam there's nothing we can do
|
||||
if (!greenworks.isCloudEnabledForUser()) return false;
|
||||
|
||||
// Let's check the config file to see if it's been overriden
|
||||
const enabledInConf = config.get("cloud-enabled", true);
|
||||
if (!enabledInConf) return false;
|
||||
|
||||
const isAppEnabled = greenworks.isCloudEnabled();
|
||||
if (!isAppEnabled) greenworks.enableCloud(true);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
function saveCloudFile(name, content) {
|
||||
return new Promise((resolve, reject) => {
|
||||
greenworks.saveTextToFile(name, content, resolve, reject);
|
||||
})
|
||||
}
|
||||
|
||||
function getFirstCloudFile() {
|
||||
const nbFiles = greenworks.getFileCount();
|
||||
if (nbFiles === 0) throw new Error('No files in cloud');
|
||||
const file = greenworks.getFileNameAndSize(0);
|
||||
log.silly(`Found ${nbFiles} files.`)
|
||||
log.silly(`First File: ${file.name} (${file.size} bytes)`);
|
||||
return file.name;
|
||||
}
|
||||
|
||||
function getCloudFile() {
|
||||
const file = getFirstCloudFile();
|
||||
return new Promise((resolve, reject) => {
|
||||
greenworks.readTextFromFile(file, resolve, reject);
|
||||
});
|
||||
}
|
||||
|
||||
function deleteCloudFile() {
|
||||
const file = getFirstCloudFile();
|
||||
return new Promise((resolve, reject) => {
|
||||
greenworks.deleteFile(file, resolve, reject);
|
||||
});
|
||||
}
|
||||
|
||||
async function getSteamCloudQuota() {
|
||||
return new Promise((resolve, reject) => {
|
||||
greenworks.getCloudQuota(resolve, reject)
|
||||
});
|
||||
}
|
||||
|
||||
async function backupSteamDataToDisk(currentPlayerId) {
|
||||
const nbFiles = greenworks.getFileCount();
|
||||
if (nbFiles === 0) return;
|
||||
|
||||
const file = greenworks.getFileNameAndSize(0);
|
||||
const previousPlayerId = file.name.replace(".json.gz", "");
|
||||
if (previousPlayerId !== currentPlayerId) {
|
||||
const backupSave = await getSteamCloudSaveString();
|
||||
const backupFile = path.join(app.getPath("userData"), "/saves/_backups", `${previousPlayerId}.json.gz`);
|
||||
const buffer = Buffer.from(backupSave, 'base64').toString('utf8');
|
||||
saveContent = await gzip(buffer);
|
||||
await fs.writeFile(backupFile, saveContent, 'utf8');
|
||||
log.debug(`Saved backup game to '${backupFile}`);
|
||||
}
|
||||
}
|
||||
|
||||
async function pushGameSaveToSteamCloud(base64save, currentPlayerId) {
|
||||
if (!isCloudEnabled) return Promise.reject("Steam Cloud is not Enabled");
|
||||
|
||||
try {
|
||||
backupSteamDataToDisk(currentPlayerId);
|
||||
} catch (error) {
|
||||
log.error(error);
|
||||
}
|
||||
|
||||
const steamSaveName = `${currentPlayerId}.json.gz`;
|
||||
|
||||
// Let's decode the base64 string so GZIP is more efficient.
|
||||
const buffer = Buffer.from(base64save, "base64");
|
||||
const compressedBuffer = await gzip(buffer);
|
||||
// We can't use utf8 for some reason, steamworks is unhappy.
|
||||
const content = compressedBuffer.toString("base64");
|
||||
log.debug(`Uncompressed: ${base64save.length} bytes`);
|
||||
log.debug(`Compressed: ${content.length} bytes`);
|
||||
log.debug(`Saving to Steam Cloud as ${steamSaveName}`);
|
||||
|
||||
try {
|
||||
await saveCloudFile(steamSaveName, content);
|
||||
} catch (error) {
|
||||
log.error(error);
|
||||
}
|
||||
}
|
||||
|
||||
async function getSteamCloudSaveString() {
|
||||
if (!isCloudEnabled()) return Promise.reject("Steam Cloud is not Enabled");
|
||||
log.debug(`Fetching Save in Steam Cloud`);
|
||||
const cloudString = await getCloudFile();
|
||||
const gzippedBase64Buffer = Buffer.from(cloudString, "base64");
|
||||
const uncompressedBuffer = await gunzip(gzippedBase64Buffer);
|
||||
const content = uncompressedBuffer.toString("base64");
|
||||
log.debug(`Compressed: ${cloudString.length} bytes`);
|
||||
log.debug(`Uncompressed: ${content.length} bytes`);
|
||||
return content;
|
||||
}
|
||||
|
||||
async function saveGameToDisk(window, saveData) {
|
||||
const currentFolder = await getSaveFolder(window);
|
||||
let saveFolderSizeBytes = await getFolderSizeInBytes(currentFolder);
|
||||
const maxFolderSizeBytes = config.get("autosave-quota", 1e8); // 100Mb per playerIndentifier
|
||||
const remainingSpaceBytes = maxFolderSizeBytes - saveFolderSizeBytes;
|
||||
log.debug(`Folder Usage: ${saveFolderSizeBytes} bytes`);
|
||||
log.debug(`Folder Capacity: ${maxFolderSizeBytes} bytes`);
|
||||
log.debug(`Remaining: ${remainingSpaceBytes} bytes (${(saveFolderSizeBytes / maxFolderSizeBytes * 100).toFixed(2)}% used)`)
|
||||
const shouldCompress = isSaveCompressionEnabled();
|
||||
const fileName = saveData.fileName;
|
||||
const file = path.join(currentFolder, fileName + (shouldCompress ? ".gz" : ""));
|
||||
try {
|
||||
let saveContent = saveData.save;
|
||||
if (shouldCompress) {
|
||||
// Let's decode the base64 string so GZIP is more efficient.
|
||||
const buffer = Buffer.from(saveContent, 'base64').toString('utf8');
|
||||
saveContent = await gzip(buffer);
|
||||
}
|
||||
await fs.writeFile(file, saveContent, 'utf8');
|
||||
log.debug(`Saved Game to '${file}'`);
|
||||
log.debug(`Save Size: ${saveContent.length} bytes`);
|
||||
} catch (error) {
|
||||
log.error(error);
|
||||
}
|
||||
|
||||
const fileStats = await getDirFileStats(currentFolder);
|
||||
const oldestFiles = fileStats
|
||||
.sort((a, b) => a.stat.mtime.getTime() - b.stat.mtime.getTime())
|
||||
.map(f => f.file).filter(f => f !== file);
|
||||
|
||||
while (saveFolderSizeBytes > maxFolderSizeBytes && oldestFiles.length > 0) {
|
||||
const fileToRemove = oldestFiles.shift();
|
||||
log.debug(`Over Quota -> Removing "${fileToRemove}"`);
|
||||
try {
|
||||
// eslint-disable-next-line no-await-in-loop
|
||||
await fs.unlink(fileToRemove);
|
||||
} catch (error) {
|
||||
log.error(error);
|
||||
}
|
||||
|
||||
// eslint-disable-next-line no-await-in-loop
|
||||
saveFolderSizeBytes = await getFolderSizeInBytes(currentFolder);
|
||||
log.debug(`Save Folder: ${saveFolderSizeBytes} bytes`);
|
||||
log.debug(`Remaining: ${maxFolderSizeBytes - saveFolderSizeBytes} bytes (${(saveFolderSizeBytes / maxFolderSizeBytes * 100).toFixed(2)}% used)`)
|
||||
}
|
||||
|
||||
return file;
|
||||
}
|
||||
|
||||
async function loadLastFromDisk(window) {
|
||||
const folder = await getSaveFolder(window);
|
||||
const last = await getNewestFile(folder);
|
||||
log.debug(`Last modified file: "${last.file}" (${last.stat.mtime.toLocaleString()})`);
|
||||
return loadFileFromDisk(last.file);
|
||||
}
|
||||
|
||||
async function loadFileFromDisk(path) {
|
||||
const buffer = await fs.readFile(path);
|
||||
let content;
|
||||
if (path.endsWith('.gz')) {
|
||||
const uncompressedBuffer = await gunzip(buffer);
|
||||
content = uncompressedBuffer.toString('base64');
|
||||
log.debug(`Uncompressed file content (new size: ${content.length} bytes)`);
|
||||
} else {
|
||||
content = buffer.toString('utf8');
|
||||
log.debug(`Loaded file with ${content.length} bytes`)
|
||||
}
|
||||
return content;
|
||||
}
|
||||
|
||||
function getSaveInformation(window, save) {
|
||||
return new Promise((resolve) => {
|
||||
ipcMain.once("get-save-info-response", async (event, data) => {
|
||||
resolve(data);
|
||||
});
|
||||
window.webContents.send("get-save-info-request", save);
|
||||
});
|
||||
}
|
||||
|
||||
function getCurrentSave(window) {
|
||||
return new Promise((resolve) => {
|
||||
ipcMain.once('get-save-data-response', (event, data) => {
|
||||
resolve(data);
|
||||
});
|
||||
window.webContents.send('get-save-data-request');
|
||||
});
|
||||
}
|
||||
|
||||
function pushSaveGameForImport(window, save, automatic) {
|
||||
ipcMain.once("push-import-result", async (event, arg) => {
|
||||
log.debug(`Was save imported? ${arg.wasImported ? "Yes" : "No"}`);
|
||||
});
|
||||
window.webContents.send("push-save-request", { save, automatic });
|
||||
}
|
||||
|
||||
async function restoreIfNewerExists(window) {
|
||||
const currentSave = await getCurrentSave(window);
|
||||
const currentData = await getSaveInformation(window, currentSave.save);
|
||||
const steam = {};
|
||||
const disk = {};
|
||||
|
||||
try {
|
||||
steam.save = await getSteamCloudSaveString();
|
||||
steam.data = await getSaveInformation(window, steam.save);
|
||||
} catch (error) {
|
||||
log.error("Could not retrieve steam file");
|
||||
log.debug(error);
|
||||
}
|
||||
|
||||
try {
|
||||
const saves = (await getAllSaves()).
|
||||
sort((a, b) => b.stat.mtime.getTime() - a.stat.mtime.getTime());
|
||||
if (saves.length > 0) {
|
||||
disk.save = await loadFileFromDisk(saves[0].file);
|
||||
disk.data = await getSaveInformation(window, disk.save);
|
||||
}
|
||||
} catch(error) {
|
||||
log.error("Could not retrieve disk file");
|
||||
log.debug(error);
|
||||
}
|
||||
|
||||
const lowPlaytime = 1000 * 60 * 15;
|
||||
let bestMatch;
|
||||
if (!steam.data && !disk.data) {
|
||||
log.info("No data to import");
|
||||
} else {
|
||||
// We'll just compare using the lastSave field for now.
|
||||
if (!steam.data) {
|
||||
log.debug('Best potential save match: Disk');
|
||||
bestMatch = disk;
|
||||
} else if (!disk.data) {
|
||||
log.debug('Best potential save match: Steam Cloud');
|
||||
bestMatch = steam;
|
||||
} else if ((steam.data.lastSave >= disk.data.lastSave)
|
||||
|| (steam.data.playtime + lowPlaytime > disk.data.playtime)) {
|
||||
// We want to prioritze steam data if the playtime is very close
|
||||
log.debug('Best potential save match: Steam Cloud');
|
||||
bestMatch = steam;
|
||||
} else {
|
||||
log.debug('Best potential save match: disk');
|
||||
bestMatch = disk;
|
||||
}
|
||||
}
|
||||
if (bestMatch) {
|
||||
if (bestMatch.data.lastSave > currentData.lastSave + 5000) {
|
||||
// We add a few seconds to the currentSave's lastSave to prioritize it
|
||||
log.info("Found newer data than the current's save file");
|
||||
log.silly(bestMatch.data);
|
||||
await pushSaveGameForImport(window, bestMatch.save, true);
|
||||
return true;
|
||||
} else if(bestMatch.data.playtime > currentData.playtime && currentData.playtime < lowPlaytime) {
|
||||
log.info("Found older save, but with more playtime, and current less than 15 mins played");
|
||||
log.silly(bestMatch.data);
|
||||
await pushSaveGameForImport(window, bestMatch.save, true);
|
||||
return true;
|
||||
} else {
|
||||
log.debug("Current save data is the freshest");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
getCurrentSave, getSaveInformation,
|
||||
restoreIfNewerExists, pushSaveGameForImport,
|
||||
pushGameSaveToSteamCloud, getSteamCloudSaveString, getSteamCloudQuota, deleteCloudFile,
|
||||
saveGameToDisk, loadLastFromDisk, loadFileFromDisk,
|
||||
getSaveFolder, prepareSaveFolders, getAllSaves,
|
||||
isCloudEnabled, setCloudEnabledConfig,
|
||||
isAutosaveEnabled, setAutosaveConfig,
|
||||
isSaveCompressionEnabled, setSaveCompressionConfig,
|
||||
};
|
63
electron/windowTracker.js
Normal file
@ -0,0 +1,63 @@
|
||||
/* eslint-disable @typescript-eslint/no-var-requires */
|
||||
const { screen } = require("electron");
|
||||
const log = require("electron-log");
|
||||
const debounce = require("lodash/debounce");
|
||||
const Config = require("electron-config");
|
||||
const config = new Config();
|
||||
|
||||
// https://stackoverflow.com/a/68627253
|
||||
const windowTracker = (windowName) => {
|
||||
let window, windowState;
|
||||
|
||||
const setBounds = () => {
|
||||
// Restore from appConfig
|
||||
if (config.has(`window.${windowName}`)) {
|
||||
windowState = config.get(`window.${windowName}`);
|
||||
return;
|
||||
}
|
||||
|
||||
const size = screen.getPrimaryDisplay().workAreaSize;
|
||||
|
||||
// Default
|
||||
windowState = {
|
||||
x: undefined,
|
||||
y: undefined,
|
||||
width: size.width,
|
||||
height: size.height,
|
||||
isMaximized: true,
|
||||
};
|
||||
};
|
||||
|
||||
const saveState = debounce(() => {
|
||||
if (!windowState.isMaximized) {
|
||||
windowState = window.getBounds();
|
||||
}
|
||||
|
||||
windowState.isMaximized = window.isMaximized();
|
||||
log.silly(`Saving window.${windowName} to configs`);
|
||||
config.set(`window.${windowName}`, windowState);
|
||||
log.silly(windowState);
|
||||
}, 1000);
|
||||
|
||||
const track = (win) => {
|
||||
window = win;
|
||||
['resize', 'move', 'close'].forEach((event) => {
|
||||
win.on(event, saveState);
|
||||
});
|
||||
};
|
||||
|
||||
setBounds();
|
||||
|
||||
return {
|
||||
state: {
|
||||
x: windowState.x,
|
||||
y: windowState.y,
|
||||
width: windowState.width,
|
||||
height: windowState.height,
|
||||
isMaximized: windowState.isMaximized,
|
||||
},
|
||||
track,
|
||||
};
|
||||
};
|
||||
|
||||
module.exports = { windowTracker };
|
@ -73,5 +73,5 @@
|
||||
<link rel="shortcut icon" href="favicon.ico"></head>
|
||||
<body>
|
||||
<div id="root"/>
|
||||
<script type="text/javascript" src="dist/vendor.bundle.js"></script><script type="text/javascript" src="main.bundle.js"></script></body>
|
||||
<script type="text/javascript" src="dist/vendor.bundle.js"></script><script type="text/javascript" src="dist/main.bundle.js"></script></body>
|
||||
</html>
|
||||
|
@ -8,4 +8,8 @@ module.exports = {
|
||||
'.cypress', 'node_modules', 'dist',
|
||||
],
|
||||
testEnvironment: "jsdom",
|
||||
moduleNameMapper: {
|
||||
"\\.(jpg|jpeg|png|gif|eot|otf|webp|svg|ttf|woff|woff2|mp4|webm|wav|mp3|m4a|aac|oga)$": "<rootDir>/test/__mocks__/fileMock.js",
|
||||
"\\.(css|less)$": "<rootDir>/test/__mocks__/styleMock.js"
|
||||
}
|
||||
};
|
||||
|
@ -28,5 +28,5 @@ Parameter that affects the percentage by which the server’s money is increased
|
||||
|
||||
RAM cost: 0.1 GB
|
||||
|
||||
Returns the server’s instrinsic “growth parameter”. This growth parameter is a number between 0 and 100 that represents how quickly the server’s money grows. This parameter affects the percentage by which the server’s money is increased when using the grow function. A higher growth parameter will result in a higher percentage increase from grow.
|
||||
Returns the server’s intrinsic “growth parameter”. This growth parameter is a number typically between 0 and 100 that represents how quickly the server’s money grows. This parameter affects the percentage by which the server’s money is increased when using the grow function. A higher growth parameter will result in a higher percentage increase from grow.
|
||||
|
||||
|
21
markdown/bitburner.ns.getsharepower.md
Normal file
@ -0,0 +1,21 @@
|
||||
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
|
||||
|
||||
[Home](./index.md) > [bitburner](./bitburner.md) > [NS](./bitburner.ns.md) > [getSharePower](./bitburner.ns.getsharepower.md)
|
||||
|
||||
## NS.getSharePower() method
|
||||
|
||||
Calculate your share power. Based on all the active share calls.
|
||||
|
||||
<b>Signature:</b>
|
||||
|
||||
```typescript
|
||||
getSharePower(): number;
|
||||
```
|
||||
<b>Returns:</b>
|
||||
|
||||
number
|
||||
|
||||
## Remarks
|
||||
|
||||
RAM cost: 0.2 GB
|
||||
|
@ -107,6 +107,7 @@ export async function main(ns) {
|
||||
| [getServerRequiredHackingLevel(host)](./bitburner.ns.getserverrequiredhackinglevel.md) | Returns the required hacking level of the target server. |
|
||||
| [getServerSecurityLevel(host)](./bitburner.ns.getserversecuritylevel.md) | Get server security level. |
|
||||
| [getServerUsedRam(host)](./bitburner.ns.getserverusedram.md) | Get the used RAM on a server. |
|
||||
| [getSharePower()](./bitburner.ns.getsharepower.md) | Calculate your share power. Based on all the active share calls. |
|
||||
| [getTimeSinceLastAug()](./bitburner.ns.gettimesincelastaug.md) | Returns the amount of time in milliseconds that have passed since you last installed Augmentations. |
|
||||
| [getWeakenTime(host)](./bitburner.ns.getweakentime.md) | Get the execution time of a weaken() call. |
|
||||
| [grow(host, opts)](./bitburner.ns.grow.md) | Spoof money in a servers bank account, increasing the amount available. |
|
||||
@ -144,6 +145,7 @@ export async function main(ns) {
|
||||
| [scriptKill(script, host)](./bitburner.ns.scriptkill.md) | Kill all scripts with a filename. |
|
||||
| [scriptRunning(script, host)](./bitburner.ns.scriptrunning.md) | Check if any script with a filename is running. |
|
||||
| [serverExists(host)](./bitburner.ns.serverexists.md) | Returns a boolean denoting whether or not the specified server exists. |
|
||||
| [share()](./bitburner.ns.share.md) | Share your computer with your factions. |
|
||||
| [sleep(millis)](./bitburner.ns.sleep.md) | Suspends the script for n milliseconds. |
|
||||
| [spawn(script, numThreads, args)](./bitburner.ns.spawn.md) | Terminate current script and start another in 10s. |
|
||||
| [sprintf(format, args)](./bitburner.ns.sprintf.md) | Format a string. |
|
||||
|
@ -48,7 +48,7 @@ for (let i = 0; i < scripts.length; ++i) {
|
||||
```ts
|
||||
// NS2:
|
||||
const ps = ns.ps("home");
|
||||
for (script of ps) {
|
||||
for (let script of ps) {
|
||||
ns.tprint(`${script.filename} ${ps[i].threads}`);
|
||||
ns.tprint(script.args);
|
||||
}
|
||||
|
23
markdown/bitburner.ns.share.md
Normal file
@ -0,0 +1,23 @@
|
||||
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
|
||||
|
||||
[Home](./index.md) > [bitburner](./bitburner.md) > [NS](./bitburner.ns.md) > [share](./bitburner.ns.share.md)
|
||||
|
||||
## NS.share() method
|
||||
|
||||
Share your computer with your factions.
|
||||
|
||||
<b>Signature:</b>
|
||||
|
||||
```typescript
|
||||
share(): Promise<void>;
|
||||
```
|
||||
<b>Returns:</b>
|
||||
|
||||
Promise<void>
|
||||
|
||||
## Remarks
|
||||
|
||||
RAM cost: 2.4 GB
|
||||
|
||||
Increases your rep gain of hacking contracts while share is called. Scales with thread count.
|
||||
|
@ -9,5 +9,5 @@ Data refers to the production, sale, and quantity of the products These values a
|
||||
<b>Signature:</b>
|
||||
|
||||
```typescript
|
||||
cityData: {[key: string]:number[]};
|
||||
cityData: { [key: string]: number[] };
|
||||
```
|
||||
|
@ -16,7 +16,7 @@ interface Product
|
||||
|
||||
| Property | Type | Description |
|
||||
| --- | --- | --- |
|
||||
| [cityData](./bitburner.product.citydata.md) | {\[key: string\]:number\[\]} | Data refers to the production, sale, and quantity of the products These values are specific to a city For each city, the data is \[qty, prod, sell\] |
|
||||
| [cityData](./bitburner.product.citydata.md) | { \[key: string\]: number\[\] } | Data refers to the production, sale, and quantity of the products These values are specific to a city For each city, the data is \[qty, prod, sell\] |
|
||||
| [cmp](./bitburner.product.cmp.md) | number | Competition for the product |
|
||||
| [developmentProgress](./bitburner.product.developmentprogress.md) | number | Creation progress - A number between 0-100 representing percentage |
|
||||
| [dmd](./bitburner.product.dmd.md) | number | Demand for the product |
|
||||
|
@ -19,5 +19,5 @@ List of active fragments placed on Stanek's Gift.
|
||||
|
||||
## Remarks
|
||||
|
||||
RAM cost: cost: 5 GB
|
||||
RAM cost: 5 GB
|
||||
|
||||
|
@ -29,5 +29,5 @@ true if the fragment can be placed at that position. false otherwise.
|
||||
|
||||
## Remarks
|
||||
|
||||
RAM cost: cost: 0.5 GB
|
||||
RAM cost: 0.5 GB
|
||||
|
||||
|
@ -17,5 +17,5 @@ void
|
||||
|
||||
## Remarks
|
||||
|
||||
RAM cost: cost: 0 GB
|
||||
RAM cost: 0 GB
|
||||
|
||||
|
@ -19,5 +19,5 @@ List of possible fragments.
|
||||
|
||||
## Remarks
|
||||
|
||||
RAM cost: cost: 0 GB
|
||||
RAM cost: 0 GB
|
||||
|
||||
|
@ -27,5 +27,5 @@ The fragment at \[rootX, rootY\], if any.
|
||||
|
||||
## Remarks
|
||||
|
||||
RAM cost: cost: 5 GB
|
||||
RAM cost: 5 GB
|
||||
|
||||
|
@ -29,5 +29,5 @@ true if the fragment can be placed at that position. false otherwise.
|
||||
|
||||
## Remarks
|
||||
|
||||
RAM cost: cost: 5 GB
|
||||
RAM cost: 5 GB
|
||||
|
||||
|
@ -27,5 +27,5 @@ The fragment at \[rootX, rootY\], if any.
|
||||
|
||||
## Remarks
|
||||
|
||||
RAM cost: cost: 0.15 GB
|
||||
RAM cost: 0.15 GB
|
||||
|
||||
|
21
markdown/bitburner.userinterface.getgameinfo.md
Normal file
@ -0,0 +1,21 @@
|
||||
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
|
||||
|
||||
[Home](./index.md) > [bitburner](./bitburner.md) > [UserInterface](./bitburner.userinterface.md) > [getGameInfo](./bitburner.userinterface.getgameinfo.md)
|
||||
|
||||
## UserInterface.getGameInfo() method
|
||||
|
||||
Gets the current game information (version, commit, ...)
|
||||
|
||||
<b>Signature:</b>
|
||||
|
||||
```typescript
|
||||
getGameInfo(): GameInfo;
|
||||
```
|
||||
<b>Returns:</b>
|
||||
|
||||
GameInfo
|
||||
|
||||
## Remarks
|
||||
|
||||
RAM cost: 0 GB
|
||||
|
@ -19,5 +19,5 @@ An object containing the player's styles
|
||||
|
||||
## Remarks
|
||||
|
||||
RAM cost: cost: 0 GB
|
||||
RAM cost: 0 GB
|
||||
|
||||
|
@ -19,5 +19,5 @@ An object containing the theme's colors
|
||||
|
||||
## Remarks
|
||||
|
||||
RAM cost: cost: 0 GB
|
||||
RAM cost: 0 GB
|
||||
|
||||
|
@ -16,6 +16,7 @@ interface UserInterface
|
||||
|
||||
| Method | Description |
|
||||
| --- | --- |
|
||||
| [getGameInfo()](./bitburner.userinterface.getgameinfo.md) | Gets the current game information (version, commit, ...) |
|
||||
| [getStyles()](./bitburner.userinterface.getstyles.md) | Get the current styles |
|
||||
| [getTheme()](./bitburner.userinterface.gettheme.md) | Get the current theme |
|
||||
| [resetStyles()](./bitburner.userinterface.resetstyles.md) | Resets the player's styles to the default values |
|
||||
|
@ -17,5 +17,5 @@ void
|
||||
|
||||
## Remarks
|
||||
|
||||
RAM cost: cost: 0 GB
|
||||
RAM cost: 0 GB
|
||||
|
||||
|
@ -17,5 +17,5 @@ void
|
||||
|
||||
## Remarks
|
||||
|
||||
RAM cost: cost: 0 GB
|
||||
RAM cost: 0 GB
|
||||
|
||||
|
@ -24,7 +24,7 @@ void
|
||||
|
||||
## Remarks
|
||||
|
||||
RAM cost: cost: 0 GB
|
||||
RAM cost: 0 GB
|
||||
|
||||
## Example
|
||||
|
||||
|
@ -24,7 +24,7 @@ void
|
||||
|
||||
## Remarks
|
||||
|
||||
RAM cost: cost: 0 GB
|
||||
RAM cost: 0 GB
|
||||
|
||||
## Example
|
||||
|
||||
|
1911
package-lock.json
generated
29
package.json
@ -13,19 +13,10 @@
|
||||
"@emotion/react": "^11.4.1",
|
||||
"@emotion/styled": "^11.3.0",
|
||||
"@material-ui/core": "^4.12.3",
|
||||
"@microsoft/api-documenter": "^7.13.65",
|
||||
"@microsoft/api-extractor": "^7.18.17",
|
||||
"@monaco-editor/react": "^4.2.2",
|
||||
"@mui/icons-material": "^5.0.3",
|
||||
"@mui/material": "^5.0.3",
|
||||
"@mui/styles": "^5.0.1",
|
||||
"@types/acorn": "^4.0.6",
|
||||
"@types/escodegen": "^0.0.7",
|
||||
"@types/numeral": "0.0.25",
|
||||
"@types/react": "^17.0.21",
|
||||
"@types/react-beautiful-dnd": "^13.1.2",
|
||||
"@types/react-dom": "^17.0.9",
|
||||
"@types/react-resizable": "^1.7.3",
|
||||
"acorn": "^8.4.1",
|
||||
"acorn-walk": "^8.1.1",
|
||||
"arg": "^5.0.0",
|
||||
@ -45,7 +36,6 @@
|
||||
"notistack": "^2.0.2",
|
||||
"numeral": "2.0.6",
|
||||
"prop-types": "^15.8.0",
|
||||
"raw-loader": "^4.0.2",
|
||||
"react": "^17.0.2",
|
||||
"react-beautiful-dnd": "^13.1.0",
|
||||
"react-dom": "^17.0.2",
|
||||
@ -59,10 +49,19 @@
|
||||
"@babel/preset-env": "^7.15.0",
|
||||
"@babel/preset-react": "^7.0.0",
|
||||
"@babel/preset-typescript": "^7.15.0",
|
||||
"@microsoft/api-documenter": "^7.13.65",
|
||||
"@microsoft/api-extractor": "^7.18.17",
|
||||
"@pmmmwh/react-refresh-webpack-plugin": "^0.5.1",
|
||||
"@testing-library/cypress": "^8.0.1",
|
||||
"@types/acorn": "^4.0.6",
|
||||
"@types/escodegen": "^0.0.7",
|
||||
"@types/file-saver": "^2.0.3",
|
||||
"@types/lodash": "^4.14.168",
|
||||
"@types/numeral": "0.0.25",
|
||||
"@types/react": "^17.0.21",
|
||||
"@types/react-beautiful-dnd": "^13.1.2",
|
||||
"@types/react-dom": "^17.0.9",
|
||||
"@types/react-resizable": "^1.7.3",
|
||||
"@typescript-eslint/eslint-plugin": "^4.22.0",
|
||||
"@typescript-eslint/parser": "^4.22.0",
|
||||
"babel-jest": "^27.0.6",
|
||||
@ -71,6 +70,7 @@
|
||||
"electron": "^14.0.2",
|
||||
"electron-packager": "^15.4.0",
|
||||
"eslint": "^7.24.0",
|
||||
"file-loader": "^6.2.0",
|
||||
"fork-ts-checker-webpack-plugin": "^6.3.3",
|
||||
"html-webpack-plugin": "^3.2.0",
|
||||
"http-server": "^13.0.1",
|
||||
@ -79,6 +79,7 @@
|
||||
"lodash": "^4.17.21",
|
||||
"mini-css-extract-plugin": "^0.4.1",
|
||||
"prettier": "^2.3.2",
|
||||
"raw-loader": "^4.0.2",
|
||||
"react-refresh": "^0.10.0",
|
||||
"regenerator-runtime": "^0.13.9",
|
||||
"source-map": "^0.7.3",
|
||||
@ -102,7 +103,7 @@
|
||||
"cy:dev": "start-server-and-test start:dev http://localhost:8000 cy:open",
|
||||
"cy:open": "cypress open",
|
||||
"cy:run": "cypress run",
|
||||
"doc": "npx api-extractor run && npx api-documenter markdown",
|
||||
"doc": "npx api-extractor run && npx api-documenter markdown && rm input/bitburner.api.json && rm -r input",
|
||||
"format": "prettier --write .",
|
||||
"start": "http-server -p 8000",
|
||||
"start:dev": "webpack-dev-server --progress --env.devServer --mode development",
|
||||
@ -112,13 +113,17 @@
|
||||
"build:dev": "webpack --mode development",
|
||||
"lint": "eslint --fix . --ext js,jsx,ts,tsx",
|
||||
"lint:report": "eslint --ext js,jsx,ts,tsx .",
|
||||
"preinstall": "node ./scripts/engines-check.js",
|
||||
"preinstall": "node ./tools/engines-check/engines-check.js",
|
||||
"test": "jest",
|
||||
"test:watch": "jest --watch",
|
||||
"watch": "webpack --watch --mode production",
|
||||
"watch:dev": "webpack --watch --mode development",
|
||||
"electron": "sh ./package.sh",
|
||||
"electron:packager": "electron-packager .package bitburner --all --out .build --overwrite --icon .package/icon.png --no-prune",
|
||||
"electron:packager-all": "electron-packager .package bitburner --all --out .build --overwrite --icon .package/icon.png",
|
||||
"electron:packager-win": "electron-packager .package bitburner --platform win32 --arch x64 --out .build --overwrite --icon .package/icon.png",
|
||||
"electron:packager-mac": "electron-packager .package bitburner --platform darwin --arch x64 --out .build --overwrite --icon .package/icon.png",
|
||||
"electron:packager-linux": "electron-packager .package bitburner --platform linux --arch x64 --out .build --overwrite --icon .package/icon.png",
|
||||
"allbuild": "npm run build && npm run electron && git add --all && git commit --amend --no-edit && git push -f -u origin dev"
|
||||
}
|
||||
}
|
||||
|
@ -1,5 +1,8 @@
|
||||
#!/bin/sh
|
||||
|
||||
# Clear out any files remaining from old builds
|
||||
rm -rf .package
|
||||
|
||||
mkdir -p .package/dist/src/ThirdParty || true
|
||||
mkdir -p .package/src/ThirdParty || true
|
||||
mkdir -p .package/node_modules || true
|
||||
@ -8,6 +11,7 @@ cp index.html .package
|
||||
cp -r electron/* .package
|
||||
cp -r dist/ext .package/dist
|
||||
cp -r dist/icons .package/dist
|
||||
cp -r dist/images .package/dist
|
||||
|
||||
# The css files
|
||||
cp dist/vendor.css .package/dist
|
||||
@ -26,5 +30,6 @@ cd electron
|
||||
npm install
|
||||
cd ..
|
||||
|
||||
BUILD_PLATFORM="${1:-"all"}"
|
||||
# And finally build the app.
|
||||
npm run electron:packager
|
||||
npm run electron:packager-$BUILD_PLATFORM
|
6
src/@types/global.d.ts
vendored
@ -1,2 +1,8 @@
|
||||
// Defined by webpack on startup or compilation
|
||||
declare let __COMMIT_HASH__: string;
|
||||
|
||||
// When using file-loader, we'll get a path to the resource
|
||||
declare module "*.png" {
|
||||
const value: string;
|
||||
export default value;
|
||||
}
|
||||
|
@ -380,7 +380,7 @@ export const achievements: IMap<Achievement> = {
|
||||
},
|
||||
TRAVEL: {
|
||||
...achievementData["TRAVEL"],
|
||||
Icon: "travel",
|
||||
Icon: "TRAVEL",
|
||||
Condition: () => Player.city !== CityName.Sector12,
|
||||
},
|
||||
WORKOUT: {
|
||||
@ -553,7 +553,9 @@ export const achievements: IMap<Achievement> = {
|
||||
...achievementData["MAX_CACHE"],
|
||||
Icon: "HASHNETCAP",
|
||||
Visible: () => hasAccessToSF(Player, 9),
|
||||
Condition: () => hasHacknetServers(Player) && Player.hashManager.hashes === Player.hashManager.capacity,
|
||||
Condition: () => hasHacknetServers(Player) &&
|
||||
Player.hashManager.hashes === Player.hashManager.capacity &&
|
||||
Player.hashManager.capacity > 0,
|
||||
},
|
||||
SLEEVE_8: {
|
||||
...achievementData["SLEEVE_8"],
|
||||
|
@ -22,12 +22,12 @@ export function loadGlobalAliases(saveString: string): void {
|
||||
|
||||
// Prints all aliases to terminal
|
||||
export function printAliases(): void {
|
||||
for (const name in Aliases) {
|
||||
for (const name of Object.keys(Aliases)) {
|
||||
if (Aliases.hasOwnProperty(name)) {
|
||||
Terminal.print("alias " + name + "=" + Aliases[name]);
|
||||
}
|
||||
}
|
||||
for (const name in GlobalAliases) {
|
||||
for (const name of Object.keys(GlobalAliases)) {
|
||||
if (GlobalAliases.hasOwnProperty(name)) {
|
||||
Terminal.print("global alias " + name + "=" + GlobalAliases[name]);
|
||||
}
|
||||
|
@ -526,7 +526,7 @@ export class Augmentation {
|
||||
|
||||
// Adds this Augmentation to all Factions
|
||||
addToAllFactions(): void {
|
||||
for (const fac in Factions) {
|
||||
for (const fac of Object.keys(Factions)) {
|
||||
if (Factions.hasOwnProperty(fac)) {
|
||||
const facObj: Faction | null = Factions[fac];
|
||||
if (facObj == null) {
|
||||
|
@ -112,7 +112,7 @@ function getRandomBonus(): any {
|
||||
}
|
||||
|
||||
function initAugmentations(): void {
|
||||
for (const name in Factions) {
|
||||
for (const name of Object.keys(Factions)) {
|
||||
if (Factions.hasOwnProperty(name)) {
|
||||
Factions[name].augmentations = [];
|
||||
}
|
||||
@ -2050,7 +2050,7 @@ function initAugmentations(): void {
|
||||
info:
|
||||
"A brain implant carefully assembled around the synapses, which " +
|
||||
"micromanages the activity and levels of various neuroreceptor " +
|
||||
"chemicals and modulates electrical acvitiy to optimize concentration, " +
|
||||
"chemicals and modulates electrical activity to optimize concentration, " +
|
||||
"allowing the user to multitask much more effectively.",
|
||||
stats: (
|
||||
<>
|
||||
@ -2498,7 +2498,7 @@ function initAugmentations(): void {
|
||||
CONSTANTS.MultipleAugMultiplier * [1, 0.96, 0.94, 0.93][SourceFileFlags[11]],
|
||||
Player.queuedAugmentations.length,
|
||||
);
|
||||
for (const name in Augmentations) {
|
||||
for (const name of Object.keys(Augmentations)) {
|
||||
if (Augmentations.hasOwnProperty(name)) {
|
||||
Augmentations[name].baseCost *= mult;
|
||||
}
|
||||
@ -2525,7 +2525,7 @@ function applyAugmentation(aug: IPlayerOwnedAugmentation, reapply = false): void
|
||||
const augObj = Augmentations[aug.name];
|
||||
|
||||
// Apply multipliers
|
||||
for (const mult in augObj.mults) {
|
||||
for (const mult of Object.keys(augObj.mults)) {
|
||||
const v = Player.getMult(mult) * augObj.mults[mult];
|
||||
Player.setMult(mult, v);
|
||||
}
|
||||
|
@ -26,7 +26,7 @@ export function InstalledAugmentations(): React.ReactElement {
|
||||
|
||||
if (Settings.OwnedAugmentationsOrder === OwnedAugmentationsOrderSetting.Alphabetically) {
|
||||
sourceAugs.sort((aug1, aug2) => {
|
||||
return aug1.name <= aug2.name ? -1 : 1;
|
||||
return aug1.name.localeCompare(aug2.name);
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -17,7 +17,7 @@ function calculateAugmentedStats(): any {
|
||||
const augP: any = {};
|
||||
for (const aug of Player.queuedAugmentations) {
|
||||
const augObj = Augmentations[aug.name];
|
||||
for (const mult in augObj.mults) {
|
||||
for (const mult of Object.keys(augObj.mults)) {
|
||||
const v = augP[mult] ? augP[mult] : 1;
|
||||
augP[mult] = v * augObj.mults[mult];
|
||||
}
|
||||
|
@ -578,7 +578,7 @@ export function initBitNodeMultipliers(p: IPlayer): void {
|
||||
if (p.bitNodeN == null) {
|
||||
p.bitNodeN = 1;
|
||||
}
|
||||
for (const mult in BitNodeMultipliers) {
|
||||
for (const mult of Object.keys(BitNodeMultipliers)) {
|
||||
if (BitNodeMultipliers.hasOwnProperty(mult)) {
|
||||
BitNodeMultipliers[mult] = 1;
|
||||
}
|
||||
|
@ -85,7 +85,7 @@ function BitNodePortal(props: IPortalProps): React.ReactElement {
|
||||
</Typography>
|
||||
}
|
||||
>
|
||||
<span onClick={() => setPortalOpen(true)} className={cssClass}>
|
||||
<span onClick={() => setPortalOpen(true)} className={cssClass} aria-label={`enter-bitnode-${bitNode.number.toString()}`}>
|
||||
<b>O</b>
|
||||
</span>
|
||||
</Tooltip>
|
||||
|
@ -117,7 +117,7 @@ export class Action implements IAction {
|
||||
|
||||
// Check to make sure weights are summed properly
|
||||
let sum = 0;
|
||||
for (const weight in this.weights) {
|
||||
for (const weight of Object.keys(this.weights)) {
|
||||
if (this.weights.hasOwnProperty(weight)) {
|
||||
sum += this.weights[weight];
|
||||
}
|
||||
@ -131,7 +131,7 @@ export class Action implements IAction {
|
||||
);
|
||||
}
|
||||
|
||||
for (const decay in this.decays) {
|
||||
for (const decay of Object.keys(this.decays)) {
|
||||
if (this.decays.hasOwnProperty(decay)) {
|
||||
if (this.decays[decay] > 1) {
|
||||
throw new Error(
|
||||
@ -240,7 +240,7 @@ export class Action implements IAction {
|
||||
}
|
||||
let difficulty = this.getDifficulty();
|
||||
let competence = 0;
|
||||
for (const stat in this.weights) {
|
||||
for (const stat of Object.keys(this.weights)) {
|
||||
if (this.weights.hasOwnProperty(stat)) {
|
||||
const playerStatLvl = Player.queryStatFromString(stat);
|
||||
const key = "eff" + stat.charAt(0).toUpperCase() + stat.slice(1);
|
||||
|
@ -135,7 +135,7 @@ export class Bladeburner implements IBladeburner {
|
||||
|
||||
// Can't start a BlackOp if you haven't done the one before it
|
||||
const blackops = [];
|
||||
for (const nm in BlackOperations) {
|
||||
for (const nm of Object.keys(BlackOperations)) {
|
||||
if (BlackOperations.hasOwnProperty(nm)) {
|
||||
blackops.push(nm);
|
||||
}
|
||||
@ -1074,7 +1074,7 @@ export class Bladeburner implements IBladeburner {
|
||||
|
||||
updateSkillMultipliers(): void {
|
||||
this.resetSkillMultipliers();
|
||||
for (const skillName in this.skills) {
|
||||
for (const skillName of Object.keys(this.skills)) {
|
||||
if (this.skills.hasOwnProperty(skillName)) {
|
||||
const skill = Skills[skillName];
|
||||
if (skill == null) {
|
||||
|
@ -12,7 +12,7 @@ interface IProps {
|
||||
|
||||
export function BlackOpList(props: IProps): React.ReactElement {
|
||||
let blackops: BlackOperation[] = [];
|
||||
for (const blackopName in BlackOperations) {
|
||||
for (const blackopName of Object.keys(BlackOperations)) {
|
||||
if (BlackOperations.hasOwnProperty(blackopName)) {
|
||||
blackops.push(BlackOperations[blackopName]);
|
||||
}
|
||||
|
@ -12,7 +12,7 @@ interface IProps {
|
||||
|
||||
export function GeneralActionList(props: IProps): React.ReactElement {
|
||||
const actions: Action[] = [];
|
||||
for (const name in GeneralActions) {
|
||||
for (const name of Object.keys(GeneralActions)) {
|
||||
if (GeneralActions.hasOwnProperty(name)) {
|
||||
actions.push(GeneralActions[name]);
|
||||
}
|
||||
|
@ -14,6 +14,7 @@ import Typography from "@mui/material/Typography";
|
||||
import TextField from "@mui/material/TextField";
|
||||
|
||||
const MAX_BET = 100e6;
|
||||
export const DECK_COUNT = 5; // 5-deck multideck
|
||||
|
||||
enum Result {
|
||||
Pending = "",
|
||||
@ -45,7 +46,7 @@ export class Blackjack extends Game<Props, State> {
|
||||
constructor(props: Props) {
|
||||
super(props);
|
||||
|
||||
this.deck = new Deck(5); // 5-deck multideck
|
||||
this.deck = new Deck(DECK_COUNT);
|
||||
|
||||
const initialBet = 1e6;
|
||||
|
||||
|
@ -40,13 +40,13 @@ const strategies: {
|
||||
} = {
|
||||
Red: {
|
||||
match: (n: number): boolean => {
|
||||
if (n === 0) return false;
|
||||
return redNumbers.includes(n);
|
||||
},
|
||||
payout: 1,
|
||||
},
|
||||
Black: {
|
||||
match: (n: number): boolean => {
|
||||
if (n === 0) return false;
|
||||
return !redNumbers.includes(n);
|
||||
},
|
||||
payout: 1,
|
||||
@ -118,12 +118,6 @@ export function Roulette(props: IProps): React.ReactElement {
|
||||
const [status, setStatus] = useState<string | JSX.Element>("waiting");
|
||||
const [n, setN] = useState(0);
|
||||
const [lock, setLock] = useState(true);
|
||||
const [strategy, setStrategy] = useState<Strategy>({
|
||||
payout: 0,
|
||||
match: (): boolean => {
|
||||
return false;
|
||||
},
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
const i = window.setInterval(step, 50);
|
||||
@ -156,13 +150,12 @@ export function Roulette(props: IProps): React.ReactElement {
|
||||
return `${n}${color}`;
|
||||
}
|
||||
|
||||
function play(s: Strategy): void {
|
||||
function play(strategy: Strategy): void {
|
||||
if (reachedLimit(props.p)) return;
|
||||
|
||||
setCanPlay(false);
|
||||
setLock(false);
|
||||
setStatus("playing");
|
||||
setStrategy(s);
|
||||
|
||||
setTimeout(() => {
|
||||
let n = Math.floor(rng.random() * 37);
|
||||
|
@ -157,20 +157,20 @@ export function SlotMachine(props: IProps): React.ReactElement {
|
||||
function step(): void {
|
||||
let stoppedOne = false;
|
||||
const copy = index.slice();
|
||||
for (const i in copy) {
|
||||
for (let i = 0; i < copy.length; i++) {
|
||||
if (copy[i] === locks[i] && !stoppedOne) continue;
|
||||
copy[i] = (copy[i] + 1) % symbols.length;
|
||||
copy[i] = (copy[i] - 1 >= 0) ? copy[i] - 1 : symbols.length - 1;
|
||||
stoppedOne = true;
|
||||
}
|
||||
|
||||
setIndex(copy);
|
||||
|
||||
if (stoppedOne && copy.every((e, i) => e === locks[i])) {
|
||||
checkWinnings();
|
||||
checkWinnings(getTable(copy, symbols));
|
||||
}
|
||||
}
|
||||
|
||||
function getTable(): string[][] {
|
||||
function getTable(index:number[], symbols:string[]): string[][] {
|
||||
return [
|
||||
[
|
||||
symbols[(index[0] + symbols.length - 1) % symbols.length],
|
||||
@ -209,8 +209,7 @@ export function SlotMachine(props: IProps): React.ReactElement {
|
||||
]);
|
||||
}
|
||||
|
||||
function checkWinnings(): void {
|
||||
const t = getTable();
|
||||
function checkWinnings(t:string[][]): void {
|
||||
const getPaylineData = function (payline: number[][]): string[] {
|
||||
const data = [];
|
||||
for (const point of payline) {
|
||||
@ -267,7 +266,7 @@ export function SlotMachine(props: IProps): React.ReactElement {
|
||||
setInvestment(investment);
|
||||
}
|
||||
|
||||
const t = getTable();
|
||||
const t = getTable(index, symbols);
|
||||
// prettier-ignore
|
||||
return (
|
||||
<>
|
||||
|
@ -26,7 +26,7 @@ export function initCompanies(): void {
|
||||
});
|
||||
|
||||
// Reset data
|
||||
for (const companyName in Companies) {
|
||||
for (const companyName of Object.keys(Companies)) {
|
||||
const company = Companies[companyName];
|
||||
const oldCompany = oldCompanies[companyName];
|
||||
if (!(oldCompany instanceof Company)) {
|
||||
|
@ -111,8 +111,8 @@ export const CONSTANTS: {
|
||||
TotalNumBitNodes: number;
|
||||
LatestUpdate: string;
|
||||
} = {
|
||||
VersionString: "1.3.0",
|
||||
VersionNumber: 9,
|
||||
VersionString: "1.4.0",
|
||||
VersionNumber: 10,
|
||||
|
||||
// Speed (in ms) at which the main loop is updated
|
||||
_idleSpeed: 200,
|
||||
@ -273,89 +273,22 @@ export const CONSTANTS: {
|
||||
TotalNumBitNodes: 24,
|
||||
|
||||
LatestUpdate: `
|
||||
v1.3.0 - 2022-01-04 Cleaning up
|
||||
-------------------------------
|
||||
v1.4.0 - 2022-01-18 Sharing is caring
|
||||
-------------------------------------
|
||||
|
||||
** External IDE integration **
|
||||
** Computer sharing **
|
||||
|
||||
* The Steam version has a webserver that allows integration with external IDEs.
|
||||
A VSCode extension is available on the market place. (The documentation for the ext. isn't
|
||||
written yet)
|
||||
* A new mechanic has been added, it's is invoked by calling the new function 'share'.
|
||||
This mechanic helps you farm reputation faster.
|
||||
|
||||
** Source-Files **
|
||||
** gang **
|
||||
|
||||
* SF4 has been reworked.
|
||||
* New SF -1.
|
||||
* Installing augs means losing a little bit of ascension multipliers.
|
||||
|
||||
** UI **
|
||||
|
||||
* Fix some edge case with skill bat tooltips (@MartinFournier)
|
||||
* Made some background match theme color (@Kejikus)
|
||||
* Fix problem with script editor height not adjusting correctly (@billyvg)
|
||||
* Fix some formatting issues with Bladeburner (@MartinFournier, @nickofolas)
|
||||
* Fix some functions like 'alert' format messages better (@MageKing17)
|
||||
* Many community themes added.
|
||||
* New script editor theme (@Hedrauta, @Dexalt142)
|
||||
* Improvements to tail windows (@theit8514)
|
||||
* Training is more consise (@mikomyazaki)
|
||||
* Fix Investopedia not displaying properly (@JotaroS)
|
||||
* Remove alpha from theme editor (@MartinFournier)
|
||||
* Fix corporation tooltip not displaying properly (@MartinFournier)
|
||||
* Add tooltip on backdoored location names (@MartinFournier)
|
||||
* Allow toasts to be dismissed by clicking them (@nickofolas)
|
||||
* Darkweb item listing now shows what you own. (@hexnaught)
|
||||
|
||||
** Bug fix **
|
||||
|
||||
* Fix unit tests (@MartinFournier)
|
||||
* Fixed issue with 'cat' and 'read' not finding foldered files (@Nick-Colclasure)
|
||||
* Buying on the dark web will remove incomplete exe (@hexnaught)
|
||||
* Fix bug that would cause the game to crash trying to go to a job without a job (@hexnaught)
|
||||
* purchaseServer validation (@nickofolas)
|
||||
* Script Editor focuses code when changing tab (@MartinFournier)
|
||||
* Fix script editor for .txt files (@65-7a)
|
||||
* Fix 'buy' command not displaying correctly. (@hexnaught)
|
||||
* Fix hackAnalyzeThread returning NaN (@mikomyazaki)
|
||||
* Electron handles exceptions better (@MageKing17)
|
||||
* Electron will handle 'unresponsive' event and present the opportunity to reload the game with no scripts (@MartinFournier)
|
||||
* Fix 'cp' between folders (@theit8514)
|
||||
* Fix throwing null/undefined errors (@nickofolas)
|
||||
* Allow shortcuts to work when unfocused (@MageKing17)
|
||||
* Fix some dependency issue (@locriacyber)
|
||||
* Fix corporation state returning an object instead of a string (@antonvmironov)
|
||||
* Fix 'mv' overwriting files (@theit8514)
|
||||
* Fix joesguns not being influenced by hack/grow (@dou867, @MartinFournier)
|
||||
* Added warning when opening external links. (@MartinFournier)
|
||||
* Prevent applying for positions that aren't offered (@TheMas3212)
|
||||
* Import has validation (@MartinFournier)
|
||||
** There's more but I'm going to write it later. **
|
||||
|
||||
** Misc. **
|
||||
|
||||
* Added vim mode to script editor (@billyvg)
|
||||
* Clean up script editor code (@Rez855)
|
||||
* 'cat' works on scripts (@65-7a)
|
||||
* Add wordWrap for Monaco (@MartinFournier)
|
||||
* Include map bundles in electron for easier debugging (@MartinFournier)
|
||||
* Fix importing very large files (@MartinFournier)
|
||||
* Cache program blob, reducing ram usage of the game (@theit8514)
|
||||
* Dev menu can set server to $0 (@mikomyazaki)
|
||||
* 'backdoor' allows direct connect (@mikomyazaki)
|
||||
* Github workflow work (@MartinFournier)
|
||||
* workForFaction / workForCompany have a new parameter (@theit8514)
|
||||
* Alias accept single quotes (@sporkwitch, @FaintSpeaker)
|
||||
* Add grep options to 'ps' (@maxtimum)
|
||||
* Added buy all option to 'buy' (@anthonydroberts)
|
||||
* Added more shortcuts to terminal input (@Frank-py)
|
||||
* Refactor some port code (@ErzengelLichtes)
|
||||
* Settings to control GiB vs GB (@ErzengelLichtes)
|
||||
* Add electron option to export save game (@MartinFournier)
|
||||
* Electron improvements (@MartinFournier)
|
||||
* Expose some notifications functions to electron (@MartinFournier)
|
||||
* Documentation (@MartinFournier, @cyn, @millennIumAMbiguity, @2PacIsAlive,
|
||||
@TheCoderJT, @hexnaught, @sschmidTU, @FOLLGAD, @Hedrauta, @Xynrati,
|
||||
@mikomyazaki, @Icehawk78, @aaronransley, @TheMas3212, @Hedrauta, @alkemann,
|
||||
@ReeseJones, @amclark42, @thadguidry, @jasonhaxstuff, @pan-kuleczka, @jhollowe,
|
||||
@ApatheticsAnonymous, @erplsf, @daanflore, @nickofolas, @Kebap, @smolgumball,
|
||||
@woody-lam-cwl)
|
||||
* Nerf noodle bar.
|
||||
`,
|
||||
};
|
||||
|
@ -307,7 +307,7 @@ export class Corporation {
|
||||
if (upgN === 1) {
|
||||
for (let i = 0; i < this.divisions.length; ++i) {
|
||||
const industry = this.divisions[i];
|
||||
for (const city in industry.warehouses) {
|
||||
for (const city of Object.keys(industry.warehouses)) {
|
||||
const warehouse = industry.warehouses[city];
|
||||
if (warehouse === 0) continue;
|
||||
if (industry.warehouses.hasOwnProperty(city) && warehouse instanceof Warehouse) {
|
||||
|
@ -378,7 +378,7 @@ export class Industry implements IIndustry {
|
||||
updateWarehouseSizeUsed(warehouse: Warehouse): void {
|
||||
warehouse.updateMaterialSizeUsed();
|
||||
|
||||
for (const prodName in this.products) {
|
||||
for (const prodName of Object.keys(this.products)) {
|
||||
if (this.products.hasOwnProperty(prodName)) {
|
||||
const prod = this.products[prodName];
|
||||
if (prod === undefined) continue;
|
||||
@ -414,7 +414,7 @@ export class Industry implements IIndustry {
|
||||
|
||||
// Process offices (and the employees in them)
|
||||
let employeeSalary = 0;
|
||||
for (const officeLoc in this.offices) {
|
||||
for (const officeLoc of Object.keys(this.offices)) {
|
||||
const office = this.offices[officeLoc];
|
||||
if (office === 0) continue;
|
||||
if (office instanceof OfficeSpace) {
|
||||
@ -473,7 +473,7 @@ export class Industry implements IIndustry {
|
||||
if (this.warehouses[CorporationConstants.Cities[i]] instanceof Warehouse) {
|
||||
const wh = this.warehouses[CorporationConstants.Cities[i]];
|
||||
if (wh === 0) continue;
|
||||
for (const name in reqMats) {
|
||||
for (const name of Object.keys(reqMats)) {
|
||||
if (reqMats.hasOwnProperty(name)) {
|
||||
wh.materials[name].processMarket();
|
||||
}
|
||||
@ -496,7 +496,7 @@ export class Industry implements IIndustry {
|
||||
// Process change in demand and competition for this industry's products
|
||||
processProductMarket(marketCycles = 1): void {
|
||||
// Demand gradually decreases, and competition gradually increases
|
||||
for (const name in this.products) {
|
||||
for (const name of Object.keys(this.products)) {
|
||||
if (this.products.hasOwnProperty(name)) {
|
||||
const product = this.products[name];
|
||||
if (product === undefined) continue;
|
||||
@ -534,7 +534,7 @@ export class Industry implements IIndustry {
|
||||
}
|
||||
const warehouse = this.warehouses[city];
|
||||
if (warehouse === 0) continue;
|
||||
for (const matName in warehouse.materials) {
|
||||
for (const matName of Object.keys(warehouse.materials)) {
|
||||
if (warehouse.materials.hasOwnProperty(matName)) {
|
||||
const mat = warehouse.materials[matName];
|
||||
mat.imp = 0;
|
||||
@ -555,7 +555,7 @@ export class Industry implements IIndustry {
|
||||
switch (this.state) {
|
||||
case "PURCHASE": {
|
||||
/* Process purchase of materials */
|
||||
for (const matName in warehouse.materials) {
|
||||
for (const matName of Object.keys(warehouse.materials)) {
|
||||
if (!warehouse.materials.hasOwnProperty(matName)) continue;
|
||||
const mat = warehouse.materials[matName];
|
||||
let buyAmt = 0;
|
||||
@ -577,7 +577,7 @@ export class Industry implements IIndustry {
|
||||
|
||||
// smart supply
|
||||
const smartBuy: { [key: string]: number | undefined } = {};
|
||||
for (const matName in warehouse.materials) {
|
||||
for (const matName of Object.keys(warehouse.materials)) {
|
||||
if (!warehouse.materials.hasOwnProperty(matName)) continue;
|
||||
if (!warehouse.smartSupplyEnabled || !Object.keys(this.reqMats).includes(matName)) continue;
|
||||
const mat = warehouse.materials[matName];
|
||||
@ -594,7 +594,7 @@ export class Industry implements IIndustry {
|
||||
|
||||
// Find which material were trying to create the least amount of product with.
|
||||
let worseAmt = 1e99;
|
||||
for (const matName in smartBuy) {
|
||||
for (const matName of Object.keys(smartBuy)) {
|
||||
const buyAmt = smartBuy[matName];
|
||||
if (buyAmt === undefined) throw new Error(`Somehow smartbuy matname is undefined`);
|
||||
const reqMat = this.reqMats[matName];
|
||||
@ -604,7 +604,7 @@ export class Industry implements IIndustry {
|
||||
}
|
||||
|
||||
// Align all the materials to the smallest amount.
|
||||
for (const matName in smartBuy) {
|
||||
for (const matName of Object.keys(smartBuy)) {
|
||||
const reqMat = this.reqMats[matName];
|
||||
if (reqMat === undefined) throw new Error(`reqMat "${matName}" is undefined`);
|
||||
smartBuy[matName] = worseAmt * reqMat;
|
||||
@ -612,7 +612,7 @@ export class Industry implements IIndustry {
|
||||
|
||||
// Calculate the total size of all things were trying to buy
|
||||
let totalSize = 0;
|
||||
for (const matName in smartBuy) {
|
||||
for (const matName of Object.keys(smartBuy)) {
|
||||
const buyAmt = smartBuy[matName];
|
||||
if (buyAmt === undefined) throw new Error(`Somehow smartbuy matname is undefined`);
|
||||
totalSize += buyAmt * MaterialSizes[matName];
|
||||
@ -621,7 +621,7 @@ export class Industry implements IIndustry {
|
||||
// Shrink to the size of available space.
|
||||
const freeSpace = warehouse.size - warehouse.sizeUsed;
|
||||
if (totalSize > freeSpace) {
|
||||
for (const matName in smartBuy) {
|
||||
for (const matName of Object.keys(smartBuy)) {
|
||||
const buyAmt = smartBuy[matName];
|
||||
if (buyAmt === undefined) throw new Error(`Somehow smartbuy matname is undefined`);
|
||||
smartBuy[matName] = Math.floor((buyAmt * freeSpace) / totalSize);
|
||||
@ -629,7 +629,7 @@ export class Industry implements IIndustry {
|
||||
}
|
||||
|
||||
// Use the materials already in the warehouse if the option is on.
|
||||
for (const matName in smartBuy) {
|
||||
for (const matName of Object.keys(smartBuy)) {
|
||||
if (!warehouse.smartSupplyUseLeftovers[matName]) continue;
|
||||
const mat = warehouse.materials[matName];
|
||||
const buyAmt = smartBuy[matName];
|
||||
@ -638,7 +638,7 @@ export class Industry implements IIndustry {
|
||||
}
|
||||
|
||||
// buy them
|
||||
for (const matName in smartBuy) {
|
||||
for (const matName of Object.keys(smartBuy)) {
|
||||
const mat = warehouse.materials[matName];
|
||||
const buyAmt = smartBuy[matName];
|
||||
if (buyAmt === undefined) throw new Error(`Somehow smartbuy matname is undefined`);
|
||||
@ -675,7 +675,7 @@ export class Industry implements IIndustry {
|
||||
for (let tmp = 0; tmp < this.prodMats.length; ++tmp) {
|
||||
totalMatSize += MaterialSizes[this.prodMats[tmp]];
|
||||
}
|
||||
for (const reqMatName in this.reqMats) {
|
||||
for (const reqMatName of Object.keys(this.reqMats)) {
|
||||
const normQty = this.reqMats[reqMatName];
|
||||
if (normQty === undefined) continue;
|
||||
totalMatSize -= MaterialSizes[reqMatName] * normQty;
|
||||
@ -695,7 +695,7 @@ export class Industry implements IIndustry {
|
||||
|
||||
// Make sure we have enough resource to make our materials
|
||||
let producableFrac = 1;
|
||||
for (const reqMatName in this.reqMats) {
|
||||
for (const reqMatName of Object.keys(this.reqMats)) {
|
||||
if (this.reqMats.hasOwnProperty(reqMatName)) {
|
||||
const reqMat = this.reqMats[reqMatName];
|
||||
if (reqMat === undefined) continue;
|
||||
@ -712,7 +712,7 @@ export class Industry implements IIndustry {
|
||||
|
||||
// Make our materials if they are producable
|
||||
if (producableFrac > 0 && prod > 0) {
|
||||
for (const reqMatName in this.reqMats) {
|
||||
for (const reqMatName of Object.keys(this.reqMats)) {
|
||||
const reqMat = this.reqMats[reqMatName];
|
||||
if (reqMat === undefined) continue;
|
||||
const reqMatQtyNeeded = reqMat * prod * producableFrac;
|
||||
@ -729,7 +729,7 @@ export class Industry implements IIndustry {
|
||||
Math.pow(warehouse.materials["AICores"].qty, this.aiFac) / 10e3;
|
||||
}
|
||||
} else {
|
||||
for (const reqMatName in this.reqMats) {
|
||||
for (const reqMatName of Object.keys(this.reqMats)) {
|
||||
if (this.reqMats.hasOwnProperty(reqMatName)) {
|
||||
warehouse.materials[reqMatName].prd = 0;
|
||||
}
|
||||
@ -745,7 +745,7 @@ export class Industry implements IIndustry {
|
||||
//If this doesn't produce any materials, then it only creates
|
||||
//Products. Creating products will consume materials. The
|
||||
//Production of all consumed materials must be set to 0
|
||||
for (const reqMatName in this.reqMats) {
|
||||
for (const reqMatName of Object.keys(this.reqMats)) {
|
||||
warehouse.materials[reqMatName].prd = 0;
|
||||
}
|
||||
}
|
||||
@ -753,7 +753,7 @@ export class Industry implements IIndustry {
|
||||
|
||||
case "SALE":
|
||||
/* Process sale of materials */
|
||||
for (const matName in warehouse.materials) {
|
||||
for (const matName of Object.keys(warehouse.materials)) {
|
||||
if (warehouse.materials.hasOwnProperty(matName)) {
|
||||
const mat = warehouse.materials[matName];
|
||||
if (mat.sCost < 0 || mat.sllman[0] === false) {
|
||||
@ -884,7 +884,7 @@ export class Industry implements IIndustry {
|
||||
break;
|
||||
|
||||
case "EXPORT":
|
||||
for (const matName in warehouse.materials) {
|
||||
for (const matName of Object.keys(warehouse.materials)) {
|
||||
if (warehouse.materials.hasOwnProperty(matName)) {
|
||||
const mat = warehouse.materials[matName];
|
||||
mat.totalExp = 0; //Reset export
|
||||
@ -996,7 +996,7 @@ export class Industry implements IIndustry {
|
||||
|
||||
//Create products
|
||||
if (this.state === "PRODUCTION") {
|
||||
for (const prodName in this.products) {
|
||||
for (const prodName of Object.keys(this.products)) {
|
||||
const prod = this.products[prodName];
|
||||
if (prod === undefined) continue;
|
||||
if (!prod.fin) {
|
||||
@ -1028,7 +1028,7 @@ export class Industry implements IIndustry {
|
||||
}
|
||||
|
||||
//Produce Products
|
||||
for (const prodName in this.products) {
|
||||
for (const prodName of Object.keys(this.products)) {
|
||||
if (this.products.hasOwnProperty(prodName)) {
|
||||
const prod = this.products[prodName];
|
||||
if (prod instanceof Product && prod.fin) {
|
||||
@ -1070,7 +1070,7 @@ export class Industry implements IIndustry {
|
||||
|
||||
//Calculate net change in warehouse storage making the Products will cost
|
||||
let netStorageSize = product.siz;
|
||||
for (const reqMatName in product.reqMats) {
|
||||
for (const reqMatName of Object.keys(product.reqMats)) {
|
||||
if (product.reqMats.hasOwnProperty(reqMatName)) {
|
||||
const normQty = product.reqMats[reqMatName];
|
||||
netStorageSize -= MaterialSizes[reqMatName] * normQty;
|
||||
@ -1087,7 +1087,7 @@ export class Industry implements IIndustry {
|
||||
|
||||
//Make sure we have enough resources to make our Products
|
||||
let producableFrac = 1;
|
||||
for (const reqMatName in product.reqMats) {
|
||||
for (const reqMatName of Object.keys(product.reqMats)) {
|
||||
if (product.reqMats.hasOwnProperty(reqMatName)) {
|
||||
const req = product.reqMats[reqMatName] * prod;
|
||||
if (warehouse.materials[reqMatName].qty < req) {
|
||||
@ -1098,7 +1098,7 @@ export class Industry implements IIndustry {
|
||||
|
||||
//Make our Products if they are producable
|
||||
if (producableFrac > 0 && prod > 0) {
|
||||
for (const reqMatName in product.reqMats) {
|
||||
for (const reqMatName of Object.keys(product.reqMats)) {
|
||||
if (product.reqMats.hasOwnProperty(reqMatName)) {
|
||||
const reqMatQtyNeeded = product.reqMats[reqMatName] * prod * producableFrac;
|
||||
warehouse.materials[reqMatName].qty -= reqMatQtyNeeded;
|
||||
@ -1117,7 +1117,7 @@ export class Industry implements IIndustry {
|
||||
case "SALE": {
|
||||
//Process sale of Products
|
||||
product.pCost = 0; //Estimated production cost
|
||||
for (const reqMatName in product.reqMats) {
|
||||
for (const reqMatName of Object.keys(product.reqMats)) {
|
||||
if (product.reqMats.hasOwnProperty(reqMatName)) {
|
||||
product.pCost += product.reqMats[reqMatName] * warehouse.materials[reqMatName].bCost;
|
||||
}
|
||||
@ -1253,7 +1253,7 @@ export class Industry implements IIndustry {
|
||||
}
|
||||
|
||||
discontinueProduct(product: Product): void {
|
||||
for (const productName in this.products) {
|
||||
for (const productName of Object.keys(this.products)) {
|
||||
if (this.products.hasOwnProperty(productName)) {
|
||||
if (product === this.products[productName]) {
|
||||
delete this.products[productName];
|
||||
@ -1358,7 +1358,7 @@ export class Industry implements IIndustry {
|
||||
// Since ResearchTree data isnt saved, we'll update the Research Tree data
|
||||
// based on the stored 'researched' property in the Industry object
|
||||
if (Object.keys(researchTree.researched).length !== Object.keys(this.researched).length) {
|
||||
for (const research in this.researched) {
|
||||
for (const research of Object.keys(this.researched)) {
|
||||
researchTree.research(research);
|
||||
}
|
||||
}
|
||||
|