Merge pull request #642 from danielyxie/dev

Dev
This commit is contained in:
danielyxie 2019-06-28 13:22:53 -07:00 committed by GitHub
commit 4476d6b258
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
86 changed files with 4388 additions and 3446 deletions

126
css/activescripts.scss Normal file

@ -0,0 +1,126 @@
@import "theme";
.active-scripts-list {
list-style-type: none;
}
#active-scripts-container {
position: fixed;
padding-top: 10px;
> p {
width: 70%;
margin: 6px;
padding: 4px;
}
.accordion-header {
> pre {
color: white;
}
}
}
.active-scripts-server-header {
background-color: #444;
font-size: $defaultFontSize * 1.25;
color: #fff;
margin: 6px 6px 0 6px;
padding: 6px;
cursor: pointer;
width: 60%;
text-align: left;
border: none;
outline: none;
&:after {
content: '\02795'; /* "plus" sign (+) */
font-size: $defaultFontSize * 0.8125;
color: #fff;
float: right;
margin-left: 5px;
}
&.active, &:hover {
background-color: #555;
}
}
.active-scripts-server-header.active {
&:after {
content: "\2796"; /* "minus" sign (-) */
font-size: $defaultFontSize * 0.8125;
color: #fff;
float: right;
margin-left: 5px;
}
&:hover {
background-color: #666;
}
}
.active-scripts-server-panel {
margin: 0 6px 6px 6px;
padding: 0 6px 6px 6px;
width: 55%;
margin-left: 5%;
display: none;
div, ul, ul > li {
background-color: #555;
}
}
.active-scripts-script-header {
background-color: #555;
border: none;
color: var(--my-font-color);
cursor: pointer;
display: block;
outline: none;
padding: 4px 25px 4px 10px;
position: relative;
text-align: left;
width: auto;
&:after {
content: '\02795'; /* "plus" sign (+) */
font-size: $defaultFontSize * 0.8125;
float: right;
margin-left: 5px;
color: transparent;
text-shadow: 0 0 0 var(--my-font-color);
position: absolute;
bottom: 4px;
}
&.active:after {
content: "\2796"; /* "minus" sign (-) */
}
&:hover,
&.active:hover {
background-color: #666;
}
&.active {
background-color: #555;
}
}
.active-scripts-script-panel {
background-color: #555;
display: none;
font-size: 14px;
margin-bottom: 6px;
padding: 0 18px;
width: auto;
pre, h2, ul, li {
background-color: #555;
width: auto;
color: #fff;
margin-left: 5%;
}
}

31
css/augmentations.scss Normal file

@ -0,0 +1,31 @@
/**
* Styling for the Augmentations UI. This is the page that displays all of the
* player's owned and purchased Augmentations and Source-Files. It also allows
* the player to install Augmentations
*/
@import "theme";
#augmentations-container {
position: fixed;
padding-top: 10px;
}
#augmentations-content {
> p {
font-size: $defaultFontSize * 0.875;
width: 70%;
}
}
.augmentations-list {
button,
div {
color: var(--my-font-color);
text-decoration: none;
}
button {
padding: 4px;
}
}

@ -18,126 +18,6 @@
position: fixed;
}
/* Active scripts */
.active-scripts-list {
list-style-type: none;
}
#active-scripts-container {
position: fixed;
padding-top: 10px;
}
#active-scripts-text,
#active-scripts-total-prod {
width: 70%;
margin: 6px;
padding: 4px;
}
.active-scripts-server-header {
background-color: #444;
font-size: $defaultFontSize * 1.25;
color: #fff;
margin: 6px 6px 0 6px;
padding: 6px;
cursor: pointer;
width: 60%;
text-align: left;
border: none;
outline: none;
}
.active-scripts-server-header.active,
.active-scripts-server-header:hover {
background-color: #555;
}
.active-scripts-server-header.active:hover {
background-color: #666;
}
.active-scripts-server-header:after {
content: '\02795'; /* "plus" sign (+) */
font-size: $defaultFontSize * 0.8125;
color: #fff;
float: right;
margin-left: 5px;
}
.active-scripts-server-header.active:after {
content: "\2796"; /* "minus" sign (-) */
font-size: $defaultFontSize * 0.8125;
color: #fff;
float: right;
margin-left: 5px;
}
.active-scripts-server-panel {
margin: 0 6px 6px 6px;
padding: 0 6px 6px 6px;
width: 55%;
margin-left: 5%;
display: none;
}
.active-scripts-server-panel div,
.active-scripts-server-panel ul,
.active-scripts-server-panel ul > li {
background-color: #555;
}
.active-scripts-script-header {
background-color: #555;
color: var(--my-font-color);
padding: 4px 25px 4px 10px;
cursor: pointer;
width: auto;
text-align: left;
border: none;
outline: none;
position: relative;
&:after {
content: '\02795'; /* "plus" sign (+) */
font-size: $defaultFontSize * 0.8125;
float: right;
margin-left: 5px;
color: transparent;
text-shadow: 0 0 0 var(--my-font-color);
position: absolute;
bottom: 4px;
}
&.active:after {
content: "\2796"; /* "minus" sign (-) */
}
&:hover,
&.active:hover {
background-color: #666;
}
&.active {
background-color: #555;
}
}
.active-scripts-script-panel {
padding: 0 18px;
background-color: #555;
width: auto;
display: none;
margin-bottom: 6px;
p, h2, ul, li {
background-color: #555;
width: auto;
color: #fff;
margin-left: 5%;
}
}
/* World */
#world-container {
position: fixed;
@ -185,19 +65,6 @@
width: 70%;
}
#faction-donate-amount-txt,
#faction-donate-input {
padding: 6px;
margin: 6px;
display: inline-block;
color: var(--my-font-color);
background-color: #000;
}
#faction-donate-amount-txt {
width: 50%;
}
#faction-container p,
#faction-container pre {
padding: 4px 6px;
@ -213,45 +80,12 @@
word-wrap: break-word; /* Internet Explorer 5.5+ */
}
/* Faction Augmentations */
#faction-augmentations-container {
position: fixed;
padding-top: 10px;
p, a, ul, h1 {
margin: 8px;
padding: 4px;
}
}
/* World */
#world-container li {
margin: 0 0 15px 0;
list-style-type: none;
}
/* Augmentations */
#augmentations-container {
position: fixed;
padding-top: 10px;
}
.augmentations-list {
button,
div {
color: var(--my-font-color);
text-decoration: none;
}
button {
padding: 2px 5px;
}
div {
padding: 6px;
}
}
/* Tutorial */
#tutorial-container {
position: fixed;

@ -243,8 +243,8 @@ a:visited {
/* Accordion menus (Header with collapsible panel) */
.accordion-header {
background-color: #444;
font-size: $defaultFontSize * 1.25;
color: #fff;
font-size: $defaultFontSize * 1.25;
margin: 6px 6px 0 6px;
padding: 4px 6px;
cursor: pointer;

File diff suppressed because one or more lines are too long

@ -1,2 +1,2 @@
!function(n){function t(t){for(var e,i,f=t[0],c=t[1],l=t[2],p=0,s=[];p<f.length;p++)i=f[p],r[i]&&s.push(r[i][0]),r[i]=0;for(e in c)Object.prototype.hasOwnProperty.call(c,e)&&(n[e]=c[e]);for(a&&a(t);s.length;)s.shift()();return u.push.apply(u,l||[]),o()}function o(){for(var n,t=0;t<u.length;t++){for(var o=u[t],e=!0,f=1;f<o.length;f++){var c=o[f];0!==r[c]&&(e=!1)}e&&(u.splice(t--,1),n=i(i.s=o[0]))}return n}var e={},r={1:0},u=[];function i(t){if(e[t])return e[t].exports;var o=e[t]={i:t,l:!1,exports:{}};return n[t].call(o.exports,o,o.exports,i),o.l=!0,o.exports}i.m=n,i.c=e,i.d=function(n,t,o){i.o(n,t)||Object.defineProperty(n,t,{enumerable:!0,get:o})},i.r=function(n){"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(n,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(n,"__esModule",{value:!0})},i.t=function(n,t){if(1&t&&(n=i(n)),8&t)return n;if(4&t&&"object"==typeof n&&n&&n.__esModule)return n;var o=Object.create(null);if(i.r(o),Object.defineProperty(o,"default",{enumerable:!0,value:n}),2&t&&"string"!=typeof n)for(var e in n)i.d(o,e,function(t){return n[t]}.bind(null,e));return o},i.n=function(n){var t=n&&n.__esModule?function(){return n.default}:function(){return n};return i.d(t,"a",t),t},i.o=function(n,t){return Object.prototype.hasOwnProperty.call(n,t)},i.p="";var f=window.webpackJsonp=window.webpackJsonp||[],c=f.push.bind(f);f.push=t,f=f.slice();for(var l=0;l<f.length;l++)t(f[l]);var a=c;u.push([353,0]),o()}({300:function(n,t,o){},302:function(n,t,o){},304:function(n,t,o){},306:function(n,t,o){},308:function(n,t,o){},310:function(n,t,o){},312:function(n,t,o){},314:function(n,t,o){},316:function(n,t,o){},318:function(n,t,o){},320:function(n,t,o){},322:function(n,t,o){},324:function(n,t,o){},326:function(n,t,o){},328:function(n,t,o){},330:function(n,t,o){},332:function(n,t,o){},334:function(n,t,o){},336:function(n,t,o){},338:function(n,t,o){},340:function(n,t,o){},342:function(n,t,o){},344:function(n,t,o){},346:function(n,t,o){},348:function(n,t,o){},350:function(n,t,o){},353:function(n,t,o){"use strict";o.r(t);o(352),o(350),o(348),o(346),o(344),o(342),o(340),o(338),o(336),o(334),o(332),o(330),o(328),o(326),o(324),o(322),o(320),o(318),o(316),o(314),o(312),o(310),o(308),o(306),o(304),o(302),o(300)}});
!function(n){function t(t){for(var e,i,f=t[0],c=t[1],l=t[2],p=0,s=[];p<f.length;p++)i=f[p],u[i]&&s.push(u[i][0]),u[i]=0;for(e in c)Object.prototype.hasOwnProperty.call(c,e)&&(n[e]=c[e]);for(a&&a(t);s.length;)s.shift()();return r.push.apply(r,l||[]),o()}function o(){for(var n,t=0;t<r.length;t++){for(var o=r[t],e=!0,f=1;f<o.length;f++){var c=o[f];0!==u[c]&&(e=!1)}e&&(r.splice(t--,1),n=i(i.s=o[0]))}return n}var e={},u={1:0},r=[];function i(t){if(e[t])return e[t].exports;var o=e[t]={i:t,l:!1,exports:{}};return n[t].call(o.exports,o,o.exports,i),o.l=!0,o.exports}i.m=n,i.c=e,i.d=function(n,t,o){i.o(n,t)||Object.defineProperty(n,t,{enumerable:!0,get:o})},i.r=function(n){"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(n,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(n,"__esModule",{value:!0})},i.t=function(n,t){if(1&t&&(n=i(n)),8&t)return n;if(4&t&&"object"==typeof n&&n&&n.__esModule)return n;var o=Object.create(null);if(i.r(o),Object.defineProperty(o,"default",{enumerable:!0,value:n}),2&t&&"string"!=typeof n)for(var e in n)i.d(o,e,function(t){return n[t]}.bind(null,e));return o},i.n=function(n){var t=n&&n.__esModule?function(){return n.default}:function(){return n};return i.d(t,"a",t),t},i.o=function(n,t){return Object.prototype.hasOwnProperty.call(n,t)},i.p="";var f=window.webpackJsonp=window.webpackJsonp||[],c=f.push.bind(f);f.push=t,f=f.slice();for(var l=0;l<f.length;l++)t(f[l]);var a=c;r.push([361,0]),o()}({304:function(n,t,o){},306:function(n,t,o){},308:function(n,t,o){},310:function(n,t,o){},312:function(n,t,o){},314:function(n,t,o){},316:function(n,t,o){},318:function(n,t,o){},320:function(n,t,o){},322:function(n,t,o){},324:function(n,t,o){},326:function(n,t,o){},328:function(n,t,o){},330:function(n,t,o){},332:function(n,t,o){},334:function(n,t,o){},336:function(n,t,o){},338:function(n,t,o){},340:function(n,t,o){},342:function(n,t,o){},344:function(n,t,o){},346:function(n,t,o){},348:function(n,t,o){},350:function(n,t,o){},352:function(n,t,o){},354:function(n,t,o){},356:function(n,t,o){},358:function(n,t,o){},361:function(n,t,o){"use strict";o.r(t);o(360),o(358),o(356),o(354),o(352),o(350),o(348),o(346),o(344),o(342),o(340),o(338),o(336),o(334),o(332),o(330),o(328),o(326),o(324),o(322),o(320),o(318),o(316),o(314),o(312),o(310),o(308),o(306),o(304)}});
//# sourceMappingURL=engineStyle.bundle.js.map

259
dist/engineStyle.css vendored

@ -261,8 +261,8 @@ a:visited {
/* Accordion menus (Header with collapsible panel) */
.accordion-header {
background-color: #444;
font-size: 20px;
color: #fff;
font-size: 20px;
margin: 6px 6px 0 6px;
padding: 4px 6px;
cursor: pointer;
@ -933,6 +933,104 @@ button {
height: 30px;
margin-left: 6px; }
/* COLORS */
/* Attributes */
.active-scripts-list {
list-style-type: none; }
#active-scripts-container {
position: fixed;
padding-top: 10px; }
#active-scripts-container > p {
width: 70%;
margin: 6px;
padding: 4px; }
#active-scripts-container .accordion-header > pre {
color: white; }
.active-scripts-server-header {
background-color: #444;
font-size: 20px;
color: #fff;
margin: 6px 6px 0 6px;
padding: 6px;
cursor: pointer;
width: 60%;
text-align: left;
border: none;
outline: none; }
.active-scripts-server-header:after {
content: '\2795';
/* "plus" sign (+) */
font-size: 13px;
color: #fff;
float: right;
margin-left: 5px; }
.active-scripts-server-header.active, .active-scripts-server-header:hover {
background-color: #555; }
.active-scripts-server-header.active:after {
content: "\2796";
/* "minus" sign (-) */
font-size: 13px;
color: #fff;
float: right;
margin-left: 5px; }
.active-scripts-server-header.active:hover {
background-color: #666; }
.active-scripts-server-panel {
margin: 0 6px 6px 6px;
padding: 0 6px 6px 6px;
width: 55%;
margin-left: 5%;
display: none; }
.active-scripts-server-panel div, .active-scripts-server-panel ul, .active-scripts-server-panel ul > li {
background-color: #555; }
.active-scripts-script-header {
background-color: #555;
border: none;
color: var(--my-font-color);
cursor: pointer;
display: block;
outline: none;
padding: 4px 25px 4px 10px;
position: relative;
text-align: left;
width: auto; }
.active-scripts-script-header:after {
content: '\2795';
/* "plus" sign (+) */
font-size: 13px;
float: right;
margin-left: 5px;
color: transparent;
text-shadow: 0 0 0 var(--my-font-color);
position: absolute;
bottom: 4px; }
.active-scripts-script-header.active:after {
content: "\2796";
/* "minus" sign (-) */ }
.active-scripts-script-header:hover, .active-scripts-script-header.active:hover {
background-color: #666; }
.active-scripts-script-header.active {
background-color: #555; }
.active-scripts-script-panel {
background-color: #555;
display: none;
font-size: 14px;
margin-bottom: 6px;
padding: 0 18px;
width: auto; }
.active-scripts-script-panel pre, .active-scripts-script-panel h2, .active-scripts-script-panel ul, .active-scripts-script-panel li {
background-color: #555;
width: auto;
color: #fff;
margin-left: 5%; }
/* COLORS */
/* Attributes */
/**
@ -1007,107 +1105,6 @@ button {
padding-top: 10px;
position: fixed; }
/* Active scripts */
.active-scripts-list {
list-style-type: none; }
#active-scripts-container {
position: fixed;
padding-top: 10px; }
#active-scripts-text,
#active-scripts-total-prod {
width: 70%;
margin: 6px;
padding: 4px; }
.active-scripts-server-header {
background-color: #444;
font-size: 20px;
color: #fff;
margin: 6px 6px 0 6px;
padding: 6px;
cursor: pointer;
width: 60%;
text-align: left;
border: none;
outline: none; }
.active-scripts-server-header.active,
.active-scripts-server-header:hover {
background-color: #555; }
.active-scripts-server-header.active:hover {
background-color: #666; }
.active-scripts-server-header:after {
content: '\2795';
/* "plus" sign (+) */
font-size: 13px;
color: #fff;
float: right;
margin-left: 5px; }
.active-scripts-server-header.active:after {
content: "\2796";
/* "minus" sign (-) */
font-size: 13px;
color: #fff;
float: right;
margin-left: 5px; }
.active-scripts-server-panel {
margin: 0 6px 6px 6px;
padding: 0 6px 6px 6px;
width: 55%;
margin-left: 5%;
display: none; }
.active-scripts-server-panel div,
.active-scripts-server-panel ul,
.active-scripts-server-panel ul > li {
background-color: #555; }
.active-scripts-script-header {
background-color: #555;
color: var(--my-font-color);
padding: 4px 25px 4px 10px;
cursor: pointer;
width: auto;
text-align: left;
border: none;
outline: none;
position: relative; }
.active-scripts-script-header:after {
content: '\2795';
/* "plus" sign (+) */
font-size: 13px;
float: right;
margin-left: 5px;
color: transparent;
text-shadow: 0 0 0 var(--my-font-color);
position: absolute;
bottom: 4px; }
.active-scripts-script-header.active:after {
content: "\2796";
/* "minus" sign (-) */ }
.active-scripts-script-header:hover, .active-scripts-script-header.active:hover {
background-color: #666; }
.active-scripts-script-header.active {
background-color: #555; }
.active-scripts-script-panel {
padding: 0 18px;
background-color: #555;
width: auto;
display: none;
margin-bottom: 6px; }
.active-scripts-script-panel p, .active-scripts-script-panel h2, .active-scripts-script-panel ul, .active-scripts-script-panel li {
background-color: #555;
width: auto;
color: #fff;
margin-left: 5%; }
/* World */
#world-container {
position: fixed;
@ -1147,17 +1144,6 @@ button {
margin: 6px;
width: 70%; }
#faction-donate-amount-txt,
#faction-donate-input {
padding: 6px;
margin: 6px;
display: inline-block;
color: var(--my-font-color);
background-color: #000; }
#faction-donate-amount-txt {
width: 50%; }
#faction-container p,
#faction-container pre {
padding: 4px 6px;
@ -1176,35 +1162,11 @@ button {
word-wrap: break-word;
/* Internet Explorer 5.5+ */ }
/* Faction Augmentations */
#faction-augmentations-container {
position: fixed;
padding-top: 10px; }
#faction-augmentations-container p, #faction-augmentations-container a, #faction-augmentations-container ul, #faction-augmentations-container h1 {
margin: 8px;
padding: 4px; }
/* World */
#world-container li {
margin: 0 0 15px 0;
list-style-type: none; }
/* Augmentations */
#augmentations-container {
position: fixed;
padding-top: 10px; }
.augmentations-list button,
.augmentations-list div {
color: var(--my-font-color);
text-decoration: none; }
.augmentations-list button {
padding: 2px 5px; }
.augmentations-list div {
padding: 6px; }
/* Tutorial */
#tutorial-container {
position: fixed;
@ -1286,6 +1248,29 @@ button {
display: inline;
width: 25%; }
/**
* Styling for the Augmentations UI. This is the page that displays all of the
* player's owned and purchased Augmentations and Source-Files. It also allows
* the player to install Augmentations
*/
/* COLORS */
/* Attributes */
#augmentations-container {
position: fixed;
padding-top: 10px; }
#augmentations-content > p {
font-size: 14px;
width: 70%; }
.augmentations-list button,
.augmentations-list div {
color: var(--my-font-color);
text-decoration: none; }
.augmentations-list button {
padding: 4px; }
/* COLORS */
/* Attributes */
/**

26
dist/vendor.bundle.js vendored

File diff suppressed because one or more lines are too long

@ -28,6 +28,27 @@ a stock.
.. note:: Shorting stocks is not available immediately, and must be unlocked later in the
game.
Forecast & Second-Order Forecast
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
A stock's forecast is its likelihood of increasing or decreasing in value. The
forecast is typically represented by its probability of increasing in either
a decimal or percentage form. For example, a forecast of 70% means the stock
has a 70% chance of increasing and a 30% chance of decreasing.
A stock's second-order forecast is the target value that its forecast trends towards.
For example, if a stock has a forecast of 60% and a second-order forecast of 70%,
then the stock's forecast should slowly trend towards 70% over time. However, this is
determined by RNG so there is a chance that it may never reach 70%.
Both the forecast and the second-order forecast change over time.
A stock's forecast can be viewed after purchasing Four Sigma (4S) Market Data
access. This lets you see the forecast info on the Stock Market UI. If you also
purchase access to the 4S Market Data TIX API, then you can view a stock's forecast
using the :js:func:`getStockForecast` function.
A stock's second-order forecast is always hidden.
.. _gameplay_stock_market_spread:
Spread (Bid Price & Ask Price)
@ -51,38 +72,16 @@ bid price. Note that this is reversed for a short position. Purchasing a stock
in the short position will occur at the stock's bid price, and selling a stock
in the short position will occur at the stock's ask price.
.. _gameplay_stock_market_spread_price_movement:
Transactions Influencing Stock Price
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
Buying or selling a large number of shares of a stock will influence that stock's price.
Buying a stock in the long position will cause that stock's price to
increase. Selling a stock in the long position will cause the stock's price to decrease.
The reverse occurs for the short position. The effect of this depends on how many shares
are being transacted. More shares will have a bigger effect on the stock price. If
only a small number of shares are being transacted, the stock price may not be affected.
Note that this "influencing" of the stock price **can happen in the middle of a transaction**.
For example, assume you are selling 1m shares of a stock. That stock's bid price
is currently $100. However, selling 100k shares of the stock causes its price to drop
by 1%. This means that for your transaction of 1m shares, the first 100k shares will be
sold at $100. Then the next 100k shares will be sold at $99. Then the next 100k shares will
be sold at $98.01, and so on.
This is an important concept to keep in mind if you are trying to purchase/sell a
large number of shares, as **it can negatively affect your profits**.
.. note:: On the Stock Market UI, the displayed profit of a position does not take
this "influencing" into account.
Transactions Influencing Stock Forecast
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
In addition to influencing stock price, buying or selling a large number of shares
of a stock will also influence that stock's "forecast". The forecast is the likelihood
that the stock will increase or decrease in price. The magnitude of this effect depends
on the number of shares being transacted. More shares will have a bigger effect on the
stock forecast.
Buying or selling a large number of shares
of a stock will influence that stock's forecast & second-order forecast.
The forecast is the likelihood that the stock will increase or decrease in price.
The magnitude of this effect depends on the number of shares being transacted.
More shares will have a bigger effect.
The effect that transactions have on a stock's second-order forecast is
significantly smaller than the effect on its forecast.
.. _gameplay_stock_market_order_types:
@ -134,15 +133,49 @@ A Stop Order to buy will execute if the stock's price <= order's price
A Stop Order to sell will execute if the stock's price >= order's price.
.. note:: Stop Orders do **not** take into account the fact that transactions can
influence a stock's price. Limit Orders, however, do take this into account.
.. _gameplay_stock_market_player_actions_influencing_stock:
For example, assume you have a Limit Order set to purchase a stock at
$5. Then, the stock's price drops to $5 and your Limit Order executes.
However, the transaction causes the stock's price to increase before
the order finishes executing all of the shares. Your Limit Order will
stop executing, and will continue only when the stock's price drops back to
$5 or below.
Player Actions Influencing Stocks
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
It is possible for your actions elsewhere in the game to influence the stock market.
Hacking
If a server has a corresponding stock (e.g. *foodnstuff* server -> FoodNStuff
stock), then hacking that server can decrease the stock's second-order
forecast. This causes the corresponding stock's forecast to trend downwards in value
over time.
This effect only occurs if you set the *stock* option to
true when calling the :js:func:`hack` function. The chance that hacking a
server will cause this effect is based on what percentage of the
server's total money you steal.
A single hack will have a minor
effect, but continuously hacking a server for lots of money over time
will have a noticeable effect in making the stock's forecast trend downwards.
Growing
If a server has a corresponding stock (e.g. *foodnstuff* server -> FoodNStuff
stock), then growing that server's money can increase the stock's
second-order forecast. This causes the corresponding stock's
forecast to trend upwards in value over time.
This effect only occurs if you set the *stock* option to true when calling the
:js:func:`grow` function. The chance that growing a server will cause this
effect is based on what percentage of the server's total money to add to it.
A single grow operation will have a minor effect, but continuously growing
a server for lots of money over time will have a noticeable effect in making
the stock's forecast trend upwards.
Working for a Company
If a company has a corresponding stock, then working for that company will
increase the corresponding stock's second-order forecast. This will
cause the stock's forecast to (slowly) trend upwards in value
over time.
The potency of this effect is based on how "effective" you are when you work
(i.e. its based on your stats and multipliers).
Automating the Stock Market
---------------------------
@ -159,7 +192,8 @@ are hidden, although some of them can be made visible by purchasing the
Four Sigma (4S) Market Data upgrade. Some examples of these properties are:
* Volatility
* Likelihood of increasing or decreasing
* Likelihood of increasing or decreasing (i.e. the stock's forecast)
* Likelihood of forecast increasing or decreasing (i.e. the stock's second-order forecast)
* How easily a stock's price/forecast is influenced by transactions
* Spread percentage
* Maximum price (not a real maximum, more of a "soft cap")

@ -3,6 +3,21 @@
Changelog
=========
v0.47.1 - 6/27/2019
-------------------
* Stock Market changes:
* Transactions no longer influence stock prices (but they still influence forecast)
* Changed the way stocks behave, particularly with regard to how the stock forecast occasionally "flips"
* Hacking & growing a server can potentially affect the way the corresponding stock's forecast changes
* Working for a company positively affects the way the corresponding stock's forecast changes
* Scripts now start/stop instantly
* Improved performance when starting up many copies of a new NetscriptJS script (by Ornedan)
* Improved performance when killing scripts
* Dialog boxes can now be closed with the ESC key (by jaguilar)
* NetscriptJS scripts should now be "re-compiled" if their dependencies change (by jaguilar)
* write() function should now properly cause NetscriptJS scripts to "re-compile" (by jaguilar)
v0.47.0 - 5/17/2019
-------------------
* Stock Market changes:

@ -8,6 +8,8 @@ grow() Netscript Function
* threads (*number*) - Number of threads to use for this function.
Must be less than or equal to the number of threads the script is running with.
* stock (*boolean*) - If true, the function can affect the stock market. See
:ref:`gameplay_stock_market_player_actions_influencing_stock`
:returns: The number by which the money on the server was multiplied for the growth
:RAM cost: 0.15 GB

@ -8,6 +8,8 @@ hack() Netscript Function
* threads (*number*) - Number of threads to use for this function.
Must be less than or equal to the number of threads the script is running with.
* stock (*boolean*) - If true, the function can affect the stock market. See
:ref:`gameplay_stock_market_player_actions_influencing_stock`
:returns: The amount of money stolen if the hack is successful, and zero otherwise
:RAM cost: 0.1 GB

@ -16,8 +16,6 @@ You can use the Singularity Functions in other BitNodes if and only if you have
Source-File 4 will open up additional Singularity Functions that you can use in other BitNodes. If your Source-File 4 is upgraded all the way to
level 3, then you will be able to access all of the Singularity Functions.
Note that Singularity Functions require twice as much RAM outside of BitNode-4
.. toctree::
:caption: Functions:

@ -1,7 +1,7 @@
getStockPurchaseCost() Netscript Function
=========================================
.. js:function:: getStockPurchaseCost(sym, shares, posType)
.. js:function:: getStockPurchaseCost(sym, shares, posType)
:param string sym: Stock symbol
:param number shares: Number of shares to purchase
@ -10,6 +10,5 @@ getStockPurchaseCost() Netscript Function
:RAM cost: 2 GB
Calculates and returns how much it would cost to buy a given number of
shares of a stock. This takes into account :ref:`spread <gameplay_stock_market_spread>`,
:ref:`large transactions influencing the price of the stock <gameplay_stock_market_spread_price_movement>`
shares of a stock. This takes into account :ref:`spread <gameplay_stock_market_spread>`
and commission fees.

@ -1,15 +1,14 @@
getStockSaleGain() Netscript Function
=====================================
.. js:function:: getStockSaleGain(sym, shares, posType)
.. js:function:: getStockSaleGain(sym, shares, posType)
:param string sym: Stock symbol
:param number shares: Number of shares to purchase
:param number shares: Number of shares to sell
:param string posType: Specifies whether the order is a "Long" or "Short" position.
The values "L" or "S" can also be used.
:RAM cost: 2 GB
Calculates and returns how much you would gain from selling a given number of
shares of a stock. This takes into account :ref:`spread <gameplay_stock_market_spread>`,
:ref:`large transactions influencing the price of the stock <gameplay_stock_market_spread_price_movement>`
shares of a stock. This takes into account :ref:`spread <gameplay_stock_market_spread>`
and commission fees.

14
package-lock.json generated

@ -1,6 +1,6 @@
{
"name": "bitburner",
"version": "0.46.2",
"version": "0.47.0",
"lockfileVersion": 1,
"requires": true,
"dependencies": {
@ -361,6 +361,18 @@
"integrity": "sha512-LAQ1d4OPfSJ/BMbI2DuizmYrrkD9JMaTdi2hQTlI53lQ4kRQPyZQRS4CYQ7O66bnBBnP/oYdRxbk++X0xuFU6A==",
"dev": true
},
"@types/chai": {
"version": "4.1.7",
"resolved": "https://registry.npmjs.org/@types/chai/-/chai-4.1.7.tgz",
"integrity": "sha512-2Y8uPt0/jwjhQ6EiluT0XCri1Dbplr0ZxfFXUz+ye13gaqE8u5gL5ppao1JrUYr9cIip5S6MvQzBS7Kke7U9VA==",
"dev": true
},
"@types/mocha": {
"version": "5.2.7",
"resolved": "https://registry.npmjs.org/@types/mocha/-/mocha-5.2.7.tgz",
"integrity": "sha512-NYrtPht0wGzhwe9+/idPaBB+TqkY9AhTvOLMkThm0IoEfLaiVQZwBwyJ5puCkO3AUCWrmcoePjp2mbFocKy4SQ==",
"dev": true
},
"@types/numeral": {
"version": "0.0.25",
"resolved": "https://registry.npmjs.org/@types/numeral/-/numeral-0.0.25.tgz",

@ -45,6 +45,8 @@
"devDependencies": {
"@babel/core": "^7.3.4",
"@babel/preset-react": "^7.0.0",
"@types/chai": "^4.1.7",
"@types/mocha": "^5.2.7",
"babel-loader": "^8.0.5",
"beautify-lint": "^1.0.3",
"benchmark": "^2.1.1",

@ -1,337 +0,0 @@
// TODO - Convert this to React
import { workerScripts, killWorkerScript } from "./NetscriptWorker";
import { Player } from "./Player";
import { getServer } from "./Server/ServerHelpers";
import { Page, routing } from "./ui/navigationTracking";
import { numeralWrapper } from "./ui/numeralFormat";
import { dialogBoxCreate } from "../utils/DialogBox";
import { logBoxCreate } from "../utils/LogBox";
import { convertTimeMsToTimeElapsedString } from "../utils/StringHelperFunctions";
import { arrayToString } from "../utils/helpers/arrayToString";
import { createProgressBarText } from "../utils/helpers/createProgressBarText";
import { exceptionAlert } from "../utils/helpers/exceptionAlert";
import { roundToTwo } from "../utils/helpers/roundToTwo";
import { createAccordionElement } from "../utils/uiHelpers/createAccordionElement";
import { createElement } from "../utils/uiHelpers/createElement";
import { getElementById } from "../utils/uiHelpers/getElementById";
import { removeChildrenFromElement } from "../utils/uiHelpers/removeChildrenFromElement";
import { removeElement } from "../utils/uiHelpers/removeElement";
/**
* {
* serverName: {
* header: Server Header Element
* panel: Server Panel List (ul) element
* scripts: {
* script id: Ref to Script information
* }
* }
* ...
*/
const ActiveScriptsUI = {};
const ActiveScriptsTasks = []; // Sequentially schedule the creation/deletion of UI elements
const getHeaderHtml = (server) => {
// TODO: calculate the longest hostname length rather than hard coding it
const longestHostnameLength = 18;
const paddedName = `${server.hostname}${" ".repeat(longestHostnameLength)}`.slice(0, Math.max(server.hostname.length, longestHostnameLength));
const barOptions = {
progress: server.ramUsed / server.maxRam,
totalTicks: 30
};
return `${paddedName} ${createProgressBarText(barOptions)}`.replace(/\s/g, '&nbsp;');
};
const updateHeaderHtml = (server) => {
const accordion = ActiveScriptsUI[server.hostname];
if (accordion === null || accordion === undefined) {
return;
}
// Convert it to a string, as that's how it's stored it will come out of the data attributes
const ramPercentage = '' + roundToTwo(server.ramUsed / server.maxRam);
if (accordion.header.dataset.ramPercentage !== ramPercentage) {
accordion.header.dataset.ramPercentage = ramPercentage;
accordion.header.innerHTML = getHeaderHtml(server);
}
}
function createActiveScriptsServerPanel(server) {
let hostname = server.hostname;
var activeScriptsList = document.getElementById("active-scripts-list");
let res = createAccordionElement({
hdrText: getHeaderHtml(server)
});
let li = res[0];
var hdr = res[1];
let panel = res[2];
if (ActiveScriptsUI[hostname] != null) {
console.log("WARNING: Tried to create already-existing Active Scripts Server panel. This is most likely fine. It probably means many scripts just got started up on a new server. Aborting");
return;
}
var panelScriptList = createElement("ul");
panel.appendChild(panelScriptList);
activeScriptsList.appendChild(li);
ActiveScriptsUI[hostname] = {
header: hdr,
panel: panel,
panelList: panelScriptList,
scripts: {}, // Holds references to li elements for each active script
scriptHdrs: {}, // Holds references to header elements for each active script
scriptStats: {}, // Holds references to the p elements containing text for each active script
};
return li;
}
/**
* Deletes the info for a particular server (Dropdown header + Panel with all info)
* in the Active Scripts page if it exists
*/
function deleteActiveScriptsServerPanel(server) {
let hostname = server.hostname;
if (ActiveScriptsUI[hostname] == null) {
console.log("WARNING: Tried to delete non-existent Active Scripts Server panel. Aborting");
return;
}
// Make sure it's empty
if (Object.keys(ActiveScriptsUI[hostname].scripts).length > 0) {
console.warn("Tried to delete Active Scripts Server panel that still has scripts. Aborting");
return;
}
removeElement(ActiveScriptsUI[hostname].panel);
removeElement(ActiveScriptsUI[hostname].header);
delete ActiveScriptsUI[hostname];
}
function addActiveScriptsItem(workerscript) {
var server = getServer(workerscript.serverIp);
if (server == null) {
console.warn("Invalid server IP for workerscript in addActiveScriptsItem()");
return;
}
let hostname = server.hostname;
ActiveScriptsTasks.push(function(workerscript, hostname) {
if (ActiveScriptsUI[hostname] == null) {
createActiveScriptsServerPanel(server);
}
// Create the unique identifier (key) for this script
var itemNameArray = ["active", "scripts", hostname, workerscript.name];
for (var i = 0; i < workerscript.args.length; ++i) {
itemNameArray.push(String(workerscript.args[i]));
}
var itemName = itemNameArray.join("-");
let res = createAccordionElement({hdrText:workerscript.name});
let li = res[0];
let hdr = res[1];
let panel = res[2];
hdr.classList.remove("accordion-header");
hdr.classList.add("active-scripts-script-header");
panel.classList.remove("accordion-panel");
panel.classList.add("active-scripts-script-panel");
/**
* Handle the constant elements on the panel that don't change after creation:
* Threads, args, kill/log button
*/
panel.appendChild(createElement("p", {
innerHTML: "Threads: " + workerscript.scriptRef.threads + "<br>" +
"Args: " + arrayToString(workerscript.args)
}));
var panelText = createElement("p", {
innerText: "Loading...",
fontSize: "14px",
});
panel.appendChild(panelText);
panel.appendChild(createElement("br"));
panel.appendChild(createElement("span", {
innerText: "Log",
class: "accordion-button",
margin: "4px",
padding: "4px",
clickListener: () => {
logBoxCreate(workerscript.scriptRef);
return false;
}
}));
panel.appendChild(createElement("span", {
innerText: "Kill Script",
class: "accordion-button",
margin: "4px",
padding: "4px",
clickListener: () => {
killWorkerScript(workerscript.scriptRef, workerscript.scriptRef.server);
dialogBoxCreate("Killing script, may take a few minutes to complete...");
return false;
}
}));
// Append element to list
ActiveScriptsUI[hostname]["panelList"].appendChild(li);
ActiveScriptsUI[hostname].scripts[itemName] = li;
ActiveScriptsUI[hostname].scriptHdrs[itemName] = hdr;
ActiveScriptsUI[hostname].scriptStats[itemName] = panelText;
}.bind(null, workerscript, hostname));
}
function deleteActiveScriptsItem(workerscript) {
ActiveScriptsTasks.push(function(workerscript) {
var server = getServer(workerscript.serverIp);
if (server == null) {
throw new Error("ERROR: Invalid server IP for workerscript. This most likely occurred because " +
"you tried to delete a large number of scripts and also deleted servers at the " +
"same time. It's not a big deal, just save and refresh the game.");
return;
}
let hostname = server.hostname;
if (ActiveScriptsUI[hostname] == null) {
console.log("ERROR: Trying to delete Active Script UI Element with a hostname that cant be found in ActiveScriptsUI: " + hostname);
return;
}
var itemNameArray = ["active", "scripts", server.hostname, workerscript.name];
for (var i = 0; i < workerscript.args.length; ++i) {
itemNameArray.push(String(workerscript.args[i]));
}
var itemName = itemNameArray.join("-");
let li = ActiveScriptsUI[hostname].scripts[itemName];
if (li == null) {
console.log("ERROR: Cannot find Active Script UI element for workerscript: ");
console.log(workerscript);
return;
}
removeElement(li);
delete ActiveScriptsUI[hostname].scripts[itemName];
delete ActiveScriptsUI[hostname].scriptHdrs[itemName];
delete ActiveScriptsUI[hostname].scriptStats[itemName];
if (Object.keys(ActiveScriptsUI[hostname].scripts).length === 0) {
deleteActiveScriptsServerPanel(server);
}
}.bind(null, workerscript));
}
function updateActiveScriptsItems(maxTasks=150) {
/**
* Run tasks that need to be done sequentially (adding items, creating/deleting server panels)
* We'll limit this to 150 at a time for performance (in case someone decides to start a
* bunch of scripts all at once...)
*/
const numTasks = Math.min(maxTasks, ActiveScriptsTasks.length);
for (let i = 0; i < numTasks; ++i) {
let task = ActiveScriptsTasks.shift();
try {
task();
} catch(e) {
exceptionAlert(e);
console.log(task);
}
}
let total = 0;
for (var i = 0; i < workerScripts.length; ++i) {
try {
total += updateActiveScriptsItemContent(workerScripts[i]);
} catch(e) {
exceptionAlert(e);
}
}
if (!routing.isOn(Page.ActiveScripts)) { return total; }
getElementById("active-scripts-total-production-active").innerText = numeralWrapper.formatMoney(total);
getElementById("active-scripts-total-prod-aug-total").innerText = numeralWrapper.formatMoney(Player.scriptProdSinceLastAug);
getElementById("active-scripts-total-prod-aug-avg").innerText = numeralWrapper.formatMoney(Player.scriptProdSinceLastAug / (Player.playtimeSinceLastAug/1000));
return total;
}
function updateActiveScriptsItemContent(workerscript) {
var server = getServer(workerscript.serverIp);
if (server == null) {
console.log("ERROR: Invalid server IP for workerscript in updateActiveScriptsItemContent().");
return;
}
let hostname = server.hostname;
if (ActiveScriptsUI[hostname] == null) {
return; // Hasn't been created yet. We'll skip it
}
updateHeaderHtml(server);
var itemNameArray = ["active", "scripts", server.hostname, workerscript.name];
for (var i = 0; i < workerscript.args.length; ++i) {
itemNameArray.push(String(workerscript.args[i]));
}
var itemName = itemNameArray.join("-");
if (ActiveScriptsUI[hostname].scriptStats[itemName] == null) {
return; // Hasn't been fully added yet. We'll skip it
}
var item = ActiveScriptsUI[hostname].scriptStats[itemName];
// Update the text if necessary. This fn returns the online $/s production
return updateActiveScriptsText(workerscript, item, itemName);
}
function updateActiveScriptsText(workerscript, item, itemName) {
var server = getServer(workerscript.serverIp);
if (server == null) {
console.log("ERROR: Invalid server IP for workerscript for updateActiveScriptsText()");
return;
}
let hostname = server.hostname;
if (ActiveScriptsUI[hostname] == null || ActiveScriptsUI[hostname].scriptHdrs[itemName] == null) {
console.log("ERROR: Trying to update Active Script UI Element with a hostname that cant be found in ActiveScriptsUI: " + hostname);
return;
}
updateHeaderHtml(server);
var onlineMps = workerscript.scriptRef.onlineMoneyMade / workerscript.scriptRef.onlineRunningTime;
// Only update if the item is visible
if (ActiveScriptsUI[hostname].header.classList.contains("active") === false) {return onlineMps;}
if (ActiveScriptsUI[hostname].scriptHdrs[itemName].classList.contains("active") === false) {return onlineMps;}
removeChildrenFromElement(item);
var onlineTime = "Online Time: " + convertTimeMsToTimeElapsedString(workerscript.scriptRef.onlineRunningTime * 1e3);
var offlineTime = "Offline Time: " + convertTimeMsToTimeElapsedString(workerscript.scriptRef.offlineRunningTime * 1e3);
// Online
var onlineTotalMoneyMade = "Total online production: " + numeralWrapper.formatMoney(workerscript.scriptRef.onlineMoneyMade);
var onlineTotalExpEarned = (Array(26).join(" ") + numeralWrapper.formatBigNumber(workerscript.scriptRef.onlineExpGained) + " hacking exp").replace( / /g, "&nbsp;");
var onlineMpsText = "Online production rate: " + numeralWrapper.formatMoney(onlineMps) + " / second";
var onlineEps = workerscript.scriptRef.onlineExpGained / workerscript.scriptRef.onlineRunningTime;
var onlineEpsText = (Array(25).join(" ") + numeralWrapper.formatBigNumber(onlineEps) + " hacking exp / second").replace( / /g, "&nbsp;");
// Offline
var offlineTotalMoneyMade = "Total offline production: " + numeralWrapper.formatMoney(workerscript.scriptRef.offlineMoneyMade);
var offlineTotalExpEarned = (Array(27).join(" ") + numeralWrapper.formatBigNumber(workerscript.scriptRef.offlineExpGained) + " hacking exp").replace( / /g, "&nbsp;");
var offlineMps = workerscript.scriptRef.offlineMoneyMade / workerscript.scriptRef.offlineRunningTime;
var offlineMpsText = "Offline production rate: " + numeralWrapper.formatMoney(offlineMps) + " / second";
var offlineEps = workerscript.scriptRef.offlineExpGained / workerscript.scriptRef.offlineRunningTime;
var offlineEpsText = (Array(26).join(" ") + numeralWrapper.formatBigNumber(offlineEps) + " hacking exp / second").replace( / /g, "&nbsp;");
item.innerHTML = onlineTime + "<br>" + offlineTime + "<br>" + onlineTotalMoneyMade + "<br>" + onlineTotalExpEarned + "<br>" +
onlineMpsText + "<br>" + onlineEpsText + "<br>" + offlineTotalMoneyMade + "<br>" + offlineTotalExpEarned + "<br>" +
offlineMpsText + "<br>" + offlineEpsText + "<br>";
return onlineMps;
}
export {addActiveScriptsItem, deleteActiveScriptsItem, updateActiveScriptsItems};

@ -1,33 +1,39 @@
import { Augmentation } from "./Augmentation";
import { Augmentations } from "./Augmentations";
import { PlayerOwnedAugmentation } from "./PlayerOwnedAugmentation";
import { AugmentationNames } from "./data/AugmentationNames";
import { Augmentation } from "./Augmentation";
import { Augmentations } from "./Augmentations";
import { PlayerOwnedAugmentation } from "./PlayerOwnedAugmentation";
import { AugmentationNames } from "./data/AugmentationNames";
import { BitNodeMultipliers } from "../BitNode/BitNodeMultipliers";
import { CONSTANTS } from "../Constants";
import { Factions,
factionExists } from "../Faction/Factions";
import { addWorkerScript } from "../NetscriptWorker";
import { Player } from "../Player";
import { prestigeAugmentation } from "../Prestige";
import { saveObject } from "../SaveObject";
import { RunningScript } from "../Script/RunningScript";
import { Script } from "../Script/Script";
import { Server } from "../Server/Server";
import { OwnedAugmentationsOrderSetting } from "../Settings/SettingEnums";
import { Settings } from "../Settings/Settings";
import { AugmentationsRoot } from "./ui/Root";
import { SourceFiles } from "../SourceFile";
import { dialogBoxCreate } from "../../utils/DialogBox";
import { createAccordionElement } from "../../utils/uiHelpers/createAccordionElement";
import { Reviver, Generic_toJSON,
Generic_fromJSON } from "../../utils/JSONReviver";
import { formatNumber } from "../../utils/StringHelperFunctions";
import { clearObject } from "../../utils/helpers/clearObject";
import { createElement } from "../../utils/uiHelpers/createElement";
import { isString } from "../../utils/helpers/isString";
import { removeChildrenFromElement } from "../../utils/uiHelpers/removeChildrenFromElement";
import { BitNodeMultipliers } from "../BitNode/BitNodeMultipliers";
import { CONSTANTS } from "../Constants";
import { Factions, factionExists } from "../Faction/Factions";
import { addWorkerScript } from "../NetscriptWorker";
import { Player } from "../Player";
import { prestigeAugmentation } from "../Prestige";
import { saveObject } from "../SaveObject";
import { RunningScript } from "../Script/RunningScript";
import { Script } from "../Script/Script";
import { Server } from "../Server/Server";
import { OwnedAugmentationsOrderSetting } from "../Settings/SettingEnums";
import { Settings } from "../Settings/Settings";
import { Page, routing } from "../ui/navigationTracking";
import { dialogBoxCreate } from "../../utils/DialogBox";
import { createAccordionElement } from "../../utils/uiHelpers/createAccordionElement";
import {
Reviver,
Generic_toJSON,
Generic_fromJSON
} from "../../utils/JSONReviver";
import { formatNumber } from "../../utils/StringHelperFunctions";
import { clearObject } from "../../utils/helpers/clearObject";
import { createElement } from "../../utils/uiHelpers/createElement";
import { isString } from "../../utils/helpers/isString";
import { removeChildrenFromElement } from "../../utils/uiHelpers/removeChildrenFromElement";
import React from "react";
import ReactDOM from "react-dom";
function AddToAugmentations(aug) {
var name = aug.name;
@ -2041,17 +2047,6 @@ function applyAugmentation(aug, reapply=false) {
}
}
/*
if (aug.name === AugmentationNames.NeuroFluxGovernor) {
for (var i = 0; i < Player.augmentations.length; ++i) {
if (Player.augmentations[i].name == AugmentationNames.NeuroFluxGovernor) {
//Already have this aug, just upgrade the level
return;
}
}
}
*/
// Push onto Player's Augmentation list
if (!reapply) {
var ownedAug = new PlayerOwnedAugmentation(aug.name);
@ -2104,211 +2099,17 @@ function augmentationExists(name) {
return Augmentations.hasOwnProperty(name);
}
function displayAugmentationsContent(contentEl) {
removeChildrenFromElement(contentEl);
contentEl.appendChild(createElement("h1", {
innerText:"Purchased Augmentations",
}));
export function displayAugmentationsContent(contentEl) {
if (!routing.isOn(Page.Augmentations)) { return; }
if (!(contentEl instanceof HTMLElement)) { return; }
contentEl.appendChild(createElement("pre", {
width:"70%", whiteSpace:"pre-wrap", display:"block",
innerText:"Below is a list of all Augmentations you have purchased but not yet installed. Click the button below to install them.\n" +
"WARNING: Installing your Augmentations resets most of your progress, including:\n\n" +
"Stats/Skill levels and Experience\n" +
"Money\n" +
"Scripts on every computer but your home computer\n" +
"Purchased servers\n" +
"Hacknet Nodes\n" +
"Faction/Company reputation\n" +
"Stocks\n" +
"Installing Augmentations lets you start over with the perks and benefits granted by all " +
"of the Augmentations you have ever installed. Also, you will keep any scripts and RAM/Core upgrades " +
"on your home computer (but you will lose all programs besides NUKE.exe)."
}));
//Install Augmentations button
contentEl.appendChild(createElement("a", {
class:"a-link-button", innerText:"Install Augmentations",
tooltip:"'I never asked for this'",
clickListener:()=>{
installAugmentations();
return false;
}
}));
//Backup button
contentEl.appendChild(createElement("a", {
class:"a-link-button flashing-button", innerText:"Backup Save (Export)",
tooltip:"It's always a good idea to backup/export your save!",
clickListener:()=>{
saveObject.exportGame();
return false;
}
}));
//Purchased/queued augmentations list
var queuedAugmentationsList = createElement("ul", {class:"augmentations-list"});
for (var i = 0; i < Player.queuedAugmentations.length; ++i) {
var augName = Player.queuedAugmentations[i].name;
var aug = Augmentations[augName];
var displayName = augName;
if (augName === AugmentationNames.NeuroFluxGovernor) {
displayName += " - Level " + (Player.queuedAugmentations[i].level);
}
var accordion = createAccordionElement({hdrText:displayName, panelText:aug.info});
queuedAugmentationsList.appendChild(accordion[0]);
}
contentEl.appendChild(queuedAugmentationsList);
//Installed augmentations list
contentEl.appendChild(createElement("h1", {
innerText:"Installed Augmentations", marginTop:"8px",
}));
contentEl.appendChild(createElement("p", {
width:"70%", whiteSpace:"pre-wrap",
innerText:"List of all Augmentations (including Source Files) that have been " +
"installed. You have gained the effects of these Augmentations."
}));
var augmentationsList = createElement("ul", {class:"augmentations-list"});
//Expand/Collapse All buttons
contentEl.appendChild(createElement("a", {
class:"a-link-button", fontSize:"14px", innerText:"Expand All", display:"inline-block",
clickListener:()=>{
var allHeaders = augmentationsList.getElementsByClassName("accordion-header");
for (var i = 0; i < allHeaders.length; ++i) {
if (!allHeaders[i].classList.contains("active")) {allHeaders[i].click();}
}
}
}));
contentEl.appendChild(createElement("a", {
class:"a-link-button", fontSize:"14px", innerText:"Collapse All", display:"inline-block",
clickListener:()=>{
var allHeaders = augmentationsList.getElementsByClassName("accordion-header");
for (var i = 0; i < allHeaders.length; ++i) {
if (allHeaders[i].classList.contains("active")) {allHeaders[i].click();}
}
}
}));
//Sort Buttons
const sortInOrderButton = createElement("a", {
class:"a-link-button", fontSize:"14px", innerText:"Sort in Order",
tooltip:"Sorts the Augmentations alphabetically and Source Files in numerical order (1, 2, 3,...)",
clickListener:()=>{
removeChildrenFromElement(augmentationsList);
//Create a copy of Player's Source Files and augs array and sort them
var sourceFiles = Player.sourceFiles.slice();
var augs = Player.augmentations.slice();
sourceFiles.sort((sf1, sf2)=>{
return sf1.n - sf2.n;
});
augs.sort((aug1, aug2)=>{
return aug1.name <= aug2.name ? -1 : 1;
});
displaySourceFiles(augmentationsList, sourceFiles);
displayAugmentations(augmentationsList, augs);
Settings.OwnedAugmentationsOrder = OwnedAugmentationsOrderSetting.Alphabetically;
}
});
contentEl.appendChild(sortInOrderButton);
const sortByAcquirementTimeButton = createElement("a", {
class:"a-link-button", fontSize:"14px", innerText:"Sort by Acquirement Time",
tooltip:"Sorts the Augmentations and Source Files based on when you acquired them (same as default)",
clickListener:()=>{
removeChildrenFromElement(augmentationsList);
displaySourceFiles(augmentationsList, Player.sourceFiles);
displayAugmentations(augmentationsList, Player.augmentations);
Settings.OwnedAugmentationsOrder = OwnedAugmentationsOrderSetting.AcquirementTime;
}
});
contentEl.appendChild(sortByAcquirementTimeButton);
if (Settings.OwnedAugmentationsOrder === OwnedAugmentationsOrderSetting.Alphabetically) {
sortInOrderButton.click();
} else {
sortByAcquirementTimeButton.click();
}
contentEl.appendChild(augmentationsList);
// Display multiplier information at the bottom
contentEl.appendChild(createElement("p", {
display: "block",
innerHTML:
`<br><br><strong><u>Total Multipliers:</u></strong><br>` +
'Hacking Chance multiplier: ' + formatNumber(Player.hacking_chance_mult * 100, 2) + '%<br>' +
'Hacking Speed multiplier: ' + formatNumber(Player.hacking_speed_mult * 100, 2) + '%<br>' +
'Hacking Money multiplier: ' + formatNumber(Player.hacking_money_mult * 100, 2) + '%<br>' +
'Hacking Growth multiplier: ' + formatNumber(Player.hacking_grow_mult * 100, 2) + '%<br><br>' +
'Hacking Level multiplier: ' + formatNumber(Player.hacking_mult * 100, 2) + '%<br>' +
'Hacking Experience multiplier: ' + formatNumber(Player.hacking_exp_mult * 100, 2) + '%<br><br>' +
'Strength Level multiplier: ' + formatNumber(Player.strength_mult * 100, 2) + '%<br>' +
'Strength Experience multiplier: ' + formatNumber(Player.strength_exp_mult * 100, 2) + '%<br><br>' +
'Defense Level multiplier: ' + formatNumber(Player.defense_mult * 100, 2) + '%<br>' +
'Defense Experience multiplier: ' + formatNumber(Player.defense_exp_mult * 100, 2) + '%<br><br>' +
'Dexterity Level multiplier: ' + formatNumber(Player.dexterity_mult * 100, 2) + '%<br>' +
'Dexterity Experience multiplier: ' + formatNumber(Player.dexterity_exp_mult * 100, 2) + '%<br><br>' +
'Agility Level multiplier: ' + formatNumber(Player.agility_mult * 100, 2) + '%<br>' +
'Agility Experience multiplier: ' + formatNumber(Player.agility_exp_mult * 100, 2) + '%<br><br>' +
'Charisma Level multiplier: ' + formatNumber(Player.charisma_mult * 100, 2) + '%<br>' +
'Charisma Experience multiplier: ' + formatNumber(Player.charisma_exp_mult * 100, 2) + '%<br><br>' +
'Hacknet Node production multiplier: ' + formatNumber(Player.hacknet_node_money_mult * 100, 2) + '%<br>' +
'Hacknet Node purchase cost multiplier: ' + formatNumber(Player.hacknet_node_purchase_cost_mult * 100, 2) + '%<br>' +
'Hacknet Node RAM upgrade cost multiplier: ' + formatNumber(Player.hacknet_node_ram_cost_mult * 100, 2) + '%<br>' +
'Hacknet Node Core purchase cost multiplier: ' + formatNumber(Player.hacknet_node_core_cost_mult * 100, 2) + '%<br>' +
'Hacknet Node level upgrade cost multiplier: ' + formatNumber(Player.hacknet_node_level_cost_mult * 100, 2) + '%<br><br>' +
'Company reputation gain multiplier: ' + formatNumber(Player.company_rep_mult * 100, 2) + '%<br>' +
'Faction reputation gain multiplier: ' + formatNumber(Player.faction_rep_mult * 100, 2) + '%<br>' +
'Salary multiplier: ' + formatNumber(Player.work_money_mult * 100, 2) + '%<br>' +
'Crime success multiplier: ' + formatNumber(Player.crime_success_mult * 100, 2) + '%<br>' +
'Crime money multiplier: ' + formatNumber(Player.crime_money_mult * 100, 2) + '%<br><br><br>',
}))
}
//Creates the accordion elements to display Augmentations
// @listElement - List DOM element to append accordion elements to
// @augs - Array of Augmentation objects
function displayAugmentations(listElement, augs) {
for (var i = 0; i < augs.length; ++i) {
var augName = augs[i].name;
var aug = Augmentations[augName];
var displayName = augName;
if (augName === AugmentationNames.NeuroFluxGovernor) {
displayName += " - Level " + (augs[i].level);
}
var accordion = createAccordionElement({hdrText:displayName, panelText:aug.info});
listElement.appendChild(accordion[0]);
}
}
//Creates the accordion elements to display Source Files
// @listElement - List DOM element to append accordion elements to
// @sourceFiles - Array of Source File objects
function displaySourceFiles(listElement, sourceFiles) {
for (var i = 0; i < sourceFiles.length; ++i) {
var srcFileKey = "SourceFile" + sourceFiles[i].n;
var sourceFileObject = SourceFiles[srcFileKey];
if (sourceFileObject == null) {
console.log("ERROR: Invalid source file number: " + sourceFiles[i].n);
continue;
}
const maxLevel = sourceFiles[i].n == 12 ? "∞" : "3";
var accordion = createAccordionElement({
hdrText:sourceFileObject.name + "<br>" + "Level " + (sourceFiles[i].lvl) + " / "+maxLevel,
panelText:sourceFileObject.info
});
listElement.appendChild(accordion[0]);
}
ReactDOM.render(
<AugmentationsRoot
exportGameFn={saveObject.exportGame.bind(saveObject)}
installAugmentationsFn={installAugmentations}
/>,
contentEl
);
}
export function isRepeatableAug(aug) {
@ -2319,6 +2120,9 @@ export function isRepeatableAug(aug) {
return false;
}
export {installAugmentations,
initAugmentations, applyAugmentation, augmentationExists,
displayAugmentationsContent};
export {
installAugmentations,
initAugmentations,
applyAugmentation,
augmentationExists,
};

@ -0,0 +1,42 @@
/**
* React Component for displaying a list of the player's installed Augmentations
* on the Augmentations UI
*/
import * as React from "react";
import { Player } from "../../Player";
import { Augmentations } from "../../Augmentation/Augmentations";
import { AugmentationNames } from "../../Augmentation/data/AugmentationNames";
import { Settings } from "../../Settings/Settings";
import { OwnedAugmentationsOrderSetting } from "../../Settings/SettingEnums";
import { AugmentationAccordion } from "../../ui/React/AugmentationAccordion";
export function InstalledAugmentations(): React.ReactElement {
const sourceAugs = Player.augmentations.slice();
if (Settings.OwnedAugmentationsOrder === OwnedAugmentationsOrderSetting.Alphabetically) {
sourceAugs.sort((aug1, aug2) => {
return aug1.name <= aug2.name ? -1 : 1;
});
}
const augs = sourceAugs.map((e) => {
const aug = Augmentations[e.name];
let level = null;
if (e.name === AugmentationNames.NeuroFluxGovernor) {
level = e.level;
}
return (
<li key={e.name}>
<AugmentationAccordion aug={aug} level={level} />
</li>
)
});
return (
<>{augs}</>
)
}

@ -0,0 +1,107 @@
/**
* React Component for displaying all of the player's installed Augmentations and
* Source-Files.
*
* It also contains 'configuration' buttons that allow you to change how the
* Augs/SF's are displayed
*/
import * as React from "react";
import { InstalledAugmentations } from "./InstalledAugmentations";
import { ListConfiguration } from "./ListConfiguration";
import { OwnedSourceFiles } from "./OwnedSourceFiles";
import { Settings } from "../../Settings/Settings";
import { OwnedAugmentationsOrderSetting } from "../../Settings/SettingEnums";
type IProps = {}
type IState = {
rerenderFlag: boolean;
}
export class InstalledAugmentationsAndSourceFiles extends React.Component<IProps, IState> {
listRef: React.RefObject<HTMLUListElement>;
constructor(props: IProps) {
super(props);
this.state = {
rerenderFlag: false,
}
this.collapseAllHeaders = this.collapseAllHeaders.bind(this);
this.expandAllHeaders = this.expandAllHeaders.bind(this);
this.sortByAcquirementTime = this.sortByAcquirementTime.bind(this);
this.sortInOrder = this.sortInOrder.bind(this);
this.listRef = React.createRef();
}
collapseAllHeaders() {
const ul = this.listRef.current;
if (ul == null) { return; }
const tickers = ul.getElementsByClassName("accordion-header");
for (let i = 0; i < tickers.length; ++i) {
const ticker = tickers[i];
if (!(ticker instanceof HTMLButtonElement)) {
continue;
}
if (ticker.classList.contains("active")) {
ticker.click();
}
}
}
expandAllHeaders() {
const ul = this.listRef.current;
if (ul == null) { return; }
const tickers = ul.getElementsByClassName("accordion-header");
for (let i = 0; i < tickers.length; ++i) {
const ticker = tickers[i];
if (!(ticker instanceof HTMLButtonElement)) {
continue;
}
if (!ticker.classList.contains("active")) {
ticker.click();
}
}
}
rerender() {
this.setState((prevState) => {
return {
rerenderFlag: !prevState.rerenderFlag,
}
});
}
sortByAcquirementTime() {
Settings.OwnedAugmentationsOrder = OwnedAugmentationsOrderSetting.AcquirementTime;
this.rerender();
}
sortInOrder() {
Settings.OwnedAugmentationsOrder = OwnedAugmentationsOrderSetting.Alphabetically
this.rerender();
}
render() {
return (
<>
<ListConfiguration
collapseAllButtonsFn={this.collapseAllHeaders}
expandAllButtonsFn={this.expandAllHeaders}
sortByAcquirementTimeFn={this.sortByAcquirementTime}
sortInOrderFn={this.sortInOrder}
/>
<ul className="augmentations-list" ref={this.listRef}>
<OwnedSourceFiles />
<InstalledAugmentations />
</ul>
</>
)
}
}

@ -0,0 +1,39 @@
/**
* React Component for configuring the way installed augmentations and
* Source-Files are displayed in the Augmentations UI
*/
import * as React from "react";
import { StdButton } from "../../ui/React/StdButton";
type IProps = {
collapseAllButtonsFn: () => void;
expandAllButtonsFn: () => void;
sortByAcquirementTimeFn: () => void;
sortInOrderFn: () => void;
}
export function ListConfiguration(props: IProps): React.ReactElement {
return (
<>
<StdButton
onClick={props.expandAllButtonsFn}
text="Expand All"
/>
<StdButton
onClick={props.collapseAllButtonsFn}
text="Collapse All"
/>
<StdButton
onClick={props.sortInOrderFn}
text="Sort in Order"
tooltip="Sorts the Augmentations alphabetically and Source-Files in numeral order"
/>
<StdButton
onClick={props.sortByAcquirementTimeFn}
text="Sort by Acquirement Time"
tooltip="Sorts the Augmentations and Source-Files based on when you acquired them (same as default)"
/>
</>
)
}

@ -0,0 +1,41 @@
/**
* React Component for displaying a list of the player's Source-Files
* on the Augmentations UI
*/
import * as React from "react";
import { Player } from "../../Player";
import { Settings } from "../../Settings/Settings";
import { OwnedAugmentationsOrderSetting } from "../../Settings/SettingEnums";
import { SourceFiles } from "../../SourceFile/SourceFiles";
import { SourceFileAccordion } from "../../ui/React/SourceFileAccordion";
export function OwnedSourceFiles(): React.ReactElement {
const sourceSfs = Player.sourceFiles.slice();
if (Settings.OwnedAugmentationsOrder === OwnedAugmentationsOrderSetting.Alphabetically) {
sourceSfs.sort((sf1, sf2) => {
return sf1.n - sf2.n;
});
}
const sfs = sourceSfs.map((e) => {
const srcFileKey = "SourceFile" + e.n;
const sfObj = SourceFiles[srcFileKey];
if (sfObj == null) {
console.error(`Invalid source file number: ${e.n}`);
return null;
}
return (
<li key={e.n}>
<SourceFileAccordion level={e.lvl} sf={sfObj} />
</li>
)
});
return (
<>{sfs}</>
);
}

@ -0,0 +1,96 @@
/**
* React component for displaying the player's multipliers on the Augmentation UI page
*/
import * as React from "react";
import { Player } from "../../Player";
import { numeralWrapper } from "../../ui/numeralFormat";
export function PlayerMultipliers(): React.ReactElement {
return (
<>
<p><strong><u>Total Multipliers:</u></strong></p>
<pre>
{'Hacking Chance multiplier: ' + numeralWrapper.formatPercentage(Player.hacking_chance_mult)}
</pre>
<pre>
{'Hacking Speed multiplier: ' + numeralWrapper.formatPercentage(Player.hacking_speed_mult)}
</pre>
<pre>
{'Hacking Money multiplier: ' + numeralWrapper.formatPercentage(Player.hacking_money_mult)}
</pre>
<pre>
{'Hacking Growth multiplier: ' + numeralWrapper.formatPercentage(Player.hacking_grow_mult)}
</pre><br />
<pre>
{'Hacking Level multiplier: ' + numeralWrapper.formatPercentage(Player.hacking_mult)}
</pre>
<pre>
{'Hacking Experience multiplier: ' + numeralWrapper.formatPercentage(Player.hacking_exp_mult)}
</pre>
<br />
<pre>
{'Strength Level multiplier: ' + numeralWrapper.formatPercentage(Player.strength_mult)}
</pre>
<pre>
{'Strength Experience multiplier: ' + numeralWrapper.formatPercentage(Player.strength_exp_mult)}
</pre>
<br />
<pre>
{'Defense Level multiplier: ' + numeralWrapper.formatPercentage(Player.defense_mult)}
</pre>
<pre>
{'Defense Experience multiplier: ' + numeralWrapper.formatPercentage(Player.defense_exp_mult)}
</pre><br />
<pre>
{'Dexterity Level multiplier: ' + numeralWrapper.formatPercentage(Player.dexterity_mult)}
</pre>
<pre>
{'Dexterity Experience multiplier: ' + numeralWrapper.formatPercentage(Player.dexterity_exp_mult)}
</pre><br />
<pre>
{'Agility Level multiplier: ' + numeralWrapper.formatPercentage(Player.agility_mult)}
</pre>
<pre>
{'Agility Experience multiplier: ' + numeralWrapper.formatPercentage(Player.agility_exp_mult)}
</pre><br />
<pre>
{'Charisma Level multiplier: ' + numeralWrapper.formatPercentage(Player.charisma_mult)}
</pre>
<pre>
{'Charisma Experience multiplier: ' + numeralWrapper.formatPercentage(Player.charisma_exp_mult)}
</pre><br />
<pre>
{'Hacknet Node production multiplier: ' + numeralWrapper.formatPercentage(Player.hacknet_node_money_mult)}
</pre>
<pre>
{'Hacknet Node purchase cost multiplier: ' + numeralWrapper.formatPercentage(Player.hacknet_node_purchase_cost_mult)}
</pre>
<pre>
{'Hacknet Node RAM upgrade cost multiplier: ' + numeralWrapper.formatPercentage(Player.hacknet_node_ram_cost_mult)}
</pre>
<pre>
{'Hacknet Node Core purchase cost multiplier: ' + numeralWrapper.formatPercentage(Player.hacknet_node_core_cost_mult)}
</pre>
<pre>
{'Hacknet Node level upgrade cost multiplier: ' + numeralWrapper.formatPercentage(Player.hacknet_node_level_cost_mult)}
</pre><br />
<pre>
{'Company reputation gain multiplier: ' + numeralWrapper.formatPercentage(Player.company_rep_mult)}
</pre>
<pre>
{'Faction reputation gain multiplier: ' + numeralWrapper.formatPercentage(Player.faction_rep_mult)}
</pre>
<pre>
{'Salary multiplier: ' + numeralWrapper.formatPercentage(Player.work_money_mult)}
</pre><br />
<pre>
{'Crime success multiplier: ' + numeralWrapper.formatPercentage(Player.crime_success_mult)}
</pre>
<pre>
{'Crime money multiplier: ' + numeralWrapper.formatPercentage(Player.crime_money_mult)}
</pre>
</>
)
}

@ -0,0 +1,32 @@
/**
* React component for displaying all of the player's purchased (but not installed)
* Augmentations on the Augmentations UI.
*/
import * as React from "react";
import { Augmentations } from "../../Augmentation/Augmentations";
import { AugmentationNames } from "../../Augmentation/data/AugmentationNames";
import { Player } from "../../Player";
import { AugmentationAccordion } from "../../ui/React/AugmentationAccordion";
export function PurchasedAugmentations(): React.ReactElement {
const augs: React.ReactElement[] = [];
for (const ownedAug of Player.queuedAugmentations) {
const aug = Augmentations[ownedAug.name];
let level = null;
if (ownedAug.name === AugmentationNames.NeuroFluxGovernor) {
level = ownedAug.level;
}
augs.push(
<li key={`${ownedAug.name}${ownedAug.level}`}>
<AugmentationAccordion aug={aug} level={level} />
</li>
)
}
return (
<ul className="augmentations-list">{augs}</ul>
)
}

@ -0,0 +1,83 @@
/**
* Root React component for the Augmentations UI page that display all of your
* owned and purchased Augmentations and Source-Files.
*/
import * as React from "react";
import { InstalledAugmentationsAndSourceFiles } from "./InstalledAugmentationsAndSourceFiles";
import { PlayerMultipliers } from "./PlayerMultipliers";
import { PurchasedAugmentations } from "./PurchasedAugmentations";
import { Player } from "../../Player";
import { StdButton } from "../../ui/React/StdButton";
type IProps = {
exportGameFn: () => void;
installAugmentationsFn: () => void;
}
type IState = {
}
export class AugmentationsRoot extends React.Component<IProps, IState> {
constructor(props: IProps) {
super(props);
}
render() {
return (
<div id="augmentations-content">
<h1>Purchased Augmentations</h1>
<p>
Below is a list of all Augmentations you have purchased but not
yet installed. Click the button below to install them.
</p>
<p>
WARNING: Installing your Augmentations resets most of your progress,
including:
</p><br />
<p>- Stats/Skill levels and Experience</p>
<p>- Money</p>
<p>- Scripts on every computer but your home computer</p>
<p>- Purchased servers</p>
<p>- Hacknet Nodes</p>
<p>- Faction/Company reputation</p>
<p>- Stocks</p><br />
<p>
Installing Augmentations lets you start over with the perks and
benefits granted by all of the Augmentations you have ever
installed. Also, you will keep any scripts and RAM/Core upgrades
on your home computer (but you will lose all programs besides
NUKE.exe)
</p>
<StdButton
onClick={this.props.installAugmentationsFn}
text="Install Augmentations"
tooltip="'I never asked for this'"
/>
<StdButton
addClasses="flashing-button"
onClick={this.props.exportGameFn}
text="Backup Save (Export)"
tooltip="It's always a good idea to backup/export your save!"
/>
<PurchasedAugmentations />
<h1>Installed Augmentations</h1>
<p>
{
`List of all Augmentations ${Player.sourceFiles.length > 0 ? "and Source Files " : ""} ` +
`that have been installed. You have gained the effects of these.`
}
</p>
<InstalledAugmentationsAndSourceFiles />
<br /> <br />
<PlayerMultipliers />
</div>
)
}
}

@ -25,235 +25,232 @@ class BitNode {
}
export let BitNodes: IMap<BitNode> = {};
export const BitNodes: IMap<BitNode> = {};
export function initBitNodes() {
BitNodes = {};
BitNodes["BitNode1"] = new BitNode(1, "Source Genesis", "The original BitNode",
"The first BitNode created by the Enders to imprison the minds of humans. It became " +
"the prototype and testing-grounds for all of the BitNodes that followed.<br><br>" +
"This is the first BitNode that you play through. It has no special " +
"modifications or mechanics.<br><br>" +
"Destroying this BitNode will give you Source-File 1, or if you already have " +
"this Source-File it will upgrade its level up to a maximum of 3. This Source-File " +
"lets the player start with 32GB of RAM on his/her home computer when entering a " +
"new BitNode, and also increases all of the player's multipliers by:<br><br>" +
"Level 1: 16%<br>" +
"Level 2: 24%<br>" +
"Level 3: 28%");
BitNodes["BitNode2"] = new BitNode(2, "Rise of the Underworld", "From the shadows, they rose", //Gangs
"From the shadows, they rose.<br><br>Organized crime groups quickly filled the void of power " +
"left behind from the collapse of Western government in the 2050s. As society and civlization broke down, " +
"people quickly succumbed to the innate human impulse of evil and savagery. The organized crime " +
"factions quickly rose to the top of the modern world.<br><br>" +
"In this BitNode:<br><br>" +
"Your hacking level is reduced by 20%<br>" +
"The growth rate and maximum amount of money available on servers are significantly decreased<br>" +
"The amount of money gained from crimes and Infiltration is tripled<br>" +
"Certain Factions (Slum Snakes, Tetrads, The Syndicate, The Dark Army, Speakers for the Dead, " +
"NiteSec, The Black Hand) give the player the ability to form and manage their own gangs. These gangs " +
"will earn the player money and reputation with the corresponding Faction<br>" +
"Every Augmentation in the game will be available through the Factions listed above<br>" +
"For every Faction NOT listed above, reputation gains are halved<br>" +
"You will no longer gain passive reputation with Factions<br><br>" +
"Destroying this BitNode will give you Source-File 2, or if you already have this Source-File it will " +
"upgrade its level up to a maximum of 3. This Source-File allows you to form gangs in other BitNodes " +
"once your karma decreases to a certain value. " +
"It also increases the player's crime success rate, crime money, and charisma multipliers by:<br><br>" +
"Level 1: 24%<br>" +
"Level 2: 36%<br>" +
"Level 3: 42%");
BitNodes["BitNode3"] = new BitNode(3, "Corporatocracy", "The Price of Civilization",
"Our greatest illusion is that a healthy society can revolve around a " +
"single-minded pursuit of wealth.<br><br>" +
"Sometime in the early 21st century economic and political globalization turned " +
"the world into a corporatocracy, and it never looked back. Now, the privileged " +
"elite will happily bankrupt their own countrymen, decimate their own community, " +
"and evict their neighbors from houses in their desperate bid to increase their wealth.<br><br>" +
"In this BitNode you can create and manage your own corporation. Running a successful corporation " +
"has the potential of generating massive profits. All other forms of income are reduced by 75%. Furthermore: <br><br>" +
"The price and reputation cost of all Augmentations is tripled<br>" +
"The starting and maximum amount of money on servers is reduced by 75%<br>" +
"Server growth rate is reduced by 80%<br>" +
"You now only need 75 favour with a faction in order to donate to it, rather than 150<br><br>" +
"Destroying this BitNode will give you Source-File 3, or if you already have this Source-File it will " +
"upgrade its level up to a maximum of 3. This Source-File lets you create corporations on other BitNodes (although " +
"some BitNodes will disable this mechanic). This Source-File also increases your charisma and company salary multipliers by:<br>" +
"Level 1: 8%<br>" +
"Level 2: 12%<br>" +
"Level 3: 14%");
BitNodes["BitNode4"] = new BitNode(4, "The Singularity", "The Man and the Machine",
"The Singularity has arrived. The human race is gone, replaced " +
"by artificially superintelligent beings that are more machine than man. <br><br>" +
"In this BitNode, progressing is significantly harder. Experience gain rates " +
"for all stats are reduced. Most methods of earning money will now give significantly less.<br><br>" +
"In this BitNode you will gain access to a new set of Netscript Functions known as Singularity Functions. " +
"These functions allow you to control most aspects of the game through scripts, including working for factions/companies, " +
"purchasing/installing Augmentations, and creating programs.<br><br>" +
"Destroying this BitNode will give you Source-File 4, or if you already have this Source-File it will " +
"upgrade its level up to a maximum of 3. This Source-File lets you access and use the Singularity " +
"Functions in other BitNodes. Each level of this Source-File will open up more Singularity Functions " +
"that you can use.");
BitNodes["BitNode5"] = new BitNode(5, "Artificial Intelligence", "Posthuman",
"They said it couldn't be done. They said the human brain, " +
"along with its consciousness and intelligence, couldn't be replicated. They said the complexity " +
"of the brain results from unpredictable, nonlinear interactions that couldn't be modeled " +
"by 1's and 0's. They were wrong.<br><br>" +
"In this BitNode:<br><br>" +
"The base security level of servers is doubled<br>" +
"The starting money on servers is halved, but the maximum money remains the same<br>" +
"Most methods of earning money now give significantly less<br>" +
"Infiltration gives 50% more reputation and money<br>" +
"Corporations have 50% lower valuations and are therefore less profitable<br>" +
"Augmentations are more expensive<br>" +
"Hacking experience gain rates are reduced<br><br>" +
"Destroying this BitNode will give you Source-File 5, or if you already have this Source-File it will " +
"upgrade its level up to a maximum of 3. This Source-File grants you a special new stat called Intelligence. " +
"Intelligence is unique because it is permanent and persistent (it never gets reset back to 1). However " +
"gaining Intelligence experience is much slower than other stats, and it is also hidden (you won't know " +
"when you gain experience and how much). Higher Intelligence levels will boost your production for many actions " +
"in the game. <br><br>" +
"In addition, this Source-File will unlock the getBitNodeMultipliers() Netscript function, " +
"and will also raise all of your hacking-related multipliers by:<br><br>" +
"Level 1: 8%<br>" +
"Level 2: 12%<br>" +
"Level 3: 14%");
BitNodes["BitNode6"] = new BitNode(6, "Bladeburners", "Like Tears in Rain",
"In the middle of the 21st century, OmniTek Incorporated began designing and manufacturing advanced synthetic " +
"androids, or Synthoids for short. They achieved a major technological breakthrough in the sixth generation " +
"of their Synthoid design, called MK-VI, by developing a hyperintelligent AI. Many argue that this was " +
"the first sentient AI ever created. This resulted in Synthoid models that were stronger, faster, and more intelligent " +
"than the humans that had created them.<br><br>" +
"In this BitNode you will be able to access the Bladeburner Division at the NSA, which provides a new mechanic " +
"for progression. Furthermore:<br><br>" +
"Hacking and Hacknet Nodes will be less profitable<br>" +
"Your hacking level is reduced by 65%<br>" +
"Hacking experience gain from scripts is reduced by 75%<br>" +
"Corporations have 80% lower valuations and are therefore less profitable<br>" +
"Working for companies is 50% less profitable<br>" +
"Crimes and Infiltration are 25% less profitable<br><br>" +
"Destroying this BitNode will give you Source-File 6, or if you already have this Source-File it will upgrade " +
"its level up to a maximum of 3. This Source-File allows you to access the NSA's Bladeburner Division in other " +
"BitNodes. In addition, this Source-File will raise both the level and experience gain rate of all your combat stats by:<br><br>" +
"Level 1: 8%<br>" +
"Level 2: 12%<br>" +
"Level 3: 14%");
BitNodes["BitNode7"] = new BitNode(7, "Bladeburners 2079", "More human than humans",
"In the middle of the 21st century, you were doing cutting-edge work at OmniTek Incorporated as part of the AI design team " +
"for advanced synthetic androids, or Synthoids for short. You helped achieve a major technological " +
"breakthrough in the sixth generation of the company's Synthoid design, called MK-VI, by developing a hyperintelligent AI. " +
"Many argue that this was the first sentient AI ever created. This resulted in Synthoid models that were stronger, faster, " +
"and more intelligent than the humans that had created them.<br><br>" +
"In this BitNode you will be able to access the Bladeburner API, which allows you to access Bladeburner " +
"functionality through Netscript. Furthermore: <br><br>" +
"The rank you gain from Bladeburner contracts/operations is reduced by 40%<br>" +
"Bladeburner skills cost twice as many skill points<br>" +
"Augmentations are 3x more expensive<br>" +
"Hacking and Hacknet Nodes will be significantly less profitable<br>" +
"Your hacking level is reduced by 65%<br>" +
"Hacking experience gain from scripts is reduced by 75%<br>" +
"Corporations have 80% lower valuations and are therefore less profitable<br>" +
"Working for companies is 50% less profitable<br>" +
"Crimes and Infiltration are 25% less profitable<br><br>" +
"Destroying this BitNode will give you Source-File 7, or if you already have this Source-File it will upgrade " +
"its level up to a maximum of 3. This Source-File allows you to access the Bladeburner Netscript API in other " +
"BitNodes. In addition, this Source-File will increase all of your Bladeburner multipliers by:<br><br>" +
"Level 1: 8%<br>" +
"Level 2: 12%<br>" +
"Level 3: 14%");
BitNodes["BitNode8"] = new BitNode(8, "Ghost of Wall Street", "Money never sleeps",
"You are trying to make a name for yourself as an up-and-coming hedge fund manager on Wall Street.<br><br>" +
"In this BitNode:<br><br>" +
"You start with $250 million<br>" +
"The only way to earn money is by trading on the stock market<br>" +
"You start with a WSE membership and access to the TIX API<br>" +
"You are able to short stocks and place different types of orders (limit/stop)<br>" +
"You can immediately donate to factions to gain reputation<br><br>" +
"Destroying this BitNode will give you Source-File 8, or if you already have this Source-File it will " +
"upgrade its level up to a maximum of 3. This Source-File grants the following benefits:<br><br>" +
"Level 1: Permanent access to WSE and TIX API<br>" +
"Level 2: Ability to short stocks in other BitNodes<br>" +
"Level 3: Ability to use limit/stop orders in other BitNodes<br><br>" +
"This Source-File also increases your hacking growth multipliers by: " +
"<br>Level 1: 12%<br>Level 2: 18%<br>Level 3: 21%");
BitNodes["BitNode9"] = new BitNode(9, "Hacktocracy", "Hacknet Unleashed",
"When Fulcrum Technologies released their open-source Linux distro Chapeau, it quickly " +
"became the OS of choice for the underground hacking community. Chapeau became especially notorious for " +
"powering the Hacknet, a global, decentralized network used for nefarious purposes. Fulcrum quickly " +
"abandoned the project and dissociated themselves from it.<br><br>" +
"This BitNode unlocks the Hacknet Server, an upgraded version of the Hacknet Node. Hacknet Servers generate " +
"hashes, which can be spent on a variety of different upgrades.<br><br>" +
"In this BitNode:<br><br>" +
"Your stats are significantly decreased<br>" +
"You cannnot purchase additional servers<br>" +
"Hacking is significantly less profitable<br><br>" +
"Destroying this BitNode will give you Source-File 9, or if you already have this Source-File it will " +
"upgrade its level up to a maximum of 3. This Source-File grants the following benefits:<br><br>" +
"Level 1: Permanently unlocks the Hacknet Server in other BitNodes<br>" +
"Level 2: You start with 128GB of RAM on your home computer when entering a new BitNode<br>" +
"Level 3: Grants a highly-upgraded Hacknet Server when entering a new BitNode<br><br>" +
"(Note that the Level 3 effect of this Source-File only applies when entering a new BitNode, NOT " +
"when installing Augmentations)");
BitNodes["BitNode10"] = new BitNode(10, "Digital Carbon", "Your body is not who you are",
"In 2084, VitaLife unveiled to the world the Persona Core, a technology that allowed people " +
"to digitize their consciousness. Their consciousness could then be transferred into Synthoids " +
"or other bodies by trasmitting the digitized data. Human bodies became nothing more than 'sleeves' for the " +
"human consciousness. Mankind had finally achieved immortality - at least for those that could afford it.<br><br>" +
"This BitNode unlocks Sleeve technology. Sleeve technology allows you to:<br><br>" +
"1. Re-sleeve: Purchase and transfer your consciousness into a new body<br>" +
"2. Duplicate Sleeves: Duplicate your consciousness into Synthoids, allowing you to perform different tasks synchronously<br><br>" +
"In this BitNode:<br><br>" +
"Your stats are significantly decreased<br>" +
"All methods of gaining money are half as profitable (except Stock Market)<br>" +
"Purchased servers are more expensive, have less max RAM, and a lower maximum limit<br>" +
"Augmentations are 5x as expensive and require twice as much reputation<br><br>" +
"Destroying this BitNode will give you Source-File 10, or if you already have this Source-File it will " +
"upgrade its level up to a maximum of 3. This Source-File unlocks Sleeve technology in other BitNodes. " +
"Each level of this Source-File also grants you a Duplicate Sleeve");
BitNodes["BitNode11"] = new BitNode(11, "The Big Crash", "Okay. Sell it all.",
"The 2050s was defined by the massive amounts of violent civil unrest and anarchic rebellion that rose all around the world. It was this period " +
"of disorder that eventually lead to the governmental reformation of many global superpowers, most notably " +
"the USA and China. But just as the world was slowly beginning to recover from these dark times, financial catastrophe hit.<br><br>" +
"In many countries, the high cost of trying to deal with the civil disorder bankrupted the governments. In all of this chaos and confusion, hackers " +
"were able to steal billions of dollars from the world's largest electronic banks, prompting an international banking crisis as " +
"governments were unable to bail out insolvent banks. Now, the world is slowly crumbling in the middle of the biggest economic crisis of all time.<br><br>" +
"In this BitNode:<br><br>" +
"Your hacking stat and experience gain are halved<br>" +
"The starting and maximum amount of money available on servers is significantly decreased<br>" +
"The growth rate of servers is significantly reduced<br>" +
"Weakening a server is twice as effective<br>" +
"Company wages are decreased by 50%<br>" +
"Corporation valuations are 99% lower and are therefore significantly less profitable<br>" +
"Hacknet Node production is significantly decreased<br>" +
"Crime and Infiltration are more lucrative<br>" +
"Augmentations are twice as expensive<br><br>" +
"Destroying this BitNode will give you Source-File 11, or if you already have this Source-File it will " +
"upgrade its level up to a maximum of 3. This Source-File makes it so that company favor increases BOTH " +
"the player's salary and reputation gain rate at that company by 1% per favor (rather than just the reputation gain). " +
"This Source-File also increases the player's company salary and reputation gain multipliers by:<br><br>" +
"Level 1: 32%<br>" +
"Level 2: 48%<br>" +
"Level 3: 56%");
BitNodes["BitNode12"] = new BitNode(12, "The Recursion", "Repeat.",
"To iterate is human, to recurse divine.<br><br>" +
"Every time this BitNode is destroyed, it becomes slightly harder. Destroying this BitNode will give your Souce-File 12, or " +
"if you already have this Source-File it will upgrade its level. There is no maximum level for Source-File 12. Each level " +
"of Source-File 12 will increase all of your multipliers by 1%. This effect is multiplicative with itself. " +
"In other words, level N of this Source-File will result in a multiplier of 1.01^N (or 0.99^N for multipliers that decrease)");
//Books: Frontera, Shiner
BitNodes["BitNode13"] = new BitNode(13, "fOS", "COMING SOON"); //Unlocks the new game mode and the rest of the BitNodes
BitNodes["BitNode14"] = new BitNode(14, "", "COMING SOON");
BitNodes["BitNode15"] = new BitNode(15, "", "COMING SOON");
BitNodes["BitNode16"] = new BitNode(16, "", "COMING SOON");
BitNodes["BitNode17"] = new BitNode(17, "", "COMING SOON");
BitNodes["BitNode18"] = new BitNode(18, "", "COMING SOON");
BitNodes["BitNode19"] = new BitNode(19, "", "COMING SOON");
BitNodes["BitNode20"] = new BitNode(20, "", "COMING SOON");
BitNodes["BitNode21"] = new BitNode(21, "", "COMING SOON");
BitNodes["BitNode22"] = new BitNode(22, "", "COMING SOON");
BitNodes["BitNode23"] = new BitNode(23, "", "COMING SOON");
BitNodes["BitNode24"] = new BitNode(24, "", "COMING SOON");
}
BitNodes["BitNode1"] = new BitNode(1, "Source Genesis", "The original BitNode",
"The first BitNode created by the Enders to imprison the minds of humans. It became " +
"the prototype and testing-grounds for all of the BitNodes that followed.<br><br>" +
"This is the first BitNode that you play through. It has no special " +
"modifications or mechanics.<br><br>" +
"Destroying this BitNode will give you Source-File 1, or if you already have " +
"this Source-File it will upgrade its level up to a maximum of 3. This Source-File " +
"lets the player start with 32GB of RAM on his/her home computer when entering a " +
"new BitNode, and also increases all of the player's multipliers by:<br><br>" +
"Level 1: 16%<br>" +
"Level 2: 24%<br>" +
"Level 3: 28%");
BitNodes["BitNode2"] = new BitNode(2, "Rise of the Underworld", "From the shadows, they rose", //Gangs
"From the shadows, they rose.<br><br>Organized crime groups quickly filled the void of power " +
"left behind from the collapse of Western government in the 2050s. As society and civlization broke down, " +
"people quickly succumbed to the innate human impulse of evil and savagery. The organized crime " +
"factions quickly rose to the top of the modern world.<br><br>" +
"In this BitNode:<br><br>" +
"Your hacking level is reduced by 20%<br>" +
"The growth rate and maximum amount of money available on servers are significantly decreased<br>" +
"The amount of money gained from crimes and Infiltration is tripled<br>" +
"Certain Factions (Slum Snakes, Tetrads, The Syndicate, The Dark Army, Speakers for the Dead, " +
"NiteSec, The Black Hand) give the player the ability to form and manage their own gangs. These gangs " +
"will earn the player money and reputation with the corresponding Faction<br>" +
"Every Augmentation in the game will be available through the Factions listed above<br>" +
"For every Faction NOT listed above, reputation gains are halved<br>" +
"You will no longer gain passive reputation with Factions<br><br>" +
"Destroying this BitNode will give you Source-File 2, or if you already have this Source-File it will " +
"upgrade its level up to a maximum of 3. This Source-File allows you to form gangs in other BitNodes " +
"once your karma decreases to a certain value. " +
"It also increases the player's crime success rate, crime money, and charisma multipliers by:<br><br>" +
"Level 1: 24%<br>" +
"Level 2: 36%<br>" +
"Level 3: 42%");
BitNodes["BitNode3"] = new BitNode(3, "Corporatocracy", "The Price of Civilization",
"Our greatest illusion is that a healthy society can revolve around a " +
"single-minded pursuit of wealth.<br><br>" +
"Sometime in the early 21st century economic and political globalization turned " +
"the world into a corporatocracy, and it never looked back. Now, the privileged " +
"elite will happily bankrupt their own countrymen, decimate their own community, " +
"and evict their neighbors from houses in their desperate bid to increase their wealth.<br><br>" +
"In this BitNode you can create and manage your own corporation. Running a successful corporation " +
"has the potential of generating massive profits. All other forms of income are reduced by 75%. Furthermore: <br><br>" +
"The price and reputation cost of all Augmentations is tripled<br>" +
"The starting and maximum amount of money on servers is reduced by 75%<br>" +
"Server growth rate is reduced by 80%<br>" +
"You now only need 75 favour with a faction in order to donate to it, rather than 150<br><br>" +
"Destroying this BitNode will give you Source-File 3, or if you already have this Source-File it will " +
"upgrade its level up to a maximum of 3. This Source-File lets you create corporations on other BitNodes (although " +
"some BitNodes will disable this mechanic). This Source-File also increases your charisma and company salary multipliers by:<br>" +
"Level 1: 8%<br>" +
"Level 2: 12%<br>" +
"Level 3: 14%");
BitNodes["BitNode4"] = new BitNode(4, "The Singularity", "The Man and the Machine",
"The Singularity has arrived. The human race is gone, replaced " +
"by artificially superintelligent beings that are more machine than man. <br><br>" +
"In this BitNode, progressing is significantly harder. Experience gain rates " +
"for all stats are reduced. Most methods of earning money will now give significantly less.<br><br>" +
"In this BitNode you will gain access to a new set of Netscript Functions known as Singularity Functions. " +
"These functions allow you to control most aspects of the game through scripts, including working for factions/companies, " +
"purchasing/installing Augmentations, and creating programs.<br><br>" +
"Destroying this BitNode will give you Source-File 4, or if you already have this Source-File it will " +
"upgrade its level up to a maximum of 3. This Source-File lets you access and use the Singularity " +
"Functions in other BitNodes. Each level of this Source-File will open up more Singularity Functions " +
"that you can use.");
BitNodes["BitNode5"] = new BitNode(5, "Artificial Intelligence", "Posthuman",
"They said it couldn't be done. They said the human brain, " +
"along with its consciousness and intelligence, couldn't be replicated. They said the complexity " +
"of the brain results from unpredictable, nonlinear interactions that couldn't be modeled " +
"by 1's and 0's. They were wrong.<br><br>" +
"In this BitNode:<br><br>" +
"The base security level of servers is doubled<br>" +
"The starting money on servers is halved, but the maximum money remains the same<br>" +
"Most methods of earning money now give significantly less<br>" +
"Infiltration gives 50% more reputation and money<br>" +
"Corporations have 50% lower valuations and are therefore less profitable<br>" +
"Augmentations are more expensive<br>" +
"Hacking experience gain rates are reduced<br><br>" +
"Destroying this BitNode will give you Source-File 5, or if you already have this Source-File it will " +
"upgrade its level up to a maximum of 3. This Source-File grants you a special new stat called Intelligence. " +
"Intelligence is unique because it is permanent and persistent (it never gets reset back to 1). However " +
"gaining Intelligence experience is much slower than other stats, and it is also hidden (you won't know " +
"when you gain experience and how much). Higher Intelligence levels will boost your production for many actions " +
"in the game. <br><br>" +
"In addition, this Source-File will unlock the getBitNodeMultipliers() Netscript function, " +
"and will also raise all of your hacking-related multipliers by:<br><br>" +
"Level 1: 8%<br>" +
"Level 2: 12%<br>" +
"Level 3: 14%");
BitNodes["BitNode6"] = new BitNode(6, "Bladeburners", "Like Tears in Rain",
"In the middle of the 21st century, OmniTek Incorporated began designing and manufacturing advanced synthetic " +
"androids, or Synthoids for short. They achieved a major technological breakthrough in the sixth generation " +
"of their Synthoid design, called MK-VI, by developing a hyperintelligent AI. Many argue that this was " +
"the first sentient AI ever created. This resulted in Synthoid models that were stronger, faster, and more intelligent " +
"than the humans that had created them.<br><br>" +
"In this BitNode you will be able to access the Bladeburner Division at the NSA, which provides a new mechanic " +
"for progression. Furthermore:<br><br>" +
"Hacking and Hacknet Nodes will be less profitable<br>" +
"Your hacking level is reduced by 65%<br>" +
"Hacking experience gain from scripts is reduced by 75%<br>" +
"Corporations have 80% lower valuations and are therefore less profitable<br>" +
"Working for companies is 50% less profitable<br>" +
"Crimes and Infiltration are 25% less profitable<br><br>" +
"Destroying this BitNode will give you Source-File 6, or if you already have this Source-File it will upgrade " +
"its level up to a maximum of 3. This Source-File allows you to access the NSA's Bladeburner Division in other " +
"BitNodes. In addition, this Source-File will raise both the level and experience gain rate of all your combat stats by:<br><br>" +
"Level 1: 8%<br>" +
"Level 2: 12%<br>" +
"Level 3: 14%");
BitNodes["BitNode7"] = new BitNode(7, "Bladeburners 2079", "More human than humans",
"In the middle of the 21st century, you were doing cutting-edge work at OmniTek Incorporated as part of the AI design team " +
"for advanced synthetic androids, or Synthoids for short. You helped achieve a major technological " +
"breakthrough in the sixth generation of the company's Synthoid design, called MK-VI, by developing a hyperintelligent AI. " +
"Many argue that this was the first sentient AI ever created. This resulted in Synthoid models that were stronger, faster, " +
"and more intelligent than the humans that had created them.<br><br>" +
"In this BitNode you will be able to access the Bladeburner API, which allows you to access Bladeburner " +
"functionality through Netscript. Furthermore: <br><br>" +
"The rank you gain from Bladeburner contracts/operations is reduced by 40%<br>" +
"Bladeburner skills cost twice as many skill points<br>" +
"Augmentations are 3x more expensive<br>" +
"Hacking and Hacknet Nodes will be significantly less profitable<br>" +
"Your hacking level is reduced by 65%<br>" +
"Hacking experience gain from scripts is reduced by 75%<br>" +
"Corporations have 80% lower valuations and are therefore less profitable<br>" +
"Working for companies is 50% less profitable<br>" +
"Crimes and Infiltration are 25% less profitable<br><br>" +
"Destroying this BitNode will give you Source-File 7, or if you already have this Source-File it will upgrade " +
"its level up to a maximum of 3. This Source-File allows you to access the Bladeburner Netscript API in other " +
"BitNodes. In addition, this Source-File will increase all of your Bladeburner multipliers by:<br><br>" +
"Level 1: 8%<br>" +
"Level 2: 12%<br>" +
"Level 3: 14%");
BitNodes["BitNode8"] = new BitNode(8, "Ghost of Wall Street", "Money never sleeps",
"You are trying to make a name for yourself as an up-and-coming hedge fund manager on Wall Street.<br><br>" +
"In this BitNode:<br><br>" +
"You start with $250 million<br>" +
"The only way to earn money is by trading on the stock market<br>" +
"You start with a WSE membership and access to the TIX API<br>" +
"You are able to short stocks and place different types of orders (limit/stop)<br>" +
"You can immediately donate to factions to gain reputation<br><br>" +
"Destroying this BitNode will give you Source-File 8, or if you already have this Source-File it will " +
"upgrade its level up to a maximum of 3. This Source-File grants the following benefits:<br><br>" +
"Level 1: Permanent access to WSE and TIX API<br>" +
"Level 2: Ability to short stocks in other BitNodes<br>" +
"Level 3: Ability to use limit/stop orders in other BitNodes<br><br>" +
"This Source-File also increases your hacking growth multipliers by: " +
"<br>Level 1: 12%<br>Level 2: 18%<br>Level 3: 21%");
BitNodes["BitNode9"] = new BitNode(9, "Hacktocracy", "Hacknet Unleashed",
"When Fulcrum Technologies released their open-source Linux distro Chapeau, it quickly " +
"became the OS of choice for the underground hacking community. Chapeau became especially notorious for " +
"powering the Hacknet, a global, decentralized network used for nefarious purposes. Fulcrum quickly " +
"abandoned the project and dissociated themselves from it.<br><br>" +
"This BitNode unlocks the Hacknet Server, an upgraded version of the Hacknet Node. Hacknet Servers generate " +
"hashes, which can be spent on a variety of different upgrades.<br><br>" +
"In this BitNode:<br><br>" +
"Your stats are significantly decreased<br>" +
"You cannnot purchase additional servers<br>" +
"Hacking is significantly less profitable<br><br>" +
"Destroying this BitNode will give you Source-File 9, or if you already have this Source-File it will " +
"upgrade its level up to a maximum of 3. This Source-File grants the following benefits:<br><br>" +
"Level 1: Permanently unlocks the Hacknet Server in other BitNodes<br>" +
"Level 2: You start with 128GB of RAM on your home computer when entering a new BitNode<br>" +
"Level 3: Grants a highly-upgraded Hacknet Server when entering a new BitNode<br><br>" +
"(Note that the Level 3 effect of this Source-File only applies when entering a new BitNode, NOT " +
"when installing Augmentations)");
BitNodes["BitNode10"] = new BitNode(10, "Digital Carbon", "Your body is not who you are",
"In 2084, VitaLife unveiled to the world the Persona Core, a technology that allowed people " +
"to digitize their consciousness. Their consciousness could then be transferred into Synthoids " +
"or other bodies by trasmitting the digitized data. Human bodies became nothing more than 'sleeves' for the " +
"human consciousness. Mankind had finally achieved immortality - at least for those that could afford it.<br><br>" +
"This BitNode unlocks Sleeve technology. Sleeve technology allows you to:<br><br>" +
"1. Re-sleeve: Purchase and transfer your consciousness into a new body<br>" +
"2. Duplicate Sleeves: Duplicate your consciousness into Synthoids, allowing you to perform different tasks synchronously<br><br>" +
"In this BitNode:<br><br>" +
"Your stats are significantly decreased<br>" +
"All methods of gaining money are half as profitable (except Stock Market)<br>" +
"Purchased servers are more expensive, have less max RAM, and a lower maximum limit<br>" +
"Augmentations are 5x as expensive and require twice as much reputation<br><br>" +
"Destroying this BitNode will give you Source-File 10, or if you already have this Source-File it will " +
"upgrade its level up to a maximum of 3. This Source-File unlocks Sleeve technology in other BitNodes. " +
"Each level of this Source-File also grants you a Duplicate Sleeve");
BitNodes["BitNode11"] = new BitNode(11, "The Big Crash", "Okay. Sell it all.",
"The 2050s was defined by the massive amounts of violent civil unrest and anarchic rebellion that rose all around the world. It was this period " +
"of disorder that eventually lead to the governmental reformation of many global superpowers, most notably " +
"the USA and China. But just as the world was slowly beginning to recover from these dark times, financial catastrophe hit.<br><br>" +
"In many countries, the high cost of trying to deal with the civil disorder bankrupted the governments. In all of this chaos and confusion, hackers " +
"were able to steal billions of dollars from the world's largest electronic banks, prompting an international banking crisis as " +
"governments were unable to bail out insolvent banks. Now, the world is slowly crumbling in the middle of the biggest economic crisis of all time.<br><br>" +
"In this BitNode:<br><br>" +
"Your hacking stat and experience gain are halved<br>" +
"The starting and maximum amount of money available on servers is significantly decreased<br>" +
"The growth rate of servers is significantly reduced<br>" +
"Weakening a server is twice as effective<br>" +
"Company wages are decreased by 50%<br>" +
"Corporation valuations are 99% lower and are therefore significantly less profitable<br>" +
"Hacknet Node production is significantly decreased<br>" +
"Crime and Infiltration are more lucrative<br>" +
"Augmentations are twice as expensive<br><br>" +
"Destroying this BitNode will give you Source-File 11, or if you already have this Source-File it will " +
"upgrade its level up to a maximum of 3. This Source-File makes it so that company favor increases BOTH " +
"the player's salary and reputation gain rate at that company by 1% per favor (rather than just the reputation gain). " +
"This Source-File also increases the player's company salary and reputation gain multipliers by:<br><br>" +
"Level 1: 32%<br>" +
"Level 2: 48%<br>" +
"Level 3: 56%");
BitNodes["BitNode12"] = new BitNode(12, "The Recursion", "Repeat.",
"To iterate is human, to recurse divine.<br><br>" +
"Every time this BitNode is destroyed, it becomes slightly harder. Destroying this BitNode will give your Souce-File 12, or " +
"if you already have this Source-File it will upgrade its level. There is no maximum level for Source-File 12. Each level " +
"of Source-File 12 will increase all of your multipliers by 1%. This effect is multiplicative with itself. " +
"In other words, level N of this Source-File will result in a multiplier of 1.01^N (or 0.99^N for multipliers that decrease)");
// Books: Frontera, Shiner
BitNodes["BitNode13"] = new BitNode(13, "fOS", "COMING SOON"); //Unlocks the new game mode and the rest of the BitNodes
BitNodes["BitNode14"] = new BitNode(14, "", "COMING SOON");
BitNodes["BitNode15"] = new BitNode(15, "", "COMING SOON");
BitNodes["BitNode16"] = new BitNode(16, "", "COMING SOON");
BitNodes["BitNode17"] = new BitNode(17, "", "COMING SOON");
BitNodes["BitNode18"] = new BitNode(18, "", "COMING SOON");
BitNodes["BitNode19"] = new BitNode(19, "", "COMING SOON");
BitNodes["BitNode20"] = new BitNode(20, "", "COMING SOON");
BitNodes["BitNode21"] = new BitNode(21, "", "COMING SOON");
BitNodes["BitNode22"] = new BitNode(22, "", "COMING SOON");
BitNodes["BitNode23"] = new BitNode(23, "", "COMING SOON");
BitNodes["BitNode24"] = new BitNode(24, "", "COMING SOON");
export function initBitNodeMultipliers(p: IPlayer) {
if (p.bitNodeN == null) {

@ -6,7 +6,7 @@
import { IMap } from "./types";
export let CONSTANTS: IMap<any> = {
Version: "0.47.0",
Version: "0.47.1",
/** Max level for any skill, assuming no multipliers. Determined by max numerical value in javascript for experience
* and the skill level formula in Player.js. Note that all this means it that when experience hits MAX_INT, then
@ -221,47 +221,18 @@ export let CONSTANTS: IMap<any> = {
LatestUpdate:
`
v0.47.0
v0.47.1
* Stock Market changes:
** Implemented spread. Stock's now have bid and ask prices at which transactions occur
** Large transactions will now influence a stock's price and forecast
** This "influencing" can take effect in the middle of a transaction
** See documentation for more details on these changes
** Added getStockAskPrice(), getStockBidPrice() Netscript functions to the TIX API
** Added getStockPurchaseCost(), getStockSaleGain() Netscript functions to the TIX API
** Transactions no longer influence stock prices (but they still influence forecast)
** Changed the way stocks behave, particularly with regard to how the stock forecast occasionally "flips"
** Hacking & growing a server can potentially affect the way the corresponding stock's forecast changes
** Working for a company positively affects the way the corresponding stock's forecast changes
* Re-sleeves can no longer have the NeuroFlux Governor augmentation
** This is just a temporary patch until the mechanic gets re-worked
* hack(), grow(), and weaken() functions now take optional arguments for number of threads to use (by MasonD)
* codingcontract.attempt() now takes an optional argument that allows you to configure the function to return a contract's reward
* Adjusted RAM costs of Netscript Singularity functions (mostly increased)
* Adjusted RAM cost of codingcontract.getNumTriesRemaining() Netscript function
* Netscript Singularity functions no longer cost extra RAM outside of BitNode-4
* Corporation employees no longer have an "age" stat
* Gang Wanted level gain rate capped at 100 (per employee)
* Script startup/kill is now processed every 3 seconds, instead of 6 seconds
* getHackTime(), getGrowTime(), and getWeakenTime() now return Infinity if called on a Hacknet Server
* Money/Income tracker now displays money lost from hospitalizations
* Exported saves now have a unique filename based on current BitNode and timestamp
* Maximum number of Hacknet Servers decreased from 25 to 20
* Bug Fix: Corporation employees stats should no longer become negative
* Bug Fix: Fixed sleeve.getInformation() throwing error in certain scenarios
* Bug Fix: Coding contracts should no longer generate on the w0r1d_d43m0n server
* Bug Fix: Duplicate Sleeves now properly have access to all Augmentations if you have a gang
* Bug Fix: getAugmentationsFromFaction() & purchaseAugmentation() functions should now work properly if you have a gang
* Bug Fix: Fixed issue that caused messages (.msg) to be sent when refreshing/reloading the game
* Bug Fix: Purchasing hash upgrades for Bladeburner/Corporation when you don't actually have access to those mechanics no longer gives hashes
* Bug Fix: run(), exec(), and spawn() Netscript functions now throw if called with 0 threads
* Bug Fix: Faction UI should now automatically update reputation
* Bug Fix: Fixed purchase4SMarketData()
* Bug Fix: Netscript1.0 now works properly for multiple 'namespace' imports (import * as namespace from "script")
* Bug Fix: Terminal 'wget' command now correctly evaluates directory paths
* Bug Fix: wget(), write(), and scp() Netscript functions now fail if an invalid filepath is passed in
* Bug Fix: Having Corporation warehouses at full capacity should no longer freeze game in certain conditions
* Bug Fix: Prevented an exploit that allows you to buy multiple copies of an Augmentation by holding the 'Enter' button
* Bug Fix: gang.getOtherGangInformation() now properly returns a deep copy
* Bug Fix: Fixed getScriptIncome() returning an undefined value
* Bug Fix: Fixed an issue with Hacknet Server hash rate not always updating
* Scripts now start/stop instantly
* Improved performance when starting up many copies of a new NetscriptJS script (by Ornedan)
* Improved performance when killing scripts
* Dialog boxes can now be closed with the ESC key (by jaguilar)
* NetscriptJS scripts should now be "re-compiled" if their dependencies change (by jaguilar)
* write() function should now properly cause NetscriptJS scripts to "re-compile" (by jaguilar)
`
}

@ -0,0 +1,4 @@
/**
* Utility functions for calculating the maximum number of Hacknet upgrades the player
* can purchase for a Node with his/her current money
*/

@ -1,3 +1,13 @@
/**
* Generic helper/utility functions for the Hacknet mechanic:
* - Purchase nodes/upgrades
* - Calculating maximum number of upgrades
* - Processing Hacknet earnings
* - Updating Hash Manager capacity
* - Purchasing hash upgrades
*
* TODO Should probably split the different types of functions into their own modules
*/
import {
HacknetNode,
BaseCostForHacknetNode,
@ -26,7 +36,7 @@ import {
ITutorial
} from "../InteractiveTutorial";
import { Player } from "../Player";
import { AddToAllServers, AllServers } from "../Server/AllServers";
import { AllServers } from "../Server/AllServers";
import { GetServerByHostname } from "../Server/ServerHelpers";
import { SourceFileFlags } from "../SourceFile/SourceFileFlags";
import { Page, routing } from "../ui/navigationTracking";
@ -115,7 +125,7 @@ export function getCostOfNextHacknetServer() {
return BaseCostForHacknetServer * Math.pow(mult, numOwned) * Player.hacknet_node_purchase_cost_mult;
}
//Calculate the maximum number of times the Player can afford to upgrade a Hacknet Node
// Calculate the maximum number of times the Player can afford to upgrade a Hacknet Node's level
export function getMaxNumberLevelUpgrades(nodeObj, maxLevel) {
if (maxLevel == null) {
throw new Error(`getMaxNumberLevelUpgrades() called without maxLevel arg`);
@ -149,6 +159,7 @@ export function getMaxNumberLevelUpgrades(nodeObj, maxLevel) {
return 0;
}
// Calculate the maximum number of times the Player can afford to upgrade a Hacknet Node's RAM
export function getMaxNumberRamUpgrades(nodeObj, maxLevel) {
if (maxLevel == null) {
throw new Error(`getMaxNumberRamUpgrades() called without maxLevel arg`);
@ -177,6 +188,7 @@ export function getMaxNumberRamUpgrades(nodeObj, maxLevel) {
return 0;
}
// Calculate the maximum number of times the Player can afford to upgrade a Hacknet Node's cores
export function getMaxNumberCoreUpgrades(nodeObj, maxLevel) {
if (maxLevel == null) {
throw new Error(`getMaxNumberCoreUpgrades() called without maxLevel arg`);
@ -193,7 +205,7 @@ export function getMaxNumberCoreUpgrades(nodeObj, maxLevel) {
return levelsToMax;
}
//Use a binary search to find the max possible number of upgrades
// Use a binary search to find the max possible number of upgrades
while (min <= max) {
let curr = (min + max) / 2 | 0;
if (curr != maxLevel &&
@ -212,6 +224,7 @@ export function getMaxNumberCoreUpgrades(nodeObj, maxLevel) {
return 0;
}
// Calculate the maximum number of times the Player can afford to upgrade a Hacknet Node's cache
export function getMaxNumberCacheUpgrades(nodeObj, maxLevel) {
if (maxLevel == null) {
throw new Error(`getMaxNumberCacheUpgrades() called without maxLevel arg`);

@ -176,33 +176,28 @@ export class HacknetServer extends BaseServer implements IHacknetNode {
return totalCost;
}
// Process this Hacknet Server in the game loop.
// Returns the number of hashes generated
// Process this Hacknet Server in the game loop. Returns the number of hashes generated
process(numCycles: number=1): number {
const seconds = numCycles * CONSTANTS.MilliPerCycle / 1000;
return this.hashRate * seconds;
}
// Returns a boolean indicating whether the cache was successfully upgraded
upgradeCache(levels: number): void {
this.cache = Math.min(HacknetServerMaxCache, Math.round(this.cache + levels));
this.updateHashCapacity();
}
// Returns a boolean indicating whether the number of cores was successfully upgraded
upgradeCore(levels: number, prodMult: number): void {
this.cores = Math.min(HacknetServerMaxCores, Math.round(this.cores + levels));
this.updateHashRate(prodMult);
}
// Returns a boolean indicating whether the level was successfully upgraded
upgradeLevel(levels: number, prodMult: number): void {
this.level = Math.min(HacknetServerMaxLevel, Math.round(this.level + levels));
this.updateHashRate(prodMult);
}
// Returns a boolean indicating whether the RAM was successfully upgraded
upgradeRam(levels: number, prodMult: number): boolean {
for (let i = 0; i < levels; ++i) {
this.maxRam *= 2;
@ -212,10 +207,8 @@ export class HacknetServer extends BaseServer implements IHacknetNode {
return true;
}
/**
* Whenever a script is run, we must update this server's hash rate
*/
// Whenever a script is run, we must update this server's hash rate
runScript(script: RunningScript, prodMult?: number): void {
super.runScript(script);
if (prodMult != null && typeof prodMult === "number") {

@ -40,13 +40,15 @@ import { createPopupCloseButton } from "../../utils/uiHelpers/createPopupCloseBu
import { removeElementById } from "../../utils/uiHelpers/removeElementById";
/**
* Create a pop-up box that lets the player confirm traveling to a different city
* If settings are configured to suppress this popup, just instantly travel
* Create a pop-up box that lets the player confirm traveling to a different city.
* If settings are configured to suppress this popup, just instantly travel.
* The actual "Travel" implementation is implemented in the UI, and is passed in
* as an argument
* as an argument.
* @param {CityName} destination - City that the player is traveling to
* @param {Function} travelFn - Function that changes the player's state for traveling
*/
type TravelFunction = (to: CityName) => void;
export function createTravelPopup(destination: CityName, travelFn: TravelFunction) {
export function createTravelPopup(destination: CityName, travelFn: TravelFunction): void {
const cost = CONSTANTS.TravelCost;
if (Settings.SuppressTravelConfirmation) {
@ -80,10 +82,10 @@ export function createTravelPopup(destination: CityName, travelFn: TravelFunctio
/**
* Create a pop-up box that lets the player purchase a server.
* @param ram - Amount of RAM (GB) on server
* @param p - Player object
* @param {number} ram - Amount of RAM (GB) on server
* @param {IPlayer} p - Player object
*/
export function createPurchaseServerPopup(ram: number, p: IPlayer) {
export function createPurchaseServerPopup(ram: number, p: IPlayer): void {
const cost = getPurchaseServerCost(ram);
if (cost === Infinity) {
dialogBoxCreate("Something went wrong when trying to purchase this server. Please contact developer");
@ -111,6 +113,7 @@ export function createPurchaseServerPopup(ram: number, p: IPlayer) {
/**
* Create a popup that lets the player start a Corporation
* @param {IPlayer} p - Player object
*/
export function createStartCorporationPopup(p: IPlayer) {
if (!p.canAccessCorporation() || p.hasCorporation()) { return; }
@ -172,8 +175,10 @@ export function createStartCorporationPopup(p: IPlayer) {
if (worldHeader instanceof HTMLElement) {
worldHeader.click(); worldHeader.click();
}
dialogBoxCreate("Congratulations! You just started your own corporation with government seed money. " +
"You can visit and manage your company in the City");
dialogBoxCreate(
"Congratulations! You just started your own corporation with government seed money. " +
"You can visit and manage your company in the City"
);
removeElementById(popupId);
return false;
}
@ -187,21 +192,23 @@ export function createStartCorporationPopup(p: IPlayer) {
/**
* Create a popup that lets the player upgrade the cores on his/her home computer
* @param p - Player object
* @param {IPlayer} p - Player object
*/
export function createUpgradeHomeCoresPopup(p: IPlayer) {
const currentCores = p.getHomeComputer().cpuCores;
if (currentCores >= 8) { return; } // Max of 8 cores
//Cost of purchasing another cost is found by indexing this array with number of current cores
const allCosts = [0,
10e9, // 1->2 Cores - 10 bn
250e9, // 2->3 Cores - 250 bn
5e12, // 3->4 Cores - 5 trillion
100e12, // 4->5 Cores - 100 trillion
1e15, // 5->6 Cores - 1 quadrillion
20e15, // 6->7 Cores - 20 quadrillion
200e15]; // 7->8 Cores - 200 quadrillion
// Cost of purchasing another cost is found by indexing this array with number of current cores
const allCosts = [
0,
10e9,
250e9,
5e12,
100e12,
1e15,
20e15,
200e15
];
const cost: number = allCosts[currentCores];
const yesBtn = yesNoBoxGetYesButton();
@ -215,8 +222,10 @@ export function createUpgradeHomeCoresPopup(p: IPlayer) {
} else {
p.loseMoney(cost);
p.getHomeComputer().cpuCores++;
dialogBoxCreate("You purchased an additional CPU Core for your home computer! It now has " +
p.getHomeComputer().cpuCores + " cores.");
dialogBoxCreate(
"You purchased an additional CPU Core for your home computer! It now has " +
p.getHomeComputer().cpuCores + " cores."
);
}
yesNoBoxClose();
});
@ -226,15 +235,17 @@ export function createUpgradeHomeCoresPopup(p: IPlayer) {
yesNoBoxClose();
});
yesNoBoxCreate("Would you like to purchase an additional CPU Core for your home computer? Each CPU Core " +
"lets you start with an additional Core Node in Hacking Missions.<br><br>" +
"Purchasing an additional core (for a total of " + (p.getHomeComputer().cpuCores + 1) + ") will " +
"cost " + numeralWrapper.formatMoney(cost));
yesNoBoxCreate(
"Would you like to purchase an additional CPU Core for your home computer? Each CPU Core " +
"lets you start with an additional Core Node in Hacking Missions.<br><br>" +
"Purchasing an additional core (for a total of " + (p.getHomeComputer().cpuCores + 1) + ") will " +
"cost " + numeralWrapper.formatMoney(cost)
);
}
/**
* Create a popup that lets the player upgrade the RAM on his/her home computer
* @param p - Player object
* @param {IPlayer} p - Player object
*/
export function createUpgradeHomeRamPopup(p: IPlayer) {
const cost: number = p.getUpgradeHomeRamCost();
@ -255,15 +266,17 @@ export function createUpgradeHomeRamPopup(p: IPlayer) {
yesNoBoxClose();
});
yesNoBoxCreate("Would you like to purchase additional RAM for your home computer? <br><br>" +
"This will upgrade your RAM from " + ram + "GB to " + ram*2 + "GB. <br><br>" +
"This will cost " + numeralWrapper.format(cost, '$0.000a'));
yesNoBoxCreate(
"Would you like to purchase additional RAM for your home computer? <br><br>" +
"This will upgrade your RAM from " + ram + "GB to " + ram*2 + "GB. <br><br>" +
"This will cost " + numeralWrapper.format(cost, '$0.000a')
);
}
/**
* Attempt to purchase a TOR router
* @param p - Player object
* @param {IPlayer} p - Player object
*/
export function purchaseTorRouter(p: IPlayer) {
if (p.hasTorRouter()) {
@ -285,7 +298,9 @@ export function purchaseTorRouter(p: IPlayer) {
p.getHomeComputer().serversOnNetwork.push(darkweb.ip);
darkweb.serversOnNetwork.push(p.getHomeComputer().ip);
dialogBoxCreate("You have purchased a Tor router!<br>" +
"You now have access to the dark web from your home computer<br>" +
"Use the scan/scan-analyze commands to search for the dark web connection.");
dialogBoxCreate(
"You have purchased a Tor router!<br>" +
"You now have access to the dark web from your home computer<br>" +
"Use the scan/scan-analyze commands to search for the dark web connection."
);
}

@ -32,6 +32,11 @@ export class WorkerScript {
*/
delay: number | null = null;
/**
* Holds the Promise resolve() function for when the script is "blocked" by an async op
*/
delayResolve?: () => void;
/**
* Stores names of all functions that have logging disabled
*/
@ -75,6 +80,12 @@ export class WorkerScript {
*/
output: string = "";
/**
* Process ID. Must be an integer. Used for efficient script
* killing and removal.
*/
pid: number;
/**
* Script's Static RAM usage. Equivalent to underlying script's RAM usage
*/
@ -95,10 +106,17 @@ export class WorkerScript {
*/
serverIp: string;
constructor(runningScriptObj: RunningScript, nsFuncsGenerator?: (ws: WorkerScript) => object) {
constructor(runningScriptObj: RunningScript, pid: number, nsFuncsGenerator?: (ws: WorkerScript) => object) {
this.name = runningScriptObj.filename;
this.serverIp = runningScriptObj.server;
const sanitizedPid = Math.round(pid);
if (typeof sanitizedPid !== "number" || isNaN(sanitizedPid)) {
throw new Error(`Invalid PID when constructing WorkerScript: ${pid}`);
}
this.pid = sanitizedPid;
runningScriptObj.pid = sanitizedPid;
// Get the underlying script's code
const server = AllServers[this.serverIp];
if (server == null) {

@ -0,0 +1,6 @@
/**
* Event emitter that triggers when scripts are started/stopped
*/
import { EventEmitter } from "../utils/EventEmitter";
export const WorkerScriptStartStopEventEmitter = new EventEmitter();

@ -0,0 +1,6 @@
/**
* Global pool of all active scripts (scripts that are currently running)
*/
import { WorkerScript } from "./WorkerScript";
export const workerScripts: Map<number, WorkerScript> = new Map();

@ -0,0 +1,136 @@
/**
* Stops an actively-running script (represented by a WorkerScript object)
* and removes it from the global pool of active scripts.
*/
import { WorkerScript } from "./WorkerScript";
import { workerScripts } from "./WorkerScripts";
import { WorkerScriptStartStopEventEmitter } from "./WorkerScriptStartStopEventEmitter";
import { RunningScript } from "../Script/RunningScript";
import { AllServers } from "../Server/AllServers";
import { compareArrays } from "../../utils/helpers/compareArrays";
import { roundToTwo } from "../../utils/helpers/roundToTwo";
export function killWorkerScript(runningScriptObj: RunningScript, serverIp: string, rerenderUi: boolean): boolean;
export function killWorkerScript(workerScript: WorkerScript): boolean;
export function killWorkerScript(pid: number): boolean;
export function killWorkerScript(script: RunningScript | WorkerScript | number, serverIp?: string, rerenderUi?: boolean): boolean {
if (rerenderUi == null || typeof rerenderUi !== "boolean") {
rerenderUi = true;
}
if (script instanceof WorkerScript) {
stopAndCleanUpWorkerScript(script);
return true;
} else if (script instanceof RunningScript && typeof serverIp === "string") {
// Try to kill by PID
const res = killWorkerScriptByPid(script.pid, rerenderUi);
if (res) { return res; }
// If for some reason that doesn't work, we'll try the old way
for (const ws of workerScripts.values()) {
if (ws.name == script.filename && ws.serverIp == serverIp &&
compareArrays(ws.args, script.args)) {
stopAndCleanUpWorkerScript(ws, rerenderUi);
return true;
}
}
return false;
} else if (typeof script === "number") {
return killWorkerScriptByPid(script, rerenderUi);
} else {
console.error(`killWorkerScript() called with invalid argument:`);
console.error(script);
return false;
}
}
function killWorkerScriptByPid(pid: number, rerenderUi: boolean=true): boolean {
const ws = workerScripts.get(pid);
if (ws instanceof WorkerScript) {
stopAndCleanUpWorkerScript(ws, rerenderUi);
return true;
}
return false;
}
function stopAndCleanUpWorkerScript(workerScript: WorkerScript, rerenderUi: boolean=true): void {
workerScript.env.stopFlag = true;
killNetscriptDelay(workerScript);
removeWorkerScript(workerScript, rerenderUi);
}
/**
* Helper function that removes the script being killed from the global pool.
* Also handles other cleanup-time operations
*
* @param {WorkerScript | number} - Identifier for WorkerScript. Either the object itself, or
* its index in the global workerScripts array
*/
function removeWorkerScript(workerScript: WorkerScript, rerenderUi: boolean=true): void {
if (workerScript instanceof WorkerScript) {
const ip = workerScript.serverIp;
const name = workerScript.name;
// Get the server on which the script runs
const server = AllServers[ip];
if (server == null) {
console.error(`Could not find server on which this script is running: ${ip}`);
return;
}
// Recalculate ram used on that server
server.ramUsed = roundToTwo(server.ramUsed - workerScript.ramUsage);
if (server.ramUsed < 0) {
console.warn(`Server RAM usage went negative (if it's due to floating pt imprecision, it's okay): ${server.ramUsed}`);
server.ramUsed = 0;
}
// Delete the RunningScript object from that server
for (let i = 0; i < server.runningScripts.length; ++i) {
const runningScript = server.runningScripts[i];
if (runningScript.filename === name && compareArrays(runningScript.args, workerScript.args)) {
server.runningScripts.splice(i, 1);
break;
}
}
// Delete script from global pool (workerScripts)
const res = workerScripts.delete(workerScript.pid);
if (!res) {
console.warn(`removeWorkerScript() called with WorkerScript that wasn't in the global map:`);
console.warn(workerScript);
}
if (rerenderUi) {
WorkerScriptStartStopEventEmitter.emitEvent();
}
} else {
console.error(`Invalid argument passed into removeWorkerScript():`);
console.error(workerScript);
return;
}
}
/**
* Helper function that interrupts a script's delay if it is in the middle of a
* timed, blocked operation (like hack(), sleep(), etc.). This allows scripts to
* be killed immediately even if they're in the middle of one of those long operations
*/
function killNetscriptDelay(workerScript: WorkerScript) {
if (workerScript instanceof WorkerScript) {
if (workerScript.delay) {
clearTimeout(workerScript.delay);
if (workerScript.delayResolve) {
workerScript.delayResolve();
}
}
}
}

@ -1,21 +1,9 @@
import { WorkerScript } from "./Netscript/WorkerScript";
import { getServer } from "./Server/ServerHelpers";
import { setTimeoutRef } from "./utils/SetTimeoutRef";
import { parse, Node } from "../utils/acorn";
import { isValidIPAddress } from "../utils/helpers/isValidIPAddress";
import { isString } from "../utils/helpers/isString";
export function killNetscriptDelay(workerScript) {
if (workerScript instanceof WorkerScript) {
if (workerScript.delay) {
clearTimeout(workerScript.delay);
workerScript.delayResolve();
}
}
}
export function netscriptDelay(time, workerScript) {
return new Promise(function(resolve, reject) {
workerScript.delay = setTimeoutRef(() => {

@ -2,8 +2,8 @@ const sprintf = require("sprintf-js").sprintf;
const vsprintf = require("sprintf-js").vsprintf;
import { getRamCost } from "./Netscript/RamCostGenerator";
import { WorkerScriptStartStopEventEmitter } from "./Netscript/WorkerScriptStartStopEventEmitter";
import { updateActiveScriptsItems } from "./ActiveScriptsUI";
import { Augmentation } from "./Augmentation/Augmentation";
import { Augmentations } from "./Augmentation/Augmentations";
import {
@ -91,6 +91,10 @@ import {
shortStock,
sellShort,
} from "./StockMarket/BuyingAndSelling";
import {
influenceStockThroughServerHack,
influenceStockThroughServerGrow,
} from "./StockMarket/PlayerInfluencing";
import { Stock } from "./StockMarket/Stock";
import {
StockMarket,
@ -120,11 +124,11 @@ import {
} from "./NetscriptBladeburner";
import * as nsGang from "./NetscriptGang";
import {
workerScripts,
killWorkerScript,
NetscriptPorts,
runScriptFromScript,
} from "./NetscriptWorker";
import { killWorkerScript } from "./Netscript/killWorkerScript";
import { workerScripts } from "./Netscript/WorkerScripts";
import {
makeRuntimeRejectMsg,
netscriptDelay,
@ -440,7 +444,7 @@ function NetscriptFunctions(workerScript) {
}
return out;
},
hack : function(ip, { threads: requestedThreads } = {}){
hack : function(ip, { threads: requestedThreads, stock } = {}){
updateDynamicRam("hack", getRamCost("hack"));
if (ip === undefined) {
throw makeRuntimeRejectMsg(workerScript, "Hack() call has incorrect number of arguments. Takes 1 argument");
@ -502,6 +506,9 @@ function NetscriptFunctions(workerScript) {
workerScript.scriptRef.log("Script SUCCESSFULLY hacked " + server.hostname + " for $" + formatNumber(moneyGained, 2) + " and " + formatNumber(expGainedOnSuccess, 4) + " exp (t=" + threads + ")");
}
server.fortify(CONSTANTS.ServerFortifyAmount * Math.min(threads, maxThreadNeeded));
if (stock) {
influenceStockThroughServerHack(server, moneyGained);
}
return Promise.resolve(moneyGained);
} else {
// Player only gains 25% exp for failure?
@ -556,7 +563,7 @@ function NetscriptFunctions(workerScript) {
return Promise.resolve(true);
});
},
grow : function(ip, { threads: requestedThreads } = {}){
grow : function(ip, { threads: requestedThreads, stock } = {}){
updateDynamicRam("grow", getRamCost("grow"));
const threads = resolveNetscriptRequestedThreads(workerScript, "grow", requestedThreads);
if (ip === undefined) {
@ -597,6 +604,9 @@ function NetscriptFunctions(workerScript) {
}
workerScript.scriptRef.onlineExpGained += expGain;
Player.gainHackingExp(expGain);
if (stock) {
influenceStockThroughServerGrow(server, moneyAfter - moneyBefore);
}
return Promise.resolve(moneyAfter/moneyBefore);
});
},
@ -974,18 +984,20 @@ function NetscriptFunctions(workerScript) {
if (ip === undefined) {
throw makeRuntimeRejectMsg(workerScript, "killall() call has incorrect number of arguments. Takes 1 argument");
}
var server = getServer(ip);
const server = getServer(ip);
if (server == null) {
workerScript.scriptRef.log("killall() failed. Invalid IP or hostname passed in: " + ip);
throw makeRuntimeRejectMsg(workerScript, "killall() failed. Invalid IP or hostname passed in: " + ip);
}
var scriptsRunning = (server.runningScripts.length > 0);
for (var i = server.runningScripts.length-1; i >= 0; --i) {
killWorkerScript(server.runningScripts[i], server.ip);
const scriptsRunning = (server.runningScripts.length > 0);
for (let i = server.runningScripts.length-1; i >= 0; --i) {
killWorkerScript(server.runningScripts[i], server.ip, false);
}
WorkerScriptStartStopEventEmitter.emitEvent();
if (workerScript.disableLogs.ALL == null && workerScript.disableLogs.killall == null) {
workerScript.scriptRef.log("killall(): Killing all scripts on " + server.hostname + ". May take a few minutes for the scripts to die");
}
return scriptsRunning;
},
exit : function() {
@ -1144,7 +1156,7 @@ function NetscriptFunctions(workerScript) {
var oldScript = destServer.scripts[i];
oldScript.code = sourceScript.code;
oldScript.ramUsage = sourceScript.ramUsage;
oldScript.module = "";
oldScript.markUpdated();
return true;
}
}
@ -1537,7 +1549,7 @@ function NetscriptFunctions(workerScript) {
const res = getSellTransactionGain(stock, shares, pos);
if (res == null) { return 0; }
return res;
return res;
},
buyStock: function(symbol, shares) {
updateDynamicRam("buyStock", getRamCost("buyStock"));
@ -1948,6 +1960,7 @@ function NetscriptFunctions(workerScript) {
}
mode === "w" ? script.code = data : script.code += data;
script.updateRamUsage(server.scripts);
script.markUpdated();
} else {
// Write to text file
let txtFile = getTextFile(fn, server);
@ -2168,7 +2181,7 @@ function NetscriptFunctions(workerScript) {
// First element is total income of all currently running scripts
let total = 0;
for (const script of workerScripts) {
for (const script of workerScripts.values()) {
total += (script.scriptRef.onlineMoneyMade / script.scriptRef.onlineRunningTime);
}
res.push(total);
@ -2199,8 +2212,8 @@ function NetscriptFunctions(workerScript) {
updateDynamicRam("getScriptExpGain", getRamCost("getScriptExpGain"));
if (arguments.length === 0) {
var total = 0;
for (var i = 0; i < workerScripts.length; ++i) {
total += (workerScripts[i].scriptRef.onlineExpGained / workerScripts[i].scriptRef.onlineRunningTime);
for (const ws of workerScripts.values()) {
total += (ws.scriptRef.onlineExpGained / ws.scriptRef.onlineRunningTime);
}
return total;
} else {

@ -1,10 +1,22 @@
import { makeRuntimeRejectMsg } from "./NetscriptEvaluator";
import { Script } from "./Script/Script";
// Makes a blob that contains the code of a given script.
export function makeScriptBlob(code) {
return new Blob([code], {type: "text/javascript"});
}
class ScriptUrl {
/**
* @param {string} filename
* @param {string} url
*/
constructor(filename, url) {
this.filename = filename;
this.url = url;
}
}
// Begin executing a user JS script, and return a promise that resolves
// or rejects when the script finishes.
// - script is a script to execute (see Script.js). We depend only on .filename and .code.
@ -15,9 +27,9 @@ export function makeScriptBlob(code) {
// running the main function of the script.
export async function executeJSScript(scripts = [], workerScript) {
let loadedModule;
let urlStack = null;
let urls = null;
let script = workerScript.getScript();
if (script.module === "") {
if (shouldCompile(script, scripts)) {
// The URL at the top is the one we want to import. It will
// recursively import all the other modules in the urlStack.
//
@ -25,10 +37,11 @@ export async function executeJSScript(scripts = [], workerScript) {
// but not really behaves like import. Particularly, it cannot
// load fully dynamic content. So we hide the import from webpack
// by placing it inside an eval call.
urlStack = _getScriptUrls(script, scripts, []);
script.module = await eval('import(urlStack[urlStack.length - 1])');
urls = _getScriptUrls(script, scripts, []);
script.module = new Promise(resolve => resolve(eval('import(urls[urls.length - 1].url)')));
script.dependencies = urls.map(u => u.filename);
}
loadedModule = script.module;
loadedModule = await script.module;
let ns = workerScript.env.vars;
@ -41,12 +54,31 @@ export async function executeJSScript(scripts = [], workerScript) {
return loadedModule.main(ns);
} finally {
// Revoke the generated URLs
if (urlStack != null) {
for (const url in urlStack) URL.revokeObjectURL(url);
if (urls != null) {
for (const b in urls) URL.revokeObjectURL(b.url);
}
};
}
/** Returns whether we should compile the script parameter.
*
* @param {Script} script
* @param {Script[]} scripts
*/
function shouldCompile(script, scripts) {
if (script.module === "") return true;
return script.dependencies.some(dep => {
const depScript = scripts.find(s => s.filename == dep);
// If the script is not present on the server, we should recompile, if only to get any necessary
// compilation errors.
if (!depScript) return true;
const depIsMoreRecent = depScript.moduleSequenceNumber > script.moduleSequenceNumber
return depIsMoreRecent;
});
}
// Gets a stack of blob urls, the top/right-most element being
// the blob url for the named script on the named server.
//
@ -58,8 +90,18 @@ export async function executeJSScript(scripts = [], workerScript) {
// different parts of the tree. That hasn't presented any problem with during
// testing, but it might be an idea for the future. Would require a topo-sort
// then url-izing from leaf-most to root-most.
/**
* @param {Script} script
* @param {Script[]} scripts
* @param {Script[]} seen
* @returns {ScriptUrl[]} All of the compiled scripts, with the final one
* in the list containing the blob corresponding to
* the script parameter.
*/
// BUG: apparently seen is never consulted. Oops.
export function _getScriptUrls(script, scripts, seen) {
// Inspired by: https://stackoverflow.com/a/43834063/91401
/** @type {ScriptUrl[]} */
const urlStack = [];
seen.push(script);
try {
@ -86,7 +128,7 @@ export function _getScriptUrls(script, scripts, seen) {
// The top url in the stack is the replacement import file for this script.
urlStack.push(...urls);
return [prefix, urls[urls.length - 1], suffix].join('');
return [prefix, urls[urls.length - 1].url, suffix].join('');
}
);
@ -96,7 +138,7 @@ export function _getScriptUrls(script, scripts, seen) {
// If we successfully transformed the code, create a blob url for it and
// push that URL onto the top of the stack.
urlStack.push(URL.createObjectURL(makeScriptBlob(transformedCode)));
urlStack.push(new ScriptUrl(script.filename, URL.createObjectURL(makeScriptBlob(transformedCode))));
return urlStack;
} catch (err) {
// If there is an error, we need to clean up the URLs.

@ -2,20 +2,17 @@
* Functions for handling WorkerScripts, which are the underlying mechanism
* that allows for scripts to run
*/
import { killWorkerScript } from "./Netscript/killWorkerScript";
import { WorkerScript } from "./Netscript/WorkerScript";
import { workerScripts } from "./Netscript/WorkerScripts";
import { WorkerScriptStartStopEventEmitter } from "./Netscript/WorkerScriptStartStopEventEmitter";
import {
addActiveScriptsItem,
deleteActiveScriptsItem,
updateActiveScriptsItems
} from "./ActiveScriptsUI";
import { CONSTANTS } from "./Constants";
import { Engine } from "./engine";
import { Interpreter } from "./JSInterpreter";
import {
isScriptErrorMessage,
makeRuntimeRejectMsg,
killNetscriptDelay
} from "./NetscriptEvaluator";
import { NetscriptFunctions } from "./NetscriptFunctions";
import { executeJSScript } from "./NetscriptJSEvaluator";
@ -42,21 +39,19 @@ import { isString } from "../utils/StringHelperFunctions";
const walk = require("acorn/dist/walk");
//Array containing all scripts that are running across all servers, to easily run them all
export const workerScripts = [];
// Netscript Ports are instantiated here
export const NetscriptPorts = [];
for (var i = 0; i < CONSTANTS.NumNetscriptPorts; ++i) {
NetscriptPorts.push(new NetscriptPort());
}
export function prestigeWorkerScripts() {
for (var i = 0; i < workerScripts.length; ++i) {
deleteActiveScriptsItem(workerScripts[i]);
workerScripts[i].env.stopFlag = true;
for (const ws of workerScripts.values()) {
ws.env.stopFlag = true;
}
updateActiveScriptsItems(5000); //Force UI to update
workerScripts.length = 0;
WorkerScriptStartStopEventEmitter.emitEvent();
workerScripts.clear();
}
// JS script promises need a little massaging to have the same guarantees as netscript
@ -141,7 +136,7 @@ function startNetscript2Script(workerScript) {
}
function startNetscript1Script(workerScript) {
var code = workerScript.code;
const code = workerScript.code;
workerScript.running = true;
//Process imports
@ -413,164 +408,149 @@ function processNetscript1Imports(code, workerScript) {
return res;
}
// Loop through workerScripts and run every script that is not currently running
export function runScriptsLoop() {
let scriptDeleted = false;
/**
* Find and return the next availble PID for a script
*/
let pidCounter = 1;
function generateNextPid() {
let tempCounter = pidCounter;
// Delete any scripts that finished or have been killed. Loop backwards bc removing items screws up indexing
for (let i = workerScripts.length - 1; i >= 0; i--) {
if (workerScripts[i].running == false && workerScripts[i].env.stopFlag == true) {
scriptDeleted = true;
// Delete script from the runningScripts array on its host serverIp
const ip = workerScripts[i].serverIp;
const name = workerScripts[i].name;
// Cap the number of search iterations at some arbitrary value to avoid
// infinite loops. We'll assume that players wont have 1mil+ running scripts
let found = false;
for (let i = 0; i < 1e6;) {
if (!workerScripts.has(tempCounter + i)) {
found = true;
tempCounter = tempCounter + i;
break;
}
// Recalculate ram used
AllServers[ip].ramUsed = 0;
for (let j = 0; j < workerScripts.length; j++) {
if (workerScripts[j].serverIp !== ip) {
continue;
}
if (j === i) { // not this one
continue;
}
AllServers[ip].ramUsed += workerScripts[j].ramUsage;
}
// Delete script from Active Scripts
deleteActiveScriptsItem(workerScripts[i]);
for (let j = 0; j < AllServers[ip].runningScripts.length; j++) {
if (AllServers[ip].runningScripts[j].filename == name &&
compareArrays(AllServers[ip].runningScripts[j].args, workerScripts[i].args)) {
AllServers[ip].runningScripts.splice(j, 1);
break;
}
}
// Delete script from workerScripts
workerScripts.splice(i, 1);
if (i === Number.MAX_SAFE_INTEGER - 1) {
i = 1;
} else {
++i;
}
}
if (scriptDeleted) { updateActiveScriptsItems(); } // Force Update
if (found) {
pidCounter = tempCounter + 1;
if (pidCounter >= Number.MAX_SAFE_INTEGER) {
pidCounter = 1;
}
// Run any scripts that haven't been started
for (let i = 0; i < workerScripts.length; i++) {
// If it isn't running, start the script
if (workerScripts[i].running == false && workerScripts[i].env.stopFlag == false) {
let p = null; // p is the script's result promise.
if (workerScripts[i].name.endsWith(".js") || workerScripts[i].name.endsWith(".ns")) {
p = startNetscript2Script(workerScripts[i]);
} else {
p = startNetscript1Script(workerScripts[i]);
if (!(p instanceof Promise)) { continue; }
}
// Once the code finishes (either resolved or rejected, doesnt matter), set its
// running status to false
p.then(function(w) {
console.log("Stopping script " + w.name + " because it finished running naturally");
w.running = false;
w.env.stopFlag = true;
w.scriptRef.log("Script finished running");
}).catch(function(w) {
if (w instanceof Error) {
dialogBoxCreate("Script runtime unknown error. This is a bug please contact game developer");
console.error("Evaluating workerscript returns an Error. THIS SHOULDN'T HAPPEN: " + w.toString());
return;
} else if (w.constructor === Array && w.length === 2 && w[0] === "RETURNSTATEMENT") {
// Script ends with a return statement
console.log("Script returning with value: " + w[1]);
// TODO maybe do something with this in the future
return;
} else if (w instanceof WorkerScript) {
if (isScriptErrorMessage(w.errorMessage)) {
var errorTextArray = w.errorMessage.split("|");
if (errorTextArray.length != 4) {
console.log("ERROR: Something wrong with Error text in evaluator...");
console.log("Error text: " + errorText);
return;
}
var serverIp = errorTextArray[1];
var scriptName = errorTextArray[2];
var errorMsg = errorTextArray[3];
dialogBoxCreate("Script runtime error: <br>Server Ip: " + serverIp +
"<br>Script name: " + scriptName +
"<br>Args:" + arrayToString(w.args) + "<br>" + errorMsg);
w.scriptRef.log("Script crashed with runtime error");
} else {
w.scriptRef.log("Script killed");
}
w.running = false;
w.env.stopFlag = true;
} else if (isScriptErrorMessage(w)) {
dialogBoxCreate("Script runtime unknown error. This is a bug please contact game developer");
console.log("ERROR: Evaluating workerscript returns only error message rather than WorkerScript object. THIS SHOULDN'T HAPPEN: " + w.toString());
return;
} else {
dialogBoxCreate("An unknown script died for an unknown reason. This is a bug please contact game dev");
console.log(w);
}
});
}
}
setTimeoutRef(runScriptsLoop, 3e3);
return tempCounter;
} else {
return -1;
}
}
/**
* Queues a script to be killed by setting its stop flag to true. This
* kills and timed/blocking Netscript functions (like hack(), sleep(), etc.) and
* prevents any further execution of Netscript functions.
* The runScriptsLoop() handles the actual deletion of the WorkerScript
*/
export function killWorkerScript(runningScriptObj, serverIp) {
for (var i = 0; i < workerScripts.length; i++) {
if (workerScripts[i].name == runningScriptObj.filename && workerScripts[i].serverIp == serverIp &&
compareArrays(workerScripts[i].args, runningScriptObj.args)) {
workerScripts[i].env.stopFlag = true;
killNetscriptDelay(workerScripts[i]);
return true;
}
}
return false;
}
/**
* Given a RunningScript object, queues that script to be run
* Start a script
*
* Given a RunningScript object, constructs a corresponding WorkerScript,
* adds it to the global 'workerScripts' pool, and begins executing it.
* @param {RunningScript} runningScriptObj - Script that's being run
* @param {Server} server - Server on which the script is to be run
*/
export function addWorkerScript(runningScriptObj, server) {
var filename = runningScriptObj.filename;
const filename = runningScriptObj.filename;
//Update server's ram usage
var threads = 1;
// Update server's ram usage
let threads = 1;
if (runningScriptObj.threads && !isNaN(runningScriptObj.threads)) {
threads = runningScriptObj.threads;
} else {
runningScriptObj.threads = 1;
}
var ramUsage = roundToTwo(getRamUsageFromRunningScript(runningScriptObj) * threads);
var ramAvailable = server.maxRam - server.ramUsed;
const ramUsage = roundToTwo(getRamUsageFromRunningScript(runningScriptObj) * threads);
const ramAvailable = server.maxRam - server.ramUsed;
if (ramUsage > ramAvailable) {
dialogBoxCreate("Not enough RAM to run script " + runningScriptObj.filename + " with args " +
arrayToString(runningScriptObj.args) + ". This likely occurred because you re-loaded " +
"the game and the script's RAM usage increased (either because of an update to the game or " +
"your changes to the script.)");
dialogBoxCreate(
`Not enough RAM to run script ${runningScriptObj.filename} with args ` +
`${arrayToString(runningScriptObj.args)}. This likely occurred because you re-loaded ` +
`the game and the script's RAM usage increased (either because of an update to the game or ` +
`your changes to the script.)`
);
return;
}
server.ramUsed = roundToTwo(server.ramUsed + ramUsage);
//Create the WorkerScript
var s = new WorkerScript(runningScriptObj, NetscriptFunctions);
// Get the pid
const pid = generateNextPid();
if (pid === -1) {
throw new Error(
`Failed to start script because could not find available PID. This is most ` +
`because you have too many scripts running.`
);
}
// Create the WorkerScript. NOTE: WorkerScript ctor will set the underlying
// RunningScript's PID as well
const s = new WorkerScript(runningScriptObj, pid, NetscriptFunctions);
s.ramUsage = ramUsage;
//Add the WorkerScript to the Active Scripts list
addActiveScriptsItem(s);
// Start the script's execution
let p = null; // Script's resulting promise
if (s.name.endsWith(".js") || s.name.endsWith(".ns")) {
p = startNetscript2Script(s);
} else {
p = startNetscript1Script(s);
if (!(p instanceof Promise)) { return; }
}
//Add the WorkerScript
workerScripts.push(s);
// Once the code finishes (either resolved or rejected, doesnt matter), set its
// running status to false
p.then(function(w) {
console.log("Stopping script " + w.name + " because it finished running naturally");
killWorkerScript(s);
w.scriptRef.log("Script finished running");
}).catch(function(w) {
if (w instanceof Error) {
dialogBoxCreate("Script runtime unknown error. This is a bug please contact game developer");
console.error("Evaluating workerscript returns an Error. THIS SHOULDN'T HAPPEN: " + w.toString());
return;
} else if (w.constructor === Array && w.length === 2 && w[0] === "RETURNSTATEMENT") {
// Script ends with a return statement
console.log("Script returning with value: " + w[1]);
// TODO maybe do something with this in the future
return;
} else if (w instanceof WorkerScript) {
if (isScriptErrorMessage(w.errorMessage)) {
var errorTextArray = w.errorMessage.split("|");
if (errorTextArray.length != 4) {
console.log("ERROR: Something wrong with Error text in evaluator...");
console.log("Error text: " + errorText);
return;
}
var serverIp = errorTextArray[1];
var scriptName = errorTextArray[2];
var errorMsg = errorTextArray[3];
dialogBoxCreate("Script runtime error: <br>Server Ip: " + serverIp +
"<br>Script name: " + scriptName +
"<br>Args:" + arrayToString(w.args) + "<br>" + errorMsg);
w.scriptRef.log("Script crashed with runtime error");
} else {
w.scriptRef.log("Script killed");
return; // Already killed, so stop here
}
w.running = false;
w.env.stopFlag = true;
} else if (isScriptErrorMessage(w)) {
dialogBoxCreate("Script runtime unknown error. This is a bug please contact game developer");
console.log("ERROR: Evaluating workerscript returns only error message rather than WorkerScript object. THIS SHOULDN'T HAPPEN: " + w.toString());
return;
} else {
dialogBoxCreate("An unknown script died for an unknown reason. This is a bug please contact game dev");
console.log(w);
}
killWorkerScript(s);
});
// Add the WorkerScript to the global pool
workerScripts.set(pid, s);
WorkerScriptStartStopEventEmitter.emitEvent();
return;
}
@ -579,9 +559,9 @@ export function addWorkerScript(runningScriptObj, server) {
*/
export function updateOnlineScriptTimes(numCycles = 1) {
var time = (numCycles * Engine._idleSpeed) / 1000; //seconds
for (var i = 0; i < workerScripts.length; ++i) {
workerScripts[i].scriptRef.onlineRunningTime += time;
}
for (const ws of workerScripts.values()) {
ws.scriptRef.onlineRunningTime += time;
}
}
/**
@ -601,7 +581,7 @@ export function loadAllRunningScripts() {
// Reset modules on all scripts
for (let i = 0; i < server.scripts.length; ++i) {
server.scripts[i].module = "";
server.scripts[i].markUpdated();
}
if (skipScriptLoad) {

@ -101,6 +101,10 @@ export interface IPlayer {
work_money_mult: number;
crime_success_mult: number;
crime_money_mult: number;
bladeburner_max_stamina_mult: number;
bladeburner_stamina_gain_mult: number;
bladeburner_analysis_mult: number;
bladeburner_success_chance_mult: number;
// Methods
applyForAgentJob(sing?: boolean): boolean | void;
@ -147,6 +151,7 @@ export interface IPlayer {
reapplyAllSourceFiles(): void;
regenerateHp(amt: number): void;
recordMoneySource(amt: number, source: string): void;
setMoney(amt: number): void;
startBladeburner(p: object): void;
startClass(costMult: number, expMult: number, className: string): void;
startCorporation(corpName: string, additionalShares?: number): void;

@ -36,8 +36,10 @@ import {
import { safetlyCreateUniqueServer } from "../../Server/ServerHelpers";
import { Settings } from "../../Settings/Settings";
import { SpecialServerIps, SpecialServerNames } from "../../Server/SpecialServerIps";
import { SourceFiles, applySourceFile } from "../../SourceFile";
import { applySourceFile } from "../../SourceFile/applySourceFile";
import { SourceFiles } from "../../SourceFile/SourceFiles";
import { SourceFileFlags } from "../../SourceFile/SourceFileFlags";
import { influenceStockThroughCompanyWork } from "../../StockMarket/PlayerInfluencing";
import Decimal from "decimal.js";
@ -579,8 +581,8 @@ export function startWork(companyName) {
}
export function work(numCycles) {
//Cap the number of cycles being processed to whatever would put you at
//the work time limit (8 hours)
// Cap the number of cycles being processed to whatever would put you at
// the work time limit (8 hours)
var overMax = false;
if (this.timeWorked + (Engine._idleSpeed * numCycles) >= CONSTANTS.MillisecondsPer8Hours) {
overMax = true;
@ -588,21 +590,24 @@ export function work(numCycles) {
}
this.timeWorked += Engine._idleSpeed * numCycles;
this.workRepGainRate = this.getWorkRepGain();
this.workRepGainRate = this.getWorkRepGain();
this.processWorkEarnings(numCycles);
//If timeWorked == 8 hours, then finish. You can only gain 8 hours worth of exp and money
// If timeWorked == 8 hours, then finish. You can only gain 8 hours worth of exp and money
if (overMax || this.timeWorked >= CONSTANTS.MillisecondsPer8Hours) {
return this.finishWork(false);
}
var comp = Companies[this.companyName], companyRep = "0";
const comp = Companies[this.companyName];
let companyRep = "0";
if (comp == null || !(comp instanceof Company)) {
console.error(`Could not find Company: ${this.companyName}`);
} else {
companyRep = comp.playerReputation;
}
influenceStockThroughCompanyWork(comp, this.workRepGainRate, numCycles);
const position = this.jobs[this.companyName];
var txt = document.getElementById("work-in-progress-text");

@ -1,4 +1,3 @@
import { deleteActiveScriptsItem } from "./ActiveScriptsUI";
import { Augmentations } from "./Augmentation/Augmentations";
import {
augmentationExists,

@ -5,7 +5,7 @@ import { BitNodes } from "./BitNode/BitNode";
import { Engine } from "./engine";
import { Player } from "./Player";
import { prestigeSourceFile } from "./Prestige";
import { SourceFiles, SourceFile } from "./SourceFile";
import { SourceFiles } from "./SourceFile/SourceFiles";
import { PlayerOwnedSourceFile } from "./SourceFile/PlayerOwnedSourceFile";
import { Terminal } from "./Terminal";
import { setTimeoutRef } from "./utils/SetTimeoutRef";
@ -20,9 +20,6 @@ import {
import { clearEventListeners } from "../utils/uiHelpers/clearEventListeners";
import { removeChildrenFromElement } from "../utils/uiHelpers/removeChildrenFromElement";
// Returns promise
function writeRedPillLine(line) {
return new Promise(function(resolve, reject) {

@ -1,4 +1,10 @@
// Calculate a script's RAM usage
/**
* Implements RAM Calculation functionality.
*
* Uses the acorn.js library to parse a script's code into an AST and
* recursively walk through that AST, calculating RAM usage along
* the way
*/
import * as walk from "acorn-walk";
import { RamCalculationErrorCode } from "./RamCalculationErrorCodes";
@ -15,19 +21,26 @@ const specialReferenceWHILE = "__SPECIAL_referenceWhile";
// The global scope of a script is registered under this key during parsing.
const memCheckGlobalKey = ".__GLOBAL__";
// Calcluates the amount of RAM a script uses. Uses parsing and AST walking only,
// rather than NetscriptEvaluator. This is useful because NetscriptJS code does
// not work under NetscriptEvaluator.
/**
* Parses code into an AST and walks through it recursively to calculate
* RAM usage. Also accounts for imported modules.
* @param {Script[]} otherScripts - All other scripts on the server. Used to account for imported scripts
* @param {string} codeCopy - The code being parsed
* @param {WorkerScript} workerScript - Object containing RAM costs of Netscript functions. Also used to
* keep track of what functions have/havent been accounted for
*/
async function parseOnlyRamCalculate(otherScripts, code, workerScript) {
try {
// Maps dependent identifiers to their dependencies.
//
// The initial identifier is __SPECIAL_INITIAL_MODULE__.__GLOBAL__.
// It depends on all the functions declared in the module, all the global scopes
// of its imports, and any identifiers referenced in this global scope. Each
// function depends on all the identifiers referenced internally.
// We walk the dependency graph to calculate RAM usage, given that some identifiers
// reference Netscript functions which have a RAM cost.
/**
* Maps dependent identifiers to their dependencies.
*
* The initial identifier is __SPECIAL_INITIAL_MODULE__.__GLOBAL__.
* It depends on all the functions declared in the module, all the global scopes
* of its imports, and any identifiers referenced in this global scope. Each
* function depends on all the identifiers referenced internally.
* We walk the dependency graph to calculate RAM usage, given that some identifiers
* reference Netscript functions which have a RAM cost.
*/
let dependencyMap = {};
// Scripts we've parsed.
@ -48,19 +61,20 @@ async function parseOnlyRamCalculate(otherScripts, code, workerScript) {
}
}
// Splice all the references in.
//Spread syntax not supported in edge, use Object.assign instead
//dependencyMap = {...dependencyMap, ...result.dependencyMap};
// Splice all the references in
dependencyMap = Object.assign(dependencyMap, result.dependencyMap);
}
// Parse the initial module, which is the "main" script that is being run
const initialModule = "__SPECIAL_INITIAL_MODULE__";
parseCode(code, initialModule);
// Process additional modules, which occurs if the "main" script has any imports
while (parseQueue.length > 0) {
// Get the code from the server.
const nextModule = parseQueue.shift();
// Additional modules can either be imported from the web (in which case we use
// a dynamic import), or from other in-game scripts
let code;
if (nextModule.startsWith("https://") || nextModule.startsWith("http://")) {
try {
@ -91,7 +105,7 @@ async function parseOnlyRamCalculate(otherScripts, code, workerScript) {
}
if (script == null) {
return RamCalculationErrorCode.ImportError; // No such script on the server
return RamCalculationErrorCode.ImportError; // No such script on the server
}
code = script.code;
@ -136,10 +150,8 @@ async function parseOnlyRamCalculate(otherScripts, code, workerScript) {
}
}
// Check if this identifier is a function in the workerscript env.
// Check if this identifier is a function in the workerScript environment.
// If it is, then we need to get its RAM cost.
//
// TODO it would be simpler to just reference a dictionary.
try {
function applyFuncRam(func) {
if (typeof func === "function") {
@ -170,7 +182,7 @@ async function parseOnlyRamCalculate(otherScripts, code, workerScript) {
workerScript.loadedFns[ref] = true;
}
// This accounts for namespaces (Bladeburner, CodingCOntract)
// This accounts for namespaces (Bladeburner, CodingCpntract, etc.)
let func;
if (ref in workerScript.env.vars.bladeburner) {
func = workerScript.env.vars.bladeburner[ref];
@ -196,9 +208,12 @@ async function parseOnlyRamCalculate(otherScripts, code, workerScript) {
}
}
// Parses one script and calculates its ram usage, for the global scope and each function.
// Returns a cost map and a dependencyMap for the module. Returns a reference map to be joined
// onto the main reference map, and a list of modules that need to be parsed.
/**
* Helper function that parses a single script. It returns a map of all dependencies,
* which are items in the code's AST that potentially need to be evaluated
* for RAM usage calculations. It also returns an array of additional modules
* that need to be parsed (i.e. are 'import'ed scripts).
*/
function parseOnlyCalculateDeps(code, currentModule) {
const ast = parse(code, {sourceType:"module", ecmaVersion: 8});
@ -296,6 +311,12 @@ function parseOnlyCalculateDeps(code, currentModule) {
return {dependencyMap: dependencyMap, additionalModules: additionalModules};
}
/**
* Calculate's a scripts RAM Usage
* @param {string} codeCopy - The script's code
* @param {Script[]} otherScripts - All other scripts on the server.
* Used to account for imported scripts
*/
export async function calculateRamUsage(codeCopy, otherScripts) {
// We don't need a real WorkerScript for this. Just an object that keeps
// track of whatever's needed for RAM calculations

@ -1,5 +1,7 @@
// Class representing a Script instance that is actively running.
// A Script can have multiple active instances
/**
* Class representing a Script instance that is actively running.
* A Script can have multiple active instances
*/
import { Script } from "./Script";
import { FconfSettings } from "../Fconf/FconfSettings";
import { Settings } from "../Settings/Settings";
@ -22,10 +24,8 @@ export class RunningScript {
// Script arguments
args: any[] = [];
// Holds a map of servers hacked, where server = key and the value for each
// server is an array of four numbers. The four numbers represent:
// [MoneyStolen, NumTimesHacked, NumTimesGrown, NumTimesWeaken]
// This data is used for offline progress
// Map of [key: server ip] -> Hacking data. Used for offline progress calculations.
// Hacking data format: [MoneyStolen, NumTimesHacked, NumTimesGrown, NumTimesWeaken]
dataMap: IMap<number[]> = {};
// Script filename
@ -56,6 +56,9 @@ export class RunningScript {
// Number of seconds that this script has been running online
onlineRunningTime: number = 0.01;
// Process ID. Must be an integer and equals the PID of corresponding WorkerScript
pid: number = -1;
// How much RAM this script uses for ONE thread
ramUsage: number = 0;
@ -69,22 +72,20 @@ export class RunningScript {
if (script == null) { return; }
this.filename = script.filename;
this.args = args;
this.server = script.server; //IP Address only
this.server = script.server;
this.ramUsage = script.ramUsage;
}
log(txt: string): void {
if (this.logs.length > Settings.MaxLogCapacity) {
//Delete first element and add new log entry to the end.
//TODO Eventually it might be better to replace this with circular array
//to improve performance
this.logs.shift();
}
let logEntry = txt;
if (FconfSettings.ENABLE_TIMESTAMPS) {
logEntry = "[" + getTimestamp() + "] " + logEntry;
}
this.logs.push(logEntry);
this.logUpd = true;
}

@ -1,6 +1,9 @@
// Class representing a script file
// This does NOT represent a script that is actively running and
// being evaluated. See RunningScript for that
/**
* Class representing a script file.
*
* This does NOT represent a script that is actively running and
* being evaluated. See RunningScript for that
*/
import { calculateRamUsage } from "./RamCalculations";
import { Page, routing } from "../ui/navigationTracking";
@ -12,6 +15,8 @@ import {
} from "../../utils/JSONReviver";
import { roundToTwo } from "../../utils/helpers/roundToTwo";
let globalModuleSequenceNumber = 0;
export class Script {
// Initializes a Script Object from a JSON save state
static fromJSON(value: any): Script {
@ -28,22 +33,33 @@ export class Script {
// This is only applicable for NetscriptJS
module: any = "";
// The timestamp when when the script was last updated.
moduleSequenceNumber: number;
// Only used with NS2 scripts; the list of dependency script filenames. This is constructed
// whenever the script is first evaluated, and therefore may be out of date if the script
// has been updated since it was last run.
dependencies: string[] = [];
// Amount of RAM this Script requres to run
ramUsage: number = 0;
// IP of server that this script is on.
server: string = "";
constructor(fn: string="", code: string="", server: string="", otherScripts: Script[]=[]) {
this.filename = fn;
this.code = code;
this.ramUsage = 0;
this.server = server; // IP of server this script is on
this.module = "";
this.moduleSequenceNumber = ++globalModuleSequenceNumber;
if (this.code !== "") { this.updateRamUsage(otherScripts); }
};
/**
* Download the script as a file
*/
download(): void {
const filename = this.filename + ".js";
const file = new Blob([this.code], {type: 'text/plain'});
@ -63,10 +79,23 @@ export class Script {
}
}
// Save a script FROM THE SCRIPT EDITOR
/**
* Marks this script as having been updated. It will be recompiled next time something tries
* to exec it.
*/
markUpdated() {
this.module = "";
this.moduleSequenceNumber = ++globalModuleSequenceNumber;
}
/**
* Save a script from the script editor
* @param {string} code - The new contents of the script
* @param {Script[]} otherScripts - Other scripts on the server. Used to process imports
*/
saveScript(code: string, serverIp: string, otherScripts: Script[]): void {
if (routing.isOn(Page.ScriptEditor)) {
//Update code and filename
// Update code and filename
this.code = code.replace(/^\s+|\s+$/g, '');
const filenameElem: HTMLInputElement | null = document.getElementById("script-editor-filename") as HTMLInputElement;
@ -75,18 +104,16 @@ export class Script {
return;
}
this.filename = filenameElem!.value;
// Server
this.server = serverIp;
//Calculate/update ram usage, execution time, etc.
this.updateRamUsage(otherScripts);
this.module = "";
this.markUpdated();
}
}
// Updates the script's RAM usage based on its code
/**
* Calculates and updates the script's RAM usage based on its code
* @param {Script[]} otherScripts - Other scripts on the server. Used to process imports
*/
async updateRamUsage(otherScripts: Script[]) {
var res = await calculateRamUsage(this.code, otherScripts);
if (res > 0) {

@ -233,7 +233,7 @@ export class BaseServer {
let script = this.scripts[i];
script.code = code;
script.updateRamUsage(this.scripts);
script.module = "";
script.markUpdated();
ret.overwritten = true;
ret.success = true;
return ret;

@ -2,6 +2,7 @@
// This could actually be a JSON file as it should be constant metadata to be imported...
import { IMinMaxRange } from "../../types";
import { LocationName } from "../../Locations/data/LocationNames";
/**
* The metadata describing the base state of servers on the network.
@ -82,7 +83,7 @@ export const serverMetadata: IServerMetadata[] = [
},
networkLayer: 15,
numOpenPortsRequired: 5,
organizationName: "ECorp",
organizationName: LocationName.AevumECorp,
requiredHackingSkill: {
max: 1400,
min: 1050,
@ -98,7 +99,7 @@ export const serverMetadata: IServerMetadata[] = [
},
networkLayer: 15,
numOpenPortsRequired: 5,
organizationName: "MegaCorp",
organizationName: LocationName.Sector12MegaCorp,
requiredHackingSkill: {
max: 1350,
min: 1100,
@ -117,7 +118,7 @@ export const serverMetadata: IServerMetadata[] = [
},
networkLayer: 14,
numOpenPortsRequired: 5,
organizationName: "Bachman & Associates",
organizationName: LocationName.AevumBachmanAndAssociates,
requiredHackingSkill: {
max: 1150,
min: 900,
@ -144,7 +145,7 @@ export const serverMetadata: IServerMetadata[] = [
},
networkLayer: 14,
numOpenPortsRequired: 5,
organizationName: "Blade Industries",
organizationName: LocationName.Sector12BladeIndustries,
requiredHackingSkill: {
max: 1200,
min: 900,
@ -164,7 +165,7 @@ export const serverMetadata: IServerMetadata[] = [
},
networkLayer: 14,
numOpenPortsRequired: 5,
organizationName: "New World Order",
organizationName: LocationName.VolhavenNWO,
requiredHackingSkill: {
max: 1300,
min: 950,
@ -190,7 +191,7 @@ export const serverMetadata: IServerMetadata[] = [
},
networkLayer: 14,
numOpenPortsRequired: 5,
organizationName: "Clarke Incorporated",
organizationName: LocationName.AevumClarkeIncorporated,
requiredHackingSkill: {
max: 1250,
min: 950,
@ -220,7 +221,7 @@ export const serverMetadata: IServerMetadata[] = [
},
networkLayer: 13,
numOpenPortsRequired: 5,
organizationName: "OmniTek Incorporated",
organizationName: LocationName.VolhavenOmniTekIncorporated,
requiredHackingSkill: {
max: 1100,
min: 900,
@ -242,7 +243,7 @@ export const serverMetadata: IServerMetadata[] = [
},
networkLayer: 13,
numOpenPortsRequired: 5,
organizationName: "FourSigma",
organizationName: LocationName.Sector12FourSigma,
requiredHackingSkill: {
max: 1250,
min: 900,
@ -264,7 +265,7 @@ export const serverMetadata: IServerMetadata[] = [
},
networkLayer: 13,
numOpenPortsRequired: 5,
organizationName: "KuaiGong International",
organizationName: LocationName.ChongqingKuaiGongInternational,
requiredHackingSkill: {
max: 1300,
min: 950,
@ -291,7 +292,7 @@ export const serverMetadata: IServerMetadata[] = [
},
networkLayer: 12,
numOpenPortsRequired: 5,
organizationName: "Fulcrum Technologies",
organizationName: LocationName.AevumFulcrumTechnologies,
requiredHackingSkill: {
max: 1250,
min: 950,
@ -307,7 +308,7 @@ export const serverMetadata: IServerMetadata[] = [
moneyAvailable: 1e6,
networkLayer: 15,
numOpenPortsRequired: 5,
organizationName: "Fulcrum Technologies Assets",
organizationName: LocationName.AevumFulcrumTechnologies,
requiredHackingSkill: {
max: 1600,
min: 1100,
@ -327,7 +328,7 @@ export const serverMetadata: IServerMetadata[] = [
},
networkLayer: 12,
numOpenPortsRequired: 5,
organizationName: "Storm Technologies",
organizationName: LocationName.IshimaStormTechnologies,
requiredHackingSkill: {
max: 1075,
min: 875,
@ -349,7 +350,7 @@ export const serverMetadata: IServerMetadata[] = [
},
networkLayer: 9,
numOpenPortsRequired: 5,
organizationName: "DefComm",
organizationName: LocationName.NewTokyoDefComm,
requiredHackingSkill: {
max: 1050,
min: 850,
@ -398,7 +399,7 @@ export const serverMetadata: IServerMetadata[] = [
},
networkLayer: 12,
numOpenPortsRequired: 5,
organizationName: "Helios Labs",
organizationName: LocationName.VolhavenHeliosLabs,
requiredHackingSkill: {
max: 900,
min: 800,
@ -425,7 +426,7 @@ export const serverMetadata: IServerMetadata[] = [
},
networkLayer: 12,
numOpenPortsRequired: 5,
organizationName: "VitaLife",
organizationName: LocationName.NewTokyoVitaLife,
requiredHackingSkill: {
max: 900,
min: 775,
@ -447,7 +448,7 @@ export const serverMetadata: IServerMetadata[] = [
},
networkLayer: 9,
numOpenPortsRequired: 5,
organizationName: "Icarus Microsystems",
organizationName: LocationName.Sector12IcarusMicrosystems,
requiredHackingSkill: {
max: 925,
min: 850,
@ -473,7 +474,7 @@ export const serverMetadata: IServerMetadata[] = [
},
networkLayer: 9,
numOpenPortsRequired: 4,
organizationName: "Universal Energy",
organizationName: LocationName.Sector12UniversalEnergy,
requiredHackingSkill: {
max: 900,
min: 800,
@ -575,7 +576,7 @@ export const serverMetadata: IServerMetadata[] = [
},
networkLayer: 7,
numOpenPortsRequired: 5,
organizationName: "Galactic Cybersystems",
organizationName: LocationName.AevumGalacticCybersystems,
requiredHackingSkill: {
max: 875,
min: 825,
@ -598,7 +599,7 @@ export const serverMetadata: IServerMetadata[] = [
},
networkLayer: 7,
numOpenPortsRequired: 5,
organizationName: "AeroCorp",
organizationName: LocationName.AevumAeroCorp,
requiredHackingSkill: {
max: 925,
min: 850,
@ -625,7 +626,7 @@ export const serverMetadata: IServerMetadata[] = [
},
networkLayer: 8,
numOpenPortsRequired: 5,
organizationName: "Omnia Cybersystems",
organizationName: LocationName.VolhavenOmniaCybersystems,
requiredHackingSkill: {
max: 950,
min: 850,
@ -700,7 +701,7 @@ export const serverMetadata: IServerMetadata[] = [
},
networkLayer: 9,
numOpenPortsRequired: 5,
organizationName: "Solaris Space Systems",
organizationName: LocationName.ChongqingSolarisSpaceSystems,
requiredHackingSkill: {
max: 850,
min: 750,
@ -722,7 +723,7 @@ export const serverMetadata: IServerMetadata[] = [
},
networkLayer: 8,
numOpenPortsRequired: 5,
organizationName: "Delta One",
organizationName: LocationName.Sector12DeltaOne,
requiredHackingSkill: {
max: 900,
min: 800,
@ -749,7 +750,7 @@ export const serverMetadata: IServerMetadata[] = [
},
networkLayer: 7,
numOpenPortsRequired: 4,
organizationName: "Global Pharmaceuticals",
organizationName: LocationName.NewTokyoGlobalPharmaceuticals,
requiredHackingSkill: {
max: 850,
min: 750,
@ -771,7 +772,7 @@ export const serverMetadata: IServerMetadata[] = [
},
networkLayer: 10,
numOpenPortsRequired: 4,
organizationName: "Nova Medical",
organizationName: LocationName.IshimaNovaMedical,
requiredHackingSkill: {
max: 850,
min: 775,
@ -845,7 +846,7 @@ export const serverMetadata: IServerMetadata[] = [
},
networkLayer: 6,
numOpenPortsRequired: 4,
organizationName: "Lexo Corporation",
organizationName: LocationName.VolhavenLexoCorp,
requiredHackingSkill: {
max: 750,
min: 650,
@ -871,7 +872,7 @@ export const serverMetadata: IServerMetadata[] = [
},
networkLayer: 6,
numOpenPortsRequired: 3,
organizationName: "Rho Construction",
organizationName: LocationName.AevumRhoConstruction,
requiredHackingSkill: {
max: 525,
min: 475,
@ -898,7 +899,7 @@ export const serverMetadata: IServerMetadata[] = [
},
networkLayer: 6,
numOpenPortsRequired: 4,
organizationName: "Alpha Enterprises",
organizationName: LocationName.Sector12AlphaEnterprises,
requiredHackingSkill: {
max: 600,
min: 500,
@ -924,7 +925,7 @@ export const serverMetadata: IServerMetadata[] = [
},
networkLayer: 6,
numOpenPortsRequired: 4,
organizationName: "Aevum Police Network",
organizationName: LocationName.AevumPolice,
requiredHackingSkill: {
max: 450,
min: 400,
@ -955,7 +956,7 @@ export const serverMetadata: IServerMetadata[] = [
},
networkLayer: 5,
numOpenPortsRequired: 3,
organizationName: "Rothman University Network",
organizationName: LocationName.Sector12RothmanUniversity,
requiredHackingSkill: {
max: 430,
min: 370,
@ -981,7 +982,7 @@ export const serverMetadata: IServerMetadata[] = [
},
networkLayer: 5,
numOpenPortsRequired: 5,
organizationName: "ZB Institute of Technology Network",
organizationName: LocationName.VolhavenZBInstituteOfTechnology,
requiredHackingSkill: {
max: 775,
min: 725,
@ -1012,7 +1013,7 @@ export const serverMetadata: IServerMetadata[] = [
},
networkLayer: 5,
numOpenPortsRequired: 3,
organizationName: "Summit University Network",
organizationName: LocationName.AevumSummitUniversity,
requiredHackingSkill: {
max: 475,
min: 425,
@ -1034,7 +1035,7 @@ export const serverMetadata: IServerMetadata[] = [
},
networkLayer: 5,
numOpenPortsRequired: 4,
organizationName: "SysCore Securities",
organizationName: LocationName.VolhavenSysCoreSecurities,
requiredHackingSkill: {
max: 650,
min: 550,
@ -1110,7 +1111,7 @@ export const serverMetadata: IServerMetadata[] = [
},
networkLayer: 4,
numOpenPortsRequired: 3,
organizationName: "CompuTek",
organizationName: LocationName.VolhavenCompuTek,
requiredHackingSkill: {
max: 400,
min: 300,
@ -1134,7 +1135,7 @@ export const serverMetadata: IServerMetadata[] = [
moneyAvailable: 275000000,
networkLayer: 4,
numOpenPortsRequired: 3,
organizationName: "Netlink Technologies",
organizationName: LocationName.AevumNetLinkTechnologies,
requiredHackingSkill: {
max: 425,
min: 375,
@ -1174,7 +1175,7 @@ export const serverMetadata: IServerMetadata[] = [
moneyAvailable: 2000000,
networkLayer: 1,
numOpenPortsRequired: 0,
organizationName: "Food N Stuff Supermarket",
organizationName: LocationName.Sector12FoodNStuff,
requiredHackingSkill: 1,
serverGrowth: 5,
},
@ -1196,7 +1197,7 @@ export const serverMetadata: IServerMetadata[] = [
moneyAvailable: 2500000,
networkLayer: 1,
numOpenPortsRequired: 0,
organizationName: "Joe's Guns",
organizationName: "Joes Guns",
requiredHackingSkill: 10,
serverGrowth: 20,
},
@ -1305,7 +1306,7 @@ export const serverMetadata: IServerMetadata[] = [
},
networkLayer: 3,
numOpenPortsRequired: 2,
organizationName: "Omega Software",
organizationName: LocationName.IshimaOmegaSoftware,
requiredHackingSkill: {
max: 220,
min: 180,

@ -1,256 +0,0 @@
import { Player } from "./Player";
import { BitNodes } from "./BitNode/BitNode";
// Each SourceFile corresponds to a BitNode with the same number
function SourceFile(number, info="") {
var bitnodeKey = "BitNode" + number;
var bitnode = BitNodes[bitnodeKey];
if (bitnode == null) {
throw new Error("Invalid Bit Node for this Source File");
}
this.n = number;
this.name = "Source-File " + number + ": " + bitnode.name;
this.lvl = 1;
this.info = info;
this.owned = false;
}
let SourceFiles = {};
function initSourceFiles() {
SourceFiles = {};
SourceFiles["SourceFile1"] = new SourceFile(1, "This Source-File lets the player start with 32GB of RAM on his/her " +
"home computer. It also increases all of the player's multipliers by:<br><br>" +
"Level 1: 16%<br>" +
"Level 2: 24%<br>" +
"Level 3: 28%");
SourceFiles["SourceFile2"] = new SourceFile(2, "This Source-File allows you to form gangs in other BitNodes " +
"once your karma decreases to a certain value. It also increases the player's " +
"crime success rate, crime money, and charisma multipliers by:<br><br>" +
"Level 1: 24%<br>" +
"Level 2: 36%<br>" +
"Level 3: 42%");
SourceFiles["SourceFile3"] = new SourceFile(3,"This Source-File lets you create corporations on other BitNodes (although " +
"some BitNodes will disable this mechanic). This Source-File also increases your charisma and company salary multipliers by:<br>" +
"Level 1: 8%<br>" +
"Level 2: 12%<br>" +
"Level 3: 14%");
SourceFiles["SourceFile4"] = new SourceFile(4, "This Source-File lets you access and use the Singularity Functions in every BitNode. Every " +
"level of this Source-File opens up more of the Singularity Functions you can use.");
SourceFiles["SourceFile5"] = new SourceFile(5, "This Source-File grants a special new stat called Intelligence. Intelligence " +
"is unique because it is permanent and persistent (it never gets reset back to 1). However, " +
"gaining Intelligence experience is much slower than other stats, and it is also hidden (you won't " +
"know when you gain experience and how much). Higher Intelligence levels will boost your production " +
"for many actions in the game. In addition, this Source-File will unlock the getBitNodeMultipliers() " +
"Netscript function, and will raise all of your hacking-related multipliers by:<br><br> " +
"Level 1: 8%<br>" +
"Level 2: 12%<br>" +
"Level 3: 14%");
SourceFiles["SourceFile6"] = new SourceFile(6, "This Source-File allows you to access the NSA's Bladeburner Division in other " +
"BitNodes. In addition, this Source-File will raise both the level and experience gain rate of all your combat stats by:<br><br>" +
"Level 1: 8%<br>" +
"Level 2: 12%<br>" +
"Level 3: 14%");
SourceFiles["SourceFile7"] = new SourceFile(7, "This Source-File allows you to access the Bladeburner Netscript API in other " +
"BitNodes. In addition, this Source-File will increase all of your Bladeburner multipliers by:<br><br>" +
"Level 1: 8%<br>" +
"Level 2: 12%<br>" +
"Level 3: 14%");
SourceFiles["SourceFile8"] = new SourceFile(8, "This Source-File grants the following benefits:<br><br>" +
"Level 1: Permanent access to WSE and TIX API<br>" +
"Level 2: Ability to short stocks in other BitNodes<br>" +
"Level 3: Ability to use limit/stop orders in other BitNodes<br><br>" +
"This Source-File also increases your hacking growth multipliers by: " +
"<br>Level 1: 12%<br>Level 2: 18%<br>Level 3: 21%");
SourceFiles["SourceFile9"] = new SourceFile(9, "This Source-File grants the following benefits:<br><br>" +
"Level 1: Permanently unlocks the Hacknet Server in other BitNodes<br>" +
"Level 2: You start with 128GB of RAM on your home computer when entering a new BitNode<br>" +
"Level 3: Grants a highly-upgraded Hacknet Server when entering a new BitNode<br><br>" +
"(Note that the Level 3 effect of this Source-File only applies when entering a new BitNode, NOT " +
"when installing Augmentations)");
SourceFiles["SourceFile10"] = new SourceFile(10, "This Source-File unlocks Sleeve technology in other BitNodes. Each level of this " +
"Source-File also grants you a Duplicate Sleeve");
SourceFiles["SourceFile11"] = new SourceFile(11, "This Source-File makes it so that company favor increases BOTH the player's salary and reputation gain rate " +
"at that company by 1% per favor (rather than just the reputation gain). This Source-File also " +
" increases the player's company salary and reputation gain multipliers by:<br><br>" +
"Level 1: 32%<br>" +
"Level 2: 48%<br>" +
"Level 3: 56%<br>");
SourceFiles["SourceFile12"] = new SourceFile(12, "This Source-File increases all your multipliers by 1% per level. This effect is multiplicative with itself. " +
"In other words, level N of this Source-File will result in a multiplier of 1.01^N (or 0.99^N for multipliers that decrease)");
}
// Takes in a PlayerOwnedSourceFile as the "srcFile" argument
function applySourceFile(srcFile) {
var srcFileKey = "SourceFile" + srcFile.n;
var sourceFileObject = SourceFiles[srcFileKey];
if (sourceFileObject == null) {
console.log("ERROR: Invalid source file number: " + srcFile.n);
return;
}
switch(srcFile.n) {
case 1: // The Source Genesis
var mult = 0;
for (var i = 0; i < srcFile.lvl; ++i) {
mult += (16 / (Math.pow(2, i)));
}
var incMult = 1 + (mult / 100);
var decMult = 1 - (mult / 100);
Player.hacking_chance_mult *= incMult;
Player.hacking_speed_mult *= incMult;
Player.hacking_money_mult *= incMult;
Player.hacking_grow_mult *= incMult;
Player.hacking_mult *= incMult;
Player.strength_mult *= incMult;
Player.defense_mult *= incMult;
Player.dexterity_mult *= incMult;
Player.agility_mult *= incMult;
Player.charisma_mult *= incMult;
Player.hacking_exp_mult *= incMult;
Player.strength_exp_mult *= incMult;
Player.defense_exp_mult *= incMult;
Player.dexterity_exp_mult *= incMult;
Player.agility_exp_mult *= incMult;
Player.charisma_exp_mult *= incMult;
Player.company_rep_mult *= incMult;
Player.faction_rep_mult *= incMult;
Player.crime_money_mult *= incMult;
Player.crime_success_mult *= incMult;
Player.hacknet_node_money_mult *= incMult;
Player.hacknet_node_purchase_cost_mult *= decMult;
Player.hacknet_node_ram_cost_mult *= decMult;
Player.hacknet_node_core_cost_mult *= decMult;
Player.hacknet_node_level_cost_mult *= decMult;
Player.work_money_mult *= incMult;
break;
case 2: // Rise of the Underworld
var mult = 0;
for (var i = 0; i < srcFile.lvl; ++i) {
mult += (24 / (Math.pow(2, i)));
}
var incMult = 1 + (mult / 100);
Player.crime_money_mult *= incMult;
Player.crime_success_mult *= incMult;
Player.charisma_mult *= incMult;
break;
case 3: // Corporatocracy
var mult = 0;
for (var i = 0; i < srcFile.lvl; ++i) {
mult += (8 / (Math.pow(2, i)));
}
var incMult = 1 + (mult / 100);
Player.charisma_mult *= incMult;
Player.work_money_mult *= incMult;
break;
case 4: // The Singularity
// No effects, just gives access to Singularity functions
break;
case 5: // Artificial Intelligence
var mult = 0;
for (var i = 0; i < srcFile.lvl; ++i) {
mult += (8 / (Math.pow(2, i)));
}
var incMult = 1 + (mult / 100);
Player.hacking_chance_mult *= incMult;
Player.hacking_speed_mult *= incMult;
Player.hacking_money_mult *= incMult;
Player.hacking_grow_mult *= incMult;
Player.hacking_mult *= incMult;
Player.hacking_exp_mult *= incMult;
break;
case 6: // Bladeburner
var mult = 0;
for (var i = 0; i < srcFile.lvl; ++i) {
mult += (8 / (Math.pow(2, i)));
}
var incMult = 1 + (mult / 100);
Player.strength_exp_mult *= incMult;
Player.defense_exp_mult *= incMult;
Player.dexterity_exp_mult *= incMult;
Player.agility_exp_mult *= incMult;
Player.strength_mult *= incMult;
Player.defense_mult *= incMult;
Player.dexterity_mult *= incMult;
Player.agility_mult *= incMult;
break;
case 7: // Bladeburner 2079
var mult = 0;
for (var i = 0; i < srcFile.lvl; ++i) {
mult += (8 / (Math.pow(2, i)));
}
var incMult = 1 + (mult / 100);
Player.bladeburner_max_stamina_mult *= incMult;
Player.bladeburner_stamina_gain_mult *= incMult;
Player.bladeburner_analysis_mult *= incMult;
Player.bladeburner_success_chance_mult *= incMult;
break;
case 8: // Ghost of Wall Street
var mult = 0;
for (var i = 0; i < srcFile.lvl; ++i) {
mult += (12 / (Math.pow(2, i)));
}
var incMult = 1 + (mult / 100);
Player.hacking_grow_mult *= incMult;
break;
case 9: // Hacktocracy
// This has non-multiplier effects
break;
case 10: // Digital Carbon
// No effects, just grants sleeves
break;
case 11: // The Big Crash
var mult = 0;
for (var i = 0; i < srcFile.lvl; ++i) {
mult += (32 / (Math.pow(2, i)));
}
var incMult = 1 + (mult / 100);
Player.work_money_mult *= incMult;
Player.company_rep_mult *= incMult;
break;
case 12: // The Recursion
var inc = Math.pow(1.01, srcFile.lvl);
var dec = Math.pow(0.99, srcFile.lvl);
Player.hacking_chance_mult *= inc;
Player.hacking_speed_mult *= inc;
Player.hacking_money_mult *= inc;
Player.hacking_grow_mult *= inc;
Player.hacking_mult *= inc;
Player.strength_mult *= inc;
Player.defense_mult *= inc;
Player.dexterity_mult *= inc;
Player.agility_mult *= inc;
Player.charisma_mult *= inc;
Player.hacking_exp_mult *= inc;
Player.strength_exp_mult *= inc;
Player.defense_exp_mult *= inc;
Player.dexterity_exp_mult *= inc;
Player.agility_exp_mult *= inc;
Player.charisma_exp_mult *= inc;
Player.company_rep_mult *= inc;
Player.faction_rep_mult *= inc;
Player.crime_money_mult *= inc;
Player.crime_success_mult *= inc;
Player.hacknet_node_money_mult *= inc;
Player.hacknet_node_purchase_cost_mult *= dec;
Player.hacknet_node_ram_cost_mult *= dec;
Player.hacknet_node_core_cost_mult *= dec;
Player.hacknet_node_level_cost_mult *= dec;
Player.work_money_mult *= inc;
break;
default:
console.log("ERROR: Invalid source file number: " + srcFile.n);
break;
}
sourceFileObject.owned = true;
}
export {SourceFiles, applySourceFile, initSourceFiles};

@ -0,0 +1,21 @@
import { BitNodes } from "../BitNode/BitNode";
export class SourceFile {
info: string;
lvl: number = 1;
n: number;
name: string;
owned: boolean = false;
constructor(number: number, info: string="") {
const bitnodeKey = "BitNode" + number;
const bitnode = BitNodes[bitnodeKey];
if (bitnode == null) {
throw new Error("Invalid Bit Node for this Source File");
}
this.n = number;
this.name = `Source-File ${number}: ${bitnode.name}`
this.info = info;
}
}

@ -0,0 +1,64 @@
import { SourceFile } from "./SourceFile";
import { IMap } from "../types";
export const SourceFiles: IMap<SourceFile> = {};
SourceFiles["SourceFile1"] = new SourceFile(1, "This Source-File lets the player start with 32GB of RAM on his/her " +
"home computer. It also increases all of the player's multipliers by:<br><br>" +
"Level 1: 16%<br>" +
"Level 2: 24%<br>" +
"Level 3: 28%");
SourceFiles["SourceFile2"] = new SourceFile(2, "This Source-File allows you to form gangs in other BitNodes " +
"once your karma decreases to a certain value. It also increases the player's " +
"crime success rate, crime money, and charisma multipliers by:<br><br>" +
"Level 1: 24%<br>" +
"Level 2: 36%<br>" +
"Level 3: 42%");
SourceFiles["SourceFile3"] = new SourceFile(3,"This Source-File lets you create corporations on other BitNodes (although " +
"some BitNodes will disable this mechanic). This Source-File also increases your charisma and company salary multipliers by:<br>" +
"Level 1: 8%<br>" +
"Level 2: 12%<br>" +
"Level 3: 14%");
SourceFiles["SourceFile4"] = new SourceFile(4, "This Source-File lets you access and use the Singularity Functions in every BitNode. Every " +
"level of this Source-File opens up more of the Singularity Functions you can use.");
SourceFiles["SourceFile5"] = new SourceFile(5, "This Source-File grants a special new stat called Intelligence. Intelligence " +
"is unique because it is permanent and persistent (it never gets reset back to 1). However, " +
"gaining Intelligence experience is much slower than other stats, and it is also hidden (you won't " +
"know when you gain experience and how much). Higher Intelligence levels will boost your production " +
"for many actions in the game. In addition, this Source-File will unlock the getBitNodeMultipliers() " +
"Netscript function, and will raise all of your hacking-related multipliers by:<br><br> " +
"Level 1: 8%<br>" +
"Level 2: 12%<br>" +
"Level 3: 14%");
SourceFiles["SourceFile6"] = new SourceFile(6, "This Source-File allows you to access the NSA's Bladeburner Division in other " +
"BitNodes. In addition, this Source-File will raise both the level and experience gain rate of all your combat stats by:<br><br>" +
"Level 1: 8%<br>" +
"Level 2: 12%<br>" +
"Level 3: 14%");
SourceFiles["SourceFile7"] = new SourceFile(7, "This Source-File allows you to access the Bladeburner Netscript API in other " +
"BitNodes. In addition, this Source-File will increase all of your Bladeburner multipliers by:<br><br>" +
"Level 1: 8%<br>" +
"Level 2: 12%<br>" +
"Level 3: 14%");
SourceFiles["SourceFile8"] = new SourceFile(8, "This Source-File grants the following benefits:<br><br>" +
"Level 1: Permanent access to WSE and TIX API<br>" +
"Level 2: Ability to short stocks in other BitNodes<br>" +
"Level 3: Ability to use limit/stop orders in other BitNodes<br><br>" +
"This Source-File also increases your hacking growth multipliers by: " +
"<br>Level 1: 12%<br>Level 2: 18%<br>Level 3: 21%");
SourceFiles["SourceFile9"] = new SourceFile(9, "This Source-File grants the following benefits:<br><br>" +
"Level 1: Permanently unlocks the Hacknet Server in other BitNodes<br>" +
"Level 2: You start with 128GB of RAM on your home computer when entering a new BitNode<br>" +
"Level 3: Grants a highly-upgraded Hacknet Server when entering a new BitNode<br><br>" +
"(Note that the Level 3 effect of this Source-File only applies when entering a new BitNode, NOT " +
"when installing Augmentations)");
SourceFiles["SourceFile10"] = new SourceFile(10, "This Source-File unlocks Sleeve technology in other BitNodes. Each level of this " +
"Source-File also grants you a Duplicate Sleeve");
SourceFiles["SourceFile11"] = new SourceFile(11, "This Source-File makes it so that company favor increases BOTH the player's salary and reputation gain rate " +
"at that company by 1% per favor (rather than just the reputation gain). This Source-File also " +
" increases the player's company salary and reputation gain multipliers by:<br><br>" +
"Level 1: 32%<br>" +
"Level 2: 48%<br>" +
"Level 3: 56%<br>");
SourceFiles["SourceFile12"] = new SourceFile(12, "This Source-File increases all your multipliers by 1% per level. This effect is multiplicative with itself. " +
"In other words, level N of this Source-File will result in a multiplier of 1.01^N (or 0.99^N for multipliers that decrease)");

@ -0,0 +1,176 @@
import { PlayerOwnedSourceFile } from "./PlayerOwnedSourceFile";
import { SourceFiles } from "./SourceFiles";
import { Player } from "../Player";
export function applySourceFile(srcFile: PlayerOwnedSourceFile) {
const srcFileKey = "SourceFile" + srcFile.n;
const sourceFileObject = SourceFiles[srcFileKey];
if (sourceFileObject == null) {
console.error(`Invalid source file number: ${srcFile.n}`);
return;
}
switch (srcFile.n) {
case 1: // The Source Genesis
var mult = 0;
for (var i = 0; i < srcFile.lvl; ++i) {
mult += (16 / (Math.pow(2, i)));
}
var incMult = 1 + (mult / 100);
var decMult = 1 - (mult / 100);
Player.hacking_chance_mult *= incMult;
Player.hacking_speed_mult *= incMult;
Player.hacking_money_mult *= incMult;
Player.hacking_grow_mult *= incMult;
Player.hacking_mult *= incMult;
Player.strength_mult *= incMult;
Player.defense_mult *= incMult;
Player.dexterity_mult *= incMult;
Player.agility_mult *= incMult;
Player.charisma_mult *= incMult;
Player.hacking_exp_mult *= incMult;
Player.strength_exp_mult *= incMult;
Player.defense_exp_mult *= incMult;
Player.dexterity_exp_mult *= incMult;
Player.agility_exp_mult *= incMult;
Player.charisma_exp_mult *= incMult;
Player.company_rep_mult *= incMult;
Player.faction_rep_mult *= incMult;
Player.crime_money_mult *= incMult;
Player.crime_success_mult *= incMult;
Player.hacknet_node_money_mult *= incMult;
Player.hacknet_node_purchase_cost_mult *= decMult;
Player.hacknet_node_ram_cost_mult *= decMult;
Player.hacknet_node_core_cost_mult *= decMult;
Player.hacknet_node_level_cost_mult *= decMult;
Player.work_money_mult *= incMult;
break;
case 2: // Rise of the Underworld
var mult = 0;
for (var i = 0; i < srcFile.lvl; ++i) {
mult += (24 / (Math.pow(2, i)));
}
var incMult = 1 + (mult / 100);
Player.crime_money_mult *= incMult;
Player.crime_success_mult *= incMult;
Player.charisma_mult *= incMult;
break;
case 3: // Corporatocracy
var mult = 0;
for (var i = 0; i < srcFile.lvl; ++i) {
mult += (8 / (Math.pow(2, i)));
}
var incMult = 1 + (mult / 100);
Player.charisma_mult *= incMult;
Player.work_money_mult *= incMult;
break;
case 4: // The Singularity
// No effects, just gives access to Singularity functions
break;
case 5: // Artificial Intelligence
var mult = 0;
for (var i = 0; i < srcFile.lvl; ++i) {
mult += (8 / (Math.pow(2, i)));
}
var incMult = 1 + (mult / 100);
Player.hacking_chance_mult *= incMult;
Player.hacking_speed_mult *= incMult;
Player.hacking_money_mult *= incMult;
Player.hacking_grow_mult *= incMult;
Player.hacking_mult *= incMult;
Player.hacking_exp_mult *= incMult;
break;
case 6: // Bladeburner
var mult = 0;
for (var i = 0; i < srcFile.lvl; ++i) {
mult += (8 / (Math.pow(2, i)));
}
var incMult = 1 + (mult / 100);
Player.strength_exp_mult *= incMult;
Player.defense_exp_mult *= incMult;
Player.dexterity_exp_mult *= incMult;
Player.agility_exp_mult *= incMult;
Player.strength_mult *= incMult;
Player.defense_mult *= incMult;
Player.dexterity_mult *= incMult;
Player.agility_mult *= incMult;
break;
case 7: // Bladeburner 2079
var mult = 0;
for (var i = 0; i < srcFile.lvl; ++i) {
mult += (8 / (Math.pow(2, i)));
}
var incMult = 1 + (mult / 100);
Player.bladeburner_max_stamina_mult *= incMult;
Player.bladeburner_stamina_gain_mult *= incMult;
Player.bladeburner_analysis_mult *= incMult;
Player.bladeburner_success_chance_mult *= incMult;
break;
case 8: // Ghost of Wall Street
var mult = 0;
for (var i = 0; i < srcFile.lvl; ++i) {
mult += (12 / (Math.pow(2, i)));
}
var incMult = 1 + (mult / 100);
Player.hacking_grow_mult *= incMult;
break;
case 9: // Hacktocracy
// This has non-multiplier effects
break;
case 10: // Digital Carbon
// No effects, just grants sleeves
break;
case 11: // The Big Crash
var mult = 0;
for (var i = 0; i < srcFile.lvl; ++i) {
mult += (32 / (Math.pow(2, i)));
}
var incMult = 1 + (mult / 100);
Player.work_money_mult *= incMult;
Player.company_rep_mult *= incMult;
break;
case 12: // The Recursion
var inc = Math.pow(1.01, srcFile.lvl);
var dec = Math.pow(0.99, srcFile.lvl);
Player.hacking_chance_mult *= inc;
Player.hacking_speed_mult *= inc;
Player.hacking_money_mult *= inc;
Player.hacking_grow_mult *= inc;
Player.hacking_mult *= inc;
Player.strength_mult *= inc;
Player.defense_mult *= inc;
Player.dexterity_mult *= inc;
Player.agility_mult *= inc;
Player.charisma_mult *= inc;
Player.hacking_exp_mult *= inc;
Player.strength_exp_mult *= inc;
Player.defense_exp_mult *= inc;
Player.dexterity_exp_mult *= inc;
Player.agility_exp_mult *= inc;
Player.charisma_exp_mult *= inc;
Player.company_rep_mult *= inc;
Player.faction_rep_mult *= inc;
Player.crime_money_mult *= inc;
Player.crime_success_mult *= inc;
Player.hacknet_node_money_mult *= inc;
Player.hacknet_node_purchase_cost_mult *= dec;
Player.hacknet_node_ram_cost_mult *= dec;
Player.hacknet_node_core_cost_mult *= dec;
Player.hacknet_node_level_cost_mult *= dec;
Player.work_money_mult *= inc;
break;
default:
console.log("ERROR: Invalid source file number: " + srcFile.n);
break;
}
sourceFileObject.owned = true;
}

@ -6,8 +6,7 @@ import { Stock } from "./Stock";
import {
getBuyTransactionCost,
getSellTransactionGain,
processBuyTransactionPriceMovement,
processSellTransactionPriceMovement
processTransactionForecastMovement,
} from "./StockMarketHelpers";
import { PositionTypes } from "./data/PositionTypes";
@ -57,7 +56,7 @@ export function buyStock(stock: Stock, shares: number, workerScript: WorkerScrip
if (totalPrice == null) { return false; }
if (Player.money.lt(totalPrice)) {
if (tixApi) {
workerScript!.log(`ERROR: buyStock() failed because you do not have enough money to purchase this potiion. You need ${numeralWrapper.formatMoney(totalPrice)}`);
workerScript!.log(`ERROR: buyStock() failed because you do not have enough money to purchase this position. You need ${numeralWrapper.formatMoney(totalPrice)}`);
} else if (opts.suppressDialog !== true) {
dialogBoxCreate(`You do not have enough money to purchase this. You need ${numeralWrapper.formatMoney(totalPrice)}`);
}
@ -81,7 +80,7 @@ export function buyStock(stock: Stock, shares: number, workerScript: WorkerScrip
const newTotal = origTotal + totalPrice - CONSTANTS.StockMarketCommission;
stock.playerShares = Math.round(stock.playerShares + shares);
stock.playerAvgPx = newTotal / stock.playerShares;
processBuyTransactionPriceMovement(stock, shares, PositionTypes.Long);
processTransactionForecastMovement(stock, shares);
if (opts.rerenderFn != null && typeof opts.rerenderFn === "function") {
opts.rerenderFn();
}
@ -138,7 +137,7 @@ export function sellStock(stock: Stock, shares: number, workerScript: WorkerScri
stock.playerAvgPx = 0;
}
processSellTransactionPriceMovement(stock, shares, PositionTypes.Long);
processTransactionForecastMovement(stock, shares);
if (opts.rerenderFn != null && typeof opts.rerenderFn === "function") {
opts.rerenderFn();
@ -211,7 +210,7 @@ export function shortStock(stock: Stock, shares: number, workerScript: WorkerScr
const newTotal = origTotal + totalPrice - CONSTANTS.StockMarketCommission;
stock.playerShortShares = Math.round(stock.playerShortShares + shares);
stock.playerAvgShortPx = newTotal / stock.playerShortShares;
processBuyTransactionPriceMovement(stock, shares, PositionTypes.Short);
processTransactionForecastMovement(stock, shares);
if (opts.rerenderFn != null && typeof opts.rerenderFn === "function") {
opts.rerenderFn();
@ -278,7 +277,7 @@ export function sellShort(stock: Stock, shares: number, workerScript: WorkerScri
if (stock.playerShortShares === 0) {
stock.playerAvgShortPx = 0;
}
processSellTransactionPriceMovement(stock, shares, PositionTypes.Short);
processTransactionForecastMovement(stock, shares);
if (opts.rerenderFn != null && typeof opts.rerenderFn === "function") {
opts.rerenderFn();

@ -5,6 +5,7 @@ export type IStockMarket = {
[key: string]: Stock;
} & {
lastUpdate: number;
storedCycles: number;
Orders: IOrderBook;
}
storedCycles: number;
ticksUntilCycle: number;
};

@ -22,7 +22,7 @@ import { numeralWrapper } from "../ui/numeralFormat";
import { dialogBoxCreate } from "../../utils/DialogBox";
interface IProcessOrderRefs {
export interface IProcessOrderRefs {
rerenderFn: () => void;
stockMarket: IStockMarket;
symbolToStockMap: IMap<Stock>;
@ -49,7 +49,7 @@ export function processOrders(stock: Stock, orderType: OrderTypes, posType: Posi
}
let stockOrders = orderBook[stock.symbol];
if (stockOrders == null || !(stockOrders.constructor === Array)) {
console.error(`Invalid Order book for ${stock.symbol} in processOrders()`);
console.error(`Invalid Order book for ${stock.symbol} in processOrders(): ${stockOrders}`);
stockOrders = [];
return;
}
@ -96,7 +96,7 @@ export function processOrders(stock: Stock, orderType: OrderTypes, posType: Posi
/**
* Execute a Stop or Limit Order.
* @param {Order} order - Order being executed
* @param {IStockMarket} stockMarket - Reference to StockMarket object
* @param {IProcessOrderRefs} refs - References to objects/functions that are required for this function
*/
function executeOrder(order: Order, refs: IProcessOrderRefs) {
const stock = refs.symbolToStockMap[order.stockSymbol];
@ -107,8 +107,6 @@ function executeOrder(order: Order, refs: IProcessOrderRefs) {
const stockMarket = refs.stockMarket;
const orderBook = stockMarket["Orders"];
const stockOrders = orderBook[stock.symbol];
const isLimit = (order.type === OrderTypes.LimitBuy || order.type === OrderTypes.LimitSell);
let sharesTransacted = 0;
// When orders are executed, the buying and selling functions shouldn't
// emit popup dialog boxes. This options object configures the functions for that
@ -120,124 +118,37 @@ function executeOrder(order: Order, refs: IProcessOrderRefs) {
let res = true;
let isBuy = false;
switch (order.type) {
case OrderTypes.LimitBuy: {
case OrderTypes.LimitBuy:
case OrderTypes.StopBuy:
isBuy = true;
// We only execute limit orders until the price fails to match the order condition
const isLong = (order.pos === PositionTypes.Long);
const firstShares = Math.min(order.shares, isLong ? stock.shareTxUntilMovementUp : stock.shareTxUntilMovementDown);
// First transaction to trigger movement
let res = (isLong ? buyStock(stock, firstShares, null, opts) : shortStock(stock, firstShares, null, opts));
if (res) {
sharesTransacted = firstShares;
} else {
break;
}
let remainingShares = order.shares - firstShares;
let remainingIterations = Math.ceil(remainingShares / stock.shareTxForMovement);
for (let i = 0; i < remainingIterations; ++i) {
if (isLong && stock.price > order.price) {
break;
} else if (!isLong && stock.price < order.price) {
break;
}
const shares = Math.min(remainingShares, stock.shareTxForMovement);
let res = (isLong ? buyStock(stock, shares, null, opts) : shortStock(stock, shares, null, opts));
if (res) {
sharesTransacted += shares;
remainingShares -= shares;
} else {
break;
}
}
break;
}
case OrderTypes.StopBuy: {
isBuy = true;
sharesTransacted = order.shares;
if (order.pos === PositionTypes.Long) {
res = buyStock(stock, order.shares, null, opts) && res;
} else if (order.pos === PositionTypes.Short) {
res = shortStock(stock, order.shares, null, opts) && res;
}
break;
}
case OrderTypes.LimitSell: {
// TODO need to account for player's shares here
// We only execute limit orders until the price fails to match the order condition
const isLong = (order.pos === PositionTypes.Long);
const totalShares = Math.min((isLong ? stock.playerShares : stock.playerShortShares), order.shares);
if (totalShares === 0) {
return; // Player has no shares
}
const firstShares = Math.min(totalShares, isLong ? stock.shareTxUntilMovementDown : stock.shareTxUntilMovementUp);
// First transaction to trigger movement
if (isLong ? sellStock(stock, firstShares, null, opts) : sellShort(stock, firstShares, null, opts)) {
sharesTransacted = firstShares;
} else {
break;
}
let remainingShares = totalShares - firstShares;
let remainingIterations = Math.ceil(remainingShares / stock.shareTxForMovement);
for (let i = 0; i < remainingIterations; ++i) {
if (isLong && stock.price < order.price) {
break;
} else if (!isLong && stock.price > order.price) {
break;
}
const shares = Math.min(remainingShares, stock.shareTxForMovement);
if (isLong ? sellStock(stock, shares, null, opts) : sellShort(stock, shares, null, opts)) {
sharesTransacted += shares;
remainingShares -= shares;
} else {
break;
}
}
break;
}
case OrderTypes.StopSell: {
case OrderTypes.LimitSell:
case OrderTypes.StopSell:
if (order.pos === PositionTypes.Long) {
sharesTransacted = Math.min(stock.playerShares, order.shares);
if (sharesTransacted <= 0) { return; }
res = sellStock(stock, sharesTransacted, null, opts) && res;
res = sellStock(stock, order.shares, null, opts) && res;
} else if (order.pos === PositionTypes.Short) {
sharesTransacted = Math.min(stock.playerShortShares, order.shares);
if (sharesTransacted <= 0) { return; }
res = sellShort(stock, sharesTransacted, null, opts) && res;
res = sellShort(stock, order.shares, null, opts) && res;
}
break;
}
default:
console.warn(`Invalid order type: ${order.type}`);
return;
}
if (isLimit) {
res = (sharesTransacted > 0);
}
// Position type, for logging/message purposes
const pos = order.pos === PositionTypes.Long ? "Long" : "Short";
if (res) {
for (let i = 0; i < stockOrders.length; ++i) {
if (order == stockOrders[i]) {
// Limit orders might only transact a certain # of shares, so we have the adjust the order qty.
stockOrders[i].shares -= sharesTransacted;
if (stockOrders[i].shares <= 0) {
stockOrders.splice(i, 1);
dialogBoxCreate(`${order.type} for ${stock.symbol} @ ${numeralWrapper.formatMoney(order.price)} (${pos}) was filled ` +
`(${numeralWrapper.formatBigNumber(Math.round(sharesTransacted))} share)`);
} else {
dialogBoxCreate(`${order.type} for ${stock.symbol} @ ${numeralWrapper.formatMoney(order.price)} (${pos}) was partially filled ` +
`(${numeralWrapper.formatBigNumber(Math.round(sharesTransacted))} shares transacted, ${stockOrders[i].shares} shares remaining`);
}
stockOrders.splice(i, 1);
dialogBoxCreate(`${order.type} for ${stock.symbol} @ ${numeralWrapper.formatMoney(order.price)} (${pos}) was filled ` +
`(${numeralWrapper.formatBigNumber(Math.round(order.shares))} shares)`);
refs.rerenderFn();
return;
}
@ -249,9 +160,6 @@ function executeOrder(order: Order, refs: IProcessOrderRefs) {
if (isBuy) {
dialogBoxCreate(`Failed to execute ${order.type} for ${stock.symbol} @ ${numeralWrapper.formatMoney(order.price)} (${pos}). ` +
`This is most likely because you do not have enough money or the order would exceed the stock's maximum number of shares`);
} else {
dialogBoxCreate(`Failed to execute ${order.type} for ${stock.symbol} @ ${numeralWrapper.formatMoney(order.price)} (${pos}). ` +
`This may be a bug, please report to game developer with details.`);
}
}
}

@ -0,0 +1,78 @@
/**
* Implementation of the mechanisms that allow the player to affect the
* Stock Market
*/
import { Stock } from "./Stock";
import { StockMarket } from "./StockMarket";
import { Company } from "../Company/Company";
import { Server } from "../Server/Server";
// Change in second-order forecast due to hacks/grows
export const forecastForecastChangeFromHack = 0.1;
// Change in second-order forecast due to company work
export const forecastForecastChangeFromCompanyWork = 0.001;
/**
* Potentially decreases a stock's second-order forecast when its corresponding
* server is hacked. The chance of the hack decreasing the stock's second-order
* forecast is dependent on what percentage of the server's money is hacked
* @param {Server} server - Server being hack()ed
* @param {number} moneyHacked - Amount of money stolen from the server
*/
export function influenceStockThroughServerHack(server: Server, moneyHacked: number): void {
const orgName = server.organizationName;
let stock: Stock | null = null;
if (typeof orgName === "string" && orgName !== "") {
stock = StockMarket[orgName];
}
if (!(stock instanceof Stock)) { return; }
const percTotalMoneyHacked = moneyHacked / server.moneyMax;
if (Math.random() < percTotalMoneyHacked) {
stock.changeForecastForecast(stock.otlkMagForecast - forecastForecastChangeFromHack);
}
}
/**
* Potentially increases a stock's second-order forecast when its corresponding
* server is grown (grow()). The chance of the grow() to increase the stock's
* second-order forecast is dependent on how much money is added to the server
* @param {Server} server - Server being grow()n
* @param {number} moneyHacked - Amount of money added to the server
*/
export function influenceStockThroughServerGrow(server: Server, moneyGrown: number): void {
const orgName = server.organizationName;
let stock: Stock | null = null;
if (typeof orgName === "string" && orgName !== "") {
stock = StockMarket[orgName];
}
if (!(stock instanceof Stock)) { return; }
const percTotalMoneyGrown = moneyGrown / server.moneyMax;
if (Math.random() < percTotalMoneyGrown) {
stock.changeForecastForecast(stock.otlkMagForecast + forecastForecastChangeFromHack);
}
}
/**
* Potentially increases a stock's second-order forecast when the player works for
* its corresponding company.
* @param {Company} company - Company being worked for
* @param {number} performanceMult - Effectiveness of player's work. Affects influence
* @param {number} cyclesOfWork - # game cycles of work being processed
*/
export function influenceStockThroughCompanyWork(company: Company, performanceMult: number, cyclesOfWork: number): void {
const compName = company.name;
let stock: Stock | null = null;
if (typeof compName === "string" && compName !== "") {
stock = StockMarket[compName];
}
if (!(stock instanceof Stock)) { return; }
if (Math.random() < 0.002 * cyclesOfWork) {
const change = forecastForecastChangeFromCompanyWork * performanceMult;
stock.changeForecastForecast(stock.otlkMagForecast + change);
}
}

@ -6,6 +6,8 @@ import {
} from "../../utils/JSONReviver";
import { getRandomInt } from "../../utils/helpers/getRandomInt";
export const StockForecastInfluenceLimit = 5;
export interface IConstructorParams {
b: boolean;
initPrice: number | IMinMaxRange;
@ -100,6 +102,12 @@ export class Stock {
*/
otlkMag: number;
/**
* Forecast of outlook magnitude. Essentially a second-order forecast.
* Unlike 'otlkMag', this number is on an absolute scale from 0-100 (rather than 0-50)
*/
otlkMagForecast: number;
/**
* Average price of stocks that the player owns in the LONG position
*/
@ -125,12 +133,6 @@ export class Stock {
*/
price: number;
/**
* Percentage by which the stock's price changes for a transaction-induced
* price movement.
*/
readonly priceMovementPerc: number;
/**
* How many shares need to be transacted in order to trigger a price movement
*/
@ -140,8 +142,7 @@ export class Stock {
* How many share transactions remaining until a price movement occurs
* (separately tracked for upward and downward movements)
*/
shareTxUntilMovementDown: number;
shareTxUntilMovementUp: number;
shareTxUntilMovement: number;
/**
* Spread percentage. The bid/ask prices for this stock are N% above or below
@ -173,12 +174,11 @@ export class Stock {
this.mv = toNumber(p.mv);
this.b = p.b;
this.otlkMag = p.otlkMag;
this.otlkMagForecast = this.getAbsoluteForecast();
this.cap = getRandomInt(this.price * 1e3, this.price * 25e3);
this.spreadPerc = toNumber(p.spreadPerc);
this.priceMovementPerc = this.spreadPerc / (getRandomInt(10, 30) / 10);
this.shareTxForMovement = toNumber(p.shareTxForMovement);
this.shareTxUntilMovementDown = this.shareTxForMovement;
this.shareTxUntilMovementUp = this.shareTxForMovement;
this.shareTxUntilMovement = this.shareTxForMovement;
// Total shares is determined by market cap, and is rounded to nearest 100k
let totalSharesUnrounded: number = (p.marketCap / this.price);
@ -189,11 +189,86 @@ export class Stock {
this.maxShares = Math.round((this.totalShares * outstandingSharePercentage) / 1e5) * 1e5;
}
/**
* Safely set the stock's second-order forecast to a new value
*/
changeForecastForecast(newff: number): void {
this.otlkMagForecast = newff;
if (this.otlkMagForecast > 100) {
this.otlkMagForecast = 100;
} else if (this.otlkMagForecast < 0) {
this.otlkMagForecast = 0;
}
}
/**
* Set the stock to a new price. Also updates the stock's previous price tracker
*/
changePrice(newPrice: number): void {
this.lastPrice = this.price;
this.price = newPrice;
}
/**
* Change the stock's forecast during a stock market 'tick'.
* The way a stock's forecast changes depends on various internal properties,
* but is ultimately determined by RNG
*/
cycleForecast(changeAmt: number=0.1): void {
const increaseChance = this.getForecastIncreaseChance();
if (Math.random() < increaseChance) {
// Forecast increases
if (this.b) {
this.otlkMag += changeAmt;
} else {
this.otlkMag -= changeAmt;
}
} else {
// Forecast decreases
if (this.b) {
this.otlkMag -= changeAmt;
} else {
this.otlkMag += changeAmt;
}
}
this.otlkMag = Math.min(this.otlkMag, 50);
if (this.otlkMag < 0) {
this.otlkMag *= -1;
this.b = !this.b;
}
}
/**
* Change's the stock's second-order forecast during a stock market 'tick'.
* The change for the second-order forecast to increase is 50/50
*/
cycleForecastForecast(changeAmt: number=0.1): void {
if (Math.random() < 0.5) {
this.changeForecastForecast(this.otlkMagForecast + changeAmt);
} else {
this.changeForecastForecast(this.otlkMagForecast - changeAmt);
}
}
/**
* "Flip" the stock's second-order forecast. This can occur during a
* stock market "cycle" (determined by RNG). It is used to simulate
* RL stock market cycles and introduce volatility
*/
flipForecastForecast(): void {
const diff = this.otlkMagForecast - 50;
this.otlkMagForecast = 50 + (-1 * diff);
}
/**
* Returns the stock's absolute forecast, which is a number between 0-100
*/
getAbsoluteForecast(): number {
return this.b ? 50 + this.otlkMag : 50 - this.otlkMag;
}
/**
* Return the price at which YOUR stock is bought (market ask price). Accounts for spread
*/
@ -208,6 +283,41 @@ export class Stock {
return this.price * (1 - (this.spreadPerc / 100));
}
/**
* Returns the chance (0-1 decimal) that a stock has of having its forecast increase
*/
getForecastIncreaseChance(): number {
const diff = this.otlkMagForecast - this.getAbsoluteForecast();
return (50 + Math.min(Math.max(diff, -45), 45)) / 100;
}
/**
* Changes a stock's forecast. This is used when the stock is influenced
* by a transaction. The stock's forecast always goes towards 50, but the
* movement is capped by a certain threshold/limit
*/
influenceForecast(change: number): void {
if (this.otlkMag > StockForecastInfluenceLimit) {
this.otlkMag = Math.max(StockForecastInfluenceLimit, this.otlkMag - change);
}
}
/**
* Changes a stock's second-order forecast. This is used when the stock is
* influenced by a transaction. The stock's second-order forecast always
* goes towards 50.
*/
influenceForecastForecast(change: number): void {
if (this.otlkMagForecast > 50) {
this.otlkMagForecast -= change;
this.otlkMagForecast = Math.max(50, this.otlkMagForecast);
} else if (this.otlkMagForecast < 50) {
this.otlkMagForecast += change;
this.otlkMagForecast = Math.min(50, this.otlkMagForecast);
}
}
/**
* Serialize the Stock to a JSON save state.
*/

@ -4,19 +4,12 @@ import {
shortStock,
sellShort,
} from "./BuyingAndSelling";
import { IOrderBook } from "./IOrderBook";
import { IStockMarket } from "./IStockMarket";
import { Order } from "./Order";
import { processOrders } from "./OrderProcessing";
import { Stock } from "./Stock";
import {
getBuyTransactionCost,
getSellTransactionGain,
processBuyTransactionPriceMovement,
processSellTransactionPriceMovement
} from "./StockMarketHelpers";
import {
getStockMarket4SDataCost,
getStockMarket4STixApiCost
} from "./StockMarketCosts";
import { TicksPerCycle } from "./StockMarketConstants";
import { InitStockMetadata } from "./data/InitStockMetadata";
import { OrderTypes } from "./data/OrderTypes";
import { PositionTypes } from "./data/PositionTypes";
@ -26,6 +19,7 @@ import { StockMarketRoot } from "./ui/Root";
import { CONSTANTS } from "../Constants";
import { WorkerScript } from "../Netscript/WorkerScript";
import { Player } from "../Player";
import { IMap } from "../types";
import { Page, routing } from ".././ui/navigationTracking";
import { numeralWrapper } from ".././ui/numeralFormat";
@ -33,17 +27,17 @@ import { numeralWrapper } from ".././ui/numeralFormat";
import { dialogBoxCreate } from "../../utils/DialogBox";
import { Reviver } from "../../utils/JSONReviver";
import React from "react";
import ReactDOM from "react-dom";
import * as React from "react";
import * as ReactDOM from "react-dom";
export let StockMarket = {}; // Maps full stock name -> Stock object
export let SymbolToStockMap = {}; // Maps symbol -> Stock object
export let StockMarket: IStockMarket | IMap<any> = {}; // Maps full stock name -> Stock object
export let SymbolToStockMap: IMap<Stock> = {}; // Maps symbol -> Stock object
export function placeOrder(stock, shares, price, type, position, workerScript=null) {
export function placeOrder(stock: Stock, shares: number, price: number, type: OrderTypes, position: PositionTypes, workerScript: WorkerScript | null=null): boolean {
const tixApi = (workerScript instanceof WorkerScript);
if (!(stock instanceof Stock)) {
if (tixApi) {
workerScript.log(`ERROR: Invalid stock passed to placeOrder() function`);
workerScript!.log(`ERROR: Invalid stock passed to placeOrder() function`);
} else {
dialogBoxCreate(`ERROR: Invalid stock passed to placeOrder() function`);
}
@ -51,7 +45,7 @@ export function placeOrder(stock, shares, price, type, position, workerScript=nu
}
if (typeof shares !== "number" || typeof price !== "number") {
if (tixApi) {
workerScript.log("ERROR: Invalid numeric value provided for either 'shares' or 'price' argument");
workerScript!.log("ERROR: Invalid numeric value provided for either 'shares' or 'price' argument");
} else {
dialogBoxCreate("ERROR: Invalid numeric value provided for either 'shares' or 'price' argument");
}
@ -60,7 +54,7 @@ export function placeOrder(stock, shares, price, type, position, workerScript=nu
const order = new Order(stock.symbol, shares, price, type, position);
if (StockMarket["Orders"] == null) {
const orders = {};
const orders: IOrderBook = {};
for (const name in StockMarket) {
const stk = StockMarket[name];
if (!(stk instanceof Stock)) { continue; }
@ -73,7 +67,7 @@ export function placeOrder(stock, shares, price, type, position, workerScript=nu
// Process to see if it should be executed immediately
const processOrderRefs = {
rerenderFn: displayStockMarketContent,
stockMarket: StockMarket,
stockMarket: StockMarket as IStockMarket,
symbolToStockMap: SymbolToStockMap,
}
processOrders(stock, order.type, order.pos, processOrderRefs);
@ -83,7 +77,15 @@ export function placeOrder(stock, shares, price, type, position, workerScript=nu
}
// Returns true if successfully cancels an order, false otherwise
export function cancelOrder(params, workerScript=null) {
interface ICancelOrderParams {
order?: Order;
pos?: PositionTypes;
price?: number;
shares?: number;
stock?: Stock;
type?: OrderTypes;
}
export function cancelOrder(params: ICancelOrderParams, workerScript: WorkerScript | null=null): boolean {
var tixApi = (workerScript instanceof WorkerScript);
if (StockMarket["Orders"] == null) {return false;}
if (params.order && params.order instanceof Order) {
@ -113,42 +115,24 @@ export function cancelOrder(params, workerScript=null) {
stockOrders.splice(i, 1);
displayStockMarketContent();
if (tixApi) {
workerScript.scriptRef.log("Successfully cancelled order: " + orderTxt);
workerScript!.scriptRef.log("Successfully cancelled order: " + orderTxt);
}
return true;
}
}
if (tixApi) {
workerScript.scriptRef.log("Failed to cancel order: " + orderTxt);
workerScript!.scriptRef.log("Failed to cancel order: " + orderTxt);
}
return false;
}
return false;
}
export function loadStockMarket(saveString) {
export function loadStockMarket(saveString: string) {
if (saveString === "") {
StockMarket = {};
} else {
StockMarket = JSON.parse(saveString, Reviver);
// Backwards compatibility for v0.47.0
const orderBook = StockMarket["Orders"];
if (orderBook != null) {
// For each order, set its 'stockSymbol' property equal to the
// symbol of its 'stock' property
for (const stockSymbol in orderBook) {
const ordersForStock = orderBook[stockSymbol];
if (Array.isArray(ordersForStock)) {
for (const order of ordersForStock) {
if (order instanceof Order && order.stock instanceof Stock) {
order.stockSymbol = order.stock.symbol;
}
}
}
}
console.log(`Converted Stock Market order book to v0.47.0 format`);
}
}
}
@ -168,7 +152,7 @@ export function initStockMarket() {
StockMarket[name] = new Stock(metadata);
}
const orders = {};
const orders: IOrderBook = {};
for (const name in StockMarket) {
const stock = StockMarket[name];
if (!(stock instanceof Stock)) { continue; }
@ -178,6 +162,7 @@ export function initStockMarket() {
StockMarket.storedCycles = 0;
StockMarket.lastUpdate = 0;
StockMarket.ticksUntilCycle = TicksPerCycle;
}
export function initSymbolToStockMap() {
@ -198,12 +183,14 @@ export function stockMarketCycle() {
for (const name in StockMarket) {
const stock = StockMarket[name];
if (!(stock instanceof Stock)) { continue; }
let thresh = 0.6;
if (stock.b) { thresh = 0.4; }
if (Math.random() < thresh) {
const roll = Math.random();
if (roll < 0.45) {
stock.b = !stock.b;
if (stock.otlkMag < 5) { stock.otlkMag += 0.1; }
stock.flipForecastForecast();
}
StockMarket.ticksUntilCycle = TicksPerCycle;
}
}
@ -224,6 +211,15 @@ export function processStockPrices(numCycles=1) {
StockMarket.lastUpdate = timeNow;
StockMarket.storedCycles -= cyclesPerStockUpdate;
// Cycle
if (StockMarket.ticksUntilCycle == null || typeof StockMarket.ticksUntilCycle !== "number") {
StockMarket.ticksUntilCycle = TicksPerCycle;
}
--StockMarket.ticksUntilCycle;
if (StockMarket.ticksUntilCycle <= 0) {
stockMarketCycle();
}
var v = Math.random();
for (const name in StockMarket) {
const stock = StockMarket[name];
@ -246,7 +242,7 @@ export function processStockPrices(numCycles=1) {
const c = Math.random();
const processOrderRefs = {
rerenderFn: displayStockMarketContent,
stockMarket: StockMarket,
stockMarket: StockMarket as IStockMarket,
symbolToStockMap: SymbolToStockMap,
}
if (c < chc) {
@ -264,29 +260,24 @@ export function processStockPrices(numCycles=1) {
}
let otlkMagChange = stock.otlkMag * av;
if (stock.otlkMag <= 0.1) {
otlkMagChange = 1;
}
if (c < 0.5) {
stock.otlkMag += otlkMagChange;
} else {
stock.otlkMag -= otlkMagChange;
}
if (stock.otlkMag > 50) { stock.otlkMag = 50; } // Cap so the "forecast" is between 0 and 100
if (stock.otlkMag < 0) {
stock.otlkMag *= -1;
stock.b = !stock.b;
if (stock.otlkMag < 5) {
if (stock.otlkMag <= 1) {
otlkMagChange = 1;
} else {
otlkMagChange *= 10;
}
}
stock.cycleForecast(otlkMagChange);
stock.cycleForecastForecast(otlkMagChange / 2);
// Shares required for price movement gradually approaches max over time
stock.shareTxUntilMovement = Math.min(stock.shareTxUntilMovementUp + 5, stock.shareTxForMovement);
stock.shareTxUntilMovement = Math.min(stock.shareTxUntilMovementDown + 5, stock.shareTxForMovement);
stock.shareTxUntilMovement = Math.min(stock.shareTxUntilMovement + 10, stock.shareTxForMovement);
}
displayStockMarketContent();
}
let stockMarketContainer = null;
let stockMarketContainer: HTMLElement | null = null;
function setStockMarketContainer() {
stockMarketContainer = document.getElementById("stock-market-container");
document.removeEventListener("DOMContentLoaded", setStockMarketContainer);
@ -305,6 +296,7 @@ export function displayStockMarketContent() {
}
if (stockMarketContainer instanceof HTMLElement) {
const castedStockMarket = StockMarket as IStockMarket;
ReactDOM.render(
<StockMarketRoot
buyStockLong={buyStock}
@ -315,7 +307,7 @@ export function displayStockMarketContent() {
placeOrder={placeOrder}
sellStockLong={sellStock}
sellStockShort={sellShort}
stockMarket={StockMarket}
stockMarket={castedStockMarket}
/>,
stockMarketContainer
)

@ -0,0 +1,5 @@
/**
* How many stock market 'ticks' before a 'cycle' is triggered.
* A 'tick' is whenver stock prices update
*/
export const TicksPerCycle = 75;

@ -1,39 +1,15 @@
/**
* Stock Market Helper Functions
*/
import { Stock } from "./Stock";
import { PositionTypes } from "./data/PositionTypes";
import { CONSTANTS } from "../Constants";
// Amount by which a stock's forecast changes during each price movement
export const forecastChangePerPriceMovement = 0.005;
export const forecastChangePerPriceMovement = 0.006;
/**
* Given a stock, calculates the amount by which the stock price is multiplied
* for an 'upward' price movement. This does not actually increase the stock's price,
* just calculates the multiplier
* @param {Stock} stock - Stock for price movement
* @returns {number | null} Number by which stock's price should be multiplied. Null for invalid args
*/
export function calculateIncreasingPriceMovement(stock: Stock): number | null {
if (!(stock instanceof Stock)) { return null; }
return (1 + (stock.priceMovementPerc / 100));
}
/**
* Given a stock, calculates the amount by which the stock price is multiplied
* for a "downward" price movement. This does not actually increase the stock's price,
* just calculates the multiplier
* @param {Stock} stock - Stock for price movement
* @returns {number | null} Number by which stock's price should be multiplied. Null for invalid args
*/
export function calculateDecreasingPriceMovement(stock: Stock): number | null {
if (!(stock instanceof Stock)) { return null; }
return (1 - (stock.priceMovementPerc / 100));
}
/**
* Calculate the total cost of a "buy" transaction. This accounts for spread,
* price movements, and commission.
* Calculate the total cost of a "buy" transaction. This accounts for spread and commission.
* @param {Stock} stock - Stock being purchased
* @param {number} shares - Number of shares being transacted
* @param {PositionTypes} posType - Long or short position
@ -50,135 +26,15 @@ export function getBuyTransactionCost(stock: Stock, shares: number, posType: Pos
// If the number of shares doesn't trigger a price movement, its a simple calculation
if (isLong) {
if (shares <= stock.shareTxUntilMovementUp) {
return (shares * stock.getAskPrice()) + CONSTANTS.StockMarketCommission;
}
return (shares * stock.getAskPrice()) + CONSTANTS.StockMarketCommission;
} else {
if (shares <= stock.shareTxUntilMovementDown) {
return (shares * stock.getBidPrice()) + CONSTANTS.StockMarketCommission;
}
}
// Calculate how many iterations of price changes we need to account for
const firstShares = isLong ? stock.shareTxUntilMovementUp : stock.shareTxUntilMovementDown;
let remainingShares = shares - firstShares;
let numIterations = 1 + Math.ceil(remainingShares / stock.shareTxForMovement);
// The initial cost calculation takes care of the first "iteration"
let currPrice = isLong ? stock.getAskPrice() : stock.getBidPrice();
let totalCost = (firstShares * currPrice);
const increasingMvmt = calculateIncreasingPriceMovement(stock)!;
const decreasingMvmt = calculateDecreasingPriceMovement(stock)!;
function processPriceMovement() {
if (isLong) {
currPrice *= increasingMvmt;
} else {
currPrice *= decreasingMvmt;
}
}
for (let i = 1; i < numIterations; ++i) {
processPriceMovement();
const amt = Math.min(stock.shareTxForMovement, remainingShares);
totalCost += (amt * currPrice);
remainingShares -= amt;
}
return totalCost + CONSTANTS.StockMarketCommission;
}
/**
* Processes a buy transaction's resulting price AND forecast movement.
* @param {Stock} stock - Stock being purchased
* @param {number} shares - Number of shares being transacted
* @param {PositionTypes} posType - Long or short position
*/
export function processBuyTransactionPriceMovement(stock: Stock, shares: number, posType: PositionTypes): void {
if (isNaN(shares) || shares <= 0 || !(stock instanceof Stock)) { return; }
// Cap the 'shares' arg at the stock's maximum shares. This'll prevent
// hanging in the case when a really big number is passed in
shares = Math.min(shares, stock.maxShares);
const isLong = (posType === PositionTypes.Long);
let currPrice = stock.price;
function processPriceMovement() {
if (isLong) {
currPrice *= calculateIncreasingPriceMovement(stock)!;
} else {
currPrice *= calculateDecreasingPriceMovement(stock)!;
}
}
// If there's only going to be one iteration
const firstShares = isLong ? stock.shareTxUntilMovementUp : stock.shareTxUntilMovementDown;
if (shares <= firstShares) {
function triggerMovement() {
processPriceMovement();
stock.changePrice(currPrice);
stock.otlkMag -= (forecastChangePerPriceMovement);
}
if (isLong) {
stock.shareTxUntilMovementUp -= shares;
if (stock.shareTxUntilMovementUp <= 0) {
stock.shareTxUntilMovementUp = stock.shareTxForMovement;
triggerMovement();
}
} else {
stock.shareTxUntilMovementDown -= shares;
if (stock.shareTxUntilMovementDown <= 0) {
stock.shareTxUntilMovementDown = stock.shareTxForMovement;
triggerMovement();
}
}
return;
}
// Calculate how many iterations of price changes we need to account for
let remainingShares = shares - firstShares;
let numIterations = 1 + Math.ceil(remainingShares / stock.shareTxForMovement);
for (let i = 1; i < numIterations; ++i) {
processPriceMovement();
}
// If on the offchance we end up perfectly at the next price movement
if (isLong) {
stock.shareTxUntilMovementUp = stock.shareTxForMovement - ((shares - stock.shareTxUntilMovementUp) % stock.shareTxForMovement);
if (stock.shareTxUntilMovementUp === stock.shareTxForMovement || stock.shareTxUntilMovementUp <= 0) {
// The shareTxUntilMovementUp ended up at 0 at the end of the "processing"
++numIterations;
stock.shareTxUntilMovementUp = stock.shareTxForMovement;
processPriceMovement();
}
} else {
stock.shareTxUntilMovementDown = stock.shareTxForMovement - ((shares - stock.shareTxUntilMovementDown) % stock.shareTxForMovement);
if (stock.shareTxUntilMovementDown === stock.shareTxForMovement || stock.shareTxUntilMovementDown <= 0) {
// The shareTxUntilMovementDown ended up at 0 at the end of the "processing"
++numIterations;
stock.shareTxUntilMovementDown = stock.shareTxForMovement;
processPriceMovement();
}
}
stock.changePrice(currPrice);
// Forecast always decreases in magnitude
const forecastChange = Math.min(5, forecastChangePerPriceMovement * (numIterations - 1));
if (stock.otlkMag > 10) {
stock.otlkMag -= forecastChange;
return (shares * stock.getBidPrice()) + CONSTANTS.StockMarketCommission;
}
}
/**
* Calculate the TOTAL amount of money gained from a sale (NOT net profit). This accounts
* for spread, price movements, and commission.
* for spread and commission.
* @param {Stock} stock - Stock being sold
* @param {number} shares - Number of sharse being transacted
* @param {PositionTypes} posType - Long or short position
@ -192,104 +48,41 @@ export function getSellTransactionGain(stock: Stock, shares: number, posType: Po
shares = Math.min(shares, stock.maxShares);
const isLong = (posType === PositionTypes.Long);
const firstShares = isLong ? stock.shareTxUntilMovementDown : stock.shareTxUntilMovementUp;
if (isLong) {
return (shares * stock.getBidPrice()) - CONSTANTS.StockMarketCommission;
} else {
// Calculating gains for a short position requires calculating the profit made
const origCost = shares * stock.playerAvgShortPx;
const profit = ((stock.playerAvgShortPx - stock.getAskPrice()) * shares) - CONSTANTS.StockMarketCommission;
// If the number of shares doesn't trigger a price mvoement, its a simple calculation
if (shares <= firstShares) {
if (isLong) {
return (shares * stock.getBidPrice()) - CONSTANTS.StockMarketCommission;
} else {
// Calculating gains for a short position requires calculating the profit made
const origCost = shares * stock.playerAvgShortPx;
const profit = ((stock.playerAvgShortPx - stock.getAskPrice()) * shares) - CONSTANTS.StockMarketCommission;
return origCost + profit;
}
return origCost + profit;
}
// Calculate how many iterations of price changes we need to account for
let remainingShares = shares - firstShares;
let numIterations = 1 + Math.ceil(remainingShares / stock.shareTxForMovement);
// Helper function to calculate gain for a single iteration
function calculateGain(thisPrice: number, thisShares: number) {
if (isLong) {
return thisShares * thisPrice;
} else {
const origCost = thisShares * stock.playerAvgShortPx;
const profit = ((stock.playerAvgShortPx - thisPrice) * thisShares);
return origCost + profit;
}
}
// The initial cost calculation takes care of the first "iteration"
let currPrice = isLong ? stock.getBidPrice() : stock.getAskPrice();
let totalGain = calculateGain(currPrice, firstShares);
for (let i = 1; i < numIterations; ++i) {
// Price movement
if (isLong) {
currPrice *= calculateDecreasingPriceMovement(stock)!;
} else {
currPrice *= calculateIncreasingPriceMovement(stock)!;
}
const amt = Math.min(stock.shareTxForMovement, remainingShares);
totalGain += calculateGain(currPrice, amt);
remainingShares -= amt;
}
return totalGain - CONSTANTS.StockMarketCommission;
}
/**
* Processes a sell transaction's resulting price movement
* Processes a stock's change in forecast & second-order forecast
* whenever it is transacted
* @param {Stock} stock - Stock being sold
* @param {number} shares - Number of sharse being transacted
* @param {PositionTypes} posType - Long or short position
*/
export function processSellTransactionPriceMovement(stock: Stock, shares: number, posType: PositionTypes): void {
export function processTransactionForecastMovement(stock: Stock, shares: number): void {
if (isNaN(shares) || shares <= 0 || !(stock instanceof Stock)) { return; }
// Cap the 'shares' arg at the stock's maximum shares. This'll prevent
// hanging in the case when a really big number is passed in
shares = Math.min(shares, stock.maxShares);
const isLong = (posType === PositionTypes.Long);
const firstShares = isLong ? stock.shareTxUntilMovementDown : stock.shareTxUntilMovementUp;
let currPrice = stock.price;
function processPriceMovement() {
if (isLong) {
currPrice *= calculateDecreasingPriceMovement(stock)!;
} else {
currPrice *= calculateIncreasingPriceMovement(stock)!;
}
}
// If there's only going to be one iteration at most
const firstShares = stock.shareTxUntilMovement;
if (shares <= firstShares) {
function triggerPriceMovement() {
processPriceMovement();
stock.changePrice(currPrice);
stock.otlkMag -= (forecastChangePerPriceMovement);
stock.shareTxUntilMovement -= shares;
if (stock.shareTxUntilMovement <= 0) {
stock.shareTxUntilMovement = stock.shareTxForMovement;
stock.influenceForecast(forecastChangePerPriceMovement);
stock.influenceForecastForecast(forecastChangePerPriceMovement * (stock.mv / 100));
}
if (isLong) {
stock.shareTxUntilMovementDown -= shares;
if (stock.shareTxUntilMovementDown <= 0) {
stock.shareTxUntilMovementDown = stock.shareTxForMovement;
triggerPriceMovement();
}
} else {
stock.shareTxUntilMovementUp -= shares;
if (stock.shareTxUntilMovementUp <= 0) {
stock.shareTxUntilMovementUp = stock.shareTxForMovement;
triggerPriceMovement();
}
}
return;
}
@ -297,34 +90,18 @@ export function processSellTransactionPriceMovement(stock: Stock, shares: number
let remainingShares = shares - firstShares;
let numIterations = 1 + Math.ceil(remainingShares / stock.shareTxForMovement);
for (let i = 1; i < numIterations; ++i) {
processPriceMovement();
}
// If on the offchance we end up perfectly at the next price movement
if (isLong) {
stock.shareTxUntilMovementDown = stock.shareTxForMovement - ((shares - stock.shareTxUntilMovementDown) % stock.shareTxForMovement);
if (stock.shareTxUntilMovementDown === stock.shareTxForMovement || stock.shareTxUntilMovementDown <= 0) {
++numIterations;
stock.shareTxUntilMovementDown = stock.shareTxForMovement;
processPriceMovement();
}
} else {
stock.shareTxUntilMovementUp = stock.shareTxForMovement - ((shares - stock.shareTxUntilMovementUp) % stock.shareTxForMovement);
if (stock.shareTxUntilMovementUp === stock.shareTxForMovement || stock.shareTxUntilMovementUp <= 0) {
++numIterations;
stock.shareTxUntilMovementUp = stock.shareTxForMovement;
processPriceMovement();
}
stock.shareTxUntilMovement = stock.shareTxForMovement - ((shares - stock.shareTxUntilMovement) % stock.shareTxForMovement);
if (stock.shareTxUntilMovement === stock.shareTxForMovement || stock.shareTxUntilMovement <= 0) {
++numIterations;
stock.shareTxUntilMovement = stock.shareTxForMovement;
}
stock.changePrice(currPrice);
// Forecast always decreases in magnitude
const forecastChange = Math.min(5, forecastChangePerPriceMovement * (numIterations - 1));
if (stock.otlkMag > 10) {
stock.otlkMag -= forecastChange;
}
const forecastChange = forecastChangePerPriceMovement * (numIterations - 1);
const forecastForecastChange = forecastChange * (stock.mv / 100);
stock.influenceForecast(forecastChange);
stock.influenceForecastForecast(forecastForecastChange);
}
/**
@ -341,44 +118,8 @@ export function calculateBuyMaxAmount(stock: Stock, posType: PositionTypes, mone
const isLong = (posType === PositionTypes.Long);
const increasingMvmt = calculateIncreasingPriceMovement(stock);
const decreasingMvmt = calculateDecreasingPriceMovement(stock);
if (increasingMvmt == null || decreasingMvmt == null) { return 0; }
let remainingMoney = money - CONSTANTS.StockMarketCommission;
let currPrice = isLong ? stock.getAskPrice() : stock.getBidPrice();
// No price movement
const firstShares = isLong ? stock.shareTxUntilMovementUp : stock.shareTxUntilMovementDown;
const firstIterationCost = firstShares * currPrice;
if (remainingMoney < firstIterationCost) {
return Math.floor(remainingMoney / currPrice);
}
// We'll avoid any accidental infinite loops by having a hardcoded maximum number of
// iterations
let numShares = firstShares;
remainingMoney -= firstIterationCost;
for (let i = 0; i < 10e3; ++i) {
if (isLong) {
currPrice *= increasingMvmt;
} else {
currPrice *= decreasingMvmt;
}
const affordableShares = Math.floor(remainingMoney / currPrice);
const actualShares = Math.min(stock.shareTxForMovement, affordableShares);
// Can't afford any more, so we're done
if (actualShares <= 0) { break; }
numShares += actualShares;
let cost = actualShares * currPrice;
remainingMoney -= cost;
if (remainingMoney <= 0) { break; }
}
return Math.floor(numShares);
return Math.floor(remainingMoney / currPrice);
}

@ -672,8 +672,8 @@ export const InitStockMetadata: IConstructorParams[] = [
marketCap: 58e9,
mv: {
divisor: 100,
max: 430,
min: 400,
max: 400,
min: 200,
},
name: LocationName.AevumNetLinkTechnologies,
otlkMag: 1,
@ -750,8 +750,8 @@ export const InitStockMetadata: IConstructorParams[] = [
marketCap: 30e9,
mv: {
divisor: 100,
max: 300,
min: 260,
max: 275,
min: 100,
},
name: "Sigma Cosmetics",
otlkMag: 0,
@ -761,8 +761,8 @@ export const InitStockMetadata: IConstructorParams[] = [
min: 6,
},
shareTxForMovement: {
max: 84e3,
min: 24e3,
max: 70e3,
min: 20e3,
},
symbol: StockSymbols["Sigma Cosmetics"],
},
@ -776,8 +776,8 @@ export const InitStockMetadata: IConstructorParams[] = [
marketCap: 42e9,
mv: {
divisor: 100,
max: 400,
min: 360,
max: 350,
min: 200,
},
name: "Joes Guns",
otlkMag: 1,
@ -787,8 +787,8 @@ export const InitStockMetadata: IConstructorParams[] = [
min: 6,
},
shareTxForMovement: {
max: 64e3,
min: 18e3,
max: 52e3,
min: 15e3,
},
symbol: StockSymbols["Joes Guns"],
},

@ -117,12 +117,6 @@ export class StockTicker extends React.Component<IProps, IState> {
let costTxt = `Purchasing ${numeralWrapper.formatBigNumber(qty)} shares (${this.state.position === PositionTypes.Long ? "Long" : "Short"}) ` +
`will cost ${numeralWrapper.formatMoney(cost)}. `;
const amtNeededForMovement = this.state.position === PositionTypes.Long ? stock.shareTxUntilMovementUp : stock.shareTxUntilMovementDown;
const causesMovement = qty > amtNeededForMovement;
if (causesMovement) {
costTxt += `WARNING: Purchasing this many shares will influence the stock price`;
}
return costTxt;
}
@ -151,12 +145,6 @@ export class StockTicker extends React.Component<IProps, IState> {
let costTxt = `Selling ${numeralWrapper.formatBigNumber(qty)} shares (${this.state.position === PositionTypes.Long ? "Long" : "Short"}) ` +
`will result in a gain of ${numeralWrapper.formatMoney(cost)}. `;
const amtNeededForMovement = this.state.position === PositionTypes.Long ? stock.shareTxUntilMovementDown : stock.shareTxUntilMovementUp;
const causesMovement = qty > amtNeededForMovement;
if (causesMovement) {
costTxt += `WARNING: Selling this many shares will influence the stock price`;
}
return costTxt;
}
@ -357,11 +345,7 @@ export class StockTicker extends React.Component<IProps, IState> {
render() {
// Determine if the player's intended transaction will cause a price movement
let causesMovement: boolean = false;
const qty = this.getQuantity();
if (!isNaN(qty)) {
causesMovement = qty > this.props.stock.shareTxForMovement;
}
return (
<li>
@ -400,14 +384,6 @@ export class StockTicker extends React.Component<IProps, IState> {
<StockTickerTxButton onClick={this.handleSellButtonClick} text={"Sell"} tooltip={this.getSellTransactionCostText()} />
<StockTickerTxButton onClick={this.handleBuyMaxButtonClick} text={"Buy MAX"} />
<StockTickerTxButton onClick={this.handleSellAllButtonClick} text={"Sell ALL"} />
{
causesMovement &&
<p className="stock-market-price-movement-warning">
WARNING: Buying/Selling {numeralWrapper.formatBigNumber(qty)} shares may affect
the stock's price. This applies during the transaction itself as well. See Investopedia
for more details.
</p>
}
<StockTickerPositionText p={this.props.p} stock={this.props.stock} />
<StockTickerOrderList
cancelOrder={this.props.cancelOrder}

@ -27,6 +27,9 @@ export function StockTickerHeaderText(props: IProps): React.ReactElement {
let plusOrMinus = stock.b; // True for "+", false for "-"
if (stock.otlkMag < 0) { plusOrMinus = !plusOrMinus }
hdrText += (plusOrMinus ? "+" : "-").repeat(Math.floor(Math.abs(stock.otlkMag) / 10) + 1);
// Debugging:
// hdrText += ` - ${stock.getAbsoluteForecast()} / ${stock.otlkMagForecast}`;
}
let styleMarkup = {

@ -53,7 +53,9 @@ import {
import { showLiterature } from "./Literature";
import { Message } from "./Message/Message";
import { showMessage } from "./Message/MessageHelpers";
import { killWorkerScript, addWorkerScript } from "./NetscriptWorker";
import { addWorkerScript } from "./NetscriptWorker";
import { killWorkerScript } from "./Netscript/killWorkerScript";
import { WorkerScriptStartStopEventEmitter } from "./Netscript/WorkerScriptStartStopEventEmitter";
import { Player } from "./Player";
import { hackWorldDaemon } from "./RedPill";
import { RunningScript } from "./Script/RunningScript";
@ -1145,8 +1147,9 @@ let Terminal = {
}
case "killall": {
for (let i = s.runningScripts.length - 1; i >= 0; --i) {
killWorkerScript(s.runningScripts[i], s.ip);
killWorkerScript(s.runningScripts[i], s.ip, false);
}
WorkerScriptStartStopEventEmitter.emitEvent();
post("Killing all running scripts. May take up to a few minutes for the scripts to die...");
break;
}
@ -1249,7 +1252,7 @@ let Terminal = {
}
for (let i = 0; i < s.runningScripts.length; i++) {
let rsObj = s.runningScripts[i];
let res = rsObj.filename;
let res = `(PID - ${rsObj.pid}) ${rsObj.filename}`;
for (let j = 0; j < rsObj.args.length; ++j) {
res += (" " + rsObj.args[j].toString());
}
@ -1431,7 +1434,23 @@ let Terminal = {
return;
}
post("Script Threads RAM Usage");
// Headers
const scriptWidth = 40;
const pidWidth = 10;
const threadsWidth = 16;
const scriptTxt = "Script";
const pidTxt = "PID";
const threadsTxt = "Threads";
const ramTxt = "RAM Usage";
const spacesAfterScriptTxt = " ".repeat(scriptWidth - scriptTxt.length);
const spacesAfterPidTxt = " ".repeat(pidWidth - pidTxt.length);
const spacesAfterThreadsTxt = " ".repeat(threadsWidth - threadsTxt.length);
const headers = `${scriptTxt}${spacesAfterScriptTxt}${pidTxt}${spacesAfterPidTxt}${threadsTxt}${spacesAfterThreadsTxt}${ramTxt}`;
post(headers);
let currRunningScripts = s.runningScripts;
// Iterate through scripts on current server
@ -1439,19 +1458,30 @@ let Terminal = {
let script = currRunningScripts[i];
// Calculate name padding
let numSpacesScript = 32 - script.filename.length; // 26 -> width of name column
if (numSpacesScript < 0) {numSpacesScript = 0;}
let spacesScript = Array(numSpacesScript+1).join(" ");
const numSpacesScript = Math.max(0, scriptWidth - script.filename.length);
const spacesScript = " ".repeat(numSpacesScript);
// Calculate PID padding
const numSpacesPid = Math.max(0, pidWidth - (script.pid + "").length);
const spacesPid = " ".repeat(numSpacesPid);
// Calculate thread padding
let numSpacesThread = 16 - (script.threads + "").length; // 16 -> width of thread column
let spacesThread = Array(numSpacesThread+1).join(" ");
const numSpacesThread = Math.max(0, threadsWidth - (script.threads + "").length);
const spacesThread = " ".repeat(numSpacesThread);
// Calculate and transform RAM usage
let ramUsage = numeralWrapper.format(getRamUsageFromRunningScript(script) * script.threads, '0.00') + " GB";
const ramUsage = numeralWrapper.format(getRamUsageFromRunningScript(script) * script.threads, '0.00') + " GB";
var entry = [script.filename, spacesScript, script.threads, spacesThread, ramUsage];
post(entry.join(""));
const entry = [
script.filename,
spacesScript,
script.pid,
spacesPid,
script.threads,
spacesThread,
ramUsage
].join("");
post(entry);
}
break;
}

@ -65,16 +65,14 @@ export const codingContractTypesMetadata: ICodingContractTypeMetadata[] = [
solver: (data: number, ans: string) => {
let fac: number = 2;
let n: number = data;
while (n > fac) {
if (n % fac === 0) {
while (n > ((fac-1) * (fac-1))) {
while (n % fac === 0) {
n = Math.round(n / fac);
fac = 2;
} else {
++fac;
}
++fac;
}
return fac === parseInt(ans, 10);
return (n===1 ? (fac-1) : n) === parseInt(ans, 10);
},
},
{

@ -1,29 +1,26 @@
/**
* Game engine. Handles the main game loop as well as the main UI pages
*
* TODO: Separate UI functionality into its own component
*/
import {
formatNumber,
convertTimeMsToTimeElapsedString,
replaceAt
} from "../utils/StringHelperFunctions";
import { loxBoxCreate, logBoxUpdateText, logBoxOpened } from "../utils/LogBox";
import { updateActiveScriptsItems } from "./ActiveScriptsUI";
import { logBoxUpdateText, logBoxOpened } from "../utils/LogBox";
import { Augmentations } from "./Augmentation/Augmentations";
import {
installAugmentations,
initAugmentations,
displayAugmentationsContent,
PlayerOwnedAugmentation
} from "./Augmentation/AugmentationHelpers";
import { AugmentationNames } from "./Augmentation/data/AugmentationNames";
import {
BitNodes,
initBitNodes,
initBitNodeMultipliers
} from "./BitNode/BitNode";
import { Bladeburner } from "./Bladeburner";
import { CharacterOverviewComponent } from "./ui/React/CharacterOverview";
import { cinematicTextFlag } from "./CinematicText";
import { generateRandomContract } from "./CodingContractGenerator";
import { CompanyPositions } from "./Company/CompanyPositions";
import { initCompanies } from "./Company/Companies";
import { Corporation } from "./Corporation/Corporation";
import { CONSTANTS } from "./Constants";
@ -48,46 +45,35 @@ import { LocationName } from "./Locations/data/LocationNames";
import { LocationRoot } from "./Locations/ui/Root";
import { checkForMessagesToSend, initMessages } from "./Message/MessageHelpers";
import { inMission, currMission } from "./Missions";
import { workerScripts } from "./Netscript/WorkerScripts";
import {
loadAllRunningScripts,
runScriptsLoop,
updateOnlineScriptTimes,
} from "./NetscriptWorker";
import { Player } from "./Player";
import { prestigeAugmentation, prestigeSourceFile } from "./Prestige";
import { Programs } from "./Programs/Programs";
import { prestigeAugmentation } from "./Prestige";
import {
displayCreateProgramContent,
getNumAvailableCreateProgram,
initCreateProgramButtons
} from "./Programs/ProgramHelpers";
import { redPillFlag, hackWorldDaemon } from "./RedPill";
import { redPillFlag } from "./RedPill";
import { saveObject, loadGame } from "./SaveObject";
import {
getCurrentEditor,
scriptEditorInit,
updateScriptEditorContent
} from "./Script/ScriptHelpers";
import { AllServers, initForeignServers } from "./Server/AllServers";
import { Server } from "./Server/Server";
import { initForeignServers } from "./Server/AllServers";
import { Settings } from "./Settings/Settings";
import { initSourceFiles, SourceFiles } from "./SourceFile";
import { updateSourceFileFlags } from "./SourceFile/SourceFileFlags";
import { initSpecialServerIps } from "./Server/SpecialServerIps";
import {
SpecialServerIps,
initSpecialServerIps
} from "./Server/SpecialServerIps";
import {
StockMarket,
SymbolToStockMap,
initSymbolToStockMap,
stockMarketCycle,
processStockPrices,
displayStockMarketContent
} from "./StockMarket/StockMarket";
import { Terminal, postNetburnerText } from "./Terminal";
import { Sleeve } from "./PersonObjects/Sleeve/Sleeve";
import {
clearSleevesPage,
@ -104,14 +90,14 @@ import { displayCharacterInfo } from "./ui/displayCharacterInfo";
import { Page, routing } from "./ui/navigationTracking";
import { numeralWrapper } from "./ui/numeralFormat";
import { setSettingsLabels } from "./ui/setSettingsLabels";
import { ActiveScriptsRoot } from "./ui/ActiveScripts/Root";
import { initializeMainMenuHeaders } from "./ui/MainMenu/Headers";
import { initializeMainMenuLinks, MainMenuLinks } from "./ui/MainMenu/Links";
import { dialogBoxCreate } from "../utils/DialogBox";
import { gameOptionsBoxClose, gameOptionsBoxOpen } from "../utils/GameOptions";
import { getRandomInt } from "../utils/helpers/getRandomInt";
import { removeChildrenFromElement } from "../utils/uiHelpers/removeChildrenFromElement";
import { clearEventListeners } from "../utils/uiHelpers/clearEventListeners";
import { createElement } from "../utils/uiHelpers/createElement";
import { exceptionAlert } from "../utils/helpers/exceptionAlert";
import { removeLoadingScreen } from "../utils/uiHelpers/removeLoadingScreen";
@ -278,7 +264,10 @@ const Engine = {
Engine.hideAllContent();
Engine.Display.activeScriptsContent.style.display = "block";
routing.navigateTo(Page.ActiveScripts);
updateActiveScriptsItems();
ReactDOM.render(
<ActiveScriptsRoot p={Player} workerScripts={workerScripts} />,
Engine.Display.activeScriptsContent
)
MainMenuLinks.ActiveScripts.classList.add("active");
},
@ -315,8 +304,8 @@ const Engine = {
loadAugmentationsContent: function() {
Engine.hideAllContent();
Engine.Display.augmentationsContent.style.display = "block";
displayAugmentationsContent(Engine.Display.augmentationsContent);
routing.navigateTo(Page.Augmentations);
displayAugmentationsContent(Engine.Display.augmentationsContent);
MainMenuLinks.Augmentations.classList.add("active");
},
@ -490,16 +479,26 @@ const Engine = {
Engine.Display.terminalContent.style.display = "none";
Engine.Display.characterContent.style.display = "none";
Engine.Display.scriptEditorContent.style.display = "none";
Engine.Display.activeScriptsContent.style.display = "none";
ReactDOM.unmountComponentAtNode(Engine.Display.activeScriptsContent);
clearHacknetNodesUI();
Engine.Display.createProgramContent.style.display = "none";
Engine.Display.factionsContent.style.display = "none";
ReactDOM.unmountComponentAtNode(Engine.Display.factionContent);
Engine.Display.factionContent.style.display = "none";
ReactDOM.unmountComponentAtNode(Engine.Display.factionContent);
Engine.Display.augmentationsContent.style.display = "none";
ReactDOM.unmountComponentAtNode(Engine.Display.augmentationsContent);
Engine.Display.tutorialContent.style.display = "none";
Engine.Display.locationContent.style.display = "none";
ReactDOM.unmountComponentAtNode(Engine.Display.locationContent);
Engine.Display.workInProgressContent.style.display = "none";
Engine.Display.redPillContent.style.display = "none";
Engine.Display.cinematicTextContent.style.display = "none";
@ -778,7 +777,6 @@ const Engine = {
checkFactionInvitations: 100,
passiveFactionGrowth: 600,
messages: 150,
sCr: 1500,
mechanicProcess: 5, // Processes certain mechanics (Corporation, Bladeburner)
contractGeneration: 3000, // Generate Coding Contracts
},
@ -814,13 +812,14 @@ const Engine = {
}
if (Engine.Counters.updateActiveScriptsDisplay <= 0) {
// Always update, but make the interval longer if the page isn't active
updateActiveScriptsItems();
if (routing.isOn(Page.ActiveScripts)) {
Engine.Counters.updateActiveScriptsDisplay = 5;
} else {
Engine.Counters.updateActiveScriptsDisplay = 10;
ReactDOM.render(
<ActiveScriptsRoot p={Player} workerScripts={workerScripts} />,
Engine.Display.activeScriptsContent
)
}
Engine.Counters.updateActiveScriptsDisplay = 5;
}
if (Engine.Counters.updateDisplays <= 0) {
@ -900,13 +899,6 @@ const Engine = {
}
}
if (Engine.Counters.sCr <= 0) {
if (Player.hasWseAccount) {
stockMarketCycle();
}
Engine.Counters.sCr = 1500;
}
if (Engine.Counters.mechanicProcess <= 0) {
if (Player.corporation instanceof Corporation) {
Player.corporation.process();
@ -1043,9 +1035,7 @@ const Engine = {
// Load game from save or create new game
if (loadGame(saveString)) {
initBitNodes();
initBitNodeMultipliers(Player);
initSourceFiles();
Engine.setDisplayElements(); // Sets variables for important DOM elements
Engine.init(); // Initialize buttons, work, etc.
initAugmentations(); // Also calls Player.reapplyAllAugmentations()
@ -1166,9 +1156,7 @@ const Engine = {
} else {
// No save found, start new game
console.log("Initializing new game");
initBitNodes();
initBitNodeMultipliers(Player);
initSourceFiles();
initSpecialServerIps();
Engine.setDisplayElements(); // Sets variables for important DOM elements
Engine.start(); // Run main game loop and Scripts loop
@ -1531,9 +1519,6 @@ const Engine = {
start: function() {
// Run main loop
Engine.idleTimer();
// Script-processing loop
runScriptsLoop();
}
};

@ -9,8 +9,10 @@ import "../css/characteroverview.scss";
import "../css/terminal.scss";
import "../css/scripteditor.scss";
import "../css/codemirror-overrides.scss";
import "../css/activescripts.scss";
import "../css/hacknetnodes.scss";
import "../css/menupages.scss";
import "../css/augmentations.scss";
import "../css/redpill.scss";
import "../css/stockmarket.scss";
import "../css/workinprogress.scss";

@ -0,0 +1,38 @@
/**
* Root React Component for the "Active Scripts" UI page. This page displays
* and provides information about all of the player's scripts that are currently running
*/
import * as React from "react";
import { ScriptProduction } from "./ScriptProduction";
import { ServerAccordions } from "./ServerAccordions";
import { WorkerScript } from "../../Netscript/WorkerScript";
import { IPlayer } from "../../PersonObjects/IPlayer";
type IProps = {
p: IPlayer;
workerScripts: Map<number, WorkerScript>;
}
export class ActiveScriptsRoot extends React.Component<IProps> {
constructor(props: IProps) {
super(props);
}
render() {
return (
<>
<p>
This page displays a list of all of your scripts that are currently
running across every machine. It also provides information about each
script's production. The scripts are categorized by the hostname of
the servers on which they are running.
</p>
<ScriptProduction {...this.props} />
<ServerAccordions {...this.props} />
</>
)
}
}

@ -0,0 +1,46 @@
/**
* React Component for displaying the total production and production rate
* of scripts on the 'Active Scripts' UI page
*/
import * as React from "react";
import { numeralWrapper } from "../numeralFormat";
import { WorkerScript } from "../../Netscript/WorkerScript";
import { IPlayer } from "../../PersonObjects/IPlayer";
type IProps = {
p: IPlayer;
workerScripts: Map<number, WorkerScript>;
}
export function ScriptProduction(props: IProps): React.ReactElement {
const prodRateSinceLastAug = props.p.scriptProdSinceLastAug / (props.p.playtimeSinceLastAug / 1000);
let onlineProduction = 0;
for (const ws of props.workerScripts.values()) {
onlineProduction += (ws.scriptRef.onlineMoneyMade / ws.scriptRef.onlineRunningTime);
}
return (
<p id="active-scripts-total-prod">
Total online production of Active scripts:
<span className="money-gold">
<span id="active-scripts-total-production-active">
{numeralWrapper.formatMoney(onlineProduction)}
</span> / sec
</span><br />
Total online production since last Aug installation:
<span id="active-scripts-total-prod-aug-total" className="money-gold">
{numeralWrapper.formatMoney(props.p.scriptProdSinceLastAug)}
</span>
(<span className="money-gold">
<span id="active-scripts-total-prod-aug-avg" className="money-gold">
{numeralWrapper.formatMoney(prodRateSinceLastAug)}
</span> / sec
</span>)
</p>
)
}

@ -0,0 +1,49 @@
/**
* React Component for rendering the Accordion element for a single
* server in the 'Active Scripts' UI page
*/
import * as React from "react";
import { WorkerScriptAccordion } from "./WorkerScriptAccordion";
import { Accordion } from "../React/Accordion";
import { BaseServer } from "../../Server/BaseServer";
import { WorkerScript } from "../../Netscript/WorkerScript";
import { createProgressBarText } from "../../../utils/helpers/createProgressBarText";
type IProps = {
server: BaseServer;
workerScripts: WorkerScript[];
}
export function ServerAccordion(props: IProps): React.ReactElement {
const server = props.server;
// Accordion's header text
// TODO: calculate the longest hostname length rather than hard coding it
const longestHostnameLength = 18;
const paddedName = `${server.hostname}${" ".repeat(longestHostnameLength)}`.slice(0, Math.max(server.hostname.length, longestHostnameLength));
const barOptions = {
progress: server.ramUsed / server.maxRam,
totalTicks: 30
};
const headerTxt = `${paddedName} ${createProgressBarText(barOptions)}`;
const scripts = props.workerScripts.map((ws) => {
return (
<WorkerScriptAccordion key={`${ws.name}_${ws.args}`} workerScript={ws} />
)
});
return (
<Accordion
headerContent={
<pre>{headerTxt}</pre>
}
panelContent={
<ul>{scripts}</ul>
}
/>
)
}

@ -0,0 +1,109 @@
/**
* React Component for rendering the Accordion elements for all servers
* on which scripts are running
*/
import * as React from "react";
import { ServerAccordion } from "./ServerAccordion";
import { WorkerScript } from "../../Netscript/WorkerScript";
import { WorkerScriptStartStopEventEmitter } from "../../Netscript/WorkerScriptStartStopEventEmitter";
import { getServer } from "../../Server/ServerHelpers";
import { BaseServer } from "../../Server/BaseServer";
// Map of server hostname -> all workerscripts on that server for all active scripts
interface IServerData {
server: BaseServer;
workerScripts: WorkerScript[];
}
interface IServerToScriptsMap {
[key: string]: IServerData;
}
type IProps = {
workerScripts: Map<number, WorkerScript>;
};
type IState = {
rerenderFlag: boolean;
}
const subscriberId = "ActiveScriptsUI";
export class ServerAccordions extends React.Component<IProps, IState> {
serverToScriptMap: IServerToScriptsMap = {};
constructor(props: IProps) {
super(props);
this.state = {
rerenderFlag: false,
}
this.updateServerToScriptsMap();
this.rerender = this.rerender.bind(this);
}
componentDidMount() {
WorkerScriptStartStopEventEmitter.addSubscriber({
cb: this.rerender,
id: subscriberId,
})
}
componentWillUnmount() {
WorkerScriptStartStopEventEmitter.removeSubscriber(subscriberId);
}
updateServerToScriptsMap(): void {
const map: IServerToScriptsMap = {};
for (const ws of this.props.workerScripts.values()) {
const server = getServer(ws.serverIp);
if (server == null) {
console.warn(`WorkerScript has invalid IP address: ${ws.serverIp}`);
continue;
}
if (map[server.hostname] == null) {
map[server.hostname] = {
server: server,
workerScripts: [],
};
}
map[server.hostname].workerScripts.push(ws);
}
this.serverToScriptMap = map;
}
rerender() {
this.updateServerToScriptsMap();
this.setState((prevState) => {
return { rerenderFlag: !prevState.rerenderFlag }
});
}
render() {
const elems = Object.keys(this.serverToScriptMap).map((serverName) => {
const data = this.serverToScriptMap[serverName];
return (
<ServerAccordion
key={serverName}
server={data.server}
workerScripts={data.workerScripts}
/>
)
});
return (
<ul className="active-scripts-list" id="active-scripts-list">
{elems}
</ul>
)
}
}

@ -0,0 +1,77 @@
/**
* React Component for displaying a single WorkerScript's info as an
* Accordion element
*/
import * as React from "react";
import { numeralWrapper } from "../numeralFormat";
import { Accordion } from "../React/Accordion";
import { AccordionButton } from "../React/AccordionButton";
import { killWorkerScript } from "../../Netscript/killWorkerScript";
import { WorkerScript } from "../../Netscript/WorkerScript";
import { dialogBoxCreate } from "../../../utils/DialogBox";
import { logBoxCreate } from "../../../utils/LogBox";
import { convertTimeMsToTimeElapsedString } from "../../../utils/StringHelperFunctions";
import { arrayToString } from "../../../utils/helpers/arrayToString";
type IProps = {
workerScript: WorkerScript;
}
export function WorkerScriptAccordion(props: IProps): React.ReactElement {
const workerScript = props.workerScript;
const scriptRef = workerScript.scriptRef;
const logClickHandler = logBoxCreate.bind(null, scriptRef);
const killScript = killWorkerScript.bind(null, scriptRef, scriptRef.server);
function killScriptClickHandler() {
killScript();
dialogBoxCreate("Killing script");
}
// Calculations for script stats
const onlineMps = scriptRef.onlineMoneyMade / scriptRef.onlineRunningTime;
const onlineEps = scriptRef.onlineExpGained / scriptRef.onlineRunningTime;
const offlineMps = scriptRef.offlineMoneyMade / scriptRef.offlineRunningTime;
const offlineEps = scriptRef.offlineExpGained / scriptRef.offlineRunningTime;
return (
<Accordion
headerClass="active-scripts-script-header"
headerContent={
<>{props.workerScript.name}</>
}
panelClass="active-scripts-script-panel"
panelContent={
<>
<pre>Threads: {props.workerScript.scriptRef.threads}</pre>
<pre>Args: {arrayToString(props.workerScript.args)}</pre>
<pre>Online Time: {convertTimeMsToTimeElapsedString(scriptRef.onlineRunningTime * 1e3)}</pre>
<pre>Offline Time: {convertTimeMsToTimeElapsedString(scriptRef.offlineRunningTime * 1e3)}</pre>
<pre>Total online production: {numeralWrapper.formatMoney(scriptRef.onlineMoneyMade)}</pre>
<pre>{(Array(26).join(" ") + numeralWrapper.formatBigNumber(scriptRef.onlineExpGained) + " hacking exp")}</pre>
<pre>Online production rate: {numeralWrapper.formatMoney(onlineMps)} / second</pre>
<pre>{(Array(25).join(" ") + numeralWrapper.formatBigNumber(onlineEps) + " hacking exp / second")}</pre>
<pre>Total offline production: {numeralWrapper.formatMoney(scriptRef.offlineMoneyMade)}</pre>
<pre>{(Array(27).join(" ") + numeralWrapper.formatBigNumber(scriptRef.offlineExpGained) + " hacking exp")}</pre>
<pre>Offline production rate: {numeralWrapper.formatMoney(offlineMps)} / second</pre>
<pre>{(Array(26).join(" ") + numeralWrapper.formatBigNumber(offlineEps) + " hacking exp / second")}</pre>
<AccordionButton
onClick={logClickHandler}
text="Log"
/>
<AccordionButton
onClick={killScriptClickHandler}
text="Kill Script"
/>
</>
}
/>
)
}

@ -4,9 +4,12 @@
import * as React from "react";
type IProps = {
headerClass?: string; // Override default class
headerContent: React.ReactElement;
panelClass?: string; // Override default class
panelContent: React.ReactElement;
panelInitiallyOpened?: boolean;
style?: string;
}
type IState = {
@ -44,19 +47,29 @@ export class Accordion extends React.Component<IProps, IState> {
}
render() {
let className = "accordion-header";
if (typeof this.props.headerClass === "string") {
className = this.props.headerClass;
}
return (
<div>
<button className={"accordion-header"} onClick={this.handleHeaderClick}>
{this.props.headerContent}
</button>
<AccordionPanel opened={this.state.panelOpened} panelContent={this.props.panelContent} />
</div>
<>
<button className={className} onClick={this.handleHeaderClick}>
{this.props.headerContent}
</button>
<AccordionPanel
opened={this.state.panelOpened}
panelClass={this.props.panelClass}
panelContent={this.props.panelContent}
/>
</>
)
}
}
type IPanelProps = {
opened: boolean;
panelClass?: string; // Override default class
panelContent: React.ReactElement;
}
@ -66,8 +79,13 @@ class AccordionPanel extends React.Component<IPanelProps, any> {
}
render() {
let className = "accordion-panel"
if (typeof this.props.panelClass === "string") {
className = this.props.panelClass;
}
return (
<div className={"accordion-panel"}>
<div className={className}>
{this.props.panelContent}
</div>
)

@ -0,0 +1,52 @@
/**
* Basic stateless button that uses the 'accordion-button' css class.
* This class has a black background so that it does not clash with the default
* accordion coloring
*/
import * as React from "react";
interface IProps {
addClasses?: string;
disabled?: boolean;
id?: string;
onClick?: (e: React.MouseEvent<HTMLElement>) => any;
style?: object;
text: string;
tooltip?: string;
}
type IInnerHTMLMarkup = {
__html: string;
}
export function AccordionButton(props: IProps): React.ReactElement {
const hasTooltip = props.tooltip != null && props.tooltip !== "";
// TODO Add a disabled class for accordion buttons?
let className = "accordion-button";
if (hasTooltip) {
className += " tooltip";
}
if (typeof props.addClasses === "string") {
className += ` ${props.addClasses}`;
}
// Tooltip will be set using inner HTML
let tooltipMarkup: IInnerHTMLMarkup | null;
if (hasTooltip) {
tooltipMarkup = {
__html: props.tooltip!
}
}
return (
<button className={className} id={props.id} onClick={props.onClick} style={props.style}>
{props.text}
{
hasTooltip &&
<span className={"tooltiptext"} dangerouslySetInnerHTML={tooltipMarkup!}></span>
}
</button>
)
}

@ -0,0 +1,33 @@
/**
* React Component for displaying a single Augmentation as an accordion.
*
* The header of the accordion contains the Augmentation's name (and level, if
* applicable), and the accordion's panel contains the Augmentation's description.
*/
import * as React from "react";
import { Accordion } from "./Accordion";
import { Augmentation } from "../../Augmentation/Augmentation";
import { AugmentationNames } from "../../Augmentation/data/AugmentationNames";
type IProps = {
aug: Augmentation,
level?: number | string | null,
}
export function AugmentationAccordion(props: IProps): React.ReactElement {
let displayName = props.aug.name;
if (props.level != null) {
if (props.aug.name === AugmentationNames.NeuroFluxGovernor) {
displayName += (` - Level ${props.level}`)
}
}
return (
<Accordion
headerContent={<>{displayName}</>}
panelContent={<p dangerouslySetInnerHTML={{__html: props.aug.info}}></p>}
/>
)
}

@ -13,12 +13,10 @@ interface IProps {
props: object;
}
export class Popup extends React.Component<IProps, any> {
render() {
return (
<div className={"popup-box-content"} id={`${this.props.id}-content`}>
{React.createElement(this.props.content, this.props.props)}
</div>
)
}
export function Popup(props: IProps): React.ReactElement {
return (
<div className={"popup-box-content"} id={`${props.id}-content`}>
{React.createElement(props.content, props.props)}
</div>
)
}

@ -0,0 +1,35 @@
/**
* React Component for displaying a single Source-File as an accordion.
*
* The header of the accordion contains the Source-Files's name and level,
* and the accordion's panel contains the Source-File's description.
*/
import * as React from "react";
import { Accordion } from "./Accordion";
import { SourceFile } from "../../SourceFile/SourceFile";
type IProps = {
level: number,
sf: SourceFile,
}
export function SourceFileAccordion(props: IProps): React.ReactElement {
const maxLevel = props.sf.n === 3 ? "∞" : "3";
return (
<Accordion
headerContent={
<>
{props.sf.name}
<br />
{`Level ${props.level} / ${maxLevel}`}
</>
}
panelContent={
<p dangerouslySetInnerHTML={{__html: props.sf.info}}></p>
}
/>
)
}

@ -5,6 +5,7 @@
import * as React from "react";
interface IStdButtonProps {
addClasses?: string;
disabled?: boolean;
id?: string;
onClick?: (e: React.MouseEvent<HTMLElement>) => any;
@ -17,30 +18,32 @@ type IInnerHTMLMarkup = {
__html: string;
}
export class StdButton extends React.Component<IStdButtonProps, any> {
render() {
const hasTooltip = this.props.tooltip != null && this.props.tooltip !== "";
let className = this.props.disabled ? "std-button-disabled" : "std-button";
if (hasTooltip) {
className += " tooltip";
}
// Tooltip will be set using inner HTML
let tooltipMarkup: IInnerHTMLMarkup | null;
if (hasTooltip) {
tooltipMarkup = {
__html: this.props.tooltip!
}
}
return (
<button className={className} id={this.props.id} onClick={this.props.onClick} style={this.props.style}>
{this.props.text}
{
hasTooltip &&
<span className={"tooltiptext"} dangerouslySetInnerHTML={tooltipMarkup!}></span>
}
</button>
)
export function StdButton(props: IStdButtonProps): React.ReactElement {
const hasTooltip = props.tooltip != null && props.tooltip !== "";
let className = props.disabled ? "std-button-disabled" : "std-button";
if (hasTooltip) {
className += " tooltip";
}
if (typeof props.addClasses === "string") {
className += ` ${props.addClasses}`;
}
// Tooltip will be set using inner HTML
let tooltipMarkup: IInnerHTMLMarkup | null;
if (hasTooltip) {
tooltipMarkup = {
__html: props.tooltip!
}
}
return (
<button className={className} id={props.id} onClick={props.onClick} style={props.style}>
{props.text}
{
hasTooltip &&
<span className={"tooltiptext"} dangerouslySetInnerHTML={tooltipMarkup!}></span>
}
</button>
)
}

50
src/utils/EventEmitter.ts Normal file

@ -0,0 +1,50 @@
/**
* Generic Event Emitter class following a subscribe/publish paradigm.
*/
import { IMap } from "../types";
type cbFn = (...args: any[]) => any;
export interface ISubscriber {
/**
* Callback function that will be run when an event is emitted
*/
cb: cbFn;
/**
* Name/identifier for this subscriber
*/
id: string;
}
export class EventEmitter {
/**
* Map of Subscriber name -> Callback function
*/
subscribers: IMap<cbFn> = {};
constructor(subs?: ISubscriber[]) {
if (Array.isArray(subs)) {
for (const s of subs) {
this.addSubscriber(s);
}
}
}
addSubscriber(s: ISubscriber) {
this.subscribers[s.id] = s.cb;
}
emitEvent(...args: any[]): void {
for (const s in this.subscribers) {
const cb = this.subscribers[s];
cb(args);
}
}
removeSubscriber(id: string) {
delete this.subscribers[id];
}
}

@ -593,16 +593,6 @@ describe("Netscript Dynamic RAM Calculation/Generation Tests", function() {
await testNonzeroDynamicRamCost(f);
});
it("getStockPurchaseCost()", async function() {
const f = ["getStockPurchaseCost"];
await testNonzeroDynamicRamCost(f);
});
it("getStockSaleGain()", async function() {
const f = ["getStockSaleGain"];
await testNonzeroDynamicRamCost(f);
});
it("buyStock()", async function() {
const f = ["buyStock"];
await testNonzeroDynamicRamCost(f);

@ -1,926 +0,0 @@
import { CONSTANTS } from "../src/Constants";
import { Player } from "../src/Player";
import {
buyStock,
sellStock,
shortStock,
sellShort,
} from "../src/StockMarket/BuyingAndSelling";
import { Order } from "../src/StockMarket/Order";
import { processOrders } from "../src/StockMarket/OrderProcessing";
import { Stock } from "../src/StockMarket/Stock";
import {
deleteStockMarket,
initStockMarket,
initSymbolToStockMap,
loadStockMarket,
processStockPrices,
StockMarket,
SymbolToStockMap,
} from "../src/StockMarket/StockMarket";
import {
calculateIncreasingPriceMovement,
calculateDecreasingPriceMovement,
forecastChangePerPriceMovement,
getBuyTransactionCost,
getSellTransactionGain,
processBuyTransactionPriceMovement,
processSellTransactionPriceMovement,
} from "../src/StockMarket/StockMarketHelpers";
import { OrderTypes } from "../src/StockMarket/data/OrderTypes"
import { PositionTypes } from "../src/StockMarket/data/PositionTypes";
import { expect } from "chai";
console.log("Beginning Stock Market Tests");
describe("Stock Market Tests", function() {
const commission = CONSTANTS.StockMarketCommission;
// Generic Stock object that can be used by each test
let stock;
const ctorParams = {
b: true,
initPrice: 10e3,
marketCap: 5e9,
mv: 1,
name: "MockStock",
otlkMag: 20,
spreadPerc: 1,
shareTxForMovement: 5e3,
symbol: "mock",
};
beforeEach(function() {
function construct() {
stock = new Stock(ctorParams);
}
expect(construct).to.not.throw();
});
describe("Stock Class", function() {
describe("constructor", function() {
it("should have default parameters", function() {
let defaultStock;
function construct() {
defaultStock = new Stock();
}
expect(construct).to.not.throw();
expect(defaultStock.name).to.equal("");
});
it("should properly initialize props from parameters", function() {
expect(stock.name).to.equal(ctorParams.name);
expect(stock.symbol).to.equal(ctorParams.symbol);
expect(stock.price).to.equal(ctorParams.initPrice);
expect(stock.lastPrice).to.equal(ctorParams.initPrice);
expect(stock.b).to.equal(ctorParams.b);
expect(stock.mv).to.equal(ctorParams.mv);
expect(stock.shareTxForMovement).to.equal(ctorParams.shareTxForMovement);
expect(stock.shareTxUntilMovementUp).to.equal(ctorParams.shareTxForMovement);
expect(stock.shareTxUntilMovementDown).to.equal(ctorParams.shareTxForMovement);
expect(stock.maxShares).to.be.below(stock.totalShares);
expect(stock.spreadPerc).to.equal(ctorParams.spreadPerc);
expect(stock.priceMovementPerc).to.be.a("number");
expect(stock.priceMovementPerc).to.be.at.most(stock.spreadPerc);
expect(stock.priceMovementPerc).to.be.at.least(0);
});
it ("should properly initialize props from range-values", function() {
let stock;
const params = {
b: true,
initPrice: {
max: 10e3,
min: 1e3,
},
marketCap: 5e9,
mv: {
divisor: 100,
max: 150,
min: 50,
},
name: "MockStock",
otlkMag: 10,
spreadPerc: {
divisor: 10,
max: 10,
min: 1,
},
shareTxForMovement: {
max: 10e3,
min: 5e3,
},
symbol: "mock",
};
function construct() {
stock = new Stock(params);
}
expect(construct).to.not.throw();
expect(stock.price).to.be.within(params.initPrice.min, params.initPrice.max);
expect(stock.mv).to.be.within(params.mv.min / params.mv.divisor, params.mv.max / params.mv.divisor);
expect(stock.spreadPerc).to.be.within(params.spreadPerc.min / params.spreadPerc.divisor, params.spreadPerc.max / params.spreadPerc.divisor);
expect(stock.shareTxForMovement).to.be.within(params.shareTxForMovement.min, params.shareTxForMovement.max);
});
it("should round the 'totalShare' prop to the nearest 100k", function() {
expect(stock.totalShares % 100e3).to.equal(0);
});
});
describe("#changePrice()", function() {
it("should set both the last price and current price properties", function() {
const newPrice = 20e3;
stock.changePrice(newPrice);
expect(stock.lastPrice).to.equal(ctorParams.initPrice);
expect(stock.price).to.equal(newPrice);
});
});
describe("#getAskPrice()", function() {
it("should return the price increased by spread percentage", function() {
const perc = stock.spreadPerc / 100;
expect(perc).to.be.at.most(1);
expect(perc).to.be.at.least(0);
const expected = stock.price * (1 + perc);
expect(stock.getAskPrice()).to.equal(expected);
});
});
describe("#getBidPrice()", function() {
it("should return the price decreased by spread percentage", function() {
const perc = stock.spreadPerc / 100;
expect(perc).to.be.at.most(1);
expect(perc).to.be.at.least(0);
const expected = stock.price * (1 - perc);
expect(stock.getBidPrice()).to.equal(expected);
});
});
});
describe("StockMarket object", function() {
describe("Initialization", function() {
// Keeps track of initialized stocks. Contains their symbols
const stocks = [];
before(function() {
expect(initStockMarket).to.not.throw();
expect(initSymbolToStockMap).to.not.throw();
});
it("should have Stock objects", function() {
for (const prop in StockMarket) {
const stock = StockMarket[prop];
if (stock instanceof Stock) {
stocks.push(stock.symbol);
}
}
// We'll just check that there are some stocks
expect(stocks.length).to.be.at.least(1);
});
it("should have an order book in the 'Orders' property", function() {
expect(StockMarket).to.have.property("Orders");
const orderbook = StockMarket["Orders"];
for (const symbol of stocks) {
const ordersForStock = orderbook[symbol];
expect(ordersForStock).to.be.an("array");
expect(ordersForStock.length).to.equal(0);
}
});
it("should have properties for managing game cycles", function() {
expect(StockMarket).to.have.property("storedCycles");
expect(StockMarket["storedCycles"]).to.equal(0);
expect(StockMarket).to.have.property("lastUpdate");
expect(StockMarket["lastUpdate"]).to.equal(0);
});
});
// Because 'StockMarket' is a global object, the effects of initialization from
// the block above should still stand
describe("Deletion", function() {
it("should set StockMarket to be an empty object", function() {
expect(StockMarket).to.be.an("object").that.is.not.empty;
deleteStockMarket();
expect(StockMarket).to.be.an("object").that.is.empty;
});
});
describe("processStockPrices()", function() {
before(function() {
deleteStockMarket();
initStockMarket();
initSymbolToStockMap();
});
it("should store cycles until it actually processes", function() {
expect(StockMarket["storedCycles"]).to.equal(0);
processStockPrices(10);
expect(StockMarket["storedCycles"]).to.equal(10);
});
it("should trigger a price update when it has enough cycles", function() {
// Get the initial prices
const initialValues = {};
for (const stockName in StockMarket) {
const stock = StockMarket[stockName];
if (!(stock instanceof Stock)) { continue; }
initialValues[stock.symbol] = {
price: stock.price,
otlkMag: stock.otlkMag,
}
}
// Don't know or care how many exact cycles are required
processStockPrices(1e9);
// Both price and 'otlkMag' should be different
for (const stockName in StockMarket) {
const stock = StockMarket[stockName];
if (!(stock instanceof Stock)) { continue; }
expect(initialValues[stock.symbol].price).to.not.equal(stock.price);
expect(initialValues[stock.symbol].otlkMag).to.not.equal(stock.otlkMag);
}
});
});
});
describe("StockToSymbolMap", function() {
before(function() {
deleteStockMarket();
initStockMarket();
initSymbolToStockMap();
});
it("should map stock symbols to their corresponding Stock Objects", function() {
for (const stockName in StockMarket) {
const stock = StockMarket[stockName];
if (!(stock instanceof Stock)) { continue; }
expect(SymbolToStockMap[stock.symbol]).to.equal(stock);
}
});
});
describe("Transaction Cost Calculator Functions", function() {
describe("getBuyTransactionCost()", function() {
it("should fail on invalid 'stock' argument", function() {
const res = getBuyTransactionCost({}, 10, PositionTypes.Long);
expect(res).to.equal(null);
});
it("should fail on invalid 'shares' arg", function() {
let res = getBuyTransactionCost(stock, NaN, PositionTypes.Long);
expect(res).to.equal(null);
res = getBuyTransactionCost(stock, -1, PositionTypes.Long);
expect(res).to.equal(null);
});
it("should properly evaluate LONG transactions that doesn't trigger a price movement", function() {
const shares = ctorParams.shareTxForMovement / 2;
const res = getBuyTransactionCost(stock, shares, PositionTypes.Long);
expect(res).to.equal(shares * stock.getAskPrice() + commission);
});
it("should properly evaluate SHORT transactions that doesn't trigger a price movement", function() {
const shares = ctorParams.shareTxForMovement / 2;
const res = getBuyTransactionCost(stock, shares, PositionTypes.Short);
expect(res).to.equal(shares * stock.getBidPrice() + commission);
});
it("should properly evaluate LONG transactions that trigger price movements", function() {
const sharesPerMvmt = ctorParams.shareTxForMovement;
const shares = sharesPerMvmt * 3;
const res = getBuyTransactionCost(stock, shares, PositionTypes.Long);
// Calculate expected cost
const secondPrice = stock.getAskPrice() * calculateIncreasingPriceMovement(stock);
const thirdPrice = secondPrice * calculateIncreasingPriceMovement(stock);
let expected = (sharesPerMvmt * stock.getAskPrice()) + (sharesPerMvmt * secondPrice) + (sharesPerMvmt * thirdPrice);
expect(res).to.equal(expected + commission);
});
it("should properly evaluate SHORT transactions that trigger price movements", function() {
const sharesPerMvmt = ctorParams.shareTxForMovement;
const shares = sharesPerMvmt * 3;
const res = getBuyTransactionCost(stock, shares, PositionTypes.Short);
// Calculate expected cost
const secondPrice = stock.getBidPrice() * calculateDecreasingPriceMovement(stock);
const thirdPrice = secondPrice * calculateDecreasingPriceMovement(stock);
let expected = (sharesPerMvmt * stock.getBidPrice()) + (sharesPerMvmt * secondPrice) + (sharesPerMvmt * thirdPrice);
expect(res).to.equal(expected + commission);
});
it("should cap the 'shares' argument at the stock's maximum number of shares", function() {
const maxRes = getBuyTransactionCost(stock, stock.maxShares, PositionTypes.Long);
const exceedRes = getBuyTransactionCost(stock, stock.maxShares * 10, PositionTypes.Long);
expect(maxRes).to.equal(exceedRes);
});
});
describe("getSellTransactionGain()", function() {
it("should fail on invalid 'stock' argument", function() {
const res = getSellTransactionGain({}, 10, PositionTypes.Long);
expect(res).to.equal(null);
});
it("should fail on invalid 'shares' arg", function() {
let res = getSellTransactionGain(stock, NaN, PositionTypes.Long);
expect(res).to.equal(null);
res = getSellTransactionGain(stock, -1, PositionTypes.Long);
expect(res).to.equal(null);
});
it("should properly evaluate LONG transactions that doesn't trigger a price movement", function() {
const shares = ctorParams.shareTxForMovement / 2;
const res = getSellTransactionGain(stock, shares, PositionTypes.Long);
const expected = shares * stock.getBidPrice() - commission;
expect(res).to.equal(expected);
});
it("should properly evaluate SHORT transactions that doesn't trigger a price movement", function() {
// We need to set this property in order to calculate gains from short position
stock.playerAvgShortPx = stock.price * 2;
const shares = ctorParams.shareTxForMovement / 2;
const res = getSellTransactionGain(stock, shares, PositionTypes.Short);
const expected = (shares * stock.playerAvgShortPx) + (shares * (stock.playerAvgShortPx - stock.getAskPrice())) - commission;
expect(res).to.equal(expected);
});
it("should properly evaluate LONG transactions that trigger price movements", function() {
const sharesPerMvmt = ctorParams.shareTxForMovement;
const shares = sharesPerMvmt * 3;
const res = getSellTransactionGain(stock, shares, PositionTypes.Long);
// Calculated expected gain
const mvmt = calculateDecreasingPriceMovement(stock);
const secondPrice = stock.getBidPrice() * mvmt;
const thirdPrice = secondPrice * mvmt;
const expected = (sharesPerMvmt * stock.getBidPrice()) + (sharesPerMvmt * secondPrice) + (sharesPerMvmt * thirdPrice);
expect(res).to.equal(expected - commission);
});
it("should properly evaluate SHORT transactions that trigger price movements", function() {
// We need to set this property in order to calculate gains from short position
stock.playerAvgShortPx = stock.price * 2;
const sharesPerMvmt = ctorParams.shareTxForMovement;
const shares = sharesPerMvmt * 3;
const res = getSellTransactionGain(stock, shares, PositionTypes.Short);
// Calculate expected gain
const mvmt = calculateIncreasingPriceMovement(stock);
const secondPrice = stock.getAskPrice() * mvmt;
const thirdPrice = secondPrice * mvmt;
function getGainForPrice(thisPrice) {
const origCost = sharesPerMvmt * stock.playerAvgShortPx;
return origCost + ((stock.playerAvgShortPx - thisPrice) * sharesPerMvmt);
}
const expected = getGainForPrice(stock.getAskPrice()) + getGainForPrice(secondPrice) + getGainForPrice(thirdPrice);
expect(res).to.equal(expected - commission);
});
it("should cap the 'shares' argument at the stock's maximum number of shares", function() {
const maxRes = getSellTransactionGain(stock, stock.maxShares, PositionTypes.Long);
const exceedRes = getSellTransactionGain(stock, stock.maxShares * 10, PositionTypes.Long);
expect(maxRes).to.equal(exceedRes);
});
});
});
describe("Price Movement Processor Functions", function() {
// N = 1 is the original price
function getNthPriceIncreasing(origPrice, n) {
let price = origPrice;
for (let i = 1; i < n; ++i) {
price *= calculateIncreasingPriceMovement(stock);
}
return price;
}
// N = 1 is the original price
function getNthPriceDecreasing(origPrice, n) {
let price = origPrice;
for (let i = 1; i < n; ++i) {
price *= calculateDecreasingPriceMovement(stock);
}
return price;
}
// N = 1 is the original forecast
function getNthForecast(origForecast, n) {
return origForecast - forecastChangePerPriceMovement * (n - 1);
}
describe("processBuyTransactionPriceMovement()", function() {
const noMvmtShares = Math.round(ctorParams.shareTxForMovement / 2.2);
const mvmtShares = ctorParams.shareTxForMovement * 3 + noMvmtShares;
it("should do nothing on invalid 'stock' argument", function() {
const oldPrice = stock.price;
const oldTracker = stock.shareTxUntilMovementUp;
processBuyTransactionPriceMovement({}, mvmtShares, PositionTypes.Long);
expect(stock.price).to.equal(oldPrice);
expect(stock.shareTxUntilMovementUp).to.equal(oldTracker);
});
it("should do nothing on invalid 'shares' arg", function() {
const oldPrice = stock.price;
const oldTracker = stock.shareTxUntilMovementUp;
processBuyTransactionPriceMovement(stock, NaN, PositionTypes.Long);
expect(stock.price).to.equal(oldPrice);
expect(stock.shareTxUntilMovementUp).to.equal(oldTracker);
processBuyTransactionPriceMovement(stock, -1, PositionTypes.Long);
expect(stock.price).to.equal(oldPrice);
expect(stock.shareTxUntilMovementUp).to.equal(oldTracker);
});
it("should properly evaluate a LONG transaction that doesn't trigger a price movement", function() {
const oldPrice = stock.price;
const oldForecast = stock.otlkMag;
processBuyTransactionPriceMovement(stock, noMvmtShares, PositionTypes.Long);
expect(stock.price).to.equal(oldPrice);
expect(stock.otlkMag).to.equal(oldForecast);
expect(stock.shareTxUntilMovementUp).to.equal(stock.shareTxForMovement - noMvmtShares);
expect(stock.shareTxUntilMovementDown).to.equal(stock.shareTxForMovement);
});
it("should properly evaluate a SHORT transaction that doesn't trigger a price movement", function() {
const oldPrice = stock.price;
const oldForecast = stock.otlkMag;
processBuyTransactionPriceMovement(stock, noMvmtShares, PositionTypes.Short);
expect(stock.price).to.equal(oldPrice);
expect(stock.otlkMag).to.equal(oldForecast);
expect(stock.shareTxUntilMovementDown).to.equal(stock.shareTxForMovement - noMvmtShares);
expect(stock.shareTxUntilMovementUp).to.equal(stock.shareTxForMovement);
});
it("should properly evaluate LONG transactions that trigger price movements", function() {
const oldPrice = stock.price;
const oldForecast = stock.otlkMag;
processBuyTransactionPriceMovement(stock, mvmtShares, PositionTypes.Long);
expect(stock.price).to.equal(getNthPriceIncreasing(oldPrice, 4));
expect(stock.otlkMag).to.equal(getNthForecast(oldForecast, 4));
expect(stock.shareTxUntilMovementUp).to.equal(stock.shareTxForMovement - noMvmtShares);
});
it("should properly evaluate SHORT transactions that trigger price movements", function() {
const oldPrice = stock.price;
const oldForecast = stock.otlkMag;
processBuyTransactionPriceMovement(stock, mvmtShares, PositionTypes.Short);
expect(stock.price).to.equal(getNthPriceDecreasing(oldPrice, 4));
expect(stock.otlkMag).to.equal(getNthForecast(oldForecast, 4));
expect(stock.shareTxUntilMovementDown).to.equal(stock.shareTxForMovement - noMvmtShares);
});
it("should properly evaluate LONG transactions of exactly 'shareTxForMovement' shares", function() {
const oldPrice = stock.price;
const oldForecast = stock.otlkMag;
processBuyTransactionPriceMovement(stock, stock.shareTxForMovement, PositionTypes.Long);
expect(stock.price).to.equal(getNthPriceIncreasing(oldPrice, 2));
expect(stock.otlkMag).to.equal(getNthForecast(oldForecast, 2));
expect(stock.shareTxUntilMovementUp).to.equal(stock.shareTxForMovement);
expect(stock.shareTxUntilMovementDown).to.equal(stock.shareTxForMovement);
});
it("should properly evaluate LONG transactions that total to 'shareTxForMovement' shares", function() {
const oldPrice = stock.price;
const oldForecast = stock.otlkMag;
processBuyTransactionPriceMovement(stock, Math.round(stock.shareTxForMovement / 2), PositionTypes.Long);
processBuyTransactionPriceMovement(stock, stock.shareTxUntilMovementUp, PositionTypes.Long);
expect(stock.price).to.equal(getNthPriceIncreasing(oldPrice, 2));
expect(stock.otlkMag).to.equal(getNthForecast(oldForecast, 2));
expect(stock.shareTxUntilMovementUp).to.equal(stock.shareTxForMovement);
expect(stock.shareTxUntilMovementDown).to.equal(stock.shareTxForMovement);
});
it("should properly evaluate LONG transactions that are a multiple of 'shareTxForMovement' shares", function() {
const oldPrice = stock.price;
const oldForecast = stock.otlkMag;
processBuyTransactionPriceMovement(stock, 3 * stock.shareTxForMovement, PositionTypes.Long);
expect(stock.price).to.equal(getNthPriceIncreasing(oldPrice, 4));
expect(stock.otlkMag).to.equal(getNthForecast(oldForecast, 4));
expect(stock.shareTxUntilMovementUp).to.equal(stock.shareTxForMovement);
expect(stock.shareTxUntilMovementDown).to.equal(stock.shareTxForMovement);
});
it("should properly evaluate SHORT transactions of exactly 'shareTxForMovement' shares", function() {
const oldPrice = stock.price;
const oldForecast = stock.otlkMag;
processBuyTransactionPriceMovement(stock, stock.shareTxForMovement, PositionTypes.Short);
expect(stock.price).to.equal(getNthPriceDecreasing(oldPrice, 2));
expect(stock.otlkMag).to.equal(getNthForecast(oldForecast, 2));
expect(stock.shareTxUntilMovementDown).to.equal(stock.shareTxForMovement);
expect(stock.shareTxUntilMovementUp).to.equal(stock.shareTxForMovement);
});
it("should properly evaluate SHORT transactions that total to 'shareTxForMovement' shares", function() {
const oldPrice = stock.price;
const oldForecast = stock.otlkMag;
processBuyTransactionPriceMovement(stock, Math.round(stock.shareTxForMovement / 2), PositionTypes.Short);
expect(stock.shareTxUntilMovementDown).to.be.below(stock.shareTxForMovement);
processBuyTransactionPriceMovement(stock, stock.shareTxUntilMovementDown, PositionTypes.Short);
expect(stock.price).to.equal(getNthPriceDecreasing(oldPrice, 2));
expect(stock.otlkMag).to.equal(getNthForecast(oldForecast, 2));
expect(stock.shareTxUntilMovementDown).to.equal(stock.shareTxForMovement);
});
it("should properly evaluate SHORT transactions that are a multiple of 'shareTxForMovement' shares", function() {
const oldPrice = stock.price;
const oldForecast = stock.otlkMag;
processBuyTransactionPriceMovement(stock, 3 * stock.shareTxForMovement, PositionTypes.Short);
expect(stock.price).to.equal(getNthPriceDecreasing(oldPrice, 4));
expect(stock.otlkMag).to.equal(getNthForecast(oldForecast, 4));
expect(stock.shareTxUntilMovementDown).to.equal(stock.shareTxForMovement);
});
});
describe("processSellTransactionPriceMovement()", function() {
const noMvmtShares = Math.round(ctorParams.shareTxForMovement / 2.2);
const mvmtShares = ctorParams.shareTxForMovement * 3 + noMvmtShares;
it("should do nothing on invalid 'stock' argument", function() {
const oldPrice = stock.price;
const oldTracker = stock.shareTxUntilMovementDown;
processSellTransactionPriceMovement({}, mvmtShares, PositionTypes.Long);
expect(stock.price).to.equal(oldPrice);
expect(stock.shareTxUntilMovementDown).to.equal(oldTracker);
});
it("should do nothing on invalid 'shares' arg", function() {
const oldPrice = stock.price;
const oldTracker = stock.shareTxUntilMovementDown;
processSellTransactionPriceMovement(stock, NaN, PositionTypes.Long);
expect(stock.price).to.equal(oldPrice);
expect(stock.shareTxUntilMovementDown).to.equal(oldTracker);
processSellTransactionPriceMovement(stock, -1, PositionTypes.Long);
expect(stock.price).to.equal(oldPrice);
expect(stock.shareTxUntilMovementDown).to.equal(oldTracker);
});
it("should properly evaluate a LONG transaction that doesn't trigger a price movement", function() {
const oldPrice = stock.price;
const oldForecast = stock.otlkMag;
processSellTransactionPriceMovement(stock, noMvmtShares, PositionTypes.Long);
expect(stock.price).to.equal(oldPrice);
expect(stock.otlkMag).to.equal(oldForecast);
expect(stock.shareTxUntilMovementDown).to.equal(stock.shareTxForMovement - noMvmtShares);
expect(stock.shareTxUntilMovementUp).to.equal(stock.shareTxForMovement);
});
it("should properly evaluate a SHORT transaction that doesn't trigger a price movement", function() {
const oldPrice = stock.price;
const oldForecast = stock.otlkMag;
processSellTransactionPriceMovement(stock, noMvmtShares, PositionTypes.Short);
expect(stock.price).to.equal(oldPrice);
expect(stock.otlkMag).to.equal(oldForecast);
expect(stock.shareTxUntilMovementUp).to.equal(stock.shareTxForMovement - noMvmtShares);
expect(stock.shareTxUntilMovementDown).to.equal(stock.shareTxForMovement);
});
it("should properly evaluate LONG transactions that trigger price movements", function() {
const oldPrice = stock.price;
const oldForecast = stock.otlkMag;
processSellTransactionPriceMovement(stock, mvmtShares, PositionTypes.Long);
expect(stock.price).to.equal(getNthPriceDecreasing(oldPrice, 4));
expect(stock.otlkMag).to.equal(getNthForecast(oldForecast, 4));
expect(stock.shareTxUntilMovementDown).to.equal(stock.shareTxForMovement - noMvmtShares);
expect(stock.shareTxUntilMovementUp).to.equal(stock.shareTxForMovement);
});
it("should properly evaluate SHORT transactions that trigger price movements", function() {
const oldPrice = stock.price;
const oldForecast = stock.otlkMag;
processSellTransactionPriceMovement(stock, mvmtShares, PositionTypes.Short);
expect(stock.price).to.equal(getNthPriceIncreasing(oldPrice, 4));
expect(stock.otlkMag).to.equal(getNthForecast(oldForecast, 4));
expect(stock.shareTxUntilMovementUp).to.equal(stock.shareTxForMovement - noMvmtShares);
expect(stock.shareTxUntilMovementDown).to.equal(stock.shareTxForMovement);
});
it("should properly evaluate LONG transactions of exactly 'shareTxForMovement' shares", function() {
const oldPrice = stock.price;
const oldForecast = stock.otlkMag;
processSellTransactionPriceMovement(stock, stock.shareTxForMovement, PositionTypes.Long);
expect(stock.price).to.equal(getNthPriceDecreasing(oldPrice, 2));
expect(stock.otlkMag).to.equal(getNthForecast(oldForecast, 2));
expect(stock.shareTxUntilMovementDown).to.equal(stock.shareTxForMovement);
expect(stock.shareTxUntilMovementUp).to.equal(stock.shareTxForMovement);
});
it("should properly evaluate LONG transactions that total to 'shareTxForMovement' shares", function() {
const oldPrice = stock.price;
const oldForecast = stock.otlkMag;
processSellTransactionPriceMovement(stock, Math.round(stock.shareTxForMovement / 2), PositionTypes.Long);
expect(stock.shareTxUntilMovementDown).to.be.below(stock.shareTxForMovement);
processSellTransactionPriceMovement(stock, stock.shareTxUntilMovementDown, PositionTypes.Long);
expect(stock.price).to.equal(getNthPriceDecreasing(oldPrice, 2));
expect(stock.otlkMag).to.equal(getNthForecast(oldForecast, 2));
expect(stock.shareTxUntilMovementDown).to.equal(stock.shareTxForMovement);
expect(stock.shareTxUntilMovementUp).to.equal(stock.shareTxForMovement);
});
it("should properly evaluate LONG transactions that are a multiple of 'shareTxForMovement' shares", function() {
const oldPrice = stock.price;
const oldForecast = stock.otlkMag;
processSellTransactionPriceMovement(stock, 3 * stock.shareTxForMovement, PositionTypes.Long);
expect(stock.price).to.equal(getNthPriceDecreasing(oldPrice, 4));
expect(stock.otlkMag).to.equal(getNthForecast(oldForecast, 4));
expect(stock.shareTxUntilMovementDown).to.equal(stock.shareTxForMovement);
expect(stock.shareTxUntilMovementUp).to.equal(stock.shareTxForMovement);
});
it("should properly evaluate SHORT transactions of exactly 'shareTxForMovement' shares", function() {
const oldPrice = stock.price;
const oldForecast = stock.otlkMag;
processSellTransactionPriceMovement(stock, stock.shareTxForMovement, PositionTypes.Short);
expect(stock.price).to.equal(getNthPriceIncreasing(oldPrice, 2));
expect(stock.otlkMag).to.equal(getNthForecast(oldForecast, 2));
expect(stock.shareTxUntilMovementUp).to.equal(stock.shareTxForMovement);
expect(stock.shareTxUntilMovementDown).to.equal(stock.shareTxForMovement);
});
it("should properly evaluate SHORT transactions that total to 'shareTxForMovement' shares", function() {
const oldPrice = stock.price;
const oldForecast = stock.otlkMag;
processSellTransactionPriceMovement(stock, Math.round(stock.shareTxForMovement / 2), PositionTypes.Short);
expect(stock.shareTxUntilMovementUp).to.be.below(stock.shareTxForMovement);
expect(stock.shareTxUntilMovementDown).to.equal(stock.shareTxForMovement);
processSellTransactionPriceMovement(stock, stock.shareTxUntilMovementUp, PositionTypes.Short);
expect(stock.price).to.equal(getNthPriceIncreasing(oldPrice, 2));
expect(stock.otlkMag).to.equal(getNthForecast(oldForecast, 2));
expect(stock.shareTxUntilMovementUp).to.equal(stock.shareTxForMovement);
expect(stock.shareTxUntilMovementDown).to.equal(stock.shareTxForMovement);
});
it("should properly evaluate SHORT transactions that are a multiple of 'shareTxForMovement' shares", function() {
const oldPrice = stock.price;
const oldForecast = stock.otlkMag;
processSellTransactionPriceMovement(stock, 3 * stock.shareTxForMovement, PositionTypes.Short);
expect(stock.price).to.equal(getNthPriceIncreasing(oldPrice, 4));
expect(stock.otlkMag).to.equal(getNthForecast(oldForecast, 4));
expect(stock.shareTxUntilMovementUp).to.equal(stock.shareTxForMovement);
expect(stock.shareTxUntilMovementDown).to.equal(stock.shareTxForMovement);
});
});
});
describe("Transaction (Buy/Sell) Functions", function() {
const suppressDialogOpt = { suppressDialog: true };
describe("buyStock()", function() {
it("should fail for invalid arguments", function() {
expect(buyStock({}, 1, null, suppressDialogOpt)).to.equal(false);
expect(buyStock(stock, 0, null, suppressDialogOpt)).to.equal(false);
expect(buyStock(stock, -1, null, suppressDialogOpt)).to.equal(false);
expect(buyStock(stock, NaN, null, suppressDialogOpt)).to.equal(false);
});
it("should fail if player doesn't have enough money", function() {
Player.setMoney(0);
expect(buyStock(stock, 1, null, suppressDialogOpt)).to.equal(false);
});
it("should not allow for transactions that exceed the maximum shares", function() {
const maxShares = stock.maxShares;
expect(buyStock(stock, maxShares + 1, null, suppressDialogOpt)).to.equal(false);
});
it("should return true and properly update stock properties for successful transactions", function() {
const shares = 1e3;
const cost = getBuyTransactionCost(stock, shares, PositionTypes.Long);
Player.setMoney(cost);
expect(buyStock(stock, shares, null, suppressDialogOpt)).to.equal(true);
expect(stock.playerShares).to.equal(shares);
expect(stock.playerAvgPx).to.be.above(0);
expect(Player.money.toNumber()).to.equal(0);
});
});
describe("sellStock()", function() {
it("should fail for invalid arguments", function() {
expect(sellStock({}, 1, null, suppressDialogOpt)).to.equal(false);
expect(sellStock(stock, 0, null, suppressDialogOpt)).to.equal(false);
expect(sellStock(stock, -1, null, suppressDialogOpt)).to.equal(false);
expect(sellStock(stock, NaN, null, suppressDialogOpt)).to.equal(false);
});
it("should fail if player doesn't have any shares", function() {
Player.setMoney(0);
expect(sellStock(stock, 1, null, suppressDialogOpt)).to.equal(false);
});
it("should not allow for transactions that exceed the maximum shares", function() {
const maxShares = stock.maxShares;
expect(sellStock(stock, maxShares + 1, null, suppressDialogOpt)).to.equal(false);
});
it("should return true and properly update stock properties for successful transactions", function() {
const shares = 1e3;
stock.playerShares = shares;
stock.playerAvgPx = stock.price;
const gain = getSellTransactionGain(stock, shares, PositionTypes.Long);
Player.setMoney(0);
expect(sellStock(stock, shares, null, suppressDialogOpt)).to.equal(true);
expect(stock.playerShares).to.equal(0);
expect(stock.playerAvgPx).to.equal(0);
expect(Player.money.toNumber()).to.equal(gain);
});
it("should cap the number of sharse sold to however many the player owns", function() {
const attemptedShares = 2e3;
const actualShares = 1e3;
stock.playerShares = actualShares;
stock.playerAvgPx = stock.price;
const gain = getSellTransactionGain(stock, actualShares, PositionTypes.Long);
Player.setMoney(0);
expect(sellStock(stock, attemptedShares, null, suppressDialogOpt)).to.equal(true);
expect(stock.playerShares).to.equal(0);
expect(stock.playerAvgPx).to.equal(0);
expect(Player.money.toNumber()).to.equal(gain);
});
it("should properly update stock properties for partial transactions", function() {
const shares = 1e3;
const origPrice = stock.price;
stock.playerShares = 2 * shares;
stock.playerAvgPx = origPrice;
const gain = getSellTransactionGain(stock, shares, PositionTypes.Long);
Player.setMoney(0);
expect(sellStock(stock, shares, null, suppressDialogOpt)).to.equal(true);
expect(stock.playerShares).to.equal(shares);
expect(stock.playerAvgPx).to.equal(origPrice);
expect(Player.money.toNumber()).to.equal(gain);
});
});
describe("shortStock()", function() {
it("should fail for invalid arguments", function() {
expect(shortStock({}, 1, null, suppressDialogOpt)).to.equal(false);
expect(shortStock(stock, 0, null, suppressDialogOpt)).to.equal(false);
expect(shortStock(stock, -1, null, suppressDialogOpt)).to.equal(false);
expect(shortStock(stock, NaN, null, suppressDialogOpt)).to.equal(false);
});
it("should fail if player doesn't have enough money", function() {
Player.setMoney(0);
expect(shortStock(stock, 1, null, suppressDialogOpt)).to.equal(false);
});
it("should not allow for transactions that exceed the maximum shares", function() {
const maxShares = stock.maxShares;
expect(shortStock(stock, maxShares + 1, null, suppressDialogOpt)).to.equal(false);
});
it("should return true and properly update stock properties for successful transactions", function() {
const shares = 1e3;
const cost = getBuyTransactionCost(stock, shares, PositionTypes.Short);
Player.setMoney(cost);
expect(shortStock(stock, shares, null, suppressDialogOpt)).to.equal(true);
expect(stock.playerShortShares).to.equal(shares);
expect(stock.playerAvgShortPx).to.be.above(0);
expect(Player.money.toNumber()).to.equal(0);
});
});
describe("sellShort()", function() {
it("should fail for invalid arguments", function() {
expect(sellShort({}, 1, null, suppressDialogOpt)).to.equal(false);
expect(sellShort(stock, 0, null, suppressDialogOpt)).to.equal(false);
expect(sellShort(stock, -1, null, suppressDialogOpt)).to.equal(false);
expect(sellShort(stock, NaN, null, suppressDialogOpt)).to.equal(false);
});
it("should fail if player doesn't have any shares", function() {
Player.setMoney(0);
expect(sellShort(stock, 1, null, suppressDialogOpt)).to.equal(false);
});
it("should not allow for transactions that exceed the maximum shares", function() {
const maxShares = stock.maxShares;
expect(sellShort(stock, maxShares + 1, null, suppressDialogOpt)).to.equal(false);
});
it("should return true and properly update stock properties for successful transactions", function() {
const shares = 1e3;
stock.playerShortShares = shares;
stock.playerAvgShortPx = stock.price;
const gain = getSellTransactionGain(stock, shares, PositionTypes.Short);
Player.setMoney(0);
expect(sellShort(stock, shares, null, suppressDialogOpt)).to.equal(true);
expect(stock.playerShortShares).to.equal(0);
expect(stock.playerAvgShortPx).to.equal(0);
expect(Player.money.toNumber()).to.equal(gain);
});
it("should cap the number of sharse sold to however many the player owns", function() {
const attemptedShares = 2e3;
const actualShares = 1e3;
stock.playerShortShares = actualShares;
stock.playerAvgShortPx = stock.price;
const gain = getSellTransactionGain(stock, actualShares, PositionTypes.Short);
Player.setMoney(0);
expect(sellShort(stock, attemptedShares, null, suppressDialogOpt)).to.equal(true);
expect(stock.playerShortShares).to.equal(0);
expect(stock.playerAvgShortPx).to.equal(0);
expect(Player.money.toNumber()).to.equal(gain);
});
it("should properly update stock properties for partial transactions", function() {
const shares = 1e3;
const origPrice = stock.price;
stock.playerShortShares = 2 * shares;
stock.playerAvgShortPx = origPrice;
const gain = getSellTransactionGain(stock, shares, PositionTypes.Short);
Player.setMoney(0);
expect(sellShort(stock, shares, null, suppressDialogOpt)).to.equal(true);
expect(stock.playerShortShares).to.equal(shares);
expect(stock.playerAvgShortPx).to.equal(origPrice);
expect(Player.money.toNumber()).to.equal(gain);
});
});
});
describe("Order Class", function() {
it("should throw on invalid arguments", function() {
function invalid1() {
return new Order({}, 1, 1, OrderTypes.LimitBuy, PositionTypes.Long);
}
function invalid2() {
return new Order("FOO", "z", 0, OrderTypes.LimitBuy, PositionTypes.Short);
}
function invalid3() {
return new Order("FOO", 1, {}, OrderTypes.LimitBuy, PositionTypes.Short);
}
function invalid4() {
return new Order("FOO", 1, NaN, OrderTypes.LimitBuy, PositionTypes.Short);
}
function invalid5() {
return new Order("FOO", NaN, 0, OrderTypes.LimitBuy, PositionTypes.Short);
}
expect(invalid1).to.throw();
expect(invalid2).to.throw();
expect(invalid3).to.throw();
expect(invalid4).to.throw();
expect(invalid5).to.throw();
});
});
describe("Order Processing", function() {
});
});

1310
test/StockMarketTests.ts Normal file

File diff suppressed because it is too large Load Diff

@ -1,3 +1,5 @@
import { KEY } from "./helpers/keyCodes";
/**
* Create and display a pop-up dialog box.
* This dialog box does not allow for any interaction and should close when clicking
@ -9,28 +11,31 @@ let dialogBoxes = [];
$(document).click(function(event) {
if (dialogBoxOpened && dialogBoxes.length >= 1) {
if (!$(event.target).closest(dialogBoxes[0]).length){
dialogBoxes[0].remove();
dialogBoxes.splice(0, 1);
if (dialogBoxes.length == 0) {
dialogBoxOpened = false;
} else {
dialogBoxes[0].style.visibility = "visible";
}
closeTopmostDialogBox();
}
}
});
function closeTopmostDialogBox() {
if (!dialogBoxOpened || dialogBoxes.length === 0) return;
dialogBoxes[0].remove();
dialogBoxes.shift();
if (dialogBoxes.length == 0) {
dialogBoxOpened = false;
} else {
dialogBoxes[0].style.visibility = "visible";
}
}
// Dialog box close buttons
$(document).on('click', '.dialog-box-close-button', function( event ) {
if (dialogBoxOpened && dialogBoxes.length >= 1) {
dialogBoxes[0].remove();
dialogBoxes.splice(0, 1);
if (dialogBoxes.length == 0) {
dialogBoxOpened = false;
} else {
dialogBoxes[0].style.visibility = "visible";
}
closeTopmostDialogBox();
});
document.addEventListener("keydown", function (event) {
if (event.keyCode == KEY.ESC && dialogBoxOpened) {
closeTopmostDialogBox();
event.preventDefault();
}
});

@ -1,68 +0,0 @@
import {killWorkerScript} from "../src/NetscriptWorker";
import {clearEventListeners} from "./uiHelpers/clearEventListeners";
import {arrayToString} from "./helpers/arrayToString";
$(document).keydown(function(event) {
if (logBoxOpened && event.keyCode == 27) {
logBoxClose();
}
});
function logBoxInit() {
var closeButton = document.getElementById("log-box-close");
logBoxClose();
//Close Dialog box
closeButton.addEventListener("click", function() {
logBoxClose();
return false;
});
document.getElementById("log-box-text-header").style.display = "inline-block";
};
document.addEventListener("DOMContentLoaded", logBoxInit, false);
function logBoxClose() {
logBoxOpened = false;
var logBox = document.getElementById("log-box-container");
logBox.style.display = "none";
}
function logBoxOpen() {
logBoxOpened = true;
var logBox = document.getElementById("log-box-container");
logBox.style.display = "block";
}
var logBoxOpened = false;
var logBoxCurrentScript = null;
function logBoxCreate(script) {
logBoxCurrentScript = script;
var killScriptBtn = clearEventListeners("log-box-kill-script");
killScriptBtn.addEventListener("click", ()=>{
killWorkerScript(script, script.server);
return false;
});
document.getElementById('log-box-kill-script').style.display = "inline-block";
logBoxOpen();
document.getElementById("log-box-text-header").innerHTML =
logBoxCurrentScript.filename + " " + arrayToString(logBoxCurrentScript.args) + ":<br><br>";
logBoxCurrentScript.logUpd = true;
logBoxUpdateText();
}
function logBoxUpdateText() {
var txt = document.getElementById("log-box-text");
if (logBoxCurrentScript && logBoxOpened && txt && logBoxCurrentScript.logUpd) {
txt.innerHTML = "";
for (var i = 0; i < logBoxCurrentScript.logs.length; ++i) {
txt.innerHTML += logBoxCurrentScript.logs[i];
txt.innerHTML += "<br>";
}
logBoxCurrentScript.logUpd = false;
}
}
export {logBoxCreate, logBoxUpdateText, logBoxOpened, logBoxCurrentScript};

106
utils/LogBox.ts Normal file

@ -0,0 +1,106 @@
import { killWorkerScript } from "../src/Netscript/killWorkerScript";
import { RunningScript } from "../src/Script/RunningScript";
import { clearEventListeners } from "./uiHelpers/clearEventListeners";
import { arrayToString } from "./helpers/arrayToString";
import { KEY } from "./helpers/keyCodes";
document.addEventListener("keydown", function(event: KeyboardEvent) {
if (logBoxOpened && event.keyCode == KEY.ESC) {
logBoxClose();
}
});
let logBoxContainer: HTMLElement | null;
let textHeader: HTMLElement | null;
let logText: HTMLElement | null;
function logBoxInit(): void {
// Initialize Close button click listener
const closeButton = document.getElementById("log-box-close");
if (closeButton == null) {
console.error(`Could not find LogBox's close button`);
return;
}
closeButton.addEventListener("click", function() {
logBoxClose();
return false;
});
// Initialize text header
textHeader = document.getElementById("log-box-text-header");
if (textHeader instanceof HTMLElement) {
textHeader.style.display = "inline-block";
}
// Initialize references to other DOM elements
logBoxContainer = document.getElementById("log-box-container");
logText = document.getElementById("log-box-text");
logBoxClose();
document.removeEventListener("DOMContentLoaded", logBoxInit);
};
document.addEventListener("DOMContentLoaded", logBoxInit);
function logBoxClose() {
logBoxOpened = false;
if (logBoxContainer instanceof HTMLElement) {
logBoxContainer.style.display = "none";
}
}
function logBoxOpen() {
logBoxOpened = true;
if (logBoxContainer instanceof HTMLElement) {
logBoxContainer.style.display = "block";
}
}
export let logBoxOpened = false;
let logBoxCurrentScript: RunningScript | null = null;
export function logBoxCreate(script: RunningScript) {
logBoxCurrentScript = script;
const killScriptBtn = clearEventListeners("log-box-kill-script");
if (killScriptBtn == null) {
console.error(`Could not find LogBox's 'Kill Script' button`);
return;
}
killScriptBtn.addEventListener("click", () => {
killWorkerScript(script, script.server, true);
return false;
});
killScriptBtn.style.display = "inline-block";
logBoxOpen();
if (textHeader instanceof HTMLElement) {
textHeader.innerHTML = `${logBoxCurrentScript.filename} ${arrayToString(logBoxCurrentScript.args)}:<br><br>`;
} else {
console.warn(`LogBox's Text Header DOM element is null`);
}
logBoxCurrentScript.logUpd = true;
logBoxUpdateText();
}
export function logBoxUpdateText() {
if (!(logText instanceof HTMLElement)) { return; }
if (logBoxCurrentScript && logBoxOpened && logBoxCurrentScript.logUpd) {
logText.innerHTML = "";
for (let i = 0; i < logBoxCurrentScript.logs.length; ++i) {
logText.innerHTML += logBoxCurrentScript.logs[i];
logText.innerHTML += "<br>";
}
logBoxCurrentScript.logUpd = false;
}
}