Merge pull request #2002 from danielyxie/dev

v1.2.0
This commit is contained in:
hydroflame 2021-12-18 16:29:41 -05:00 committed by GitHub
commit 48b366b01f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
40 changed files with 1089 additions and 325 deletions

@ -2,12 +2,9 @@ root = true
[*] [*]
indent_style = space indent_style = space
indent_size = 4 indent_size = 2
trim_trailing_whitespace = true trim_trailing_whitespace = true
insert_final_newline = true insert_final_newline = true
[package.json]
indent_size = 2
[md] [md]
trim_trailing_whitespace = false trim_trailing_whitespace = false

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
============ ============
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 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. 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 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%. | || || * 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). | || || BitNodes will disable this mechanic). |
|| || * Increases the player's crime success rate, crime money, and | || || * Increases the player's crime success rate, crime money, and |
|| || charisma multipliers by 24%/36%/42%. | || || 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). | || || BitNodes will disable this mechanic). |
|| || * Increases the player's charisma and company salary multipliers by 8%/12%/14%. | || || * 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. | || || * Each level of this Source-File opens up more of the Singularity Functions to use. |
+-------------------------------------+--------------------------------------------------------------------------------------------------------------------------------------------------------------------------+ +-------------------------------------+--------------------------------------------------------------------------------------------------------------------------------------------------------------------------+
|| BitNode-5: Artificial Intelligence || * Unlocks :ref:`gameplay_intelligence`. | || 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. | || || * Level 3 grants permanent access to use limit/stop orders. |
+-------------------------------------+--------------------------------------------------------------------------------------------------------------------------------------------------------------------------+ +-------------------------------------+--------------------------------------------------------------------------------------------------------------------------------------------------------------------------+
|| BitNode-9: Hacktocracy || * Level 1 permanently unlocks the Hacknet Server in other BitNodes. | || 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 | || || * Level 3 grants a highly-upgraded Hacknet Server when entering a new BitNode (it |
|| || will be lost after installing augments). | || || will be lost after installing augments). |
+-------------------------------------+--------------------------------------------------------------------------------------------------------------------------------------------------------------------------+ +-------------------------------------+--------------------------------------------------------------------------------------------------------------------------------------------------------------------------+
@ -60,6 +60,6 @@ List of all Source-Files
|| || 32%/48%/56%. | || || 32%/48%/56%. |
+-------------------------------------+--------------------------------------------------------------------------------------------------------------------------------------------------------------------------+ +-------------------------------------+--------------------------------------------------------------------------------------------------------------------------------------------------------------------------+
|| BitNode-12: The Recursion || * There is no maximum level for this Source-File. | || 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. | || || Source-File. |
+-------------------------------------+--------------------------------------------------------------------------------------------------------------------------------------------------------------------------+ +-------------------------------------+--------------------------------------------------------------------------------------------------------------------------------------------------------------------------+

@ -3,8 +3,107 @@
Changelog 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) v1.1.0 - 2021-12-03 BN13: They're Lunatics (hydroflame & community)
------------------------------------------------------- -------------------------------------------------------------------
** BN13: They're Lunatics ** ** BN13: They're Lunatics **

@ -64,9 +64,9 @@ documentation_title = '{0} Documentation'.format(project)
# built documents. # built documents.
# #
# The short X.Y version. # The short X.Y version.
version = '1.1' version = '1.2'
# The full version, including alpha/beta/rc tags. # 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 # The language for content autogenerated by Sphinx. Refer to documentation
# for a list of supported languages. # 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 [home ~]> scan-analyze 2
~~~~~~~~~~ Beginning scan-analyze ~~~~~~~~~~ ~~~~~~~~~~ Beginning scan-analyze ~~~~~~~~~~
>n00dles n00dles
--Root Access: NO, Required hacking skill: 1 --Root Access: YES, Required hacking skill: 1
--Number of open ports required to NUKE: 0 --Number of open ports required to NUKE: 0
--RAM: 16 --RAM: 4.00GB
>sigma-cosmetics ----zer0
--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
------Root Access: NO, Required hacking skill: 75 ------Root Access: NO, Required hacking skill: 75
------Number of open ports required to NUKE: 1 ------Number of open ports required to NUKE: 1
------RAM: 32 ------RAM: 32.00GB
---->CSEC 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 ------Root Access: NO, Required hacking skill: 54
------Number of open ports required to NUKE: 1 ------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: 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 $ scp early-hack-template.script harakiri-sushi
$ connect n00dles $ connect n00dles
$ run NUKE.exe $ run NUKE.exe
$ run early-hack-template.script -t 2 $ run early-hack-template.script -t 1
$ home $ home
$ connect sigma-cosmetics $ connect sigma-cosmetics
$ run NUKE.exe $ run NUKE.exe

