mirror of
https://github.com/bitburner-official/bitburner-src.git
synced 2024-11-11 18:23:54 +01:00
commit
48b366b01f
@ -2,12 +2,9 @@ root = true
|
||||
|
||||
[*]
|
||||
indent_style = space
|
||||
indent_size = 4
|
||||
indent_size = 2
|
||||
trim_trailing_whitespace = true
|
||||
insert_final_newline = true
|
||||
|
||||
[package.json]
|
||||
indent_size = 2
|
||||
|
||||
[md]
|
||||
trim_trailing_whitespace = false
|
||||
|
136
dist/vendor.bundle.js
vendored
136
dist/vendor.bundle.js
vendored
File diff suppressed because one or more lines are too long
@ -4,7 +4,7 @@
|
||||
|
||||
Source-Files
|
||||
============
|
||||
Source-Files are a type of persistent upgrade that are more powerful than Augmentations.
|
||||
Source-Files are a type of persistent upgrade that is more powerful than Augmentations.
|
||||
Source-Files are received by destroying a BitNode. There are many different BitNodes
|
||||
in the game and each BitNode will grant a different Source-File when it is destroyed.
|
||||
|
||||
@ -15,19 +15,19 @@ of level 3.
|
||||
List of all Source-Files
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
+-------------------------------------+--------------------------------------------------------------------------------------------------------------------------------------------------------------------------+
|
||||
|| BitNode-1: Source Genesis || * Lets the player start with 32 GB of RAM on home computer. |
|
||||
|| BitNode-1: Source Genesis || * Let the player start with 32 GB of RAM on the home computer. |
|
||||
|| || * Increases all of the player's multipliers by 16%/24%/28%. |
|
||||
+-------------------------------------+--------------------------------------------------------------------------------------------------------------------------------------------------------------------------+
|
||||
|| BitNode-2: Rise of the Underworld || * Lets the player create Gangs in other BitNodes (although some |
|
||||
|| BitNode-2: Rise of the Underworld || * Let the player create Gangs in other BitNodes (although some |
|
||||
|| || BitNodes will disable this mechanic). |
|
||||
|| || * Increases the player's crime success rate, crime money, and |
|
||||
|| || charisma multipliers by 24%/36%/42%. |
|
||||
+-------------------------------------+--------------------------------------------------------------------------------------------------------------------------------------------------------------------------+
|
||||
|| BitNode-3: Corporatocracy || * Lets the player create Corporations in other BitNodes (although some |
|
||||
|| BitNode-3: Corporatocracy || * Let the player create Corporations in other BitNodes (although some |
|
||||
|| || BitNodes will disable this mechanic). |
|
||||
|| || * Increases the player's charisma and company salary multipliers by 8%/12%/14%. |
|
||||
+-------------------------------------+--------------------------------------------------------------------------------------------------------------------------------------------------------------------------+
|
||||
|| BitNode-4: The Singularity || * Lets the player access and use Netscript Singularity Functions in other BitNodes. |
|
||||
|| BitNode-4: The Singularity || * Let the player access and use Netscript Singularity Functions in other BitNodes. |
|
||||
|| || * Each level of this Source-File opens up more of the Singularity Functions to use. |
|
||||
+-------------------------------------+--------------------------------------------------------------------------------------------------------------------------------------------------------------------------+
|
||||
|| BitNode-5: Artificial Intelligence || * Unlocks :ref:`gameplay_intelligence`. |
|
||||
@ -47,7 +47,7 @@ List of all Source-Files
|
||||
|| || * Level 3 grants permanent access to use limit/stop orders. |
|
||||
+-------------------------------------+--------------------------------------------------------------------------------------------------------------------------------------------------------------------------+
|
||||
|| BitNode-9: Hacktocracy || * Level 1 permanently unlocks the Hacknet Server in other BitNodes. |
|
||||
|| || * Level 2 lets the player start with 128 GB of RAM on home computer. |
|
||||
|| || * Level 2 lets the player start with 128 GB of RAM on the home computer. |
|
||||
|| || * Level 3 grants a highly-upgraded Hacknet Server when entering a new BitNode (it |
|
||||
|| || will be lost after installing augments). |
|
||||
+-------------------------------------+--------------------------------------------------------------------------------------------------------------------------------------------------------------------------+
|
||||
@ -60,6 +60,6 @@ List of all Source-Files
|
||||
|| || 32%/48%/56%. |
|
||||
+-------------------------------------+--------------------------------------------------------------------------------------------------------------------------------------------------------------------------+
|
||||
|| BitNode-12: The Recursion || * There is no maximum level for this Source-File. |
|
||||
|| || * Lets the player start with Neuroflux Governor equal to the level of this |
|
||||
|| || * Let the player start with Neuroflux Governor equal to the level of this |
|
||||
|| || Source-File. |
|
||||
+-------------------------------------+--------------------------------------------------------------------------------------------------------------------------------------------------------------------------+
|
||||
+-------------------------------------+--------------------------------------------------------------------------------------------------------------------------------------------------------------------------+
|
@ -3,8 +3,107 @@
|
||||
Changelog
|
||||
=========
|
||||
|
||||
v1.1.0 - 2021-12-18 You guys are awesome (community because they're god damn awesome)
|
||||
-------------------------------------------------------------------------------------
|
||||
|
||||
** Script Editor **
|
||||
|
||||
* The text editor can open several files at once. (@Rez855 / @Shadow72)
|
||||
It's not perfect so keep the feedback coming.
|
||||
|
||||
** Steam **
|
||||
|
||||
* Windows has a new launch option that lets player start with killing all their scripts
|
||||
This is a safety net in case all the other safety nets fail.
|
||||
* Linux has several launch options that use different flags for different OS.
|
||||
* Debug and Fullscreen are available in the window utility bar.
|
||||
* Tried (and maybe failed) to make the game completely kill itself after closing.
|
||||
This one I still don't know wtf is going.
|
||||
* No longer has background throttling.
|
||||
* Default color should be pitch black when loading
|
||||
* Add BN13: Challenge achievement.
|
||||
|
||||
** Tutorial **
|
||||
|
||||
* I watched someone play bitburner on youtube and reworked part of
|
||||
the tutorial to try to make some parts of the game clearer.
|
||||
https://www.youtube.com/watch?v=-_JETXff4Zo
|
||||
* Add option to restart tutorial.
|
||||
|
||||
** Netscript **
|
||||
|
||||
* getGangInformation returns more information.
|
||||
* getAscensionResult added
|
||||
* getMemberInformation returns more info
|
||||
* Formulas API has new functions for gang.
|
||||
* Added documentation for corp API.
|
||||
* exec has clearer error message when you send invalid data.
|
||||
* getServer returns all defined field for hacknet servers.
|
||||
* Fix a bug with scp multiple files (@theit8514)
|
||||
* Stack traces should be smarter at replacing blobs with filenames
|
||||
* Fix a weird error message that would occur when throwing raw strings.
|
||||
* Fix shortcuts not working.
|
||||
* Re-added setFocus and isFocused (@theit8514)
|
||||
* new function getHashUpgrades (@MartinFournier)
|
||||
* enableLog accepts "ALL" like disableLog (@wynro)
|
||||
* toast() doesn't crash on invalid data (@ivanjermakov)
|
||||
* alert() doesn't crash on invalid data (@Siern)
|
||||
* Fixed an issue where scripts don't run where they should.
|
||||
* Sleeve getInformation now returns cha
|
||||
* getServer does work with no argument now
|
||||
* workForFaction returns false when it mistakenly returned null
|
||||
|
||||
** Character Overview **
|
||||
|
||||
* The character overview now shows the amount of exp needed to next level (@MartinFournier)
|
||||
|
||||
** Misc. **
|
||||
|
||||
* Add option to supress Game Saved! toasts (@MartinFournier)
|
||||
* Fix bug where ctrl+alt+j was eaten by the wrong process. (@billyvg)
|
||||
* Theme Editor lets you paste colors (@MartinFournier)
|
||||
* ctrl + u/k/w should work on terminal (@billyvg)
|
||||
* Game now shows commit number, this is mostly for me. (@MartinFourier)
|
||||
* running a bad script will give a clearer error message (@TheCoderJT)
|
||||
* Default terminal capacity is maximum (@SayntGarmo)
|
||||
* Fix problems with cp and mv (@theit8514)
|
||||
* Make monaco load fully offline for players behind firewalls.
|
||||
* change beginer guide to use n00dles instead of foodnstuff
|
||||
* BN13 is harder
|
||||
* nerf int gain from manualHack
|
||||
* Fix UI displaying wrong stats (@DJMatch3000)
|
||||
* Fix button not disabling as it should.
|
||||
* New location in Ishima.
|
||||
* Add setting to suppress stock market popups.
|
||||
* Typo fixes (@Hedrauta, @cvr-119, @Ationi, @millennIumAMbiguity
|
||||
@TealKoi, @TheCoderJT, @cblte, @2PacIsAlive, @MageKing17,
|
||||
@Xynrati, @Adraxas, @pobiega)
|
||||
* Fix 100% territory achievement.
|
||||
* Reword message on active scripts page.
|
||||
* Fix terminal not clearing after BN
|
||||
* Remove references to .fconf
|
||||
* Augmentation pages shows BN difficulty with SF5
|
||||
* Fix scripts saving on wrong server while 'connect'ing
|
||||
* Fix gym discount not working.
|
||||
* Fix scan-analyze not working with timestamps
|
||||
* Hash upgrades remember last choice.
|
||||
* Save files now sort by date
|
||||
* The covenant no longer supports negative memory purchases
|
||||
* Fix corp shares buyback triggering by pressing enter
|
||||
* Staneks gift display avg / num charges
|
||||
* Infiltration rewards no longer decay with better stats
|
||||
* terminal 'true' is parsed as boolean not string
|
||||
* tail and kill use autocomplete()
|
||||
* Fix focus for coding contract
|
||||
* massive boost to noodle bar.
|
||||
|
||||
** Special Thanks **
|
||||
|
||||
* Special thank you to everyone on Discord who can answer
|
||||
new player questions so I can focus on more important things.
|
||||
|
||||
v1.1.0 - 2021-12-03 BN13: They're Lunatics (hydroflame & community)
|
||||
-------------------------------------------------------
|
||||
-------------------------------------------------------------------
|
||||
|
||||
** BN13: They're Lunatics **
|
||||
|
||||
|
@ -64,9 +64,9 @@ documentation_title = '{0} Documentation'.format(project)
|
||||
# built documents.
|
||||
#
|
||||
# The short X.Y version.
|
||||
version = '1.1'
|
||||
version = '1.2'
|
||||
# The full version, including alpha/beta/rc tags.
|
||||
release = '1.1.0'
|
||||
release = '1.2.0'
|
||||
|
||||
# The language for content autogenerated by Sphinx. Refer to documentation
|
||||
# for a list of supported languages.
|
||||
|
@ -196,55 +196,60 @@ Here's what mine showed at the time I made this::
|
||||
[home ~]> scan-analyze 2
|
||||
~~~~~~~~~~ Beginning scan-analyze ~~~~~~~~~~
|
||||
|
||||
>n00dles
|
||||
--Root Access: NO, Required hacking skill: 1
|
||||
n00dles
|
||||
--Root Access: YES, Required hacking skill: 1
|
||||
--Number of open ports required to NUKE: 0
|
||||
--RAM: 16
|
||||
|
||||
>sigma-cosmetics
|
||||
--Root Access: NO, Required hacking skill: 5
|
||||
--Number of open ports required to NUKE: 0
|
||||
--RAM: 16
|
||||
|
||||
>joesguns
|
||||
--Root Access: NO, Required hacking skill: 10
|
||||
--Number of open ports required to NUKE: 0
|
||||
--RAM: 16
|
||||
|
||||
---->max-hardware
|
||||
------Root Access: NO, Required hacking skill: 80
|
||||
------Number of open ports required to NUKE: 1
|
||||
------RAM: 32
|
||||
|
||||
>hong-fang-tea
|
||||
--Root Access: NO, Required hacking skill: 30
|
||||
--Number of open ports required to NUKE: 0
|
||||
--RAM: 16
|
||||
|
||||
---->nectar-net
|
||||
------Root Access: NO, Required hacking skill: 20
|
||||
------Number of open ports required to NUKE: 0
|
||||
------RAM: 16
|
||||
|
||||
>harakiri-sushi
|
||||
--Root Access: NO, Required hacking skill: 40
|
||||
--Number of open ports required to NUKE: 0
|
||||
--RAM: 16
|
||||
|
||||
>iron-gym
|
||||
--Root Access: NO, Required hacking skill: 100
|
||||
--Number of open ports required to NUKE: 1
|
||||
--RAM: 32
|
||||
|
||||
---->zer0
|
||||
--RAM: 4.00GB
|
||||
|
||||
----zer0
|
||||
------Root Access: NO, Required hacking skill: 75
|
||||
------Number of open ports required to NUKE: 1
|
||||
------RAM: 32
|
||||
|
||||
---->CSEC
|
||||
------RAM: 32.00GB
|
||||
|
||||
foodnstuff
|
||||
--Root Access: NO, Required hacking skill: 1
|
||||
--Number of open ports required to NUKE: 0
|
||||
--RAM: 16.00GB
|
||||
|
||||
sigma-cosmetics
|
||||
--Root Access: NO, Required hacking skill: 5
|
||||
--Number of open ports required to NUKE: 0
|
||||
--RAM: 16.00GB
|
||||
|
||||
joesguns
|
||||
--Root Access: NO, Required hacking skill: 10
|
||||
--Number of open ports required to NUKE: 0
|
||||
--RAM: 16.00GB
|
||||
|
||||
----max-hardware
|
||||
------Root Access: NO, Required hacking skill: 80
|
||||
------Number of open ports required to NUKE: 1
|
||||
------RAM: 32.00GB
|
||||
|
||||
----CSEC
|
||||
------Root Access: NO, Required hacking skill: 54
|
||||
------Number of open ports required to NUKE: 1
|
||||
------RAM: 8
|
||||
------RAM: 8.00GB
|
||||
|
||||
hong-fang-tea
|
||||
--Root Access: NO, Required hacking skill: 30
|
||||
--Number of open ports required to NUKE: 0
|
||||
--RAM: 16.00GB
|
||||
|
||||
----nectar-net
|
||||
------Root Access: NO, Required hacking skill: 20
|
||||
------Number of open ports required to NUKE: 0
|
||||
------RAM: 16.00GB
|
||||
|
||||
harakiri-sushi
|
||||
--Root Access: NO, Required hacking skill: 40
|
||||
--Number of open ports required to NUKE: 0
|
||||
--RAM: 16.00GB
|
||||
|
||||
iron-gym
|
||||
--Root Access: NO, Required hacking skill: 100
|
||||
--Number of open ports required to NUKE: 1
|
||||
--RAM: 32.00GB
|
||||
|
||||
Take note of the following servers:
|
||||
|
||||
@ -287,7 +292,7 @@ Here's the sequence of |Terminal| commands I used in order to achieve this::
|
||||
$ scp early-hack-template.script harakiri-sushi
|
||||
$ connect n00dles
|
||||
$ run NUKE.exe
|
||||
$ run early-hack-template.script -t 2
|
||||
$ run early-hack-template.script -t 1
|
||||
$ home
|
||||
$ connect sigma-cosmetics
|
||||
$ run NUKE.exe
|
||||
|
@ -12,6 +12,8 @@ const debug = false;
|
||||
function createWindow(killall) {
|
||||
const win = new BrowserWindow({
|
||||
show: false,
|
||||
backgroundThrottling: false,
|
||||
backgroundColor: "#000000",
|
||||
});
|
||||
|
||||
win.removeMenu();
|
||||
@ -44,6 +46,7 @@ function createWindow(killall) {
|
||||
greenworks.activateAchievement(ach, () => undefined);
|
||||
}
|
||||
}, 1000);
|
||||
win.achievementsIntervalID = intervalID;
|
||||
|
||||
// Create the Application's main menu
|
||||
Menu.setApplicationMenu(
|
||||
@ -73,6 +76,7 @@ function createWindow(killall) {
|
||||
{
|
||||
label: "reload & kill all scripts",
|
||||
click: () => {
|
||||
setStopProcessHandler(app, win, false);
|
||||
if (intervalID) clearInterval(intervalID);
|
||||
win.webContents.forcefullyCrashRenderer();
|
||||
win.close();
|
||||
@ -108,8 +112,35 @@ function createWindow(killall) {
|
||||
},
|
||||
]),
|
||||
);
|
||||
|
||||
return win;
|
||||
}
|
||||
|
||||
function setStopProcessHandler(app, window, enabled) {
|
||||
const clearWindowHandler = () => {
|
||||
if (window.achievementsIntervalID) {
|
||||
clearInterval(window.achievementsIntervalID);
|
||||
}
|
||||
window = null;
|
||||
};
|
||||
|
||||
const stopProcessHandler = () => {
|
||||
if (process.platform !== "darwin") {
|
||||
app.quit();
|
||||
process.exit(0);
|
||||
}
|
||||
};
|
||||
|
||||
if (enabled) {
|
||||
window.on("closed", clearWindowHandler);
|
||||
app.on("window-all-closed", stopProcessHandler);
|
||||
} else {
|
||||
window.removeListener("closed", clearWindowHandler);
|
||||
app.removeListener("window-all-closed", stopProcessHandler);
|
||||
}
|
||||
}
|
||||
|
||||
app.whenReady().then(() => {
|
||||
createWindow(false);
|
||||
const win = createWindow(process.argv.includes("--no-scripts"));
|
||||
setStopProcessHandler(app, win, true);
|
||||
});
|
||||
|
@ -12012,6 +12012,34 @@
|
||||
],
|
||||
"name": "getHashUpgradeLevel"
|
||||
},
|
||||
{
|
||||
"kind": "MethodSignature",
|
||||
"canonicalReference": "bitburner!Hacknet#getHashUpgrades:member(1)",
|
||||
"docComment": "/**\n * Get the list of hash upgrades\n *\n * @remarks\n *\n * RAM cost: 0 GB\n *\n * This function is only applicable for Hacknet Servers (the upgraded version of a Hacknet Node).\n *\n * Returns the list of all available hash upgrades that can be used in the spendHashes function.\n *\n * @returns An array containing the available upgrades\n *\n * @example\n * ```ts\n * const upgrades = hacknet.getHashUpgrades(); // [\"Sell for Money\",\"Sell for Corporation Funds\",...]\n * ```\n *\n */\n",
|
||||
"excerptTokens": [
|
||||
{
|
||||
"kind": "Content",
|
||||
"text": "getHashUpgrades(): "
|
||||
},
|
||||
{
|
||||
"kind": "Content",
|
||||
"text": "string[]"
|
||||
},
|
||||
{
|
||||
"kind": "Content",
|
||||
"text": ";"
|
||||
}
|
||||
],
|
||||
"isOptional": false,
|
||||
"returnTypeTokenRange": {
|
||||
"startIndex": 1,
|
||||
"endIndex": 2
|
||||
},
|
||||
"releaseTag": "Public",
|
||||
"overloadIndex": 1,
|
||||
"parameters": [],
|
||||
"name": "getHashUpgrades"
|
||||
},
|
||||
{
|
||||
"kind": "MethodSignature",
|
||||
"canonicalReference": "bitburner!Hacknet#getLevelUpgradeCost:member(1)",
|
||||
|
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
34
markdown/bitburner.hacknet.gethashupgrades.md
Normal file
34
markdown/bitburner.hacknet.gethashupgrades.md
Normal file
@ -0,0 +1,34 @@
|
||||
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
|
||||
|
||||
[Home](./index.md) > [bitburner](./bitburner.md) > [Hacknet](./bitburner.hacknet.md) > [getHashUpgrades](./bitburner.hacknet.gethashupgrades.md)
|
||||
|
||||
## Hacknet.getHashUpgrades() method
|
||||
|
||||
Get the list of hash upgrades
|
||||
|
||||
<b>Signature:</b>
|
||||
|
||||
```typescript
|
||||
getHashUpgrades(): string[];
|
||||
```
|
||||
<b>Returns:</b>
|
||||
|
||||
string\[\]
|
||||
|
||||
An array containing the available upgrades
|
||||
|
||||
## Remarks
|
||||
|
||||
RAM cost: 0 GB
|
||||
|
||||
This function is only applicable for Hacknet Servers (the upgraded version of a Hacknet Node).
|
||||
|
||||
Returns the list of all available hash upgrades that can be used in the spendHashes function.
|
||||
|
||||
## Example
|
||||
|
||||
|
||||
```ts
|
||||
const upgrades = hacknet.getHashUpgrades(); // ["Sell for Money","Sell for Corporation Funds",...]
|
||||
```
|
||||
|
@ -23,6 +23,7 @@ Not all these functions are immediately available.
|
||||
| [getCacheUpgradeCost(index, n)](./bitburner.hacknet.getcacheupgradecost.md) | Calculate the cost of upgrading hacknet node cache. |
|
||||
| [getCoreUpgradeCost(index, n)](./bitburner.hacknet.getcoreupgradecost.md) | Calculate the cost of upgrading hacknet node cores. |
|
||||
| [getHashUpgradeLevel(upgName)](./bitburner.hacknet.gethashupgradelevel.md) | Get the level of a hash upgrade. |
|
||||
| [getHashUpgrades()](./bitburner.hacknet.gethashupgrades.md) | Get the list of hash upgrades |
|
||||
| [getLevelUpgradeCost(index, n)](./bitburner.hacknet.getlevelupgradecost.md) | Calculate the cost of upgrading hacknet node levels. |
|
||||
| [getNodeStats(index)](./bitburner.hacknet.getnodestats.md) | Get the stats of a hacknet node. |
|
||||
| [getPurchaseNodeCost()](./bitburner.hacknet.getpurchasenodecost.md) | Get the price of the next hacknet node. |
|
||||
|
@ -1,10 +1,10 @@
|
||||
{
|
||||
"name": "bitburner",
|
||||
"license": "SEE LICENSE IN license.txt",
|
||||
"version": "1.1.0",
|
||||
"version": "1.2.0",
|
||||
"main": "electron-main.js",
|
||||
"author": {
|
||||
"name": "Daniel Xie"
|
||||
"name": "Daniel Xie & Olivier Gagnon"
|
||||
},
|
||||
"bugs": {
|
||||
"url": "https://github.com/danielyxie/bitburner/issues"
|
||||
@ -109,6 +109,7 @@
|
||||
"test:watch": "jest --watch",
|
||||
"watch": "webpack --watch --mode production",
|
||||
"watch:dev": "webpack --watch --mode development",
|
||||
"electron": "cp -r electron/* .package && cp index.html .package && cp main.bundle.js .package && cp dist/vendor.bundle.js .package/dist/ && cp -r dist/ext .package/dist/ && electron-packager .package bitburner --all --out .build --overwrite --icon .package/icon.png"
|
||||
"electron": "cp -r electron/* .package && cp index.html .package && cp main.bundle.js .package && cp dist/vendor.bundle.js .package/dist/ && cp -r dist/ext .package/dist/ && electron-packager .package bitburner --all --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"
|
||||
}
|
||||
}
|
||||
|
2
src/@types/global.d.ts
vendored
Normal file
2
src/@types/global.d.ts
vendored
Normal file
@ -0,0 +1,2 @@
|
||||
// Defined by webpack on startup or compilation
|
||||
declare let __COMMIT_HASH__: string;
|
@ -29,7 +29,6 @@ export function PurchasedAugmentations(): React.ReactElement {
|
||||
if (ownedAug.name === AugmentationNames.NeuroFluxGovernor) {
|
||||
level = ownedAug.level;
|
||||
}
|
||||
|
||||
augs.push(<AugmentationAccordion key={aug.name} aug={aug} level={level} />);
|
||||
}
|
||||
|
||||
|
143
src/Constants.ts
143
src/Constants.ts
@ -111,8 +111,8 @@ export const CONSTANTS: {
|
||||
TotalNumBitNodes: number;
|
||||
LatestUpdate: string;
|
||||
} = {
|
||||
VersionString: "1.1.0",
|
||||
VersionNumber: 6,
|
||||
VersionString: "1.2.0",
|
||||
VersionNumber: 7,
|
||||
|
||||
// Speed (in ms) at which the main loop is updated
|
||||
_idleSpeed: 200,
|
||||
@ -273,72 +273,103 @@ export const CONSTANTS: {
|
||||
TotalNumBitNodes: 24,
|
||||
|
||||
LatestUpdate: `
|
||||
v1.1.0 - 2021-12-03 BN13: They're Lunatics (hydroflame & community)
|
||||
-------------------------------------------------------
|
||||
v1.1.0 - 2021-12-18 You guys are awesome (community because they're god damn awesome)
|
||||
-------------------------------------------------------------------------------------
|
||||
|
||||
** BN13: They're Lunatics **
|
||||
** Script Editor **
|
||||
|
||||
* BN13 added.
|
||||
* The text editor can open several files at once. (@Rez855 / @Shadow72)
|
||||
It's not perfect so keep the feedback coming.
|
||||
|
||||
** Steam **
|
||||
|
||||
* Tested on all 3 major OS.
|
||||
* 94 achievements added
|
||||
* Release is 2021-12-10.
|
||||
* Windows has a new launch option that lets player start with killing all their scripts
|
||||
This is a safety net in case all the other safety nets fail.
|
||||
* Linux has several launch options that use different flags for different OS.
|
||||
* Debug and Fullscreen are available in the window utility bar.
|
||||
* Tried (and maybe failed) to make the game completely kill itself after closing.
|
||||
This one I still don't know wtf is going.
|
||||
* No longer has background throttling.
|
||||
* Default color should be pitch black when loading
|
||||
* Add BN13: Challenge achievement.
|
||||
|
||||
** Corporation API **
|
||||
** Tutorial **
|
||||
|
||||
* Added corporation API. (Unstable)
|
||||
* I watched someone play bitburner on youtube and reworked part of
|
||||
the tutorial to try to make some parts of the game clearer.
|
||||
https://www.youtube.com/watch?v=-_JETXff4Zo
|
||||
* Add option to restart tutorial.
|
||||
|
||||
** Netscript **
|
||||
|
||||
* tprintf crashes when not giving a format as first arg.
|
||||
* tprintf no longer prints filename (@BartKoppelmans)
|
||||
* TIX buy/sell/sellShort all return askprice/bidprice (@Insight)
|
||||
* getRunningScript now works.
|
||||
* Fix disableLog for gang and TIX API
|
||||
* getOwnedSourceFiles is not singularity anymore (makes it easier to share scripts.) (@theit8514)
|
||||
* true/false is a valid value to send to other scripts.
|
||||
* workForFaction no longer returns null when trying to work for gang.
|
||||
* Scripts logging no longer generates the string if logging is disabled.
|
||||
This should give performance boost for some scripts.
|
||||
|
||||
** Gang **
|
||||
|
||||
* Gang with 0 territory can no longer fight
|
||||
* Territory now caps at exactly 0 or 1.
|
||||
* getGangInformation returns more information.
|
||||
* getAscensionResult added
|
||||
* getMemberInformation returns more info
|
||||
* Formulas API has new functions for gang.
|
||||
* Added documentation for corp API.
|
||||
* exec has clearer error message when you send invalid data.
|
||||
* getServer returns all defined field for hacknet servers.
|
||||
* Fix a bug with scp multiple files (@theit8514)
|
||||
* Stack traces should be smarter at replacing blobs with filenames
|
||||
* Fix a weird error message that would occur when throwing raw strings.
|
||||
* Fix shortcuts not working.
|
||||
* Re-added setFocus and isFocused (@theit8514)
|
||||
* new function getHashUpgrades (@MartinFournier)
|
||||
* enableLog accepts "ALL" like disableLog (@wynro)
|
||||
* toast() doesn't crash on invalid data (@ivanjermakov)
|
||||
* alert() doesn't crash on invalid data (@Siern)
|
||||
* Fixed an issue where scripts don't run where they should.
|
||||
* Sleeve getInformation now returns cha
|
||||
* getServer does work with no argument now
|
||||
* workForFaction returns false when it mistakenly returned null
|
||||
|
||||
** Character Overview **
|
||||
|
||||
* The character overview now shows the amount of exp needed to next level (@MartinFournier)
|
||||
|
||||
** Misc. **
|
||||
|
||||
* Clicking "previous" on the browser will not pretend you had unsaved information
|
||||
allowing you to cancel if needs be.
|
||||
* Fixed some tail box coloring issue.
|
||||
* Fixed BladeBurner getCityCommunities ram cost
|
||||
* The download terminal command no longer duplicate extensions (@Insight)
|
||||
* Fix #000 on #000 text in blackjack. (@Insight)
|
||||
* Remove reference to .fconf
|
||||
* Tail boxes all die on soft reset.
|
||||
* Fix codign contract focus bug.
|
||||
* Megacorp factions simply re-invite you instead of auto added on reset. (@theit8514)
|
||||
* Tail window is bound to html body.
|
||||
* Infiltration reward is tied to your potential stats, not your actual stats
|
||||
So you won't lose reward for doing the same thing over and over.
|
||||
* intelligence lowers program creation requirements.
|
||||
* Terminal parses true as the boolean, not the string.
|
||||
* Tail and kill autocomplete using the ns2 autocomplete feature.
|
||||
* scan-analyze doesn't take up as many terminal entries.
|
||||
* GangOtherInfo documentation now renders correctly.
|
||||
* ActiveScripts search box also searches for script names.
|
||||
* Infinite money no longer allows for infinite hacknet server.
|
||||
* Blackjack doesn't make you lose money twice.
|
||||
* Recent Scripts is now from most to least recent.
|
||||
* Fix mathjax ascii art bug in NiteSec.
|
||||
* Remove warning that the theme editor is slow, it's only slow in dev mode.
|
||||
* In BN8 is it possible to reduce the money on a server without gaining any.
|
||||
* In the options, the timestamp feature has a placeholder explaining the expected format.
|
||||
* Bunch of doc typo fix. (hydroflame & @BartKoppelmans & @cvr-119)
|
||||
* World Daemon difficulty is increased depending on BitNode.
|
||||
* hacking skill nerf was loosened in some BNs.
|
||||
* nerf noodle bar
|
||||
* Add option to supress Game Saved! toasts (@MartinFournier)
|
||||
* Fix bug where ctrl+alt+j was eaten by the wrong process. (@billyvg)
|
||||
* Theme Editor lets you paste colors (@MartinFournier)
|
||||
* ctrl + u/k/w should work on terminal (@billyvg)
|
||||
* Game now shows commit number, this is mostly for me. (@MartinFourier)
|
||||
* running a bad script will give a clearer error message (@TheCoderJT)
|
||||
* Default terminal capacity is maximum (@SayntGarmo)
|
||||
* Fix problems with cp and mv (@theit8514)
|
||||
* Make monaco load fully offline for players behind firewalls.
|
||||
* change beginer guide to use n00dles instead of foodnstuff
|
||||
* BN13 is harder
|
||||
* nerf int gain from manualHack
|
||||
* Fix UI displaying wrong stats (@DJMatch3000)
|
||||
* Fix button not disabling as it should.
|
||||
* New location in Ishima.
|
||||
* Add setting to suppress stock market popups.
|
||||
* Typo fixes (@Hedrauta, @cvr-119, @Ationi, @millennIumAMbiguity
|
||||
@TealKoi, @TheCoderJT, @cblte, @2PacIsAlive, @MageKing17,
|
||||
@Xynrati, @Adraxas, @pobiega)
|
||||
* Fix 100% territory achievement.
|
||||
* Reword message on active scripts page.
|
||||
* Fix terminal not clearing after BN
|
||||
* Remove references to .fconf
|
||||
* Augmentation pages shows BN difficulty with SF5
|
||||
* Fix scripts saving on wrong server while 'connect'ing
|
||||
* Fix gym discount not working.
|
||||
* Fix scan-analyze not working with timestamps
|
||||
* Hash upgrades remember last choice.
|
||||
* Save files now sort by date
|
||||
* The covenant no longer supports negative memory purchases
|
||||
* Fix corp shares buyback triggering by pressing enter
|
||||
* Staneks gift display avg / num charges
|
||||
* Infiltration rewards no longer decay with better stats
|
||||
* terminal 'true' is parsed as boolean not string
|
||||
* tail and kill use autocomplete()
|
||||
* Fix focus for coding contract
|
||||
* massive boost to noodle bar.
|
||||
|
||||
** Special Thanks **
|
||||
|
||||
* Special thank you to everyone on Discord who can answer
|
||||
new player questions so I can focus on more important things.
|
||||
`,
|
||||
};
|
||||
|
@ -15,6 +15,6 @@ export const DarkWebItems: IMap<DarkWebItem> = {
|
||||
),
|
||||
DeepscanV1: new DarkWebItem(Programs.DeepscanV1.name, 500000, "Enables 'scan-analyze' with a depth up to 5."),
|
||||
DeepscanV2: new DarkWebItem(Programs.DeepscanV2.name, 25e6, "Enables 'scan-analyze' with a depth up to 10."),
|
||||
AutolinkProgram: new DarkWebItem(Programs.AutoLink.name, 1e6, "Enables direct connect via 'scan-analyze."),
|
||||
AutolinkProgram: new DarkWebItem(Programs.AutoLink.name, 1e6, "Enables direct connect via 'scan-analyze'."),
|
||||
FormulasProgram: new DarkWebItem(Programs.Formulas.name, 5e9, "Unlock access to the formulas API."),
|
||||
};
|
||||
|
@ -189,7 +189,7 @@ const achievements: Achievement[] = [
|
||||
},
|
||||
{
|
||||
ID: "GANG_TERRITORY",
|
||||
Condition: () => Player.gang !== null && AllGangs[Player.gang.facName].territory === 1,
|
||||
Condition: () => Player.gang !== null && AllGangs[Player.gang.facName].territory >= 0.999,
|
||||
},
|
||||
{
|
||||
ID: "GANG_MEMBER_POWER",
|
||||
|
@ -754,7 +754,12 @@ export function NetscriptFunctions(workerScript: WorkerScript): NS {
|
||||
}
|
||||
},
|
||||
enableLog: function (fn: any): any {
|
||||
if (possibleLogs[fn] === undefined) {
|
||||
if (fn === "ALL") {
|
||||
for (fn in possibleLogs) {
|
||||
delete workerScript.disableLogs[fn];
|
||||
}
|
||||
workerScript.log("enableLog", () => `Enabled logging for all functions`);
|
||||
} else if (possibleLogs[fn] === undefined) {
|
||||
throw makeRuntimeErrorMsg("enableLog", `Invalid argument: ${fn}.`);
|
||||
}
|
||||
delete workerScript.disableLogs[fn];
|
||||
|
@ -16,6 +16,8 @@ import {
|
||||
} from "../Hacknet/HacknetHelpers";
|
||||
import { HacknetServer } from "../Hacknet/HacknetServer";
|
||||
import { HacknetNode } from "../Hacknet/HacknetNode";
|
||||
import { HashUpgrades } from "../Hacknet/HashUpgrades";
|
||||
import { HashUpgrade } from "../Hacknet/HashUpgrade";
|
||||
import { GetServer } from "../Server/AllServers";
|
||||
|
||||
import { Hacknet as IHacknet, NodeStats } from "../ScriptEditor/NetscriptDefinitions";
|
||||
@ -166,6 +168,12 @@ export function NetscriptHacknet(player: IPlayer, workerScript: WorkerScript, he
|
||||
}
|
||||
return purchaseHashUpgrade(player, upgName, upgTarget);
|
||||
},
|
||||
getHashUpgrades: function(): string[] {
|
||||
if (!hasHacknetServers(player)) {
|
||||
return [];
|
||||
}
|
||||
return Object.values(HashUpgrades).map((upgrade: HashUpgrade) => upgrade.name);
|
||||
},
|
||||
getHashUpgradeLevel: function (upgName: any): number {
|
||||
const level = player.hashManager.upgrades[upgName];
|
||||
if (level === undefined) {
|
||||
|
@ -29,6 +29,7 @@ import { ICodingContractReward } from "../CodingContracts";
|
||||
import { IRouter } from "../ui/Router";
|
||||
import { WorkerScript } from "../Netscript/WorkerScript";
|
||||
import { HacknetServer } from "../Hacknet/HacknetServer";
|
||||
import { ISkillProgress } from "./formulas/skill";
|
||||
|
||||
export interface IPlayer {
|
||||
// Class members
|
||||
@ -259,6 +260,7 @@ export interface IPlayer {
|
||||
prestigeAugmentation(): void;
|
||||
prestigeSourceFile(): void;
|
||||
calculateSkill(exp: number, mult?: number): number;
|
||||
calculateSkillProgress(exp: number, mult?: number): ISkillProgress;
|
||||
resetWorkStatus(generalType?: string, group?: string, workType?: string): void;
|
||||
getWorkHackExpGain(): number;
|
||||
getWorkStrExpGain(): number;
|
||||
|
@ -34,6 +34,7 @@ import { CityName } from "../../Locations/data/CityNames";
|
||||
|
||||
import { MoneySourceTracker } from "../../utils/MoneySourceTracker";
|
||||
import { Reviver, Generic_toJSON, Generic_fromJSON } from "../../utils/JSONReviver";
|
||||
import { ISkillProgress } from "../formulas/skill";
|
||||
|
||||
export class PlayerObject implements IPlayer {
|
||||
// Class members
|
||||
@ -265,6 +266,7 @@ export class PlayerObject implements IPlayer {
|
||||
prestigeAugmentation: () => void;
|
||||
prestigeSourceFile: () => void;
|
||||
calculateSkill: (exp: number, mult?: number) => number;
|
||||
calculateSkillProgress: (exp: number, mult?: number) => ISkillProgress;
|
||||
resetWorkStatus: (generalType?: string, group?: string, workType?: string) => void;
|
||||
getWorkHackExpGain: () => number;
|
||||
getWorkStrExpGain: () => number;
|
||||
@ -470,6 +472,7 @@ export class PlayerObject implements IPlayer {
|
||||
this.prestigeSourceFile = generalMethods.prestigeSourceFile;
|
||||
this.receiveInvite = generalMethods.receiveInvite;
|
||||
this.calculateSkill = generalMethods.calculateSkill;
|
||||
this.calculateSkillProgress = generalMethods.calculateSkillProgress;
|
||||
this.updateSkillLevels = generalMethods.updateSkillLevels;
|
||||
this.resetMultipliers = generalMethods.resetMultipliers;
|
||||
this.hasProgram = generalMethods.hasProgram;
|
||||
|
@ -26,7 +26,7 @@ import { Locations } from "../../Locations/Locations";
|
||||
import { CityName } from "../../Locations/data/CityNames";
|
||||
import { LocationName } from "../../Locations/data/LocationNames";
|
||||
import { Sleeve } from "../../PersonObjects/Sleeve/Sleeve";
|
||||
import { calculateSkill as calculateSkillF } from "../formulas/skill";
|
||||
import { calculateSkill as calculateSkillF, calculateSkillProgress as calculateSkillProgressF, getEmptySkillProgress, ISkillProgress } from "../formulas/skill";
|
||||
import { calculateIntelligenceBonus } from "../formulas/intelligence";
|
||||
import {
|
||||
getHackingWorkRepGain,
|
||||
@ -226,6 +226,11 @@ export function calculateSkill(this: IPlayer, exp: number, mult = 1): number {
|
||||
return calculateSkillF(exp, mult);
|
||||
}
|
||||
|
||||
//Calculates skill level progress based on experience. The same formula will be used for every skill
|
||||
export function calculateSkillProgress(this: IPlayer, exp: number, mult = 1): ISkillProgress {
|
||||
return calculateSkillProgressF(exp, mult);
|
||||
}
|
||||
|
||||
export function updateSkillLevels(this: IPlayer): void {
|
||||
this.hacking = Math.max(
|
||||
1,
|
||||
|
@ -5,3 +5,37 @@ export function calculateSkill(exp: number, mult = 1): number {
|
||||
export function calculateExp(skill: number, mult = 1): number {
|
||||
return Math.exp((skill / mult + 200) / 32) - 534.6;
|
||||
}
|
||||
|
||||
export function calculateSkillProgress(exp: number, mult = 1): ISkillProgress {
|
||||
const currentSkill = calculateSkill(exp, mult);
|
||||
const nextSkill = currentSkill + 1;
|
||||
let baseExperience = calculateExp(currentSkill, mult);
|
||||
if (baseExperience < 0) baseExperience = 0;
|
||||
const nextExperience = calculateExp(nextSkill, mult)
|
||||
|
||||
return {
|
||||
currentSkill,
|
||||
nextSkill,
|
||||
baseExperience,
|
||||
experience: exp,
|
||||
nextExperience,
|
||||
progress: exp / nextExperience,
|
||||
}
|
||||
}
|
||||
|
||||
export interface ISkillProgress {
|
||||
currentSkill: number;
|
||||
nextSkill: number;
|
||||
baseExperience: number;
|
||||
experience: number;
|
||||
nextExperience: number;
|
||||
progress: number;
|
||||
}
|
||||
|
||||
export function getEmptySkillProgress() {
|
||||
return {
|
||||
currentSkill: 0, nextSkill: 0,
|
||||
baseExperience: 0, experience: 0, nextExperience: 0,
|
||||
progress: 0,
|
||||
};
|
||||
}
|
||||
|
@ -67,7 +67,11 @@ class BitburnerSaveObject {
|
||||
const saveString = this.getSaveString();
|
||||
|
||||
save(saveString)
|
||||
.then(() => SnackbarEvents.emit("Game Saved!", "info"))
|
||||
.then(() => {
|
||||
if (!Settings.SuppressSavedGameToast) {
|
||||
SnackbarEvents.emit("Game Saved!", "info")
|
||||
}
|
||||
})
|
||||
.catch((err) => console.error(err));
|
||||
}
|
||||
|
||||
|
20
src/ScriptEditor/NetscriptDefinitions.d.ts
vendored
20
src/ScriptEditor/NetscriptDefinitions.d.ts
vendored
@ -2290,6 +2290,22 @@ export interface Hacknet {
|
||||
*/
|
||||
spendHashes(upgName: string, upgTarget?: string): boolean;
|
||||
|
||||
/**
|
||||
* Get the list of hash upgrades
|
||||
* @remarks
|
||||
* RAM cost: 0 GB
|
||||
*
|
||||
* This function is only applicable for Hacknet Servers (the upgraded version of a Hacknet Node).
|
||||
*
|
||||
* Returns the list of all available hash upgrades that can be used in the spendHashes function.
|
||||
* @example
|
||||
* ```ts
|
||||
* const upgrades = hacknet.getHashUpgrades(); // ["Sell for Money","Sell for Corporation Funds",...]
|
||||
* ```
|
||||
* @returns An array containing the available upgrades
|
||||
*/
|
||||
getHashUpgrades(): string[];
|
||||
|
||||
/**
|
||||
* Get the level of a hash upgrade.
|
||||
* @remarks
|
||||
@ -4362,7 +4378,7 @@ export interface NS extends Singularity {
|
||||
* @param args - Additional arguments to pass into the new script that is being run. Note that if any arguments are being passed into the new script, then the third argument numThreads must be filled in with a value.
|
||||
* @returns Returns the PID of a successfully started script, and 0 otherwise.
|
||||
*/
|
||||
exec(script: string, host: string, numThreads?: number, ...args: string[]): number;
|
||||
exec(script: string, host: string, numThreads?: number, ...args: Array<string | number | boolean>): number;
|
||||
|
||||
/**
|
||||
* Terminate current script and start another in 10s.
|
||||
@ -5242,7 +5258,7 @@ export interface NS extends Singularity {
|
||||
* @param format - Formatter.
|
||||
* @returns Formated number.
|
||||
*/
|
||||
nFormat(n: number, format: string): number;
|
||||
nFormat(n: number, format: string): string;
|
||||
|
||||
/**
|
||||
* Format time to readable string
|
||||
|
@ -1,7 +1,8 @@
|
||||
import React, { useState, useEffect, useRef, useMemo } from "react";
|
||||
import Editor from "@monaco-editor/react";
|
||||
import Editor, { Monaco } from "@monaco-editor/react";
|
||||
import * as monaco from "monaco-editor";
|
||||
type IStandaloneCodeEditor = monaco.editor.IStandaloneCodeEditor;
|
||||
type ITextModel = monaco.editor.ITextModel;
|
||||
import { OptionsModal } from "./OptionsModal";
|
||||
import { Options } from "./Options";
|
||||
import { isValidFilePath } from "../../Terminal/DirectoryHelpers";
|
||||
@ -26,10 +27,10 @@ import { loadThemes } from "./themes";
|
||||
import { GetServer } from "../../Server/AllServers";
|
||||
|
||||
import Button from "@mui/material/Button";
|
||||
import Tooltip from "@mui/material/Tooltip";
|
||||
import Typography from "@mui/material/Typography";
|
||||
import Link from "@mui/material/Link";
|
||||
import Box from "@mui/material/Box";
|
||||
import TextField from "@mui/material/TextField";
|
||||
import IconButton from "@mui/material/IconButton";
|
||||
import SettingsIcon from "@mui/icons-material/Settings";
|
||||
|
||||
@ -80,22 +81,32 @@ interface IProps {
|
||||
// https://github.com/threehams/typescript-error-guide/blob/master/stories/components/Editor.tsx#L11-L39
|
||||
// https://blog.checklyhq.com/customizing-monaco/
|
||||
|
||||
// These variables are used to reload a script when it's clicked on. Because we
|
||||
// won't have references to the old script.
|
||||
let lastFilename = "";
|
||||
let lastCode = "";
|
||||
let hostname = "";
|
||||
let lastPosition: monaco.Position | null = null;
|
||||
// Holds all the data for a open script
|
||||
class openScript {
|
||||
fileName: string;
|
||||
code: string;
|
||||
hostname: string;
|
||||
lastPosition: monaco.Position;
|
||||
model: ITextModel;
|
||||
|
||||
constructor(fileName: string, code: string, hostname: string, lastPosition: monaco.Position, model: ITextModel) {
|
||||
this.fileName = fileName;
|
||||
this.code = code;
|
||||
this.hostname = hostname;
|
||||
this.lastPosition = lastPosition;
|
||||
this.model = model;
|
||||
}
|
||||
}
|
||||
|
||||
const openScripts = new Array<openScript>(); // Holds all open scripts
|
||||
let currentScript = {} as openScript; // Script currently being viewed
|
||||
|
||||
export function Root(props: IProps): React.ReactElement {
|
||||
const editorRef = useRef<IStandaloneCodeEditor | null>(null);
|
||||
const [filename, setFilename] = useState(props.filename ? props.filename : lastFilename);
|
||||
const [code, setCode] = useState<string>(props.filename ? props.code : lastCode);
|
||||
const monacoRef = useRef<Monaco | null>(null);
|
||||
const [filename, setFilename] = useState(props.filename);
|
||||
const [code, setCode] = useState<string>(props.code);
|
||||
const [decorations, setDecorations] = useState<string[]>([]);
|
||||
hostname = props.filename ? props.hostname : hostname;
|
||||
if (hostname === "") {
|
||||
hostname = props.player.getCurrentServer().hostname;
|
||||
}
|
||||
const [ram, setRAM] = useState("RAM: ???");
|
||||
const [updatingRam, setUpdatingRam] = useState(false);
|
||||
const [optionsOpen, setOptionsOpen] = useState(false);
|
||||
@ -114,54 +125,21 @@ export function Root(props: IProps): React.ReactElement {
|
||||
[],
|
||||
);
|
||||
|
||||
// store the last known state in case we need to restart without nano.
|
||||
useEffect(() => {
|
||||
if (props.filename === undefined) return;
|
||||
lastFilename = props.filename;
|
||||
lastCode = props.code;
|
||||
lastPosition = null;
|
||||
}, []);
|
||||
|
||||
function save(): void {
|
||||
if (editorRef.current !== null) {
|
||||
const position = editorRef.current.getPosition();
|
||||
if (position !== null) {
|
||||
CursorPositions.saveCursor(filename, {
|
||||
row: position.lineNumber,
|
||||
column: position.column,
|
||||
});
|
||||
}
|
||||
}
|
||||
lastPosition = null;
|
||||
|
||||
// this is duplicate code with saving later.
|
||||
if (ITutorial.isRunning && ITutorial.currStep === iTutorialSteps.TerminalTypeScript) {
|
||||
//Make sure filename + code properly follow tutorial
|
||||
if (filename !== "n00dles.script") {
|
||||
if (currentScript.fileName !== "n00dles.script") {
|
||||
dialogBoxCreate("Leave the script name as 'n00dles.script'!");
|
||||
return;
|
||||
}
|
||||
if (code.replace(/\s/g, "").indexOf("while(true){hack('n00dles');}") == -1) {
|
||||
if (currentScript.code.replace(/\s/g, "").indexOf("while(true){hack('n00dles');}") == -1) {
|
||||
dialogBoxCreate("Please copy and paste the code from the tutorial!");
|
||||
return;
|
||||
}
|
||||
|
||||
//Save the script
|
||||
const server = GetServer(hostname);
|
||||
if (server === null) throw new Error("Server should not be null but it is.");
|
||||
let found = false;
|
||||
for (let i = 0; i < server.scripts.length; i++) {
|
||||
if (filename == server.scripts[i].filename) {
|
||||
server.scripts[i].saveScript(filename, code, hostname, server.scripts);
|
||||
found = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (!found) {
|
||||
const script = new Script();
|
||||
script.saveScript(filename, code, hostname, server.scripts);
|
||||
server.scripts.push(script);
|
||||
}
|
||||
saveScript(currentScript);
|
||||
|
||||
iTutorialNextStep();
|
||||
|
||||
@ -169,45 +147,48 @@ export function Root(props: IProps): React.ReactElement {
|
||||
return;
|
||||
}
|
||||
|
||||
if (filename == "") {
|
||||
if (currentScript.fileName == "") {
|
||||
dialogBoxCreate("You must specify a filename!");
|
||||
return;
|
||||
}
|
||||
|
||||
if (!isValidFilePath(filename)) {
|
||||
if (!isValidFilePath(currentScript.fileName)) {
|
||||
dialogBoxCreate(
|
||||
"Script filename can contain only alphanumerics, hyphens, and underscores, and must end with an extension.",
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
const server = GetServer(hostname);
|
||||
const server = GetServer(currentScript.hostname);
|
||||
if (server === null) throw new Error("Server should not be null but it is.");
|
||||
if (isScriptFilename(filename)) {
|
||||
if (isScriptFilename(currentScript.fileName)) {
|
||||
//If the current script already exists on the server, overwrite it
|
||||
for (let i = 0; i < server.scripts.length; i++) {
|
||||
if (filename == server.scripts[i].filename) {
|
||||
server.scripts[i].saveScript(filename, code, props.player.currentServer, server.scripts);
|
||||
if (currentScript.fileName == server.scripts[i].filename) {
|
||||
server.scripts[i].saveScript(
|
||||
currentScript.fileName,
|
||||
currentScript.code,
|
||||
props.player.currentServer,
|
||||
server.scripts,
|
||||
);
|
||||
if (Settings.SaveGameOnFileSave) saveObject.saveGame();
|
||||
props.router.toTerminal();
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
//If the current script does NOT exist, create a new one
|
||||
const script = new Script();
|
||||
script.saveScript(filename, code, props.player.currentServer, server.scripts);
|
||||
script.saveScript(currentScript.fileName, currentScript.code, props.player.currentServer, server.scripts);
|
||||
server.scripts.push(script);
|
||||
} else if (filename.endsWith(".txt")) {
|
||||
} else if (currentScript.fileName.endsWith(".txt")) {
|
||||
for (let i = 0; i < server.textFiles.length; ++i) {
|
||||
if (server.textFiles[i].fn === filename) {
|
||||
server.textFiles[i].write(code);
|
||||
if (server.textFiles[i].fn === currentScript.fileName) {
|
||||
server.textFiles[i].write(currentScript.code);
|
||||
if (Settings.SaveGameOnFileSave) saveObject.saveGame();
|
||||
props.router.toTerminal();
|
||||
return;
|
||||
}
|
||||
}
|
||||
const textFile = new TextFile(filename, code);
|
||||
const textFile = new TextFile(currentScript.fileName, currentScript.code);
|
||||
server.textFiles.push(textFile);
|
||||
} else {
|
||||
dialogBoxCreate("Invalid filename. Must be either a script (.script, .js, or .ns) or " + " or text file (.txt)");
|
||||
@ -215,7 +196,6 @@ export function Root(props: IProps): React.ReactElement {
|
||||
}
|
||||
|
||||
if (Settings.SaveGameOnFileSave) saveObject.saveGame();
|
||||
props.router.toTerminal();
|
||||
}
|
||||
|
||||
function beautify(): void {
|
||||
@ -223,14 +203,9 @@ export function Root(props: IProps): React.ReactElement {
|
||||
editorRef.current.getAction("editor.action.formatDocument").run();
|
||||
}
|
||||
|
||||
function onFilenameChange(event: React.ChangeEvent<HTMLInputElement>): void {
|
||||
lastFilename = event.target.value;
|
||||
setFilename(event.target.value);
|
||||
}
|
||||
|
||||
function infLoop(newCode: string): void {
|
||||
if (editorRef.current === null) return;
|
||||
if (!filename.endsWith(".ns") && !filename.endsWith(".js")) return;
|
||||
if (!currentScript.fileName.endsWith(".ns") && !currentScript.fileName.endsWith(".js")) return;
|
||||
const awaitWarning = checkInfiniteLoop(newCode);
|
||||
if (awaitWarning !== -1) {
|
||||
const newDecorations = editorRef.current.deltaDecorations(decorations, [
|
||||
@ -259,12 +234,10 @@ export function Root(props: IProps): React.ReactElement {
|
||||
|
||||
function updateCode(newCode?: string): void {
|
||||
if (newCode === undefined) return;
|
||||
lastCode = newCode;
|
||||
setCode(newCode);
|
||||
updateRAM(newCode);
|
||||
currentScript.code = newCode;
|
||||
try {
|
||||
if (editorRef.current !== null) {
|
||||
lastPosition = editorRef.current.getPosition();
|
||||
infLoop(newCode);
|
||||
}
|
||||
} catch (err) {}
|
||||
@ -272,7 +245,7 @@ export function Root(props: IProps): React.ReactElement {
|
||||
|
||||
// calculate it once the first time the file is loaded.
|
||||
useEffect(() => {
|
||||
updateRAM(code);
|
||||
updateRAM(currentScript.code);
|
||||
}, []);
|
||||
|
||||
async function updateRAM(newCode: string): Promise<void> {
|
||||
@ -314,8 +287,64 @@ export function Root(props: IProps): React.ReactElement {
|
||||
return () => document.removeEventListener("keydown", maybeSave);
|
||||
});
|
||||
|
||||
function onMount(editor: IStandaloneCodeEditor): void {
|
||||
// Generates a new model for the script
|
||||
function regenerateModel(script: openScript): void {
|
||||
if (monacoRef.current !== null) {
|
||||
script.model = monacoRef.current.editor.createModel(script.code, "javascript");
|
||||
}
|
||||
}
|
||||
|
||||
// Sets the currently viewed script
|
||||
function setCurrentScript(script: openScript): void {
|
||||
// Update last position
|
||||
if (editorRef.current !== null) {
|
||||
if (currentScript !== null) {
|
||||
const currentPosition = editorRef.current.getPosition();
|
||||
if (currentPosition !== null) {
|
||||
currentScript.lastPosition = currentPosition;
|
||||
}
|
||||
}
|
||||
|
||||
editorRef.current.setModel(script.model);
|
||||
currentScript = script;
|
||||
editorRef.current.setPosition(currentScript.lastPosition);
|
||||
editorRef.current.revealLine(currentScript.lastPosition.lineNumber);
|
||||
updateRAM(currentScript.code);
|
||||
}
|
||||
}
|
||||
|
||||
// Gets a currently opened script
|
||||
function getOpenedScript(fileName: string, hostname: string): openScript | null {
|
||||
for (const script of openScripts) {
|
||||
if (script.fileName === fileName && script.hostname === hostname) {
|
||||
return script;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
function saveScript(script: openScript): void {
|
||||
const server = GetServer(script.hostname);
|
||||
if (server === null) throw new Error("Server should not be null but it is.");
|
||||
let found = false;
|
||||
for (let i = 0; i < server.scripts.length; i++) {
|
||||
if (script.fileName == server.scripts[i].filename) {
|
||||
server.scripts[i].saveScript(script.fileName, script.code, script.hostname, server.scripts);
|
||||
found = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (!found) {
|
||||
const newScript = new Script();
|
||||
newScript.saveScript(script.fileName, script.code, script.hostname, server.scripts);
|
||||
server.scripts.push(newScript);
|
||||
}
|
||||
}
|
||||
|
||||
function onMount(editor: IStandaloneCodeEditor, monaco: Monaco): void {
|
||||
editorRef.current = editor;
|
||||
monacoRef.current = monaco;
|
||||
if (editorRef.current === null) return;
|
||||
const position = CursorPositions.getCursor(filename);
|
||||
if (position.row !== -1)
|
||||
@ -323,12 +352,56 @@ export function Root(props: IProps): React.ReactElement {
|
||||
lineNumber: position.row,
|
||||
column: position.column,
|
||||
});
|
||||
else if (lastPosition !== null)
|
||||
editorRef.current.setPosition({
|
||||
lineNumber: lastPosition.lineNumber,
|
||||
column: lastPosition.column + 1,
|
||||
});
|
||||
editorRef.current.focus();
|
||||
|
||||
const script = getOpenedScript(filename, props.player.getCurrentServer().hostname);
|
||||
|
||||
// Check if script is already opened, if so switch to that model
|
||||
if (script !== null) {
|
||||
if (script.model.isDisposed()) {
|
||||
regenerateModel(script);
|
||||
}
|
||||
|
||||
setCurrentScript(script);
|
||||
} else {
|
||||
if (filename !== undefined) {
|
||||
// Create new model
|
||||
if (monacoRef.current !== null) {
|
||||
const newScript = new openScript(
|
||||
filename,
|
||||
code,
|
||||
props.player.getCurrentServer().hostname,
|
||||
new monaco.Position(0, 0),
|
||||
monacoRef.current.editor.createModel(code, "javascript"),
|
||||
);
|
||||
setCurrentScript(newScript);
|
||||
openScripts.push(newScript);
|
||||
}
|
||||
} else {
|
||||
// Script Editor was opened by the sidebar button
|
||||
if (currentScript.model !== undefined) {
|
||||
if (currentScript.model.isDisposed()) {
|
||||
// Create new model, old one was disposed of
|
||||
regenerateModel(currentScript);
|
||||
}
|
||||
|
||||
setCurrentScript(currentScript);
|
||||
} else {
|
||||
// Create a new temporary file
|
||||
if (monacoRef.current !== null) {
|
||||
const newScript = new openScript(
|
||||
"newfile.script",
|
||||
"",
|
||||
props.player.getCurrentServer().hostname,
|
||||
new monaco.Position(0, 0),
|
||||
monacoRef.current.editor.createModel("", "javascript"),
|
||||
);
|
||||
setCurrentScript(newScript);
|
||||
openScripts.push(newScript);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function beforeMount(monaco: any): void {
|
||||
@ -368,26 +441,170 @@ export function Root(props: IProps): React.ReactElement {
|
||||
monaco.languages.typescript.typescriptDefaults.addExtraLib(source, "netscript.d.ts");
|
||||
loadThemes(monaco);
|
||||
}
|
||||
|
||||
// Change tab highlight from old tab to new tab
|
||||
function changeTabButtonColor(
|
||||
oldButtonFileName: string,
|
||||
oldButtonHostname: string,
|
||||
newButtonFileName: string,
|
||||
newButtonHostname: string,
|
||||
): void {
|
||||
const oldTabButton = document.getElementById("tabButton" + oldButtonFileName + oldButtonHostname);
|
||||
if (oldTabButton !== null) {
|
||||
oldTabButton.style.backgroundColor = "";
|
||||
}
|
||||
|
||||
const oldTabCloseButton = document.getElementById("tabCloseButton" + oldButtonFileName + oldButtonHostname);
|
||||
if (oldTabCloseButton !== null) {
|
||||
oldTabCloseButton.style.backgroundColor = "";
|
||||
}
|
||||
|
||||
const newTabButton = document.getElementById("tabButton" + newButtonFileName + newButtonHostname);
|
||||
if (newTabButton !== null) {
|
||||
newTabButton.style.backgroundColor = "#666";
|
||||
}
|
||||
|
||||
const newTabCloseButton = document.getElementById("tabCloseButton" + newButtonFileName + newButtonHostname);
|
||||
if (newTabCloseButton !== null) {
|
||||
newTabCloseButton.style.backgroundColor = "#666";
|
||||
}
|
||||
}
|
||||
|
||||
// Called when a script tab was clicked
|
||||
function onTabButtonClick(e: React.MouseEvent<HTMLButtonElement>): void {
|
||||
const valSplit = e.currentTarget.value.split(":");
|
||||
const fileName = valSplit[0];
|
||||
const hostname = valSplit[1];
|
||||
|
||||
// Change tab highlight from old tab to new tab
|
||||
changeTabButtonColor(currentScript.fileName, currentScript.hostname, fileName, hostname);
|
||||
|
||||
// Update current script
|
||||
const clickedScript = getOpenedScript(fileName, hostname);
|
||||
|
||||
if (clickedScript !== null) {
|
||||
if (clickedScript.model.isDisposed()) {
|
||||
regenerateModel(clickedScript);
|
||||
}
|
||||
|
||||
setCurrentScript(clickedScript);
|
||||
}
|
||||
}
|
||||
|
||||
// Called when a script tab close button was clicked
|
||||
function onCloseButtonClick(e: React.MouseEvent<HTMLButtonElement>): void {
|
||||
const valSplit = e.currentTarget.value.split(":");
|
||||
const fileName = valSplit[0];
|
||||
const hostname = valSplit[1];
|
||||
|
||||
const scriptToClose = getOpenedScript(fileName, hostname);
|
||||
|
||||
// Save and remove script from openScripts
|
||||
if (scriptToClose !== null) {
|
||||
saveScript(scriptToClose);
|
||||
|
||||
openScripts.splice(openScripts.indexOf(scriptToClose), 1);
|
||||
}
|
||||
|
||||
if (openScripts.length === 0) {
|
||||
// No other scripts are open, create a new temporary file
|
||||
if (monacoRef.current !== null) {
|
||||
const newScript = new openScript(
|
||||
"newfile.script",
|
||||
"",
|
||||
props.player.getCurrentServer().hostname,
|
||||
new monacoRef.current.Position(0, 0),
|
||||
monacoRef.current.editor.createModel("", "javascript"),
|
||||
);
|
||||
|
||||
setCurrentScript(newScript);
|
||||
openScripts.push(newScript);
|
||||
|
||||
// Modify button for temp file
|
||||
const parent = e.currentTarget.parentElement;
|
||||
if (parent !== null) {
|
||||
(parent.children[0] as HTMLButtonElement).value = "newfile.script:home";
|
||||
(parent.children[0] as HTMLButtonElement).textContent = "newfile.script";
|
||||
e.currentTarget.value = "newfile.script:home";
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if (openScripts[0].model.isDisposed()) {
|
||||
regenerateModel(openScripts[0]);
|
||||
}
|
||||
|
||||
changeTabButtonColor(
|
||||
currentScript.fileName,
|
||||
currentScript.hostname,
|
||||
openScripts[0].fileName,
|
||||
openScripts[0].hostname,
|
||||
);
|
||||
|
||||
setCurrentScript(openScripts[0]);
|
||||
}
|
||||
}
|
||||
|
||||
// Generate a button for each open script
|
||||
const scriptButtons = [];
|
||||
for (let i = 0; i < openScripts.length; i++) {
|
||||
if (openScripts[i].fileName !== "") {
|
||||
const fileName2 = openScripts[i].fileName;
|
||||
const hostname = openScripts[i].hostname;
|
||||
if (openScripts[i].fileName === currentScript.fileName && openScripts[i].hostname === currentScript.hostname) {
|
||||
// Set special background color for current script tab button
|
||||
scriptButtons.push(
|
||||
<Tooltip
|
||||
title={
|
||||
<Typography>
|
||||
{hostname}:~/{fileName2}
|
||||
</Typography>
|
||||
}
|
||||
>
|
||||
<div key={fileName2 + hostname} style={{ paddingRight: "5px" }}>
|
||||
<Button style={{ backgroundColor: "#666" }} value={fileName2 + ":" + hostname} onClick={onTabButtonClick}>
|
||||
{openScripts[i].fileName}
|
||||
</Button>
|
||||
<Button
|
||||
value={fileName2 + ":" + hostname}
|
||||
onClick={onCloseButtonClick}
|
||||
style={{ maxWidth: "20px", minWidth: "20px", backgroundColor: "#666" }}
|
||||
>
|
||||
x
|
||||
</Button>
|
||||
</div>
|
||||
</Tooltip>,
|
||||
);
|
||||
} else {
|
||||
scriptButtons.push(
|
||||
<div id={"scriptEditorTab" + fileName2 + hostname} key={"tabButton" + i} style={{ paddingRight: "5px" }}>
|
||||
<Button
|
||||
id={"tabButton" + openScripts[i].fileName + openScripts[i].hostname}
|
||||
value={fileName2 + ":" + hostname}
|
||||
onClick={onTabButtonClick}
|
||||
>
|
||||
{openScripts[i].fileName}
|
||||
</Button>
|
||||
<Button
|
||||
id={"tabCloseButton" + openScripts[i].fileName + openScripts[i].hostname}
|
||||
value={fileName2 + ":" + hostname}
|
||||
onClick={onCloseButtonClick}
|
||||
style={{ maxWidth: "20px", minWidth: "20px" }}
|
||||
>
|
||||
x
|
||||
</Button>
|
||||
</div>,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 370px 71%, 725px 85.1%, 1085px 90%, 1300px 91.7%
|
||||
// fuck around in desmos until you find a function
|
||||
const p = 11000 / -window.innerHeight + 100;
|
||||
return (
|
||||
<>
|
||||
<Box display="flex" flexDirection="row" alignItems="center">
|
||||
<TextField
|
||||
placeholder="filename"
|
||||
type="text"
|
||||
tabIndex={1}
|
||||
value={filename}
|
||||
onChange={onFilenameChange}
|
||||
InputProps={{ startAdornment: <Typography>{hostname}:~/</Typography> }}
|
||||
/>
|
||||
<IconButton onClick={() => setOptionsOpen(true)}>
|
||||
<>
|
||||
<SettingsIcon />
|
||||
options
|
||||
</>
|
||||
</IconButton>
|
||||
<Box display="flex" flexDirection="row" alignItems="center" paddingBottom="5px">
|
||||
{scriptButtons}
|
||||
</Box>
|
||||
<Editor
|
||||
beforeMount={beforeMount}
|
||||
@ -405,7 +622,7 @@ export function Root(props: IProps): React.ReactElement {
|
||||
<Typography color={updatingRam ? "secondary" : "primary"} sx={{ mx: 1 }}>
|
||||
{ram}
|
||||
</Typography>
|
||||
<Button onClick={save}>Save & Close (Ctrl/Cmd + b)</Button>
|
||||
<Button onClick={save}>Save (Ctrl/Cmd + b)</Button>
|
||||
<Typography sx={{ mx: 1 }}>
|
||||
{" "}
|
||||
Documentation:{" "}
|
||||
@ -417,6 +634,12 @@ export function Root(props: IProps): React.ReactElement {
|
||||
Full
|
||||
</Link>
|
||||
</Typography>
|
||||
<IconButton style={{ marginLeft: "auto" }} onClick={() => setOptionsOpen(true)}>
|
||||
<>
|
||||
<SettingsIcon />
|
||||
options
|
||||
</>
|
||||
</IconButton>
|
||||
</Box>
|
||||
<OptionsModal
|
||||
open={optionsOpen}
|
||||
|
@ -103,6 +103,11 @@ interface IDefaultSettings {
|
||||
*/
|
||||
SuppressTIXPopup: boolean;
|
||||
|
||||
/**
|
||||
* Whether the user should be displayed a toast alert when the game is saved.
|
||||
*/
|
||||
SuppressSavedGameToast: boolean;
|
||||
|
||||
/*
|
||||
* Theme colors
|
||||
*/
|
||||
@ -186,6 +191,7 @@ export const defaultSettings: IDefaultSettings = {
|
||||
SuppressTravelConfirmation: false,
|
||||
SuppressBladeburnerPopup: false,
|
||||
SuppressTIXPopup: false,
|
||||
SuppressSavedGameToast: false,
|
||||
|
||||
theme: {
|
||||
primarylight: "#0f0",
|
||||
@ -251,6 +257,7 @@ export const Settings: ISettings & ISelfInitializer & ISelfLoading = {
|
||||
SuppressTravelConfirmation: defaultSettings.SuppressTravelConfirmation,
|
||||
SuppressBladeburnerPopup: defaultSettings.SuppressBladeburnerPopup,
|
||||
SuppressTIXPopup: defaultSettings.SuppressTIXPopup,
|
||||
SuppressSavedGameToast: defaultSettings.SuppressSavedGameToast,
|
||||
MonacoTheme: "monokai",
|
||||
MonacoInsertSpaces: false,
|
||||
MonacoFontSize: 20,
|
||||
|
@ -6,6 +6,7 @@ import makeStyles from "@mui/styles/makeStyles";
|
||||
import MuiDrawer from "@mui/material/Drawer";
|
||||
import List from "@mui/material/List";
|
||||
import Divider from "@mui/material/Divider";
|
||||
import Tooltip from "@mui/material/Tooltip";
|
||||
import ChevronLeftIcon from "@mui/icons-material/ChevronLeft";
|
||||
import ChevronRightIcon from "@mui/icons-material/ChevronRight";
|
||||
import ListItem from "@mui/material/ListItem";
|
||||
@ -54,6 +55,7 @@ import { AugmentationNames } from "../../Augmentation/data/AugmentationNames";
|
||||
import { KEY } from "../../utils/helpers/keyCodes";
|
||||
import { ProgramsSeen } from "../../Programs/ui/ProgramsRoot";
|
||||
import { InvitationsSeen } from "../../Faction/ui/FactionsRoot";
|
||||
import { hash } from "../../hash/hash";
|
||||
|
||||
const openedMixin = (theme: Theme): CSSObject => ({
|
||||
width: theme.spacing(31),
|
||||
@ -290,7 +292,8 @@ export function SidebarRoot(props: IProps): React.ReactElement {
|
||||
} else if (event.keyCode === KEY.W && event.altKey) {
|
||||
event.preventDefault();
|
||||
clickCity();
|
||||
} else if (event.keyCode === KEY.J && event.altKey) {
|
||||
} else if (event.keyCode === KEY.J && event.altKey && !event.ctrlKey && !event.metaKey) {
|
||||
// ctrl/cmd + alt + j is shortcut to open Chrome dev tools
|
||||
event.preventDefault();
|
||||
clickJob();
|
||||
} else if (event.keyCode === KEY.R && event.altKey) {
|
||||
@ -337,7 +340,13 @@ export function SidebarRoot(props: IProps): React.ReactElement {
|
||||
<ListItemIcon>
|
||||
{!open ? <ChevronRightIcon color="primary" /> : <ChevronLeftIcon color="primary" />}
|
||||
</ListItemIcon>
|
||||
<ListItemText primary={<Typography>Bitburner v{CONSTANTS.VersionString}</Typography>} />
|
||||
<ListItemText
|
||||
primary={
|
||||
<Tooltip title={hash()}>
|
||||
<Typography>Bitburner v{CONSTANTS.VersionString}</Typography>
|
||||
</Tooltip>
|
||||
}
|
||||
/>
|
||||
</ListItem>
|
||||
<Divider />
|
||||
<List>
|
||||
|
@ -69,6 +69,7 @@ import { top } from "./commands/top";
|
||||
import { unalias } from "./commands/unalias";
|
||||
import { weaken } from "./commands/weaken";
|
||||
import { wget } from "./commands/wget";
|
||||
import { hash } from "../hash/hash";
|
||||
|
||||
export class Terminal implements ITerminal {
|
||||
// Flags to determine whether the player is currently running a hack or an analyze
|
||||
@ -77,7 +78,9 @@ export class Terminal implements ITerminal {
|
||||
commandHistory: string[] = [];
|
||||
commandHistoryIndex = 0;
|
||||
|
||||
outputHistory: (Output | Link | RawOutput)[] = [new Output(`Bitburner v${CONSTANTS.VersionString}`, "primary")];
|
||||
outputHistory: (Output | Link | RawOutput)[] = [
|
||||
new Output(`Bitburner v${CONSTANTS.VersionString} (${hash()})`, "primary"),
|
||||
];
|
||||
|
||||
// True if a Coding Contract prompt is opened
|
||||
contractOpen = false;
|
||||
@ -555,7 +558,7 @@ export class Terminal implements ITerminal {
|
||||
}
|
||||
|
||||
clear(): void {
|
||||
this.outputHistory = [new Output(`Bitburner v${CONSTANTS.VersionString}`, "primary")];
|
||||
this.outputHistory = [new Output(`Bitburner v${CONSTANTS.VersionString} (${hash()})`, "primary")];
|
||||
TerminalEvents.emit();
|
||||
TerminalClearEvents.emit();
|
||||
}
|
||||
@ -668,6 +671,12 @@ export class Terminal implements ITerminal {
|
||||
return;
|
||||
}
|
||||
break;
|
||||
case iTutorialSteps.TerminalHackingMechanics:
|
||||
if (commandArray.length !== 1 || !["grow", "weaken", "hack"].includes(commandArray[0] + "")) {
|
||||
this.error("Bad command. Please follow the tutorial");
|
||||
return;
|
||||
}
|
||||
break;
|
||||
case iTutorialSteps.TerminalGoHome:
|
||||
if (commandArray.length == 1 && commandArray[0] == "home") {
|
||||
iTutorialNextStep();
|
||||
|
@ -20,7 +20,7 @@ const useStyles = makeStyles((theme: Theme) =>
|
||||
margin: theme.spacing(0),
|
||||
},
|
||||
input: {
|
||||
backgroundColor: "#000",
|
||||
backgroundColor: theme.colors.backgroundprimary,
|
||||
},
|
||||
nopadding: {
|
||||
padding: theme.spacing(0),
|
||||
@ -48,12 +48,26 @@ export function TerminalInput({ terminal, router, player }: IProps): React.React
|
||||
const terminalInput = useRef<HTMLInputElement>(null);
|
||||
|
||||
const [value, setValue] = useState(command);
|
||||
const [postUpdateValue, setPostUpdateValue] = useState<{postUpdate: () => void} | null>()
|
||||
const [possibilities, setPossibilities] = useState<string[]>([]);
|
||||
const classes = useStyles();
|
||||
|
||||
function saveValue(value: string): void {
|
||||
command = value;
|
||||
setValue(value);
|
||||
// Need to run after state updates, for example if we need to move cursor
|
||||
// *after* we modify input
|
||||
useEffect(() => {
|
||||
if (postUpdateValue?.postUpdate) {
|
||||
postUpdateValue.postUpdate();
|
||||
setPostUpdateValue(null);
|
||||
}
|
||||
}, [postUpdateValue])
|
||||
|
||||
function saveValue(newValue: string, postUpdate?: () => void): void {
|
||||
command = newValue;
|
||||
setValue(newValue);
|
||||
|
||||
if (postUpdate) {
|
||||
setPostUpdateValue({postUpdate});
|
||||
}
|
||||
}
|
||||
|
||||
function handleValueChange(event: React.ChangeEvent<HTMLInputElement>): void {
|
||||
@ -76,24 +90,36 @@ export function TerminalInput({ terminal, router, player }: IProps): React.React
|
||||
}
|
||||
break;
|
||||
case "deletewordbefore": // Delete rest of word before the cursor
|
||||
for (let delStart = start - 1; delStart > 0; --delStart) {
|
||||
if (inputText.charAt(delStart) === " ") {
|
||||
saveValue(inputText.substr(0, delStart) + inputText.substr(start));
|
||||
for (let delStart = start - 1; delStart > -2; --delStart) {
|
||||
if ((inputText.charAt(delStart) === " " || delStart === -1) && delStart !== start - 1) {
|
||||
saveValue(inputText.substr(0, delStart + 1) + inputText.substr(start), () => {
|
||||
// Move cursor to correct location
|
||||
// foo bar |baz bum --> foo |baz bum
|
||||
const ref = terminalInput.current;
|
||||
ref?.setSelectionRange(delStart+1, delStart+1);
|
||||
});
|
||||
return;
|
||||
}
|
||||
}
|
||||
break;
|
||||
case "deletewordafter": // Delete rest of word after the cursor
|
||||
case "deletewordafter": // Delete rest of word after the cursor, including trailing space
|
||||
for (let delStart = start + 1; delStart <= value.length + 1; ++delStart) {
|
||||
if (inputText.charAt(delStart) === " ") {
|
||||
saveValue(inputText.substr(0, start) + inputText.substr(delStart));
|
||||
if (inputText.charAt(delStart) === " " || delStart === value.length + 1) {
|
||||
saveValue(inputText.substr(0, start) + inputText.substr(delStart + 1), () => {
|
||||
// Move cursor to correct location
|
||||
// foo bar |baz bum --> foo bar |bum
|
||||
const ref = terminalInput.current;
|
||||
ref?.setSelectionRange(start, start)
|
||||
});
|
||||
return;
|
||||
}
|
||||
}
|
||||
break;
|
||||
case "clearafter": // Deletes everything after cursor
|
||||
saveValue(inputText.substr(0, start));
|
||||
break;
|
||||
case "clearbefore:": // Deleetes everything before cursor
|
||||
case "clearbefore": // Deletes everything before cursor
|
||||
saveValue(inputText.substr(start), () => moveTextCursor("home"));
|
||||
break;
|
||||
}
|
||||
}
|
||||
@ -320,11 +346,23 @@ export function TerminalInput({ terminal, router, player }: IProps): React.React
|
||||
event.preventDefault();
|
||||
}
|
||||
|
||||
if (event.keyCode === KEY.W && event.ctrlKey) {
|
||||
event.preventDefault();
|
||||
modifyInput("deletewordbefore");
|
||||
}
|
||||
|
||||
if (event.keyCode === KEY.U && event.ctrlKey) {
|
||||
event.preventDefault();
|
||||
modifyInput("clearbefore");
|
||||
}
|
||||
|
||||
if (event.keyCode === KEY.K && event.ctrlKey) {
|
||||
event.preventDefault();
|
||||
modifyInput("clearafter");
|
||||
}
|
||||
|
||||
// TODO AFTER THIS:
|
||||
// alt + d deletes word after cursor
|
||||
// ^w deletes word before cursor
|
||||
// ^k clears line after cursor
|
||||
// ^u clears line before cursor
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1,20 +1,28 @@
|
||||
import React from "react";
|
||||
import React, { useState } from "react";
|
||||
|
||||
import Typography from "@mui/material/Typography";
|
||||
import Link from "@mui/material/Link";
|
||||
import Box from "@mui/material/Box";
|
||||
import Button from "@mui/material/Button";
|
||||
import { ConfirmationModal } from "../../ui/React/ConfirmationModal";
|
||||
|
||||
interface IProps {
|
||||
reactivateTutorial: () => void;
|
||||
}
|
||||
|
||||
export function TutorialRoot(props: IProps): React.ReactElement {
|
||||
const [confirmResetOpen, setConfirmResetOpen] = useState(false);
|
||||
return (
|
||||
<>
|
||||
<Typography variant="h4">Tutorial / Documentation</Typography>
|
||||
<Box m={2}>
|
||||
<Button onClick={props.reactivateTutorial}>Soft reset and Restart tutorial</Button>
|
||||
<Button onClick={() => setConfirmResetOpen(true)}>Soft reset and Restart tutorial</Button>
|
||||
<ConfirmationModal
|
||||
open={confirmResetOpen}
|
||||
onClose={() => setConfirmResetOpen(false)}
|
||||
onConfirm={props.reactivateTutorial}
|
||||
confirmationText={"This will reset all your stats to 1 and money to 1k. Are you sure?"}
|
||||
/>
|
||||
<Link
|
||||
color="primary"
|
||||
target="_blank"
|
||||
@ -94,6 +102,14 @@ export function TutorialRoot(props: IProps): React.ReactElement {
|
||||
<Link color="primary" target="_blank" href="https://bitburner.readthedocs.io/en/latest/shortcuts.html">
|
||||
<Typography>Keyboard Shortcuts</Typography>
|
||||
</Link>
|
||||
<br />
|
||||
<Link
|
||||
color="primary"
|
||||
target="_blank"
|
||||
href="https://bitburner.readthedocs.io/en/latest/netscript/netscriptlearntoprogram.html#netscript-1-0-vs-netscript-2-0"
|
||||
>
|
||||
<Typography>NS1 vs NS2 (or .script vs .js)</Typography>
|
||||
</Link>
|
||||
</Box>
|
||||
</>
|
||||
);
|
||||
|
8
src/hash/hash.ts
Normal file
8
src/hash/hash.ts
Normal file
@ -0,0 +1,8 @@
|
||||
export function hash(): string {
|
||||
try {
|
||||
if (__COMMIT_HASH__) {
|
||||
return __COMMIT_HASH__;
|
||||
}
|
||||
} catch (err) {}
|
||||
return "DEV";
|
||||
}
|
@ -12,6 +12,7 @@ import { CopyableText } from "../React/CopyableText";
|
||||
import ListItem from "@mui/material/ListItem";
|
||||
import EqualizerIcon from "@mui/icons-material/Equalizer";
|
||||
import LastPageIcon from "@mui/icons-material/LastPage";
|
||||
import VisibilityOffIcon from "@mui/icons-material/VisibilityOff";
|
||||
import HelpIcon from "@mui/icons-material/Help";
|
||||
import AccountTreeIcon from "@mui/icons-material/AccountTree";
|
||||
import StorageIcon from "@mui/icons-material/Storage";
|
||||
@ -58,6 +59,9 @@ export function InteractiveTutorialRoot(): React.ReactElement {
|
||||
<br />
|
||||
<br />
|
||||
This tutorial will show you the basics of the game. You may skip the tutorial at any time.
|
||||
<br />
|
||||
<br />
|
||||
You can also click the eye symbol <VisibilityOffIcon /> to temporarily hide this tutorial.
|
||||
</Typography>
|
||||
</>
|
||||
),
|
||||
@ -287,7 +291,9 @@ export function InteractiveTutorialRoot(): React.ReactElement {
|
||||
The amount of money on a server is not limitless. So, if you constantly hack a server and deplete its money,
|
||||
then you will encounter diminishing returns in your hacking. You will need to use{" "}
|
||||
<Typography classes={{ root: classes.textfield }}>{"[n00dles ~/]> grow"}</Typography>
|
||||
and <Typography classes={{ root: classes.textfield }}>{"[n00dles ~/]> weaken"}</Typography>
|
||||
which tricks the company into adding money to their server and{" "}
|
||||
<Typography classes={{ root: classes.textfield }}>{"[n00dles ~/]> weaken"}</Typography>
|
||||
which increases the speed of hack and grow.
|
||||
</Typography>
|
||||
),
|
||||
canNext: true,
|
||||
@ -345,10 +351,6 @@ export function InteractiveTutorialRoot(): React.ReactElement {
|
||||
continuously hack the n00dles server.
|
||||
<br />
|
||||
<br />
|
||||
If you're an intermediate programmer you should use ns2 instead. It is much faster and offers more
|
||||
possibilities.
|
||||
<br />
|
||||
<br />
|
||||
To save and close the script editor, press the button in the bottom left, or press ctrl + b.
|
||||
</Typography>
|
||||
</>
|
||||
@ -447,7 +449,11 @@ export function InteractiveTutorialRoot(): React.ReactElement {
|
||||
<br />
|
||||
<br />
|
||||
If you know even a little bit of programming it is highly recommended you use NS2 instead. You will enjoy
|
||||
the game much more.
|
||||
the game much more. NS1 files end with .script and are a subset of javascript. NS2 files end with .js and
|
||||
are full speed native javascript.
|
||||
<br />
|
||||
<br />
|
||||
You can learn more about the difference between them later in the documentation.
|
||||
<br />
|
||||
<br />
|
||||
For now, let's move on to something else!
|
||||
|
@ -15,6 +15,7 @@ import { GameRoot } from "./GameRoot";
|
||||
|
||||
import { CONSTANTS } from "../Constants";
|
||||
import { ActivateRecoveryMode } from "./React/RecoveryRoot";
|
||||
import { hash } from "../hash/hash";
|
||||
|
||||
const useStyles = makeStyles((theme: Theme) =>
|
||||
createStyles({
|
||||
@ -69,7 +70,9 @@ export function LoadingScreen(): React.ReactElement {
|
||||
<CircularProgress size={150} color="primary" />
|
||||
</Grid>
|
||||
<Grid item>
|
||||
<Typography variant="h3">Loading Bitburner v{CONSTANTS.VersionString}</Typography>
|
||||
<Typography variant="h3">
|
||||
Loading Bitburner v{CONSTANTS.VersionString} ({hash()})
|
||||
</Typography>
|
||||
</Grid>
|
||||
{show && (
|
||||
<Grid item>
|
||||
|
@ -1,7 +1,7 @@
|
||||
// Root React Component for the Corporation UI
|
||||
import React, { useState, useEffect } from "react";
|
||||
|
||||
import { Theme } from "@mui/material/styles";
|
||||
import { Theme, useTheme } from "@mui/material/styles";
|
||||
import makeStyles from "@mui/styles/makeStyles";
|
||||
import createStyles from "@mui/styles/createStyles";
|
||||
import { numeralWrapper } from "../../ui/numeralFormat";
|
||||
@ -20,6 +20,8 @@ import ClearAllIcon from "@mui/icons-material/ClearAll";
|
||||
|
||||
import { Settings } from "../../Settings/Settings";
|
||||
import { use } from "../Context";
|
||||
import { StatsProgressOverviewCell } from "./StatsProgressBar";
|
||||
import { BitNodeMultipliers } from "../../BitNode/BitNodeMultipliers";
|
||||
|
||||
interface IProps {
|
||||
save: () => void;
|
||||
@ -139,6 +141,8 @@ const useStyles = makeStyles((theme: Theme) =>
|
||||
}),
|
||||
);
|
||||
|
||||
export { useStyles as characterOverviewStyles };
|
||||
|
||||
export function CharacterOverview({ save, killScripts }: IProps): React.ReactElement {
|
||||
const [killOpen, setKillOpen] = useState(false);
|
||||
const player = use.Player();
|
||||
@ -151,6 +155,21 @@ export function CharacterOverview({ save, killScripts }: IProps): React.ReactEle
|
||||
}, []);
|
||||
|
||||
const classes = useStyles();
|
||||
const theme = useTheme();
|
||||
|
||||
const hackingProgress = player.calculateSkillProgress(
|
||||
player.hacking_exp, player.hacking_mult * BitNodeMultipliers.HackingLevelMultiplier);
|
||||
const strengthProgress = player.calculateSkillProgress(
|
||||
player.strength_exp, player.strength_mult * BitNodeMultipliers.StrengthLevelMultiplier);
|
||||
const defenseProgress = player.calculateSkillProgress(
|
||||
player.defense_exp, player.defense_mult * BitNodeMultipliers.DefenseLevelMultiplier);
|
||||
const dexterityProgress = player.calculateSkillProgress(
|
||||
player.dexterity_exp, player.dexterity_mult * BitNodeMultipliers.DexterityLevelMultiplier);
|
||||
const agilityProgress = player.calculateSkillProgress(
|
||||
player.agility_exp, player.agility_mult * BitNodeMultipliers.AgilityLevelMultiplier);
|
||||
const charismaProgress = player.calculateSkillProgress(
|
||||
player.charisma_exp, player.charisma_mult * BitNodeMultipliers.CharismaLevelMultiplier);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Table sx={{ display: "block", m: 1 }}>
|
||||
@ -186,12 +205,20 @@ export function CharacterOverview({ save, killScripts }: IProps): React.ReactEle
|
||||
</TableRow>
|
||||
|
||||
<TableRow>
|
||||
<TableCell component="th" scope="row" classes={{ root: classes.cell }}>
|
||||
<TableCell component="th" scope="row" classes={{ root: classes.cellNone }}>
|
||||
<Typography classes={{ root: classes.hack }}>Hack </Typography>
|
||||
</TableCell>
|
||||
<TableCell align="right" classes={{ root: classes.cell }}>
|
||||
<TableCell align="right" classes={{ root: classes.cellNone }}>
|
||||
<Typography classes={{ root: classes.hack }}>{numeralWrapper.formatSkill(player.hacking)}</Typography>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
<TableRow>
|
||||
<StatsProgressOverviewCell progress={hackingProgress} color={theme.colors.hack} />
|
||||
</TableRow>
|
||||
<TableRow>
|
||||
<TableCell component="th" scope="row" classes={{ root: classes.cell }}>
|
||||
<Typography classes={{ root: classes.hack }}></Typography>
|
||||
</TableCell>
|
||||
<TableCell align="right" classes={{ root: classes.cell }}>
|
||||
<Typography id="overview-hack-hook" classes={{ root: classes.hack }}>
|
||||
{/*Hook for player scripts*/}
|
||||
@ -212,6 +239,9 @@ export function CharacterOverview({ save, killScripts }: IProps): React.ReactEle
|
||||
</Typography>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
<TableRow>
|
||||
<StatsProgressOverviewCell progress={strengthProgress} color={theme.colors.combat} />
|
||||
</TableRow>
|
||||
|
||||
<TableRow>
|
||||
<TableCell component="th" scope="row" classes={{ root: classes.cellNone }}>
|
||||
@ -226,6 +256,9 @@ export function CharacterOverview({ save, killScripts }: IProps): React.ReactEle
|
||||
</Typography>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
<TableRow>
|
||||
<StatsProgressOverviewCell progress={defenseProgress} color={theme.colors.combat} />
|
||||
</TableRow>
|
||||
|
||||
<TableRow>
|
||||
<TableCell component="th" scope="row" classes={{ root: classes.cellNone }}>
|
||||
@ -240,6 +273,10 @@ export function CharacterOverview({ save, killScripts }: IProps): React.ReactEle
|
||||
</Typography>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
<TableRow>
|
||||
<StatsProgressOverviewCell progress={dexterityProgress} color={theme.colors.combat} />
|
||||
</TableRow>
|
||||
|
||||
<TableRow>
|
||||
<TableCell component="th" scope="row" classes={{ root: classes.cell }}>
|
||||
<Typography classes={{ root: classes.combat }}>Agi </Typography>
|
||||
@ -253,6 +290,9 @@ export function CharacterOverview({ save, killScripts }: IProps): React.ReactEle
|
||||
</Typography>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
<TableRow>
|
||||
<StatsProgressOverviewCell progress={agilityProgress} color={theme.colors.combat} />
|
||||
</TableRow>
|
||||
|
||||
<TableRow>
|
||||
<TableCell component="th" scope="row" classes={{ root: classes.cellNone }}>
|
||||
@ -267,6 +307,10 @@ export function CharacterOverview({ save, killScripts }: IProps): React.ReactEle
|
||||
</Typography>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
<TableRow>
|
||||
<StatsProgressOverviewCell progress={charismaProgress} color={theme.colors.cha} />
|
||||
</TableRow>
|
||||
|
||||
<Intelligence />
|
||||
|
||||
<TableRow>
|
||||
|
@ -70,6 +70,7 @@ export function GameOptionsRoot(props: IProps): React.ReactElement {
|
||||
);
|
||||
const [suppressTIXPopup, setSuppressTIXPopup] = useState(Settings.SuppressTIXPopup);
|
||||
const [suppressBladeburnerPopup, setSuppressBladeburnerPopup] = useState(Settings.SuppressBladeburnerPopup);
|
||||
const [suppressSavedGameToast, setSuppresSavedGameToast] = useState(Settings.SuppressSavedGameToast);
|
||||
|
||||
const [disableHotkeys, setDisableHotkeys] = useState(Settings.DisableHotkeys);
|
||||
const [disableASCIIArt, setDisableASCIIArt] = useState(Settings.DisableASCIIArt);
|
||||
@ -138,6 +139,11 @@ export function GameOptionsRoot(props: IProps): React.ReactElement {
|
||||
Settings.SuppressBladeburnerPopup = event.target.checked;
|
||||
}
|
||||
|
||||
function handleSuppressSavedGameToastChange(event: React.ChangeEvent<HTMLInputElement>): void {
|
||||
setSuppresSavedGameToast(event.target.checked);
|
||||
Settings.SuppressSavedGameToast = event.target.checked;
|
||||
}
|
||||
|
||||
function handleDisableHotkeysChange(event: React.ChangeEvent<HTMLInputElement>): void {
|
||||
setDisableHotkeys(event.target.checked);
|
||||
Settings.DisableHotkeys = event.target.checked;
|
||||
@ -420,6 +426,24 @@ export function GameOptionsRoot(props: IProps): React.ReactElement {
|
||||
/>
|
||||
</ListItem>
|
||||
)}
|
||||
<ListItem>
|
||||
<FormControlLabel
|
||||
control={
|
||||
<Switch checked={suppressSavedGameToast} onChange={handleSuppressSavedGameToastChange} />
|
||||
}
|
||||
label={
|
||||
<Tooltip
|
||||
title={
|
||||
<Typography>
|
||||
If this is set, there will be no "Saved Game" toast appearing after save.
|
||||
</Typography>
|
||||
}
|
||||
>
|
||||
<Typography>Suppress Saved Game Toast</Typography>
|
||||
</Tooltip>
|
||||
}
|
||||
/>
|
||||
</ListItem>
|
||||
<ListItem>
|
||||
<FormControlLabel
|
||||
control={<Switch checked={disableHotkeys} onChange={handleDisableHotkeysChange} />}
|
||||
|
64
src/ui/React/StatsProgressBar.tsx
Normal file
64
src/ui/React/StatsProgressBar.tsx
Normal file
@ -0,0 +1,64 @@
|
||||
import * as React from "react";
|
||||
import LinearProgress from "@mui/material/LinearProgress";
|
||||
import { TableCell, Tooltip } from "@mui/material";
|
||||
import { characterOverviewStyles } from "./CharacterOverview";
|
||||
import { ISkillProgress } from "src/PersonObjects/formulas/skill";
|
||||
import { numeralWrapper } from "../numeralFormat";
|
||||
|
||||
interface IProgressProps {
|
||||
min: number;
|
||||
max: number;
|
||||
current: number;
|
||||
color?: React.CSSProperties["color"];
|
||||
}
|
||||
|
||||
interface IStatsOverviewCellProps {
|
||||
progress: ISkillProgress;
|
||||
color?: React.CSSProperties["color"];
|
||||
}
|
||||
|
||||
export function StatsProgressBar({ min, max, current, color }: IProgressProps): React.ReactElement {
|
||||
const normalise = (value: number): number => ((value - min) * 100) / (max - min);
|
||||
const tooltipText = (
|
||||
<>
|
||||
Experience: {numeralWrapper.formatExp(current)}/{numeralWrapper.formatExp(max)}
|
||||
<br />
|
||||
{normalise(current).toFixed(2)}%
|
||||
</>
|
||||
);
|
||||
|
||||
return (
|
||||
<Tooltip title={tooltipText}>
|
||||
<LinearProgress
|
||||
variant="determinate"
|
||||
value={normalise(current)}
|
||||
sx={{
|
||||
backgroundColor: "#111111",
|
||||
"& .MuiLinearProgress-bar1Determinate": {
|
||||
backgroundColor: color,
|
||||
},
|
||||
}}
|
||||
/>
|
||||
</Tooltip>
|
||||
);
|
||||
}
|
||||
|
||||
export function StatsProgressOverviewCell({ progress, color }: IStatsOverviewCellProps): React.ReactElement {
|
||||
const classes = characterOverviewStyles();
|
||||
return (
|
||||
<TableCell
|
||||
component="th"
|
||||
scope="row"
|
||||
colSpan={2}
|
||||
classes={{ root: classes.cellNone }}
|
||||
style={{ paddingBottom: "2px", position: "relative", top: "-3px" }}
|
||||
>
|
||||
<StatsProgressBar
|
||||
min={progress.baseExperience}
|
||||
max={progress.nextExperience}
|
||||
current={progress.experience}
|
||||
color={color}
|
||||
/>
|
||||
</TableCell>
|
||||
);
|
||||
}
|
@ -40,6 +40,7 @@ function ColorEditor({ name, onColorChange, color, defaultColor }: IColorEditorP
|
||||
<>
|
||||
<ColorPicker
|
||||
hideTextfield
|
||||
deferred
|
||||
value={color}
|
||||
onChange={(newColor: Color) => onColorChange(name, "#" + newColor.hex)}
|
||||
/>
|
||||
|
@ -42,6 +42,10 @@ module.exports = (env, argv) => {
|
||||
};
|
||||
}
|
||||
|
||||
// Get the current commit hash to inject into the app
|
||||
// https://stackoverflow.com/a/38401256
|
||||
const commitHash = require("child_process").execSync("git rev-parse --short HEAD").toString().trim();
|
||||
|
||||
return {
|
||||
plugins: [
|
||||
new webpack.DefinePlugin({
|
||||
@ -105,6 +109,9 @@ module.exports = (env, argv) => {
|
||||
},
|
||||
},
|
||||
}),
|
||||
new webpack.DefinePlugin({
|
||||
__COMMIT_HASH__: JSON.stringify(commitHash || "DEV"),
|
||||
}),
|
||||
// In dev mode, use a faster method of create sourcemaps
|
||||
// while keeping lines/columns accurate
|
||||
isDevServer &&
|
||||
|
Loading…
Reference in New Issue
Block a user