@ -12,6 +12,8 @@ const debug = false;
function createWindow(killall) { function createWindow(killall) {
const win = new BrowserWindow({ const win = new BrowserWindow({
show: false, show: false,
backgroundThrottling: false,
backgroundColor: "#000000",
}); });
win.removeMenu(); win.removeMenu();
@ -44,6 +46,7 @@ function createWindow(killall) {
greenworks.activateAchievement(ach, () => undefined); greenworks.activateAchievement(ach, () => undefined);
} }
}, 1000); }, 1000);
win.achievementsIntervalID = intervalID;
// Create the Application's main menu // Create the Application's main menu
Menu.setApplicationMenu( Menu.setApplicationMenu(
@ -73,6 +76,7 @@ function createWindow(killall) {
{ {
label: "reload & kill all scripts", label: "reload & kill all scripts",
click: () => { click: () => {
setStopProcessHandler(app, win, false);
if (intervalID) clearInterval(intervalID); if (intervalID) clearInterval(intervalID);
win.webContents.forcefullyCrashRenderer(); win.webContents.forcefullyCrashRenderer();
win.close(); 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(() => { app.whenReady().then(() => {
createWindow(false); const win = createWindow(process.argv.includes("--no-scripts"));
setStopProcessHandler(app, win, true);
}); });

@ -12012,6 +12012,34 @@
], ],
"name": "getHashUpgradeLevel" "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", "kind": "MethodSignature",
"canonicalReference": "bitburner!Hacknet#getLevelUpgradeCost:member(1)", "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

@ -0,0 +1,34 @@
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
[Home](./index.md) &gt; [bitburner](./bitburner.md) &gt; [Hacknet](./bitburner.hacknet.md) &gt; [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. | | [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. | | [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. | | [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. | | [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. | | [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. | | [getPurchaseNodeCost()](./bitburner.hacknet.getpurchasenodecost.md) | Get the price of the next hacknet node. |

@ -1,10 +1,10 @@
{ {
"name": "bitburner", "name": "bitburner",
"license": "SEE LICENSE IN license.txt", "license": "SEE LICENSE IN license.txt",
"version": "1.1.0", "version": "1.2.0",
"main": "electron-main.js", "main": "electron-main.js",
"author": { "author": {
"name": "Daniel Xie" "name": "Daniel Xie & Olivier Gagnon"
}, },
"bugs": { "bugs": {
"url": "https://github.com/danielyxie/bitburner/issues" "url": "https://github.com/danielyxie/bitburner/issues"
@ -109,6 +109,7 @@
"test:watch": "jest --watch", "test:watch": "jest --watch",
"watch": "webpack --watch --mode production", "watch": "webpack --watch --mode production",
"watch:dev": "webpack --watch --mode development", "watch:dev": "webpack --watch --mode development",
"electron": "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

@ -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) { if (ownedAug.name === AugmentationNames.NeuroFluxGovernor) {
level = ownedAug.level; level = ownedAug.level;
} }
augs.push(<AugmentationAccordion key={aug.name} aug={aug} level={level} />); augs.push(<AugmentationAccordion key={aug.name} aug={aug} level={level} />);
} }

@ -111,8 +111,8 @@ export const CONSTANTS: {
TotalNumBitNodes: number; TotalNumBitNodes: number;
LatestUpdate: string; LatestUpdate: string;
} = { } = {
VersionString: "1.1.0", VersionString: "1.2.0",
VersionNumber: 6, VersionNumber: 7,
// Speed (in ms) at which the main loop is updated // Speed (in ms) at which the main loop is updated
_idleSpeed: 200, _idleSpeed: 200,
@ -273,72 +273,103 @@ export const CONSTANTS: {
TotalNumBitNodes: 24, TotalNumBitNodes: 24,
LatestUpdate: ` 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 ** ** Steam **
* Tested on all 3 major OS. * Windows has a new launch option that lets player start with killing all their scripts
* 94 achievements added This is a safety net in case all the other safety nets fail.
* Release is 2021-12-10. * 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 ** ** 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 ** * getGangInformation returns more information.
* getAscensionResult added
* Gang with 0 territory can no longer fight * getMemberInformation returns more info
* Territory now caps at exactly 0 or 1. * 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. ** ** Misc. **
* Clicking "previous" on the browser will not pretend you had unsaved information * Add option to supress Game Saved! toasts (@MartinFournier)
allowing you to cancel if needs be. * Fix bug where ctrl+alt+j was eaten by the wrong process. (@billyvg)
* Fixed some tail box coloring issue. * Theme Editor lets you paste colors (@MartinFournier)
* Fixed BladeBurner getCityCommunities ram cost * ctrl + u/k/w should work on terminal (@billyvg)
* The download terminal command no longer duplicate extensions (@Insight) * Game now shows commit number, this is mostly for me. (@MartinFourier)
* Fix #000 on #000 text in blackjack. (@Insight) * running a bad script will give a clearer error message (@TheCoderJT)
* Remove reference to .fconf * Default terminal capacity is maximum (@SayntGarmo)
* Tail boxes all die on soft reset. * Fix problems with cp and mv (@theit8514)
* Fix codign contract focus bug. * Make monaco load fully offline for players behind firewalls.
* Megacorp factions simply re-invite you instead of auto added on reset. (@theit8514) * change beginer guide to use n00dles instead of foodnstuff
* Tail window is bound to html body. * BN13 is harder
* Infiltration reward is tied to your potential stats, not your actual stats * nerf int gain from manualHack
So you won't lose reward for doing the same thing over and over. * Fix UI displaying wrong stats (@DJMatch3000)
* intelligence lowers program creation requirements. * Fix button not disabling as it should.
* Terminal parses true as the boolean, not the string. * New location in Ishima.
* Tail and kill autocomplete using the ns2 autocomplete feature. * Add setting to suppress stock market popups.
* scan-analyze doesn't take up as many terminal entries. * Typo fixes (@Hedrauta, @cvr-119, @Ationi, @millennIumAMbiguity
* GangOtherInfo documentation now renders correctly. @TealKoi, @TheCoderJT, @cblte, @2PacIsAlive, @MageKing17,
* ActiveScripts search box also searches for script names. @Xynrati, @Adraxas, @pobiega)
* Infinite money no longer allows for infinite hacknet server. * Fix 100% territory achievement.
* Blackjack doesn't make you lose money twice. * Reword message on active scripts page.
* Recent Scripts is now from most to least recent. * Fix terminal not clearing after BN
* Fix mathjax ascii art bug in NiteSec. * Remove references to .fconf
* Remove warning that the theme editor is slow, it's only slow in dev mode. * Augmentation pages shows BN difficulty with SF5
* In BN8 is it possible to reduce the money on a server without gaining any. * Fix scripts saving on wrong server while 'connect'ing
* In the options, the timestamp feature has a placeholder explaining the expected format. * Fix gym discount not working.
* Bunch of doc typo fix. (hydroflame & @BartKoppelmans & @cvr-119) * Fix scan-analyze not working with timestamps
* World Daemon difficulty is increased depending on BitNode. * Hash upgrades remember last choice.
* hacking skill nerf was loosened in some BNs. * Save files now sort by date
* nerf noodle bar * 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."), 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."), 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."), FormulasProgram: new DarkWebItem(Programs.Formulas.name, 5e9, "Unlock access to the formulas API."),
}; };

@ -189,7 +189,7 @@ const achievements: Achievement[] = [
}, },
{ {
ID: "GANG_TERRITORY", 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", ID: "GANG_MEMBER_POWER",

@ -754,7 +754,12 @@ export function NetscriptFunctions(workerScript: WorkerScript): NS {
} }
}, },
enableLog: function (fn: any): any { 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}.`); throw makeRuntimeErrorMsg("enableLog", `Invalid argument: ${fn}.`);
} }
delete workerScript.disableLogs[fn]; delete workerScript.disableLogs[fn];

@ -16,6 +16,8 @@ import {
} from "../Hacknet/HacknetHelpers"; } from "../Hacknet/HacknetHelpers";
import { HacknetServer } from "../Hacknet/HacknetServer"; import { HacknetServer } from "../Hacknet/HacknetServer";
import { HacknetNode } from "../Hacknet/HacknetNode"; import { HacknetNode } from "../Hacknet/HacknetNode";
import { HashUpgrades } from "../Hacknet/HashUpgrades";
import { HashUpgrade } from "../Hacknet/HashUpgrade";
import { GetServer } from "../Server/AllServers"; import { GetServer } from "../Server/AllServers";
import { Hacknet as IHacknet, NodeStats } from "../ScriptEditor/NetscriptDefinitions"; 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); 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 { getHashUpgradeLevel: function (upgName: any): number {
const level = player.hashManager.upgrades[upgName]; const level = player.hashManager.upgrades[upgName];
if (level === undefined) { if (level === undefined) {

@ -29,6 +29,7 @@ import { ICodingContractReward } from "../CodingContracts";
import { IRouter } from "../ui/Router"; import { IRouter } from "../ui/Router";
import { WorkerScript } from "../Netscript/WorkerScript"; import { WorkerScript } from "../Netscript/WorkerScript";
import { HacknetServer } from "../Hacknet/HacknetServer"; import { HacknetServer } from "../Hacknet/HacknetServer";
import { ISkillProgress } from "./formulas/skill";
export interface IPlayer { export interface IPlayer {
// Class members // Class members
@ -259,6 +260,7 @@ export interface IPlayer {
prestigeAugmentation(): void; prestigeAugmentation(): void;
prestigeSourceFile(): void; prestigeSourceFile(): void;
calculateSkill(exp: number, mult?: number): number; calculateSkill(exp: number, mult?: number): number;
calculateSkillProgress(exp: number, mult?: number): ISkillProgress;
resetWorkStatus(generalType?: string, group?: string, workType?: string): void; resetWorkStatus(generalType?: string, group?: string, workType?: string): void;
getWorkHackExpGain(): number; getWorkHackExpGain(): number;
getWorkStrExpGain(): number; getWorkStrExpGain(): number;

@ -34,6 +34,7 @@ import { CityName } from "../../Locations/data/CityNames";
import { MoneySourceTracker } from "../../utils/MoneySourceTracker"; import { MoneySourceTracker } from "../../utils/MoneySourceTracker";
import { Reviver, Generic_toJSON, Generic_fromJSON } from "../../utils/JSONReviver"; import { Reviver, Generic_toJSON, Generic_fromJSON } from "../../utils/JSONReviver";
import { ISkillProgress } from "../formulas/skill";
export class PlayerObject implements IPlayer { export class PlayerObject implements IPlayer {
// Class members // Class members
@ -265,6 +266,7 @@ export class PlayerObject implements IPlayer {
prestigeAugmentation: () => void; prestigeAugmentation: () => void;
prestigeSourceFile: () => void; prestigeSourceFile: () => void;
calculateSkill: (exp: number, mult?: number) => number; calculateSkill: (exp: number, mult?: number) => number;
calculateSkillProgress: (exp: number, mult?: number) => ISkillProgress;
resetWorkStatus: (generalType?: string, group?: string, workType?: string) => void; resetWorkStatus: (generalType?: string, group?: string, workType?: string) => void;
getWorkHackExpGain: () => number; getWorkHackExpGain: () => number;
getWorkStrExpGain: () => number; getWorkStrExpGain: () => number;
@ -470,6 +472,7 @@ export class PlayerObject implements IPlayer {
this.prestigeSourceFile = generalMethods.prestigeSourceFile; this.prestigeSourceFile = generalMethods.prestigeSourceFile;
this.receiveInvite = generalMethods.receiveInvite; this.receiveInvite = generalMethods.receiveInvite;
this.calculateSkill = generalMethods.calculateSkill; this.calculateSkill = generalMethods.calculateSkill;
this.calculateSkillProgress = generalMethods.calculateSkillProgress;
this.updateSkillLevels = generalMethods.updateSkillLevels; this.updateSkillLevels = generalMethods.updateSkillLevels;
this.resetMultipliers = generalMethods.resetMultipliers; this.resetMultipliers = generalMethods.resetMultipliers;
this.hasProgram = generalMethods.hasProgram; this.hasProgram = generalMethods.hasProgram;

@ -26,7 +26,7 @@ import { Locations } from "../../Locations/Locations";
import { CityName } from "../../Locations/data/CityNames"; import { CityName } from "../../Locations/data/CityNames";
import { LocationName } from "../../Locations/data/LocationNames"; import { LocationName } from "../../Locations/data/LocationNames";
import { Sleeve } from "../../PersonObjects/Sleeve/Sleeve"; 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 { calculateIntelligenceBonus } from "../formulas/intelligence";
import { import {
getHackingWorkRepGain, getHackingWorkRepGain,
@ -226,6 +226,11 @@ export function calculateSkill(this: IPlayer, exp: number, mult = 1): number {
return calculateSkillF(exp, mult); 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 { export function updateSkillLevels(this: IPlayer): void {
this.hacking = Math.max( this.hacking = Math.max(
1, 1,

@ -5,3 +5,37 @@ export function calculateSkill(exp: number, mult = 1): number {
export function calculateExp(skill: number, mult = 1): number { export function calculateExp(skill: number, mult = 1): number {
return Math.exp((skill / mult + 200) / 32) - 534.6; 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(); const saveString = this.getSaveString();
save(saveString) save(saveString)
.then(() => SnackbarEvents.emit("Game Saved!", "info")) .then(() => {
if (!Settings.SuppressSavedGameToast) {
SnackbarEvents.emit("Game Saved!", "info")
}
})
.catch((err) => console.error(err)); .catch((err) => console.error(err));
} }

@ -2290,6 +2290,22 @@ export interface Hacknet {
*/ */
spendHashes(upgName: string, upgTarget?: string): boolean; 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. * Get the level of a hash upgrade.
* @remarks * @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. * @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. * @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. * Terminate current script and start another in 10s.
@ -5242,7 +5258,7 @@ export interface NS extends Singularity {
* @param format - Formatter. * @param format - Formatter.
* @returns Formated number. * @returns Formated number.
*/ */
nFormat(n: number, format: string): number; nFormat(n: number, format: string): string;
/** /**
* Format time to readable string * Format time to readable string

@ -1,7 +1,8 @@
import React, { useState, useEffect, useRef, useMemo } from "react"; 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"; import * as monaco from "monaco-editor";
type IStandaloneCodeEditor = monaco.editor.IStandaloneCodeEditor; type IStandaloneCodeEditor = monaco.editor.IStandaloneCodeEditor;
type ITextModel = monaco.editor.ITextModel;
import { OptionsModal } from "./OptionsModal"; import { OptionsModal } from "./OptionsModal";
import { Options } from "./Options"; import { Options } from "./Options";
import { isValidFilePath } from "../../Terminal/DirectoryHelpers"; import { isValidFilePath } from "../../Terminal/DirectoryHelpers";
@ -26,10 +27,10 @@ import { loadThemes } from "./themes";
import { GetServer } from "../../Server/AllServers"; import { GetServer } from "../../Server/AllServers";
import Button from "@mui/material/Button"; import Button from "@mui/material/Button";
import Tooltip from "@mui/material/Tooltip";
import Typography from "@mui/material/Typography"; import Typography from "@mui/material/Typography";
import Link from "@mui/material/Link"; import Link from "@mui/material/Link";
import Box from "@mui/material/Box"; import Box from "@mui/material/Box";
import TextField from "@mui/material/TextField";
import IconButton from "@mui/material/IconButton"; import IconButton from "@mui/material/IconButton";
import SettingsIcon from "@mui/icons-material/Settings"; 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://github.com/threehams/typescript-error-guide/blob/master/stories/components/Editor.tsx#L11-L39
// https://blog.checklyhq.com/customizing-monaco/ // https://blog.checklyhq.com/customizing-monaco/
// These variables are used to reload a script when it's clicked on. Because we // Holds all the data for a open script
// won't have references to the old script. class openScript {
let lastFilename = ""; fileName: string;
let lastCode = ""; code: string;
let hostname = ""; hostname: string;
let lastPosition: monaco.Position | null = null; 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 { export function Root(props: IProps): React.ReactElement {
const editorRef = useRef<IStandaloneCodeEditor | null>(null); const editorRef = useRef<IStandaloneCodeEditor | null>(null);
const [filename, setFilename] = useState(props.filename ? props.filename : lastFilename); const monacoRef = useRef<Monaco | null>(null);
const [code, setCode] = useState<string>(props.filename ? props.code : lastCode); const [filename, setFilename] = useState(props.filename);
const [code, setCode] = useState<string>(props.code);
const [decorations, setDecorations] = useState<string[]>([]); const [decorations, setDecorations] = useState<string[]>([]);
hostname = props.filename ? props.hostname : hostname;
if (hostname === "") {
hostname = props.player.getCurrentServer().hostname;
}
const [ram, setRAM] = useState("RAM: ???"); const [ram, setRAM] = useState("RAM: ???");
const [updatingRam, setUpdatingRam] = useState(false); const [updatingRam, setUpdatingRam] = useState(false);
const [optionsOpen, setOptionsOpen] = 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 { 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. // this is duplicate code with saving later.
if (ITutorial.isRunning && ITutorial.currStep === iTutorialSteps.TerminalTypeScript) { if (ITutorial.isRunning && ITutorial.currStep === iTutorialSteps.TerminalTypeScript) {
//Make sure filename + code properly follow tutorial //Make sure filename + code properly follow tutorial
if (filename !== "n00dles.script") { if (currentScript.fileName !== "n00dles.script") {
dialogBoxCreate("Leave the script name as 'n00dles.script'!"); dialogBoxCreate("Leave the script name as 'n00dles.script'!");
return; 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!"); dialogBoxCreate("Please copy and paste the code from the tutorial!");
return; return;
} }
//Save the script //Save the script
const server = GetServer(hostname); saveScript(currentScript);
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);
}
iTutorialNextStep(); iTutorialNextStep();
@ -169,45 +147,48 @@ export function Root(props: IProps): React.ReactElement {
return; return;
} }
if (filename == "") { if (currentScript.fileName == "") {
dialogBoxCreate("You must specify a filename!"); dialogBoxCreate("You must specify a filename!");
return; return;
} }
if (!isValidFilePath(filename)) { if (!isValidFilePath(currentScript.fileName)) {
dialogBoxCreate( dialogBoxCreate(
"Script filename can contain only alphanumerics, hyphens, and underscores, and must end with an extension.", "Script filename can contain only alphanumerics, hyphens, and underscores, and must end with an extension.",
); );
return; 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 (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 //If the current script already exists on the server, overwrite it
for (let i = 0; i < server.scripts.length; i++) { for (let i = 0; i < server.scripts.length; i++) {
if (filename == server.scripts[i].filename) { if (currentScript.fileName == server.scripts[i].filename) {
server.scripts[i].saveScript(filename, code, props.player.currentServer, server.scripts); server.scripts[i].saveScript(
currentScript.fileName,
currentScript.code,
props.player.currentServer,
server.scripts,
);
if (Settings.SaveGameOnFileSave) saveObject.saveGame(); if (Settings.SaveGameOnFileSave) saveObject.saveGame();
props.router.toTerminal();
return; return;
} }
} }
//If the current script does NOT exist, create a new one //If the current script does NOT exist, create a new one
const script = new Script(); 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); server.scripts.push(script);
} else if (filename.endsWith(".txt")) { } else if (currentScript.fileName.endsWith(".txt")) {
for (let i = 0; i < server.textFiles.length; ++i) { for (let i = 0; i < server.textFiles.length; ++i) {
if (server.textFiles[i].fn === filename) { if (server.textFiles[i].fn === currentScript.fileName) {
server.textFiles[i].write(code); server.textFiles[i].write(currentScript.code);
if (Settings.SaveGameOnFileSave) saveObject.saveGame(); if (Settings.SaveGameOnFileSave) saveObject.saveGame();
props.router.toTerminal();
return; return;
} }
} }
const textFile = new TextFile(filename, code); const textFile = new TextFile(currentScript.fileName, currentScript.code);
server.textFiles.push(textFile); server.textFiles.push(textFile);
} else { } else {
dialogBoxCreate("Invalid filename. Must be either a script (.script, .js, or .ns) or " + " or text file (.txt)"); 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(); if (Settings.SaveGameOnFileSave) saveObject.saveGame();
props.router.toTerminal();
} }
function beautify(): void { function beautify(): void {
@ -223,14 +203,9 @@ export function Root(props: IProps): React.ReactElement {
editorRef.current.getAction("editor.action.formatDocument").run(); 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 { function infLoop(newCode: string): void {
if (editorRef.current === null) return; 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); const awaitWarning = checkInfiniteLoop(newCode);
if (awaitWarning !== -1) { if (awaitWarning !== -1) {
const newDecorations = editorRef.current.deltaDecorations(decorations, [ const newDecorations = editorRef.current.deltaDecorations(decorations, [
@ -259,12 +234,10 @@ export function Root(props: IProps): React.ReactElement {
function updateCode(newCode?: string): void { function updateCode(newCode?: string): void {
if (newCode === undefined) return; if (newCode === undefined) return;
lastCode = newCode;
setCode(newCode);
updateRAM(newCode); updateRAM(newCode);
currentScript.code = newCode;
try { try {
if (editorRef.current !== null) { if (editorRef.current !== null) {
lastPosition = editorRef.current.getPosition();
infLoop(newCode); infLoop(newCode);
} }
} catch (err) {} } catch (err) {}
@ -272,7 +245,7 @@ export function Root(props: IProps): React.ReactElement {
// calculate it once the first time the file is loaded. // calculate it once the first time the file is loaded.
useEffect(() => { useEffect(() => {
updateRAM(code); updateRAM(currentScript.code);
}, []); }, []);
async function updateRAM(newCode: string): Promise<void> { async function updateRAM(newCode: string): Promise<void> {
@ -314,8 +287,64 @@ export function Root(props: IProps): React.ReactElement {
return () => document.removeEventListener("keydown", maybeSave); 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; editorRef.current = editor;
monacoRef.current = monaco;
if (editorRef.current === null) return; if (editorRef.current === null) return;
const position = CursorPositions.getCursor(filename); const position = CursorPositions.getCursor(filename);
if (position.row !== -1) if (position.row !== -1)
@ -323,12 +352,56 @@ export function Root(props: IProps): React.ReactElement {
lineNumber: position.row, lineNumber: position.row,
column: position.column, column: position.column,
}); });
else if (lastPosition !== null)
editorRef.current.setPosition({
lineNumber: lastPosition.lineNumber,
column: lastPosition.column + 1,
});
editorRef.current.focus(); 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 { 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"); monaco.languages.typescript.typescriptDefaults.addExtraLib(source, "netscript.d.ts");
loadThemes(monaco); 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% // 370px 71%, 725px 85.1%, 1085px 90%, 1300px 91.7%
// fuck around in desmos until you find a function // fuck around in desmos until you find a function
const p = 11000 / -window.innerHeight + 100; const p = 11000 / -window.innerHeight + 100;
return ( return (
<> <>
<Box display="flex" flexDirection="row" alignItems="center"> <Box display="flex" flexDirection="row" alignItems="center" paddingBottom="5px">
<TextField {scriptButtons}
placeholder="filename"
type="text"
tabIndex={1}
value={filename}
onChange={onFilenameChange}
InputProps={{ startAdornment: <Typography>{hostname}:~/</Typography> }}
/>
<IconButton onClick={() => setOptionsOpen(true)}>
<>
<SettingsIcon />
options
</>
</IconButton>
</Box> </Box>
<Editor <Editor
beforeMount={beforeMount} beforeMount={beforeMount}
@ -405,7 +622,7 @@ export function Root(props: IProps): React.ReactElement {
<Typography color={updatingRam ? "secondary" : "primary"} sx={{ mx: 1 }}> <Typography color={updatingRam ? "secondary" : "primary"} sx={{ mx: 1 }}>
{ram} {ram}
</Typography> </Typography>
<Button onClick={save}>Save & Close (Ctrl/Cmd + b)</Button> <Button onClick={save}>Save (Ctrl/Cmd + b)</Button>
<Typography sx={{ mx: 1 }}> <Typography sx={{ mx: 1 }}>
{" "} {" "}
Documentation:{" "} Documentation:{" "}
@ -417,6 +634,12 @@ export function Root(props: IProps): React.ReactElement {
Full Full
</Link> </Link>
</Typography> </Typography>
<IconButton style={{ marginLeft: "auto" }} onClick={() => setOptionsOpen(true)}>
<>
<SettingsIcon />
options
</>
</IconButton>
</Box> </Box>
<OptionsModal <OptionsModal
open={optionsOpen} open={optionsOpen}

@ -103,6 +103,11 @@ interface IDefaultSettings {
*/ */
SuppressTIXPopup: boolean; SuppressTIXPopup: boolean;
/**
* Whether the user should be displayed a toast alert when the game is saved.
*/
SuppressSavedGameToast: boolean;
/* /*
* Theme colors * Theme colors
*/ */
@ -186,6 +191,7 @@ export const defaultSettings: IDefaultSettings = {
SuppressTravelConfirmation: false, SuppressTravelConfirmation: false,
SuppressBladeburnerPopup: false, SuppressBladeburnerPopup: false,
SuppressTIXPopup: false, SuppressTIXPopup: false,
SuppressSavedGameToast: false,
theme: { theme: {
primarylight: "#0f0", primarylight: "#0f0",
@ -251,6 +257,7 @@ export const Settings: ISettings & ISelfInitializer & ISelfLoading = {
SuppressTravelConfirmation: defaultSettings.SuppressTravelConfirmation, SuppressTravelConfirmation: defaultSettings.SuppressTravelConfirmation,
SuppressBladeburnerPopup: defaultSettings.SuppressBladeburnerPopup, SuppressBladeburnerPopup: defaultSettings.SuppressBladeburnerPopup,
SuppressTIXPopup: defaultSettings.SuppressTIXPopup, SuppressTIXPopup: defaultSettings.SuppressTIXPopup,
SuppressSavedGameToast: defaultSettings.SuppressSavedGameToast,
MonacoTheme: "monokai", MonacoTheme: "monokai",
MonacoInsertSpaces: false, MonacoInsertSpaces: false,
MonacoFontSize: 20, MonacoFontSize: 20,

@ -6,6 +6,7 @@ import makeStyles from "@mui/styles/makeStyles";
import MuiDrawer from "@mui/material/Drawer"; import MuiDrawer from "@mui/material/Drawer";
import List from "@mui/material/List"; import List from "@mui/material/List";
import Divider from "@mui/material/Divider"; import Divider from "@mui/material/Divider";
import Tooltip from "@mui/material/Tooltip";
import ChevronLeftIcon from "@mui/icons-material/ChevronLeft"; import ChevronLeftIcon from "@mui/icons-material/ChevronLeft";
import ChevronRightIcon from "@mui/icons-material/ChevronRight"; import ChevronRightIcon from "@mui/icons-material/ChevronRight";
import ListItem from "@mui/material/ListItem"; import ListItem from "@mui/material/ListItem";
@ -54,6 +55,7 @@ import { AugmentationNames } from "../../Augmentation/data/AugmentationNames";
import { KEY } from "../../utils/helpers/keyCodes"; import { KEY } from "../../utils/helpers/keyCodes";
import { ProgramsSeen } from "../../Programs/ui/ProgramsRoot"; import { ProgramsSeen } from "../../Programs/ui/ProgramsRoot";
import { InvitationsSeen } from "../../Faction/ui/FactionsRoot"; import { InvitationsSeen } from "../../Faction/ui/FactionsRoot";
import { hash } from "../../hash/hash";
const openedMixin = (theme: Theme): CSSObject => ({ const openedMixin = (theme: Theme): CSSObject => ({
width: theme.spacing(31), width: theme.spacing(31),
@ -290,7 +292,8 @@ export function SidebarRoot(props: IProps): React.ReactElement {
} else if (event.keyCode === KEY.W && event.altKey) { } else if (event.keyCode === KEY.W && event.altKey) {
event.preventDefault(); event.preventDefault();
clickCity(); 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(); event.preventDefault();
clickJob(); clickJob();
} else if (event.keyCode === KEY.R && event.altKey) { } else if (event.keyCode === KEY.R && event.altKey) {
@ -337,7 +340,13 @@ export function SidebarRoot(props: IProps): React.ReactElement {
<ListItemIcon> <ListItemIcon>
{!open ? <ChevronRightIcon color="primary" /> : <ChevronLeftIcon color="primary" />} {!open ? <ChevronRightIcon color="primary" /> : <ChevronLeftIcon color="primary" />}
</ListItemIcon> </ListItemIcon>
<ListItemText primary={<Typography>Bitburner v{CONSTANTS.VersionString}</Typography>} /> <ListItemText
primary={
<Tooltip title={hash()}>
<Typography>Bitburner v{CONSTANTS.VersionString}</Typography>
</Tooltip>
}
/>
</ListItem> </ListItem>
<Divider /> <Divider />
<List> <List>

@ -69,6 +69,7 @@ import { top } from "./commands/top";
import { unalias } from "./commands/unalias"; import { unalias } from "./commands/unalias";
import { weaken } from "./commands/weaken"; import { weaken } from "./commands/weaken";
import { wget } from "./commands/wget"; import { wget } from "./commands/wget";
import { hash } from "../hash/hash";
export class Terminal implements ITerminal { export class Terminal implements ITerminal {
// Flags to determine whether the player is currently running a hack or an analyze // 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[] = []; commandHistory: string[] = [];
commandHistoryIndex = 0; 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 // True if a Coding Contract prompt is opened
contractOpen = false; contractOpen = false;
@ -555,7 +558,7 @@ export class Terminal implements ITerminal {
} }
clear(): void { clear(): void {
this.outputHistory = [new Output(`Bitburner v${CONSTANTS.VersionString}`, "primary")]; this.outputHistory = [new Output(`Bitburner v${CONSTANTS.VersionString} (${hash()})`, "primary")];
TerminalEvents.emit(); TerminalEvents.emit();
TerminalClearEvents.emit(); TerminalClearEvents.emit();
} }
@ -668,6 +671,12 @@ export class Terminal implements ITerminal {
return; return;
} }
break; 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: case iTutorialSteps.TerminalGoHome:
if (commandArray.length == 1 && commandArray[0] == "home") { if (commandArray.length == 1 && commandArray[0] == "home") {
iTutorialNextStep(); iTutorialNextStep();

@ -20,7 +20,7 @@ const useStyles = makeStyles((theme: Theme) =>
margin: theme.spacing(0), margin: theme.spacing(0),
}, },
input: { input: {
backgroundColor: "#000", backgroundColor: theme.colors.backgroundprimary,
}, },
nopadding: { nopadding: {
padding: theme.spacing(0), padding: theme.spacing(0),
@ -48,12 +48,26 @@ export function TerminalInput({ terminal, router, player }: IProps): React.React
const terminalInput = useRef<HTMLInputElement>(null); const terminalInput = useRef<HTMLInputElement>(null);
const [value, setValue] = useState(command); const [value, setValue] = useState(command);
const [postUpdateValue, setPostUpdateValue] = useState<{postUpdate: () => void} | null>()
const [possibilities, setPossibilities] = useState<string[]>([]); const [possibilities, setPossibilities] = useState<string[]>([]);
const classes = useStyles(); const classes = useStyles();
function saveValue(value: string): void { // Need to run after state updates, for example if we need to move cursor
command = value; // *after* we modify input
setValue(value); 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 { function handleValueChange(event: React.ChangeEvent<HTMLInputElement>): void {
@ -76,24 +90,36 @@ export function TerminalInput({ terminal, router, player }: IProps): React.React
} }
break; break;
case "deletewordbefore": // Delete rest of word before the cursor case "deletewordbefore": // Delete rest of word before the cursor
for (let delStart = start - 1; delStart > 0; --delStart) { for (let delStart = start - 1; delStart > -2; --delStart) {
if (inputText.charAt(delStart) === " ") { if ((inputText.charAt(delStart) === " " || delStart === -1) && delStart !== start - 1) {
saveValue(inputText.substr(0, delStart) + inputText.substr(start)); 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; return;
} }
} }
break; 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) { for (let delStart = start + 1; delStart <= value.length + 1; ++delStart) {
if (inputText.charAt(delStart) === " ") { if (inputText.charAt(delStart) === " " || delStart === value.length + 1) {
saveValue(inputText.substr(0, start) + inputText.substr(delStart)); 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; return;
} }
} }
break; break;
case "clearafter": // Deletes everything after cursor case "clearafter": // Deletes everything after cursor
saveValue(inputText.substr(0, start));
break; break;
case "clearbefore:": // Deleetes everything before cursor case "clearbefore": // Deletes everything before cursor
saveValue(inputText.substr(start), () => moveTextCursor("home"));
break; break;
} }
} }
@ -320,11 +346,23 @@ export function TerminalInput({ terminal, router, player }: IProps): React.React
event.preventDefault(); 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: // TODO AFTER THIS:
// alt + d deletes word after cursor // 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 Typography from "@mui/material/Typography";
import Link from "@mui/material/Link"; import Link from "@mui/material/Link";
import Box from "@mui/material/Box"; import Box from "@mui/material/Box";
import Button from "@mui/material/Button"; import Button from "@mui/material/Button";
import { ConfirmationModal } from "../../ui/React/ConfirmationModal";
interface IProps { interface IProps {
reactivateTutorial: () => void; reactivateTutorial: () => void;
} }
export function TutorialRoot(props: IProps): React.ReactElement { export function TutorialRoot(props: IProps): React.ReactElement {
const [confirmResetOpen, setConfirmResetOpen] = useState(false);
return ( return (
<> <>
<Typography variant="h4">Tutorial / Documentation</Typography> <Typography variant="h4">Tutorial / Documentation</Typography>
<Box m={2}> <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 <Link
color="primary" color="primary"
target="_blank" 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"> <Link color="primary" target="_blank" href="https://bitburner.readthedocs.io/en/latest/shortcuts.html">
<Typography>Keyboard Shortcuts</Typography> <Typography>Keyboard Shortcuts</Typography>
</Link> </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> </Box>
</> </>
); );

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 ListItem from "@mui/material/ListItem";
import EqualizerIcon from "@mui/icons-material/Equalizer"; import EqualizerIcon from "@mui/icons-material/Equalizer";
import LastPageIcon from "@mui/icons-material/LastPage"; import LastPageIcon from "@mui/icons-material/LastPage";
import VisibilityOffIcon from "@mui/icons-material/VisibilityOff";
import HelpIcon from "@mui/icons-material/Help"; import HelpIcon from "@mui/icons-material/Help";
import AccountTreeIcon from "@mui/icons-material/AccountTree"; import AccountTreeIcon from "@mui/icons-material/AccountTree";
import StorageIcon from "@mui/icons-material/Storage"; import StorageIcon from "@mui/icons-material/Storage";
@ -58,6 +59,9 @@ export function InteractiveTutorialRoot(): React.ReactElement {
<br /> <br />
<br /> <br />
This tutorial will show you the basics of the game. You may skip the tutorial at any time. 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> </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, 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{" "} then you will encounter diminishing returns in your hacking. You will need to use{" "}
<Typography classes={{ root: classes.textfield }}>{"[n00dles ~/]> grow"}</Typography> <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> </Typography>
), ),
canNext: true, canNext: true,
@ -345,10 +351,6 @@ export function InteractiveTutorialRoot(): React.ReactElement {
continuously hack the n00dles server. continuously hack the n00dles server.
<br /> <br />
<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. To save and close the script editor, press the button in the bottom left, or press ctrl + b.
</Typography> </Typography>
</> </>
@ -447,7 +449,11 @@ export function InteractiveTutorialRoot(): React.ReactElement {
<br /> <br />
<br /> <br />
If you know even a little bit of programming it is highly recommended you use NS2 instead. You will enjoy 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 />
<br /> <br />
For now, let's move on to something else! For now, let's move on to something else!

@ -15,6 +15,7 @@ import { GameRoot } from "./GameRoot";
import { CONSTANTS } from "../Constants"; import { CONSTANTS } from "../Constants";
import { ActivateRecoveryMode } from "./React/RecoveryRoot"; import { ActivateRecoveryMode } from "./React/RecoveryRoot";
import { hash } from "../hash/hash";
const useStyles = makeStyles((theme: Theme) => const useStyles = makeStyles((theme: Theme) =>
createStyles({ createStyles({
@ -69,7 +70,9 @@ export function LoadingScreen(): React.ReactElement {
<CircularProgress size={150} color="primary" /> <CircularProgress size={150} color="primary" />
</Grid> </Grid>
<Grid item> <Grid item>
<Typography variant="h3">Loading Bitburner v{CONSTANTS.VersionString}</Typography> <Typography variant="h3">
Loading Bitburner v{CONSTANTS.VersionString} ({hash()})
</Typography>
</Grid> </Grid>
{show && ( {show && (
<Grid item> <Grid item>

@ -1,7 +1,7 @@
// Root React Component for the Corporation UI // Root React Component for the Corporation UI
import React, { useState, useEffect } from "react"; 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 makeStyles from "@mui/styles/makeStyles";
import createStyles from "@mui/styles/createStyles"; import createStyles from "@mui/styles/createStyles";
import { numeralWrapper } from "../../ui/numeralFormat"; import { numeralWrapper } from "../../ui/numeralFormat";
@ -20,6 +20,8 @@ import ClearAllIcon from "@mui/icons-material/ClearAll";
import { Settings } from "../../Settings/Settings"; import { Settings } from "../../Settings/Settings";
import { use } from "../Context"; import { use } from "../Context";
import { StatsProgressOverviewCell } from "./StatsProgressBar";
import { BitNodeMultipliers } from "../../BitNode/BitNodeMultipliers";
interface IProps { interface IProps {
save: () => void; save: () => void;
@ -139,6 +141,8 @@ const useStyles = makeStyles((theme: Theme) =>
}), }),
); );
export { useStyles as characterOverviewStyles };
export function CharacterOverview({ save, killScripts }: IProps): React.ReactElement { export function CharacterOverview({ save, killScripts }: IProps): React.ReactElement {
const [killOpen, setKillOpen] = useState(false); const [killOpen, setKillOpen] = useState(false);
const player = use.Player(); const player = use.Player();
@ -151,6 +155,21 @@ export function CharacterOverview({ save, killScripts }: IProps): React.ReactEle
}, []); }, []);
const classes = useStyles(); 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 ( return (
<> <>
<Table sx={{ display: "block", m: 1 }}> <Table sx={{ display: "block", m: 1 }}>
@ -186,12 +205,20 @@ export function CharacterOverview({ save, killScripts }: IProps): React.ReactEle
</TableRow> </TableRow>
<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&nbsp;</Typography> <Typography classes={{ root: classes.hack }}>Hack&nbsp;</Typography>
</TableCell> </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> <Typography classes={{ root: classes.hack }}>{numeralWrapper.formatSkill(player.hacking)}</Typography>
</TableCell> </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 }}> <TableCell align="right" classes={{ root: classes.cell }}>
<Typography id="overview-hack-hook" classes={{ root: classes.hack }}> <Typography id="overview-hack-hook" classes={{ root: classes.hack }}>
{/*Hook for player scripts*/} {/*Hook for player scripts*/}
@ -212,6 +239,9 @@ export function CharacterOverview({ save, killScripts }: IProps): React.ReactEle
</Typography> </Typography>
</TableCell> </TableCell>
</TableRow> </TableRow>
<TableRow>
<StatsProgressOverviewCell progress={strengthProgress} color={theme.colors.combat} />
</TableRow>
<TableRow> <TableRow>
<TableCell component="th" scope="row" classes={{ root: classes.cellNone }}> <TableCell component="th" scope="row" classes={{ root: classes.cellNone }}>
@ -226,6 +256,9 @@ export function CharacterOverview({ save, killScripts }: IProps): React.ReactEle
</Typography> </Typography>
</TableCell> </TableCell>
</TableRow> </TableRow>
<TableRow>
<StatsProgressOverviewCell progress={defenseProgress} color={theme.colors.combat} />
</TableRow>
<TableRow> <TableRow>
<TableCell component="th" scope="row" classes={{ root: classes.cellNone }}> <TableCell component="th" scope="row" classes={{ root: classes.cellNone }}>
@ -240,6 +273,10 @@ export function CharacterOverview({ save, killScripts }: IProps): React.ReactEle
</Typography> </Typography>
</TableCell> </TableCell>
</TableRow> </TableRow>
<TableRow>
<StatsProgressOverviewCell progress={dexterityProgress} color={theme.colors.combat} />
</TableRow>
<TableRow> <TableRow>
<TableCell component="th" scope="row" classes={{ root: classes.cell }}> <TableCell component="th" scope="row" classes={{ root: classes.cell }}>
<Typography classes={{ root: classes.combat }}>Agi&nbsp;</Typography> <Typography classes={{ root: classes.combat }}>Agi&nbsp;</Typography>
@ -253,6 +290,9 @@ export function CharacterOverview({ save, killScripts }: IProps): React.ReactEle
</Typography> </Typography>
</TableCell> </TableCell>
</TableRow> </TableRow>
<TableRow>
<StatsProgressOverviewCell progress={agilityProgress} color={theme.colors.combat} />
</TableRow>
<TableRow> <TableRow>
<TableCell component="th" scope="row" classes={{ root: classes.cellNone }}> <TableCell component="th" scope="row" classes={{ root: classes.cellNone }}>
@ -267,6 +307,10 @@ export function CharacterOverview({ save, killScripts }: IProps): React.ReactEle
</Typography> </Typography>
</TableCell> </TableCell>
</TableRow> </TableRow>
<TableRow>
<StatsProgressOverviewCell progress={charismaProgress} color={theme.colors.cha} />
</TableRow>
<Intelligence /> <Intelligence />
<TableRow> <TableRow>

@ -70,6 +70,7 @@ export function GameOptionsRoot(props: IProps): React.ReactElement {
); );
const [suppressTIXPopup, setSuppressTIXPopup] = useState(Settings.SuppressTIXPopup); const [suppressTIXPopup, setSuppressTIXPopup] = useState(Settings.SuppressTIXPopup);
const [suppressBladeburnerPopup, setSuppressBladeburnerPopup] = useState(Settings.SuppressBladeburnerPopup); const [suppressBladeburnerPopup, setSuppressBladeburnerPopup] = useState(Settings.SuppressBladeburnerPopup);
const [suppressSavedGameToast, setSuppresSavedGameToast] = useState(Settings.SuppressSavedGameToast);
const [disableHotkeys, setDisableHotkeys] = useState(Settings.DisableHotkeys); const [disableHotkeys, setDisableHotkeys] = useState(Settings.DisableHotkeys);
const [disableASCIIArt, setDisableASCIIArt] = useState(Settings.DisableASCIIArt); const [disableASCIIArt, setDisableASCIIArt] = useState(Settings.DisableASCIIArt);
@ -138,6 +139,11 @@ export function GameOptionsRoot(props: IProps): React.ReactElement {
Settings.SuppressBladeburnerPopup = event.target.checked; 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 { function handleDisableHotkeysChange(event: React.ChangeEvent<HTMLInputElement>): void {
setDisableHotkeys(event.target.checked); setDisableHotkeys(event.target.checked);
Settings.DisableHotkeys = event.target.checked; Settings.DisableHotkeys = event.target.checked;
@ -420,6 +426,24 @@ export function GameOptionsRoot(props: IProps): React.ReactElement {
/> />
</ListItem> </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> <ListItem>
<FormControlLabel <FormControlLabel
control={<Switch checked={disableHotkeys} onChange={handleDisableHotkeysChange} />} control={<Switch checked={disableHotkeys} onChange={handleDisableHotkeysChange} />}

@ -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 <ColorPicker
hideTextfield hideTextfield
deferred
value={color} value={color}
onChange={(newColor: Color) => onColorChange(name, "#" + newColor.hex)} 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 { return {
plugins: [ plugins: [
new webpack.DefinePlugin({ 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 // In dev mode, use a faster method of create sourcemaps
// while keeping lines/columns accurate // while keeping lines/columns accurate
isDevServer && isDevServer &&