Merge pull request #3269 from danielyxie/dev

v1.6.0
This commit is contained in:
hydroflame 2022-03-29 15:37:55 -04:00 committed by GitHub
commit 8ccf5a2d43
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
73 changed files with 1296 additions and 151729 deletions

@ -0,0 +1,85 @@
name: Check for Generated Files
on:
# Triggers the workflow on push or pull request events but only for the dev branch
pull_request:
branches: [dev]
# Allows you to run this workflow manually from the Actions tab
workflow_dispatch:
jobs:
checkFiles:
name: Check Files
runs-on: ubuntu-latest
steps:
- name: Checkout files
uses: actions/checkout@v2
- name: Check bundle files
id: changed-bundle-files
uses: tj-actions/changed-files@v18.4
with:
files: |
dist/*
index.html
main.bundle.js
main.bundle.js.map
- name: Check documentation changes
id: changed-markdown-doc-files
uses: tj-actions/changed-files@v18.4
with:
files: |
markdown/*.md
- name: Warn when bundle files were changed
id: warn-bundles-changed
if: steps.changed-bundle-files.outputs.any_changed == 'true'
run: |
echo "One or more files in the bundle files were changed." >> warnings.txt
- name: Warn when documentation markdown files were changed
id: warn-markdown-changed
if: steps.changed-markdown-doc-files.outputs.any_changed == 'true'
run: |
echo "One or more files in the markdown documentation were changed." >> warnings.txt
- name: Print Warnings
id: get-warnings
run: |
if [ -f warnings.txt ]
then
echo "::set-output name=has_warnings::true"
else
echo "::set-output name=has_warnings::false"
touch warnings.txt
fi
- name: Get Comment Body
id: get-comment-body
if : steps.get-warnings.outputs.has_warnings == 'true'
run: |
cat warnings.txt > comment.txt
echo "" >> comment.txt
echo "Please do not commit files generated by webpack or generated markdown" >> comment.txt
echo "" >> comment.txt
echo "See [CONTRIBUTING.md](https://github.com/danielyxie/bitburner/blob/dev/doc/CONTRIBUTING.md) for details." >> comment.txt
body=$(cat comment.txt)
body="${body//'%'/'%25'}"
body="${body//$'\n'/'%0A'}"
body="${body//$'\r'/'%0D'}"
echo ::set-output name=body::$body
- name: Add github comment on problem
if : steps.get-warnings.outputs.has_warnings == 'true'
uses: peter-evans/commit-comment@v1
with:
body: ${{ steps.get-comment-body.outputs.body }}
- name: Flag as error
if : steps.get-warnings.outputs.has_warnings == 'true'
run: |
COMMIT_WARNINGS=$(cat warnings.txt)
echo "::warning:: $COMMIT_WARNINGS"
exit 1

@ -1,23 +0,0 @@
.staneksgift_row {
padding: 0;
margin: 0;
}
.staneksgift_cell {
width: 25px;
height: 25px;
background-color: #808080;
font-color: white;
padding: 0px;
margin: 0px;
border: 1px solid black;
float: left;
}
.staneksgift_cell:first-child {
clear: left;
}
.staneksgift_container {
position: fixed;
}

@ -1,2 +0,0 @@
!function(n){function t(t){for(var e,i,f=t[0],c=t[1],l=t[2],a=0,s=[];a<f.length;a++)i=f[a],Object.prototype.hasOwnProperty.call(r,i)&&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(p&&p(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={2: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 p=c;u.push([1281,0]),o()}({1281:function(n,t,o){"use strict";o.r(t);o(1282),o(1284),o(1286),o(1288),o(1290),o(1292),o(1294),o(1296),o(1298),o(1300),o(1302),o(1304),o(1306),o(1308),o(1310),o(1312),o(1314),o(1316),o(1318),o(1320),o(1322),o(1324),o(1326),o(1328),o(1330),o(1332),o(1334),o(1336),o(1338),o(1340)},1284:function(n,t,o){},1286:function(n,t,o){},1288:function(n,t,o){},1290:function(n,t,o){},1292:function(n,t,o){},1294:function(n,t,o){},1296:function(n,t,o){},1298:function(n,t,o){},1300:function(n,t,o){},1302:function(n,t,o){},1304:function(n,t,o){},1306:function(n,t,o){},1308:function(n,t,o){},1310:function(n,t,o){},1312:function(n,t,o){},1314:function(n,t,o){},1316:function(n,t,o){},1318:function(n,t,o){},1320:function(n,t,o){},1322:function(n,t,o){},1324:function(n,t,o){},1326:function(n,t,o){},1328:function(n,t,o){},1330:function(n,t,o){},1332:function(n,t,o){},1334:function(n,t,o){},1336:function(n,t,o){},1338:function(n,t,o){},1340:function(n,t,o){}});
//# sourceMappingURL=engineStyle.bundle.js.map

6070
dist/engineStyle.css vendored

File diff suppressed because it is too large Load Diff

54
dist/vendor.bundle.js vendored

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

301
dist/vendor.css vendored

@ -1,301 +0,0 @@
/*! normalize.css v8.0.0 | MIT License | github.com/necolas/normalize.css */
/* Document
========================================================================== */
/**
* 1. Correct the line height in all browsers.
* 2. Prevent adjustments of font size after orientation changes in iOS.
*/
html {
line-height: 1.15;
/* 1 */
-webkit-text-size-adjust: 100%;
/* 2 */ }
/* Sections
========================================================================== */
/**
* Remove the margin in all browsers.
*/
body {
margin: 0; }
/**
* Correct the font size and margin on `h1` elements within `section` and
* `article` contexts in Chrome, Firefox, and Safari.
*/
h1 {
font-size: 2em;
margin: 0.67em 0; }
/* Grouping content
========================================================================== */
/**
* 1. Add the correct box sizing in Firefox.
* 2. Show the overflow in Edge and IE.
*/
hr {
box-sizing: content-box;
/* 1 */
height: 0;
/* 1 */
overflow: visible;
/* 2 */ }
/**
* 1. Correct the inheritance and scaling of font size in all browsers.
* 2. Correct the odd `em` font sizing in all browsers.
*/
pre {
font-family: monospace, monospace;
/* 1 */
font-size: 1em;
/* 2 */ }
/* Text-level semantics
========================================================================== */
/**
* Remove the gray background on active links in IE 10.
*/
a {
background-color: transparent; }
/**
* 1. Remove the bottom border in Chrome 57-
* 2. Add the correct text decoration in Chrome, Edge, IE, Opera, and Safari.
*/
abbr[title] {
border-bottom: none;
/* 1 */
text-decoration: underline;
/* 2 */
text-decoration: underline dotted;
/* 2 */ }
/**
* Add the correct font weight in Chrome, Edge, and Safari.
*/
b,
strong {
font-weight: bolder; }
/**
* 1. Correct the inheritance and scaling of font size in all browsers.
* 2. Correct the odd `em` font sizing in all browsers.
*/
code,
kbd,
samp {
font-family: monospace, monospace;
/* 1 */
font-size: 1em;
/* 2 */ }
/**
* Add the correct font size in all browsers.
*/
small {
font-size: 80%; }
/**
* Prevent `sub` and `sup` elements from affecting the line height in
* all browsers.
*/
sub,
sup {
font-size: 75%;
line-height: 0;
position: relative;
vertical-align: baseline; }
sub {
bottom: -0.25em; }
sup {
top: -0.5em; }
/* Embedded content
========================================================================== */
/**
* Remove the border on images inside links in IE 10.
*/
img {
border-style: none; }
/* Forms
========================================================================== */
/**
* 1. Change the font styles in all browsers.
* 2. Remove the margin in Firefox and Safari.
*/
button,
input,
optgroup,
select,
textarea {
font-family: inherit;
/* 1 */
font-size: 100%;
/* 1 */
line-height: 1.15;
/* 1 */
margin: 0;
/* 2 */ }
/**
* Show the overflow in IE.
* 1. Show the overflow in Edge.
*/
button,
input {
/* 1 */
overflow: visible; }
/**
* Remove the inheritance of text transform in Edge, Firefox, and IE.
* 1. Remove the inheritance of text transform in Firefox.
*/
button,
select {
/* 1 */
text-transform: none; }
/**
* Correct the inability to style clickable types in iOS and Safari.
*/
button,
[type="button"],
[type="reset"],
[type="submit"] {
-webkit-appearance: button; }
/**
* Remove the inner border and padding in Firefox.
*/
button::-moz-focus-inner,
[type="button"]::-moz-focus-inner,
[type="reset"]::-moz-focus-inner,
[type="submit"]::-moz-focus-inner {
border-style: none;
padding: 0; }
/**
* Restore the focus styles unset by the previous rule.
*/
button:-moz-focusring,
[type="button"]:-moz-focusring,
[type="reset"]:-moz-focusring,
[type="submit"]:-moz-focusring {
outline: 1px dotted ButtonText; }
/**
* Correct the padding in Firefox.
*/
fieldset {
padding: 0.35em 0.75em 0.625em; }
/**
* 1. Correct the text wrapping in Edge and IE.
* 2. Correct the color inheritance from `fieldset` elements in IE.
* 3. Remove the padding so developers are not caught out when they zero out
* `fieldset` elements in all browsers.
*/
legend {
box-sizing: border-box;
/* 1 */
color: inherit;
/* 2 */
display: table;
/* 1 */
max-width: 100%;
/* 1 */
padding: 0;
/* 3 */
white-space: normal;
/* 1 */ }
/**
* Add the correct vertical alignment in Chrome, Firefox, and Opera.
*/
progress {
vertical-align: baseline; }
/**
* Remove the default vertical scrollbar in IE 10+.
*/
textarea {
overflow: auto; }
/**
* 1. Add the correct box sizing in IE 10.
* 2. Remove the padding in IE 10.
*/
[type="checkbox"],
[type="radio"] {
box-sizing: border-box;
/* 1 */
padding: 0;
/* 2 */ }
/**
* Correct the cursor style of increment and decrement buttons in Chrome.
*/
[type="number"]::-webkit-inner-spin-button,
[type="number"]::-webkit-outer-spin-button {
height: auto; }
/**
* 1. Correct the odd appearance in Chrome and Safari.
* 2. Correct the outline style in Safari.
*/
[type="search"] {
-webkit-appearance: textfield;
/* 1 */
outline-offset: -2px;
/* 2 */ }
/**
* Remove the inner padding in Chrome and Safari on macOS.
*/
[type="search"]::-webkit-search-decoration {
-webkit-appearance: none; }
/**
* 1. Correct the inability to style clickable types in iOS and Safari.
* 2. Change font properties to `inherit` in Safari.
*/
::-webkit-file-upload-button {
-webkit-appearance: button;
/* 1 */
font: inherit;
/* 2 */ }
/* Interactive
========================================================================== */
/*
* Add the correct display in Edge, IE 10+, and Firefox.
*/
details {
display: block; }
/*
* Add the correct display in all browsers.
*/
summary {
display: list-item; }
/* Misc
========================================================================== */
/**
* Add the correct display in IE 10+.
*/
template {
display: none; }
/**
* Add the correct display in IE 10.
*/
[hidden] {
display: none; }
/*# sourceMappingURL=vendor.css.map*/

@ -12,4 +12,5 @@ must be unlocked.
Source-Files <advancedgameplay/sourcefiles>
Intelligence <advancedgameplay/intelligence>
Sleeves <advancedgameplay/sleeves>
Grafting <advancedgameplay/grafting>
Hacking algorithms <advancedgameplay/hackingalgorithms>

@ -0,0 +1,18 @@
.. _gameplay_grafting:
Grafting
========
Grafting is an experimental process through which you can obtain the benefits of
Augmentations, without needing to reboot your body.
In order to graft, you must first purchase a blueprint for and craft the Augmentation.
This can be done at VitaLife in New Tokyo, where you'll find a shady researcher with
questionable connections. Once you purchase a blueprint, you will start crafting the
Augmentation, and it will be grafted to your body once complete.
Be warned, some who have tested grafting have reported an unidentified malware. Dubbed
"Entropy", this virus seems to grow in potency as more Augmentations are grafted,
causing unpredictable affects to the victim.
Note that when crafting an Augmentation, cancelling will **not** save your progress,
and the money spent will **not** be returned.

@ -16,3 +16,4 @@ Intelligence will boost your production for many actions in the game, including:
* Crime success rate
* Bladeburner
* Reputation gain for companies & factions
* Augmentation grafting speed

@ -91,19 +91,3 @@ and above, and is only available after defeating BitNode-10 at least once.
Memory is a persistent stat, meaning it never gets reset back to 1.
The maximum possible value for a sleeve's memory is 100.
Re-sleeving
^^^^^^^^^^^
Re-sleeving is the process of digitizing and transferring your consciousness into a
new human body, or "sleeve". When you re-sleeve into a new body, your stat experience
and Augmentations get replaced with those of the new body.
In order to re-sleeve, you must purchase new bodies. This can be done at VitaLife in
New Tokyo. Once you purchase a body to re-sleeve into, the effects will take
place immediately.
Note that resleeving **REMOVES** all of your currently-installed Augmentations,
and replaces them with the ones provided by the purchased sleeve. However,
Augmentations that are purchased but not installed will **not** be removed. If you have purchased
an Augmentation and then re-sleeve into a body which already has that Augmentation,
it will be removed since you cannot have duplicate Augmentations.

@ -53,6 +53,7 @@ List of all Source-Files
+-------------------------------------+--------------------------------------------------------------------------------------------------------------------------------------------------------------------------+
|| BitNode-10: Digital Carbon || * Each level of this grants a Duplicate Sleeve. |
|| || * Allows the player to access the `Sleeve API <https://github.com/danielyxie/bitburner/blob/dev/markdown/bitburner.sleeve.md>`_ in other BitNodes. |
|| || * Grants the player access to the VitaLife secret laboratory in other BitNodes. Also grants access to the Grafting API. |
+-------------------------------------+--------------------------------------------------------------------------------------------------------------------------------------------------------------------------+
|| BitNode-11: The Big Crash || * Company favor increases both the player's salary and reputation gain at that |
|| || company by 1% per favor (rather than just the reputation gain). |

@ -3,6 +3,108 @@
Changelog
=========
v1.6.0 - 2022-03-29 Grafting
----------------------------
** Vitalife secret lab **
* A new mechanic called Augmentation Grafting has been added. Resleeving has been removed.
* Credit to @nikfolas for his incredible work.
** Stanek **
* BREAKING: Many functions in the stanek API were renamed in order to avoid name collision with things like Map.prototype.get
** UI **
* Major update to Sleeve, Gang UI, and Create Program (@nikfolas)
* re-add pre tags to support slash n in prompt (@jacktose)
* Tabelize linked output of 'ls' (@Master-Guy)
* Add the ability to filter open scripts (@phyzical)
* Add minHeight to editor tabs (@nickofolas)
* Properly expand gang equipment cards to fill entire screen (@nickofolas)
* Add shortcut to Faction augmentations page from FactionsRoot (@nickofolas)
* Fix extra space on editor tabs (@nickofolas)
* Present offline message as list (@DSteve595)
* add box showing remaining augments per faction (@jjayeon)
* Add tab switching support to vim mode (@JParisFerrer)
* Show current task on gang management screen (@zeddrak)
* Fix for ui of gang members current task when set via api (@phyzical)
* Don't hide irrelevant materials if their stock is not empty and hide irrelevant divisions from Export (@SagePtr)
* Fix regex to enable alpha transparency hex codes (8 digits) (@surdaft)
** API **
* Added dark web functions to ns api
* BREAKING: purchaseTor() should returns true if player already has Tor. (@DavidGrinberg, @waffleattack)
* Implement getBonusTime in Corporation API (@t-wolfeadam)
* Added functions to purchase TIX and WSI (@incubusnb)
* purchaseSleeveAug checks shock value (@incubusnb)
* Fix bug with hacknet api
* Fix spendHashes bug
* Added 0 cost of asleep() (@Master-Guy)
* Fix some misleading corporation errors (@TheRealMaxion)
* expose the inBladeburner on the player object (@phyzical)
* added ram charge for stanek width and height (@phyzical)
* Fix sufficient player money check to buy back shares. (@ChrissiQ)
* Fix Static Ram Circumventing for some NS functions (@CrafterKolyan)
* added CorporationSoftCap to NetscriptDefinitions (@phyzical)
* Added definition of autocomplete() 'data' argument. (@tigercat2000)
* Adding support for text/select options in Prompt command (@PhilipArmstead)
* Added the ability to exportGame via api (@phyzical)
** Arcade **
* Added an arcade to New Tokyo where you can play a 4 year old version of bitburner.
** Misc. **
* Add a warning triggered while auto-saves are off. (@MartinFournier)
* Log info for field analysis now displays actual rank gained. (@ApamNapat)
* Removed BladeburnerSkillCost from skill point cost description. (@ApamNapat)
* Fix handling for UpArrow in bladeburner console. (@dowinter)
* Add GitHub action to check PRs for generated files. (@MartinFournier)
* Cap Staneks gift at 25x25 to prevent crashes. (@waffleattack)
* Remove old & unused files from repository. (@MartinFournier)
* Factions on the factions screens are sorted by story progress / type. (@phyzical)
* Fix log manager not picking up new runs of scripts. (@phyzical)
* Added prettier to cicd.
* UI improvements (@phyzical)
* Documentation / Typos (@nanogyth, @Master-Guy, @incubusnb, @ApamNapat, @phyzical, @SagePtr)
* Give player code a copy of Division.upgrades instead of the live object (@Ornedan)
* Fix bug with small town achievement.
* Fix bug with purchaseSleeveAug (@phyzical)
* Check before unlocking corp upgrade (@gianfun)
* General codebase improvements. (@phyzical, @Master-Guy, @ApamNapat)
* Waiting on promises in NS1 no longer freezes the script. (@Master-Guy)
* Fix bug with missing ramcost for tFormat (@TheMas3212)
* Fix crash with new prompt
* Quick fix to prevent division by 0 in terminal (@Master-Guy)
* removed ip references (@phyzical, @Master-Guy)
* Terminal now supports 'ls -l'
* Fix negative number formatting (@Master-Guy)
* Fix unique ip generation (@InDieTasten)
* remove terminal command theme from docs (@phyzical)
* Fix 'Augmentations Left' with gang factions (@nickofolas)
* Attempt to fix 'bladeburner.process()' early routing issue (@MartinFournier)
* work in progress augment fix (@phyzical)
* Fixes missing space in Smart Supply (@TheRealMaxion)
* Change license to Apache 2 with Commons Clause
* updated regex sanitization (@mbrannen)
* Sleeve fix for when faction isnt found (@phyzical)
* Fix editor "close" naming (@phyzical)
* Fix bug with sleeves where some factions would be listed as workable. (@phyzical)
* Fix research tree of product industries post-prestige (@pd)
* Added a check for exisiting industry type before expanding (@phyzical)
* fix hackAnalyzeThreads returning infinity (@chrisrabe)
* Make growthAnalyze more accurate (@dwRchyngqxs)
* Add 'Zoom -> Reset Zoom' command to Steam (@smolgumball)
* Add hasOwnProperty check to GetServer (@SagePtr)
* Speed up employee productivity calculation (@pd)
* Field Work and Security Work benefit from 'share' (@SagePtr)
* Nerf noodle bar.
v1.5.0 - Steam Cloud integration
--------------------------------

@ -64,9 +64,9 @@ documentation_title = '{0} Documentation'.format(project)
# built documents.
#
# The short X.Y version.
version = '1.5'
version = '1.6'
# The full version, including alpha/beta/rc tags.
release = '1.5.0'
release = '1.6.0'
# The language for content autogenerated by Sphinx. Refer to documentation
# for a list of supported languages.

File diff suppressed because one or more lines are too long

@ -3,7 +3,6 @@
<head>
<meta charset="utf-8"/>
<title>Bitburner</title>
<link rel="stylesheet" href="main.css" />
<style>
body {
background-color: black;

@ -3,7 +3,6 @@
<head>
<meta charset="utf-8"/>
<title>Bitburner</title>
<link rel="stylesheet" href="main.css" />
<style>
body {
background-color: black;

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

7461
main.css

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

@ -1,7 +1,7 @@
{
"name": "bitburner",
"license": "SEE LICENSE IN license.txt",
"version": "1.5.0",
"version": "1.6.0",
"main": "electron-main.js",
"author": {
"name": "Daniel Xie & Olivier Gagnon"

@ -13,10 +13,6 @@ cp -r dist/ext .package/dist
cp -r dist/icons .package/dist
cp -r dist/images .package/dist
# The css files
cp dist/vendor.css .package/dist
cp main.css .package/main.css
# The js files.
cp dist/vendor.bundle.js .package/dist/vendor.bundle.js
cp main.bundle.js .package/main.bundle.js

@ -17,6 +17,10 @@ import { OwnedAugmentationsOrderSetting } from "../../Settings/SettingEnums";
import Button from "@mui/material/Button";
import Tooltip from "@mui/material/Tooltip";
import List from "@mui/material/List";
import { ExpandLess, ExpandMore } from "@mui/icons-material";
import { Box, Paper, ListItemButton, ListItemText, Typography, Collapse } from "@mui/material";
import { CONSTANTS } from "../../Constants";
import { formatNumber } from "../../utils/StringHelperFunctions";
export function InstalledAugmentations(): React.ReactElement {
const setRerender = useState(true)[1];
@ -55,6 +59,38 @@ export function InstalledAugmentations(): React.ReactElement {
</Button>
</Tooltip>
<List dense>
{player.entropy > 0 &&
(() => {
const [open, setOpen] = useState(false);
return (
<Box component={Paper}>
<ListItemButton onClick={() => setOpen((old) => !old)}>
<ListItemText
primary={
<Typography color={Settings.theme.hp} style={{ whiteSpace: "pre-wrap" }}>
Entropy Virus - Level {player.entropy}
</Typography>
}
/>
{open ? (
<ExpandLess sx={{ color: Settings.theme.hp }} />
) : (
<ExpandMore sx={{ color: Settings.theme.hp }} />
)}
</ListItemButton>
<Collapse in={open} unmountOnExit>
<Box m={4}>
<Typography color={Settings.theme.hp}>
<b>All multipliers decreased by:</b>{" "}
{formatNumber((1 - CONSTANTS.EntropyEffect ** player.entropy) * 100, 3)}% (multiplicative)
</Typography>
</Box>
</Collapse>
</Box>
);
})()}
{sourceAugs.map((e) => {
const aug = Augmentations[e.name];

@ -425,7 +425,7 @@ BitNodes["BitNode10"] = new BitNode(
This BitNode unlocks Sleeve technology. Sleeve technology allows you to:
<br />
<br />
1. Re-sleeve: Purchase and transfer your consciousness into a new body
1. Grafting: Visit VitaLife in New Tokyo to be able to obtain Augmentations without needing to install
<br />
2. Duplicate Sleeves: Duplicate your consciousness into Synthoids, allowing you to perform different tasks
synchronously

@ -1466,20 +1466,19 @@ export class Bladeburner implements IBladeburner {
if (isNaN(eff) || eff < 0) {
throw new Error("Field Analysis Effectiveness calculated to be NaN or negative");
}
const hackingExpGain = 20 * player.hacking_exp_mult,
charismaExpGain = 20 * player.charisma_exp_mult;
const hackingExpGain = 20 * player.hacking_exp_mult;
const charismaExpGain = 20 * player.charisma_exp_mult;
const rankGain = 0.1 * BitNodeMultipliers.BladeburnerRank;
player.gainHackingExp(hackingExpGain);
player.gainIntelligenceExp(BladeburnerConstants.BaseIntGain);
player.gainCharismaExp(charismaExpGain);
this.changeRank(player, 0.1 * BitNodeMultipliers.BladeburnerRank);
this.changeRank(player, rankGain);
this.getCurrentCity().improvePopulationEstimateByPercentage(eff * this.skillMultipliers.successChanceEstimate);
if (this.logging.general) {
this.log(
"Field analysis completed. Gained 0.1 rank, " +
formatNumber(hackingExpGain, 1) +
" hacking exp, and " +
formatNumber(charismaExpGain, 1) +
" charisma exp",
`Field analysis completed. Gained ${formatNumber(rankGain, 2)} rank, ` +
`${formatNumber(hackingExpGain, 1)} hacking exp, and ` +
`${formatNumber(charismaExpGain, 1)} charisma exp`,
);
}
this.startAction(player, this.action); // Repeat action

@ -4,7 +4,6 @@ import { BladeburnerConstants } from "../data/Constants";
import { formatNumber } from "../../utils/StringHelperFunctions";
import { IBladeburner } from "../IBladeburner";
import Typography from "@mui/material/Typography";
import { BitNodeMultipliers } from "../../BitNode/BitNodeMultipliers";
interface IProps {
bladeburner: IBladeburner;
}
@ -23,8 +22,7 @@ export function SkillPage(props: IProps): React.ReactElement {
<strong>Skill Points: {formatNumber(props.bladeburner.skillPoints, 0)}</strong>
</Typography>
<Typography>
You will gain one skill point every{" "}
{BladeburnerConstants.RanksPerSkillPoint * BitNodeMultipliers.BladeburnerSkillCost} ranks.
You will gain one skill point every {BladeburnerConstants.RanksPerSkillPoint} ranks.
<br />
Note that when upgrading a skill, the benefit for that skill is additive. However, the effects of different
skills with each other is multiplicative.

@ -41,6 +41,7 @@ export const CONSTANTS: {
IntelligenceInfiltrationWeight: number;
IntelligenceCrimeBaseExpGain: number;
IntelligenceProgramBaseExpGain: number;
IntelligenceGraftBaseExpGain: number;
IntelligenceTerminalHackBaseExpGain: number;
IntelligenceSingFnBaseExpGain: number;
IntelligenceClassBaseExpGain: number;
@ -71,6 +72,7 @@ export const CONSTANTS: {
WorkTypeCreateProgram: string;
WorkTypeStudyClass: string;
WorkTypeCrime: string;
WorkTypeGraftAugmentation: string;
ClassStudyComputerScience: string;
ClassDataStructures: string;
ClassNetworks: string;
@ -108,11 +110,14 @@ export const CONSTANTS: {
CodingContractBaseFactionRepGain: number;
CodingContractBaseCompanyRepGain: number;
CodingContractBaseMoneyGain: number;
AugmentationGraftingCostMult: number;
AugmentationGraftingTimeBase: number;
EntropyEffect: number;
TotalNumBitNodes: number;
LatestUpdate: string;
} = {
VersionString: "1.5.0",
VersionNumber: 11,
VersionString: "1.6.0",
VersionNumber: 12,
// Speed (in ms) at which the main loop is updated
_idleSpeed: 200,
@ -180,6 +185,7 @@ export const CONSTANTS: {
IntelligenceInfiltrationWeight: 0.1, // Weight for how much int affects infiltration success rates
IntelligenceCrimeBaseExpGain: 0.05,
IntelligenceProgramBaseExpGain: 0.1, // Program required hack level divided by this to determine int exp gain
IntelligenceGraftBaseExpGain: 0.05,
IntelligenceTerminalHackBaseExpGain: 200, // Hacking exp divided by this to determine int exp gain
IntelligenceSingFnBaseExpGain: 1.5,
IntelligenceClassBaseExpGain: 0.01,
@ -224,6 +230,7 @@ export const CONSTANTS: {
WorkTypeCreateProgram: "Working on Create a Program",
WorkTypeStudyClass: "Studying or Taking a class at university",
WorkTypeCrime: "Committing a crime",
WorkTypeGraftAugmentation: "Grafting an Augmentation",
ClassStudyComputerScience: "studying Computer Science",
ClassDataStructures: "taking a Data Structures course",
@ -269,68 +276,116 @@ export const CONSTANTS: {
CodingContractBaseCompanyRepGain: 4000,
CodingContractBaseMoneyGain: 75e6,
// Augmentation crafting multipliers
AugmentationGraftingCostMult: 3,
AugmentationGraftingTimeBase: 3600000,
// Value raised to the number of entropy stacks, then multiplied to player multipliers
EntropyEffect: 0.98,
// BitNode/Source-File related stuff
TotalNumBitNodes: 24,
LatestUpdate: `
v1.5.0 - Steam Cloud integration
--------------------------------
v1.6.0 - 2022-03-29 Grafting
----------------------------
** Steam Cloud Saving **
** Vitalife secret lab **
* Added (@MartinFournier)
* A new mechanic called Augmentation Grafting has been added. Resleeving has been removed.
* Credit to @nikfolas for his incredible work.
** Stanek **
* BREAKING: Many functions in the stanek API were renamed in order to avoid name collision with things like Map.prototype.get
** UI **
* background now matches game primary color (@nickofolas)
* page title contains version (@MartinFourier)
* Major text editor improvements (@nickofolas)
* Display bonus time on sleeve page (@MartinFourier)
* Several UI improvements (@nickofolas, @smolgumball, @DrCuriosity, @phyzical)
* Fix aug display in alpha (@Dominik Winter)
* Fix display of corporation product equation (@SagePtr)
* Make Bitverse more accessible (@ChrissiQ)
* Make corporation warehouse more accessible (@ChrissiQ)
* Make tab style more consistent (@nikfolas)
* Major update to Sleeve, Gang UI, and Create Program (@nikfolas)
* re-add pre tags to support slash n in prompt (@jacktose)
* Tabelize linked output of 'ls' (@Master-Guy)
* Add the ability to filter open scripts (@phyzical)
* Add minHeight to editor tabs (@nickofolas)
* Properly expand gang equipment cards to fill entire screen (@nickofolas)
* Add shortcut to Faction augmentations page from FactionsRoot (@nickofolas)
* Fix extra space on editor tabs (@nickofolas)
* Present offline message as list (@DSteve595)
* add box showing remaining augments per faction (@jjayeon)
* Add tab switching support to vim mode (@JParisFerrer)
* Show current task on gang management screen (@zeddrak)
* Fix for ui of gang members current task when set via api (@phyzical)
* Don't hide irrelevant materials if their stock is not empty and hide irrelevant divisions from Export (@SagePtr)
* Fix regex to enable alpha transparency hex codes (8 digits) (@surdaft)
** Netscript **
** API **
* Fix bug with async.
* Add 'printf' ns function (@Ninetailed)
* Remove blob caching.
* Fix formulas access check (@Ornedan)
* Fix bug in exp calculation (@qcorradi)
* Fix NaN comparison (@qcorradi)
* Fix travelToCity with bad argument (@SlyCedix)
* Fix bug where augs could not be purchased via sing (@reacocard)
* Fix rounding error in donateToFaction (@Risenafis)
* Fix bug with weakenAnalyze (@rhobes)
* Prevent exploit with atExit (@Ornedan)
* Double 'share' power
* Added dark web functions to ns api
* BREAKING: purchaseTor() should returns true if player already has Tor. (@DavidGrinberg, @waffleattack)
* Implement getBonusTime in Corporation API (@t-wolfeadam)
* Added functions to purchase TIX and WSI (@incubusnb)
* purchaseSleeveAug checks shock value (@incubusnb)
* Fix bug with hacknet api
* Fix spendHashes bug
* Added 0 cost of asleep() (@Master-Guy)
* Fix some misleading corporation errors (@TheRealMaxion)
* expose the inBladeburner on the player object (@phyzical)
* added ram charge for stanek width and height (@phyzical)
* Fix sufficient player money check to buy back shares. (@ChrissiQ)
* Fix Static Ram Circumventing for some NS functions (@CrafterKolyan)
* added CorporationSoftCap to NetscriptDefinitions (@phyzical)
* Added definition of autocomplete() 'data' argument. (@tigercat2000)
* Adding support for text/select options in Prompt command (@PhilipArmstead)
* Added the ability to exportGame via api (@phyzical)
** Corporations **
** Arcade **
* Fix bugs with corp API (@pigalot)
* Add smart supply func to corp API (@pd)
* Added an arcade to New Tokyo where you can play a 4 year old version of bitburner.
** Misc. **
* The file API now allows GET and DELETE (@lordducky)
* Force achievement calculation on BN completion (@SagePtr)
* Cleanup in repository (@MartinFourier)
* Several improvements to the electron version (@MartinFourier)
* Fix bug with casino roulette (@jamie-mac)
* Terminal history persists in savefile (@MartinFourier)
* Fix tests (@jamie-mac)
* Fix crash with electron windows tracker (@smolgumball)
* Fix BN6/7 passive reputation gain (@BrianLDev)
* Fix Sleeve not resetting on install (@waffleattack)
* Sort joined factions (@jjayeon)
* Update documentation / typo (@lethern, @Meowdoleon, @JohnnyUrosevic, @JosephDavidTalbot,
@pd, @lethern, @lordducky, @zeddrak, @fearnlj01, @reasonablytall, @MatthewTh0,
@SagePtr, @manniL, @Jedimaster4559, @loganville, @Arrow2thekn33, @wdpk, @fwolfst,
@fschoenfeldt, @Waladil, @AdamTReineke, @citrusmunch, @factubsio, @ashtongreen,
@ChrissiQ, @DJ-Laser, @waffleattack, @ApamNapat, @CrafterKolyan, @DSteve595)
* Add a warning triggered while auto-saves are off. (@MartinFournier)
* Log info for field analysis now displays actual rank gained. (@ApamNapat)
* Removed BladeburnerSkillCost from skill point cost description. (@ApamNapat)
* Fix handling for UpArrow in bladeburner console. (@dowinter)
* Add GitHub action to check PRs for generated files. (@MartinFournier)
* Cap Staneks gift at 25x25 to prevent crashes. (@waffleattack)
* Remove old & unused files from repository. (@MartinFournier)
* Factions on the factions screens are sorted by story progress / type. (@phyzical)
* Fix log manager not picking up new runs of scripts. (@phyzical)
* Added prettier to cicd.
* UI improvements (@phyzical)
* Documentation / Typos (@nanogyth, @Master-Guy, @incubusnb, @ApamNapat, @phyzical, @SagePtr)
* Give player code a copy of Division.upgrades instead of the live object (@Ornedan)
* Fix bug with small town achievement.
* Fix bug with purchaseSleeveAug (@phyzical)
* Check before unlocking corp upgrade (@gianfun)
* General codebase improvements. (@phyzical, @Master-Guy, @ApamNapat)
* Waiting on promises in NS1 no longer freezes the script. (@Master-Guy)
* Fix bug with missing ramcost for tFormat (@TheMas3212)
* Fix crash with new prompt
* Quick fix to prevent division by 0 in terminal (@Master-Guy)
* removed ip references (@phyzical, @Master-Guy)
* Terminal now supports 'ls -l'
* Fix negative number formatting (@Master-Guy)
* Fix unique ip generation (@InDieTasten)
* remove terminal command theme from docs (@phyzical)
* Fix 'Augmentations Left' with gang factions (@nickofolas)
* Attempt to fix 'bladeburner.process()' early routing issue (@MartinFournier)
* work in progress augment fix (@phyzical)
* Fixes missing space in Smart Supply (@TheRealMaxion)
* Change license to Apache 2 with Commons Clause
* updated regex sanitization (@mbrannen)
* Sleeve fix for when faction isnt found (@phyzical)
* Fix editor "close" naming (@phyzical)
* Fix bug with sleeves where some factions would be listed as workable. (@phyzical)
* Fix research tree of product industries post-prestige (@pd)
* Added a check for exisiting industry type before expanding (@phyzical)
* fix hackAnalyzeThreads returning infinity (@chrisrabe)
* Make growthAnalyze more accurate (@dwRchyngqxs)
* Add 'Zoom -> Reset Zoom' command to Steam (@smolgumball)
* Add hasOwnProperty check to GetServer (@SagePtr)
* Speed up employee productivity calculation (@pd)
* Field Work and Security Work benefit from 'share' (@SagePtr)
* Nerf noodle bar.
`,
};

@ -19,14 +19,14 @@ export class StaneksGift implements IStaneksGift {
fragments: ActiveFragment[] = [];
baseSize(): number {
return StanekConstants.BaseSize + BitNodeMultipliers.StaneksGiftExtraSize + Player.sourceFileLvl(13);
return StanekConstants.BaseSize + BitNodeMultipliers.StaneksGiftExtraSize + Player.sourceFileLvl(13)
}
width(): number {
return Math.floor(this.baseSize() / 2 + 1);
return Math.min(Math.floor(this.baseSize() / 2 + 1),StanekConstants.MaxSize);
}
height(): number {
return Math.floor(this.baseSize() / 2 + 0.6);
return Math.min(Math.floor(this.baseSize() / 2 + 0.6),StanekConstants.MaxSize);
}
charge(player: IPlayer, af: ActiveFragment, threads: number): void {

@ -1,7 +1,9 @@
export const StanekConstants: {
RAMBonus: number;
BaseSize: number;
MaxSize: number;
} = {
RAMBonus: 0.1,
BaseSize: 9,
MaxSize: 25
};

@ -23,6 +23,7 @@ import { Sleeves } from "./DevMenu/ui/Sleeves";
import { Stanek } from "./DevMenu/ui/Stanek";
import { TimeSkip } from "./DevMenu/ui/TimeSkip";
import { Achievements } from "./DevMenu/ui/Achievements";
import { Entropy } from "./DevMenu/ui/Entropy";
import Typography from "@mui/material/Typography";
import { Exploit } from "./Exploits/Exploit";
@ -63,6 +64,7 @@ export function DevMenuRoot(props: IProps): React.ReactElement {
<TimeSkip player={props.player} engine={props.engine} />
<Achievements player={props.player} engine={props.engine} />
<Entropy player={props.player} engine={props.engine} />
</>
);
}

@ -0,0 +1,50 @@
import React from "react";
import Accordion from "@mui/material/Accordion";
import AccordionSummary from "@mui/material/AccordionSummary";
import AccordionDetails from "@mui/material/AccordionDetails";
import ExpandMoreIcon from "@mui/icons-material/ExpandMore";
import Typography from "@mui/material/Typography";
import { IPlayer } from "../../PersonObjects/IPlayer";
import { Adjuster } from "./Adjuster";
import { IEngine } from "../../IEngine";
// Update as additional BitNodes get implemented
interface IProps {
player: IPlayer;
engine: IEngine;
}
export function Entropy(props: IProps): React.ReactElement {
return (
<Accordion TransitionProps={{ unmountOnExit: true }}>
<AccordionSummary expandIcon={<ExpandMoreIcon />}>
<Typography>Entropy</Typography>
</AccordionSummary>
<AccordionDetails>
<Adjuster
label="Set entropy"
placeholder="entropy"
add={(num) => {
props.player.entropy += num;
props.player.applyEntropy(props.player.entropy);
}}
subtract={(num) => {
props.player.entropy -= num;
props.player.applyEntropy(props.player.entropy);
}}
tons={() => {
props.player.entropy += 1e12;
props.player.applyEntropy(props.player.entropy);
}}
reset={() => {
props.player.entropy = 0;
props.player.applyEntropy(props.player.entropy);
}}
/>
</AccordionDetails>
</Accordion>
);
}

@ -43,17 +43,21 @@ export function AugmentationsPage(props: IProps): React.ReactElement {
function getAugs(): string[] {
if (isPlayersGang) {
const augs: string[] = [];
for (const augName of Object.keys(Augmentations)) {
if (augName === AugmentationNames.NeuroFluxGovernor) continue;
if (augName === AugmentationNames.TheRedPill && player.bitNodeN !== 2) continue;
const aug = Augmentations[augName];
if (!aug.isSpecial) {
augs.push(augName);
}
let augs = Object.values(Augmentations);
// Remove blacklisted augs.
const blacklist = [AugmentationNames.NeuroFluxGovernor, AugmentationNames.TheRedPill];
augs = augs.filter((a) => !blacklist.includes(a.name));
// Remove special augs.
augs = augs.filter((a) => !a.isSpecial);
// Remove faction-unique augs outside BN2. (But keep the one for this faction.)
if (player.bitNodeN !== 2) {
augs = augs.filter((a) => a.factions.length > 1 || props.faction.augmentations.includes(a.name));
}
return augs;
return augs.map((a) => a.name);
} else {
return props.faction.augmentations.slice();
}

@ -1,14 +1,6 @@
import React, { useEffect, useState } from "react";
import {
Box,
Button,
Container,
Paper,
TableBody,
TableRow,
Typography
} from "@mui/material";
import { Box, Button, Container, Paper, TableBody, TableRow, Typography } from "@mui/material";
import { Augmentations } from "../../Augmentation/Augmentations";
import { AugmentationNames } from "../../Augmentation/data/AugmentationNames";
@ -19,6 +11,7 @@ import { IRouter } from "../../ui/Router";
import { Faction } from "../Faction";
import { joinFaction } from "../FactionHelpers";
import { Factions } from "../Factions";
import { FactionNames } from "../data/FactionNames";
export const InvitationsSeen: string[] = [];
@ -64,21 +57,28 @@ export function FactionsRoot(props: IProps): React.ReactElement {
if (isPlayersGang) {
for (const augName of Object.keys(Augmentations)) {
const aug = Augmentations[augName];
if (
augName === AugmentationNames.NeuroFluxGovernor ||
augName === AugmentationNames.TheRedPill && player.bitNodeN !== 2 ||
Augmentations[augName].isSpecial
) continue;
augs.push(augName)
(augName === AugmentationNames.TheRedPill && player.bitNodeN !== 2) ||
// Special augs (i.e. Bladeburner augs)
aug.isSpecial ||
// Exclusive augs (i.e. QLink)
(aug.factions.length <= 1 && !faction.augmentations.includes(augName) && player.bitNodeN !== 2)
)
continue;
augs.push(augName);
}
} else {
augs = faction.augmentations.slice();
}
return augs.filter(
(augmentation: string) => !player.hasAugmentation(augmentation)
).length;
}
return augs.filter((augmentation: string) => !player.hasAugmentation(augmentation)).length;
};
const allFactions = Object.values(FactionNames).map((faction) => faction as string);
const allJoinedFactions = props.player.factions.slice(0);
allJoinedFactions.sort((a, b) => allFactions.indexOf(a) - allFactions.indexOf(b));
return (
<Container disableGutters maxWidth="md" sx={{ mx: 0, mb: 10 }}>
@ -92,11 +92,11 @@ export function FactionsRoot(props: IProps): React.ReactElement {
<Typography variant="h5" color="primary" mt={2} mb={1}>
Factions you have joined:
</Typography>
{(props.player.factions.length > 0 && (
{(allJoinedFactions.length > 0 && (
<Paper sx={{ my: 1, p: 1, pb: 0, display: "inline-block" }}>
<Table padding="none" style={{ width: "fit-content" }}>
<TableBody>
{props.player.factions.map((faction: string) => (
{allJoinedFactions.map((faction: string) => (
<TableRow key={faction}>
<TableCell>
<Typography noWrap mb={1}>
@ -110,7 +110,7 @@ export function FactionsRoot(props: IProps): React.ReactElement {
</TableCell>
<TableCell align="right">
<Box ml={1} mb={1}>
<Button sx={{ width: '100%' }} onClick={() => openFactionAugPage(Factions[faction])}>
<Button sx={{ width: "100%" }} onClick={() => openFactionAugPage(Factions[faction])}>
Augmentations Left: {getAugsLeft(Factions[faction], props.player)}
</Button>
</Box>

@ -227,7 +227,7 @@ export class Gang implements IGang {
AllGangs[thisGang].territory += territoryGain;
if (AllGangs[thisGang].territory > 0.999) AllGangs[thisGang].territory = 1;
AllGangs[otherGang].territory -= territoryGain;
if (AllGangs[thisGang].territory < 0.001) AllGangs[thisGang].territory = 0;
if (AllGangs[thisGang].territory) AllGangs[thisGang].territory = 0;
if (thisGang === gangName) {
this.clash(true); // Player won
AllGangs[otherGang].power *= 1 / 1.01;

@ -72,8 +72,8 @@ export function SpecialLocation(props: IProps): React.ReactElement {
/**
* Click handler for Resleeving button at New Tokyo VitaLife
*/
function handleResleeving(): void {
router.toResleeves();
function handleGrafting(): void {
router.toGrafting();
}
function renderBladeburner(): React.ReactElement {
@ -151,11 +151,11 @@ export function SpecialLocation(props: IProps): React.ReactElement {
);
}
function renderResleeving(): React.ReactElement {
if (!player.canAccessResleeving()) {
function renderGrafting(): React.ReactElement {
if (!player.canAccessGrafting()) {
return <></>;
}
return <Button onClick={handleResleeving}>Re-Sleeve</Button>;
return <Button onClick={handleGrafting} sx={{ my: 5 }}>Enter the secret lab</Button>;
}
function handleCotMG(): void {
@ -299,7 +299,7 @@ export function SpecialLocation(props: IProps): React.ReactElement {
switch (props.loc.name) {
case LocationName.NewTokyoVitaLife: {
return renderResleeving();
return renderGrafting();
}
case LocationName.Sector12CityHall: {
return <CreateCorporation />;

@ -386,6 +386,12 @@ export const RamCosts: IMap<any> = {
getGameInfo: 0,
},
grafting: {
getAugmentationCraftPrice: 3.75,
getAugmentationCraftTime: 3.75,
graftAugmentation: 7.5,
},
heart: {
// Easter egg function
break: 0,

@ -70,6 +70,7 @@ import { NetscriptCodingContract } from "./NetscriptFunctions/CodingContract";
import { NetscriptCorporation } from "./NetscriptFunctions/Corporation";
import { NetscriptFormulas } from "./NetscriptFunctions/Formulas";
import { NetscriptStockMarket } from "./NetscriptFunctions/StockMarket";
import { NetscriptGrafting } from "./NetscriptFunctions/Grafting";
import { IPort } from "./NetscriptPort";
import {
@ -480,6 +481,7 @@ export function NetscriptFunctions(workerScript: WorkerScript): NS {
const singularity = NetscriptSingularity(Player, workerScript, helper);
const stockmarket = NetscriptStockMarket(Player, workerScript, helper);
const ui = NetscriptUserInterface(Player, workerScript, helper);
const grafting = NetscriptGrafting(Player, workerScript, helper);
const base: INS = {
...singularity,
@ -493,6 +495,7 @@ export function NetscriptFunctions(workerScript: WorkerScript): NS {
ui: ui,
formulas: formulas,
stock: stockmarket,
grafting: grafting,
args: workerScript.args,
hacknet: hacknet,
sprintf: sprintf,
@ -2315,6 +2318,7 @@ export function NetscriptFunctions(workerScript: WorkerScript): NS {
tor: Player.hasTorRouter(),
inBladeburner: Player.inBladeburner(),
hasCorporation: Player.hasCorporation(),
entropy: Player.entropy,
};
Object.assign(data.jobs, Player.jobs);
return data;

@ -0,0 +1,88 @@
import { CityName } from "../Locations/data/CityNames";
import { Augmentations } from "../Augmentation/Augmentations";
import { getRamCost } from "../Netscript/RamCostGenerator";
import { WorkerScript } from "../Netscript/WorkerScript";
import { GraftableAugmentation } from "../PersonObjects/Grafting/GraftableAugmentation";
import { getAvailableAugs } from "../PersonObjects/Grafting/ui/GraftingRoot";
import { IPlayer } from "../PersonObjects/IPlayer";
import { Grafting as IGrafting } from "../ScriptEditor/NetscriptDefinitions";
import { Router } from "../ui/GameRoot";
import { INetscriptHelper } from "./INetscriptHelper";
export function NetscriptGrafting(player: IPlayer, workerScript: WorkerScript, helper: INetscriptHelper): IGrafting {
const checkGraftingAPIAccess = (func: string): void => {
if (!player.canAccessGrafting()) {
throw helper.makeRuntimeErrorMsg(
`grafting.${func}`,
"You do not currently have access to the Grafting API. This is either because you are not in BitNode 10 or because you do not have Source-File 10",
);
}
};
return {
getAugmentationGraftPrice: (_augName: unknown): number => {
const augName = helper.string("getAugmentationGraftPrice", "augName", _augName);
helper.updateDynamicRam("getAugmentationGraftPrice", getRamCost(player, "grafting", "getAugmentationGraftPrice"));
checkGraftingAPIAccess("getAugmentationGraftPrice");
if (!Augmentations.hasOwnProperty(augName)) {
throw helper.makeRuntimeErrorMsg("grafting.getAugmentationGraftPrice", `Invalid aug: ${augName}`);
}
const craftableAug = new GraftableAugmentation(Augmentations[augName]);
return craftableAug.cost;
},
getAugmentationGraftTime: (_augName: string): number => {
const augName = helper.string("getAugmentationGraftTime", "augName", _augName);
helper.updateDynamicRam("getAugmentationGraftTime", getRamCost(player, "grafting", "getAugmentationGraftTime"));
checkGraftingAPIAccess("getAugmentationGraftTime");
if (!Augmentations.hasOwnProperty(augName)) {
throw helper.makeRuntimeErrorMsg("grafting.getAugmentationGraftTime", `Invalid aug: ${augName}`);
}
const craftableAug = new GraftableAugmentation(Augmentations[augName]);
return craftableAug.time;
},
graftAugmentation: (_augName: string, _focus: unknown = true): boolean => {
const augName = helper.string("graftAugmentation", "augName", _augName);
const focus = helper.boolean(_focus);
helper.updateDynamicRam("graftAugmentation", getRamCost(player, "grafting", "graftAugmentation"));
checkGraftingAPIAccess("graftAugmentation");
if (player.city !== CityName.NewTokyo) {
throw helper.makeRuntimeErrorMsg(
"grafting.graftAugmentation",
"You must be in New Tokyo to begin crafting an Augmentation.",
);
}
if (!getAvailableAugs(player).includes(augName)) {
workerScript.log("grafting.graftAugmentation", () => `Invalid aug: ${augName}`);
return false;
}
const wasFocusing = player.focus;
if (player.isWorking) {
const txt = player.singularityStopWork();
workerScript.log("graftAugmentation", () => txt);
}
const craftableAug = new GraftableAugmentation(Augmentations[augName]);
if (player.money < craftableAug.cost) {
workerScript.log("grafting.graftAugmentation", () => `You don't have enough money to craft ${augName}`);
return false;
}
player.loseMoney(craftableAug.cost, "augmentations");
player.startGraftAugmentationWork(augName, craftableAug.time);
if (focus) {
player.startFocusing();
Router.toWork();
} else if (wasFocusing) {
player.stopFocusing();
Router.toTerminal();
}
workerScript.log("grafting.graftAugmentation", () => `Began crafting Augmentation ${augName}.`);
return true;
},
};
}

@ -42,7 +42,6 @@ import { Terminal } from "../Terminal";
import { calculateHackingTime } from "../Hacking";
import { Server } from "../Server/Server";
import { netscriptCanHack } from "../Hacking/netscriptCanHack";
import { FactionNames } from "../Faction/data/FactionNames";
import { FactionInfos } from "../Faction/FactionInfo";
export function NetscriptSingularity(
@ -114,17 +113,21 @@ export function NetscriptSingularity(
// If player has a gang with this faction, return all augmentations.
if (player.hasGangWith(facname)) {
const res = [];
for (const augName of Object.keys(Augmentations)) {
if (augName === AugmentationNames.NeuroFluxGovernor) continue;
if (augName === AugmentationNames.TheRedPill && player.bitNodeN !== 2) continue;
const aug = Augmentations[augName];
if (!aug.isSpecial) {
res.push(augName);
}
let augs = Object.values(Augmentations);
// Remove blacklisted augs.
const blacklist = [AugmentationNames.NeuroFluxGovernor, AugmentationNames.TheRedPill];
augs = augs.filter((a) => !blacklist.includes(a.name));
// Remove special augs.
augs = augs.filter((a) => !a.isSpecial);
// Remove faction-unique augs outside BN2. (But keep the one for this faction.)
if (player.bitNodeN !== 2) {
augs = augs.filter((a) => a.factions.length > 1 || Factions[facname].augmentations.includes(a.name));
}
return res;
return augs.map((a) => a.name);
}
return faction.augmentations.slice();
@ -168,12 +171,17 @@ export function NetscriptSingularity(
let augs = [];
if (player.hasGangWith(faction)) {
for (const augName of Object.keys(Augmentations)) {
if (augName === AugmentationNames.NeuroFluxGovernor) continue;
if (augName === AugmentationNames.TheRedPill && player.bitNodeN !== 2) continue;
const tempAug = Augmentations[augName];
if (!tempAug.isSpecial) {
augs.push(augName);
}
const aug = Augmentations[augName];
if (
augName === AugmentationNames.NeuroFluxGovernor ||
(augName === AugmentationNames.TheRedPill && player.bitNodeN !== 2) ||
// Special augs (i.e. Bladeburner augs)
aug.isSpecial ||
// Exclusive augs (i.e. QLink)
(aug.factions.length <= 1 && !fac.augmentations.includes(augName) && player.bitNodeN !== 2)
)
continue;
augs.push(augName);
}
} else {
augs = fac.augmentations;
@ -500,7 +508,7 @@ export function NetscriptSingularity(
if (player.hasTorRouter()) {
workerScript.log("purchaseTor", () => "You already have a TOR router!");
return false;
return true;
}
if (player.money < CONSTANTS.TorRouterCost) {

@ -22,28 +22,29 @@ export function NetscriptStanek(player: IPlayer, workerScript: WorkerScript, hel
}
return {
width: function (): number {
helper.updateDynamicRam("width", getRamCost(player, "stanek", "width"));
checkStanekAPIAccess("width");
giftWidth: function (): number {
helper.updateDynamicRam("giftWidth", getRamCost(player, "stanek", "giftWidth"));
checkStanekAPIAccess("giftWidth");
return staneksGift.width();
},
height: function (): number {
helper.updateDynamicRam("height", getRamCost(player, "stanek", "height"));
checkStanekAPIAccess("height");
giftHeight: function (): number {
helper.updateDynamicRam("giftHeight", getRamCost(player, "stanek", "giftHeight"));
checkStanekAPIAccess("giftHeight");
return staneksGift.height();
},
charge: function (_rootX: unknown, _rootY: unknown): Promise<void> {
const rootX = helper.number("stanek.charge", "rootX", _rootX);
const rootY = helper.number("stanek.charge", "rootY", _rootY);
chargeFragment: function (_rootX: unknown, _rootY: unknown): Promise<void> {
const rootX = helper.number("stanek.chargeFragment", "rootX", _rootX);
const rootY = helper.number("stanek.chargeFragment", "rootY", _rootY);
helper.updateDynamicRam("charge", getRamCost(player, "stanek", "charge"));
checkStanekAPIAccess("charge");
helper.updateDynamicRam("chargeFragment", getRamCost(player, "stanek", "chargeFragment"));
checkStanekAPIAccess("chargeFragment");
const fragment = staneksGift.findFragment(rootX, rootY);
if (!fragment) throw helper.makeRuntimeErrorMsg("stanek.charge", `No fragment with root (${rootX}, ${rootY}).`);
if (!fragment)
throw helper.makeRuntimeErrorMsg("stanek.chargeFragment", `No fragment with root (${rootX}, ${rootY}).`);
const time = staneksGift.inBonus() ? 200 : 1000;
return netscriptDelay(time, workerScript).then(function () {
const charge = staneksGift.charge(player, fragment, workerScript.scriptRef.threads);
workerScript.log("stanek.charge", () => `Charged fragment for ${charge} charge.`);
workerScript.log("stanek.chargeFragment", () => `Charged fragment for ${charge} charge.`);
return Promise.resolve();
});
},
@ -61,49 +62,49 @@ export function NetscriptStanek(player: IPlayer, workerScript: WorkerScript, hel
return { ...af.copy(), ...af.fragment().copy() };
});
},
clear: function (): void {
helper.updateDynamicRam("clear", getRamCost(player, "stanek", "clear"));
checkStanekAPIAccess("clear");
workerScript.log("stanek.clear", () => `Cleared Stanek's Gift.`);
clearGift: function (): void {
helper.updateDynamicRam("clearGift", getRamCost(player, "stanek", "clearGift"));
checkStanekAPIAccess("clearGift");
workerScript.log("stanek.clearGift", () => `Cleared Stanek's Gift.`);
staneksGift.clear();
},
canPlace: function (_rootX: unknown, _rootY: unknown, _rotation: unknown, _fragmentId: unknown): boolean {
const rootX = helper.number("stanek.canPlace", "rootX", _rootX);
const rootY = helper.number("stanek.canPlace", "rootY", _rootY);
const rotation = helper.number("stanek.canPlace", "rotation", _rotation);
const fragmentId = helper.number("stanek.canPlace", "fragmentId", _fragmentId);
helper.updateDynamicRam("canPlace", getRamCost(player, "stanek", "canPlace"));
checkStanekAPIAccess("canPlace");
canPlaceFragment: function (_rootX: unknown, _rootY: unknown, _rotation: unknown, _fragmentId: unknown): boolean {
const rootX = helper.number("stanek.canPlaceFragment", "rootX", _rootX);
const rootY = helper.number("stanek.canPlaceFragment", "rootY", _rootY);
const rotation = helper.number("stanek.canPlaceFragment", "rotation", _rotation);
const fragmentId = helper.number("stanek.canPlaceFragment", "fragmentId", _fragmentId);
helper.updateDynamicRam("canPlaceFragment", getRamCost(player, "stanek", "canPlaceFragment"));
checkStanekAPIAccess("canPlaceFragment");
const fragment = FragmentById(fragmentId);
if (!fragment) throw helper.makeRuntimeErrorMsg("stanek.canPlace", `Invalid fragment id: ${fragmentId}`);
if (!fragment) throw helper.makeRuntimeErrorMsg("stanek.canPlaceFragment", `Invalid fragment id: ${fragmentId}`);
const can = staneksGift.canPlace(rootX, rootY, rotation, fragment);
return can;
},
place: function (_rootX: unknown, _rootY: unknown, _rotation: unknown, _fragmentId: unknown): boolean {
const rootX = helper.number("stanek.place", "rootX", _rootX);
const rootY = helper.number("stanek.place", "rootY", _rootY);
const rotation = helper.number("stanek.place", "rotation", _rotation);
const fragmentId = helper.number("stanek.place", "fragmentId", _fragmentId);
helper.updateDynamicRam("place", getRamCost(player, "stanek", "place"));
checkStanekAPIAccess("place");
placeFragment: function (_rootX: unknown, _rootY: unknown, _rotation: unknown, _fragmentId: unknown): boolean {
const rootX = helper.number("stanek.placeFragment", "rootX", _rootX);
const rootY = helper.number("stanek.placeFragment", "rootY", _rootY);
const rotation = helper.number("stanek.placeFragment", "rotation", _rotation);
const fragmentId = helper.number("stanek.placeFragment", "fragmentId", _fragmentId);
helper.updateDynamicRam("placeFragment", getRamCost(player, "stanek", "placeFragment"));
checkStanekAPIAccess("placeFragment");
const fragment = FragmentById(fragmentId);
if (!fragment) throw helper.makeRuntimeErrorMsg("stanek.place", `Invalid fragment id: ${fragmentId}`);
if (!fragment) throw helper.makeRuntimeErrorMsg("stanek.placeFragment", `Invalid fragment id: ${fragmentId}`);
return staneksGift.place(rootX, rootY, rotation, fragment);
},
get: function (_rootX: unknown, _rootY: unknown): IActiveFragment | undefined {
const rootX = helper.number("stanek.get", "rootX", _rootX);
const rootY = helper.number("stanek.get", "rootY", _rootY);
helper.updateDynamicRam("get", getRamCost(player, "stanek", "get"));
checkStanekAPIAccess("get");
getFragment: function (_rootX: unknown, _rootY: unknown): IActiveFragment | undefined {
const rootX = helper.number("stanek.getFragment", "rootX", _rootX);
const rootY = helper.number("stanek.getFragment", "rootY", _rootY);
helper.updateDynamicRam("getFragment", getRamCost(player, "stanek", "getFragment"));
checkStanekAPIAccess("getFragment");
const fragment = staneksGift.findFragment(rootX, rootY);
if (fragment !== undefined) return fragment.copy();
return undefined;
},
remove: function (_rootX: unknown, _rootY: unknown): boolean {
const rootX = helper.number("stanek.remove", "rootX", _rootX);
const rootY = helper.number("stanek.remove", "rootY", _rootY);
helper.updateDynamicRam("remove", getRamCost(player, "stanek", "remove"));
checkStanekAPIAccess("remove");
removeFragment: function (_rootX: unknown, _rootY: unknown): boolean {
const rootX = helper.number("stanek.removeFragment", "rootX", _rootX);
const rootY = helper.number("stanek.removeFragment", "rootY", _rootY);
helper.updateDynamicRam("removeFragment", getRamCost(player, "stanek", "removeFragment"));
checkStanekAPIAccess("removeFragment");
return staneksGift.delete(rootX, rootY);
},
};

@ -0,0 +1,52 @@
import { IMap } from "../../types";
import { CONSTANTS } from "../../Constants";
import { IPlayer } from "../IPlayer";
export const calculateEntropy = (player: IPlayer, stacks = 1): IMap<number> => {
const multipliers: IMap<number> = {
hacking_chance_mult: player.hacking_chance_mult,
hacking_speed_mult: player.hacking_speed_mult,
hacking_money_mult: player.hacking_money_mult,
hacking_grow_mult: player.hacking_grow_mult,
hacking_mult: player.hacking_mult,
strength_mult: player.strength_mult,
defense_mult: player.defense_mult,
dexterity_mult: player.dexterity_mult,
agility_mult: player.agility_mult,
charisma_mult: player.charisma_mult,
hacking_exp_mult: player.hacking_exp_mult,
strength_exp_mult: player.strength_exp_mult,
defense_exp_mult: player.defense_exp_mult,
dexterity_exp_mult: player.dexterity_exp_mult,
agility_exp_mult: player.agility_exp_mult,
charisma_exp_mult: player.charisma_exp_mult,
company_rep_mult: player.company_rep_mult,
faction_rep_mult: player.faction_rep_mult,
crime_money_mult: player.crime_money_mult,
crime_success_mult: player.crime_success_mult,
hacknet_node_money_mult: player.hacknet_node_money_mult,
hacknet_node_purchase_cost_mult: player.hacknet_node_purchase_cost_mult,
hacknet_node_ram_cost_mult: player.hacknet_node_ram_cost_mult,
hacknet_node_core_cost_mult: player.hacknet_node_core_cost_mult,
hacknet_node_level_cost_mult: player.hacknet_node_level_cost_mult,
work_money_mult: player.work_money_mult,
bladeburner_max_stamina_mult: player.bladeburner_max_stamina_mult,
bladeburner_stamina_gain_mult: player.bladeburner_stamina_gain_mult,
bladeburner_analysis_mult: player.bladeburner_analysis_mult,
bladeburner_success_chance_mult: player.bladeburner_success_chance_mult,
};
for (const [mult, val] of Object.entries(multipliers)) {
multipliers[mult] = val * CONSTANTS.EntropyEffect ** stacks;
}
return multipliers;
};

@ -0,0 +1,31 @@
import { sum } from "lodash";
import { Augmentation } from "../../Augmentation/Augmentation";
import { CONSTANTS } from "../../Constants";
export interface IConstructorParams {
augmentation: Augmentation;
readonly cost: number;
readonly time: number;
}
export class GraftableAugmentation {
// The augmentation that this craftable corresponds to
augmentation: Augmentation;
constructor(augmentation: Augmentation) {
this.augmentation = augmentation;
}
get cost(): number {
return this.augmentation.startingCost * CONSTANTS.AugmentationGraftingCostMult;
}
get time(): number {
// Time = 1 hour * log_2(sum(aug multipliers) || 1) + 30 minutes
const antiLog = Math.max(sum(Object.values(this.augmentation.mults)), 1);
const mult = Math.log2(antiLog);
return CONSTANTS.AugmentationGraftingTimeBase * mult + CONSTANTS.MillisecondsPerHalfHour;
}
}

@ -0,0 +1,160 @@
import React, { useState } from "react";
import { Typography, Container, Box, Paper, List, ListItemButton, Button } from "@mui/material";
import { Construction } from "@mui/icons-material";
import { use } from "../../../ui/Context";
import { Money } from "../../../ui/React/Money";
import { ConfirmationModal } from "../../../ui/React/ConfirmationModal";
import { Augmentations } from "../../../Augmentation/Augmentations";
import { AugmentationNames } from "../../../Augmentation/data/AugmentationNames";
import { Settings } from "../../../Settings/Settings";
import { IMap } from "../../../types";
import { convertTimeMsToTimeElapsedString, formatNumber } from "../../../utils/StringHelperFunctions";
import { LocationName } from "../../../Locations/data/LocationNames";
import { Locations } from "../../../Locations/Locations";
import { CONSTANTS } from "../../../Constants";
import { IPlayer } from "../../IPlayer";
import { GraftableAugmentation } from "../GraftableAugmentation";
const GraftableAugmentations: IMap<GraftableAugmentation> = {};
export const getAvailableAugs = (player: IPlayer): string[] => {
const augs: string[] = [];
for (const [augName, aug] of Object.entries(Augmentations)) {
if (augName === AugmentationNames.NeuroFluxGovernor || augName === AugmentationNames.TheRedPill || aug.isSpecial)
continue;
augs.push(augName);
}
return augs.filter((augmentation: string) => !player.hasAugmentation(augmentation));
};
export const GraftingRoot = (): React.ReactElement => {
const player = use.Player();
const router = use.Router();
for (const aug of Object.values(Augmentations)) {
const name = aug.name;
const graftableAug = new GraftableAugmentation(aug);
GraftableAugmentations[name] = graftableAug;
}
const [selectedAug, setSelectedAug] = useState(getAvailableAugs(player)[0]);
const [graftOpen, setGraftOpen] = useState(false);
return (
<Container disableGutters maxWidth="lg" sx={{ mx: 0 }}>
<Button onClick={() => router.toLocation(Locations[LocationName.NewTokyoVitaLife])}>Back</Button>
<Typography variant="h4">Grafting Laboratory</Typography>
<Typography>
You find yourself in a secret laboratory, owned by a mysterious researcher.
<br />
The scientist explains that they've been studying Augmentation grafting, the process of applying Augmentations
without requiring a body reset.
<br />
<br />
Through legally questionable connections, the scientist has access to a vast array of Augmentation blueprints,
even private designs. They offer to build and graft the Augmentations to you, in exchange for both a hefty sum
of money, and being a lab rat.
</Typography>
<Box sx={{ my: 3 }}>
<Typography variant="h5">Graft Augmentations</Typography>
<Paper sx={{ my: 1, width: "fit-content", display: "grid", gridTemplateColumns: "1fr 3fr" }}>
<List sx={{ maxHeight: 400, overflowY: "scroll", borderRight: `1px solid ${Settings.theme.welllight}` }}>
{getAvailableAugs(player).map((k, i) => (
<ListItemButton key={i + 1} onClick={() => setSelectedAug(k)} selected={selectedAug === k}>
<Typography>{k}</Typography>
</ListItemButton>
))}
</List>
<Box sx={{ m: 1 }}>
<Typography variant="h6" sx={{ display: "flex", alignItems: "center", flexWrap: "wrap" }}>
<Construction sx={{ mr: 1 }} /> {selectedAug}
</Typography>
<Button
onClick={() => setGraftOpen(true)}
sx={{ width: "100%" }}
disabled={player.money < GraftableAugmentations[selectedAug].cost}
>
Graft Augmentation (
<Typography>
<Money money={GraftableAugmentations[selectedAug].cost} player={player} />
</Typography>
)
</Button>
<ConfirmationModal
open={graftOpen}
onClose={() => setGraftOpen(false)}
onConfirm={() => {
const graftableAug = GraftableAugmentations[selectedAug];
player.loseMoney(graftableAug.cost, "augmentations");
player.startGraftAugmentationWork(selectedAug, graftableAug.time);
player.startFocusing();
router.toWork();
}}
confirmationText={
<>
Cancelling grafting will <b>not</b> save grafting progress, and the money you spend will <b>not</b> be
returned.
<br />
<br />
Additionally, grafting an Augmentation will increase the potency of the Entropy virus.
</>
}
/>
<Typography color={Settings.theme.info}>
<b>Time to Graft:</b>{" "}
{convertTimeMsToTimeElapsedString(
GraftableAugmentations[selectedAug].time / (1 + (player.getIntelligenceBonus(3) - 1) / 3),
)}
{/* Use formula so the displayed creation time is accurate to player bonus */}
</Typography>
<Typography sx={{ maxHeight: 305, overflowY: "scroll" }}>
{(() => {
const aug = Augmentations[selectedAug];
const info = typeof aug.info === "string" ? <span>{aug.info}</span> : aug.info;
const tooltip = (
<>
{info}
<br />
<br />
{aug.stats}
</>
);
return tooltip;
})()}
</Typography>
</Box>
</Paper>
</Box>
<Box sx={{ my: 3 }}>
<Typography variant="h5">Entropy Virus</Typography>
<Paper sx={{ my: 1, p: 1, width: "fit-content" }}>
<Typography>
<b>Entropy strength:</b> {player.entropy}
<br />
<b>All multipliers decreased by:</b>{" "}
{formatNumber((1 - CONSTANTS.EntropyEffect ** player.entropy) * 100, 3)}% (multiplicative)
</Typography>
</Paper>
<Typography>
When installed on an unconscious individual, Augmentations are scanned by the body on awakening, eliminating
hidden malware. However, grafted Augmentations do not provide this security measure.
<br />
<br />
Individuals who tested Augmentation grafting have reported symptoms of an unknown virus, which they've dubbed
"Entropy". This virus seems to grow more potent with each grafted Augmentation...
</Typography>
</Box>
</Container>
);
};

@ -3,7 +3,6 @@
* Used because at the time of implementation, the PlayerObject
* cant be converted to TypeScript.
*/
import { Resleeve } from "./Resleeving/Resleeve";
import { Sleeve } from "./Sleeve/Sleeve";
import { IMap } from "../types";
@ -65,7 +64,6 @@ export interface IPlayer {
playtimeSinceLastBitnode: number;
purchasedServers: any[];
queuedAugmentations: IPlayerOwnedAugmentation[];
resleeves: Resleeve[];
scriptProdSinceLastAug: number;
sleeves: Sleeve[];
sleevesFromCovenant: number;
@ -130,6 +128,8 @@ export interface IPlayer {
factionWorkType: string;
createProgramName: string;
timeWorkedCreateProgram: number;
graftAugmentationName: string;
timeWorkedGraftAugmentation: number;
crimeType: string;
committingCrimeThruSingFn: boolean;
singFnCrimeWorkerScript: WorkerScript | null;
@ -160,6 +160,8 @@ export interface IPlayer {
workChaExpGainRate: number;
workMoneyLossRate: number;
entropy: number;
// Methods
work(numCycles: number): boolean;
workPartTime(numCycles: number): boolean;
@ -181,7 +183,7 @@ export interface IPlayer {
canAccessBladeburner(): boolean;
canAccessCorporation(): boolean;
canAccessGang(): boolean;
canAccessResleeving(): boolean;
canAccessGrafting(): boolean;
canAfford(cost: number): boolean;
gainHackingExp(exp: number): void;
gainStrengthExp(exp: number): void;
@ -286,4 +288,8 @@ export interface IPlayer {
setMult(name: string, mult: number): void;
canAccessCotMG(): boolean;
sourceFileLvl(n: number): number;
startGraftAugmentationWork(augmentationName: string, time: number): void;
graftAugmentationWork(numCycles: number): boolean;
finishGraftAugmentationWork(cancelled: boolean): string;
applyEntropy(stacks?: number): void;
}

@ -6,7 +6,6 @@ import * as generalMethods from "./PlayerObjectGeneralMethods";
import * as serverMethods from "./PlayerObjectServerMethods";
import { IMap } from "../../types";
import { Resleeve } from "../Resleeving/Resleeve";
import { Sleeve } from "../Sleeve/Sleeve";
import { IPlayerOwnedSourceFile } from "../../SourceFile/PlayerOwnedSourceFile";
import { Exploit } from "../../Exploits/Exploit";
@ -72,7 +71,6 @@ export class PlayerObject implements IPlayer {
playtimeSinceLastBitnode: number;
purchasedServers: any[];
queuedAugmentations: IPlayerOwnedAugmentation[];
resleeves: Resleeve[];
scriptProdSinceLastAug: number;
sleeves: Sleeve[];
sleevesFromCovenant: number;
@ -139,6 +137,8 @@ export class PlayerObject implements IPlayer {
factionWorkType: string;
createProgramName: string;
timeWorkedCreateProgram: number;
graftAugmentationName: string;
timeWorkedGraftAugmentation: number;
crimeType: string;
committingCrimeThruSingFn: boolean;
singFnCrimeWorkerScript: WorkerScript | null;
@ -169,6 +169,8 @@ export class PlayerObject implements IPlayer {
workChaExpGainRate: number;
workMoneyLossRate: number;
entropy: number;
// Methods
work: (numCycles: number) => boolean;
workPartTime: (numCycles: number) => boolean;
@ -190,7 +192,7 @@ export class PlayerObject implements IPlayer {
canAccessBladeburner: () => boolean;
canAccessCorporation: () => boolean;
canAccessGang: () => boolean;
canAccessResleeving: () => boolean;
canAccessGrafting: () => boolean;
canAfford: (cost: number) => boolean;
gainHackingExp: (exp: number) => void;
gainStrengthExp: (exp: number) => void;
@ -296,6 +298,10 @@ export class PlayerObject implements IPlayer {
setMult: (name: string, mult: number) => void;
canAccessCotMG: () => boolean;
sourceFileLvl: (n: number) => number;
startGraftAugmentationWork: (augmentationName: string, time: number) => void;
graftAugmentationWork: (numCycles: number) => boolean;
finishGraftAugmentationWork: (cancelled: boolean) => string;
applyEntropy: (stacks?: number) => void;
constructor() {
//Skills and stats
@ -419,6 +425,9 @@ export class PlayerObject implements IPlayer {
this.createProgramName = "";
this.createProgramReqLvl = 0;
this.graftAugmentationName = "";
this.timeWorkedGraftAugmentation = 0;
this.className = "";
this.crimeType = "";
@ -457,11 +466,12 @@ export class PlayerObject implements IPlayer {
// Sleeves & Re-sleeving
this.sleeves = [];
this.resleeves = [];
this.sleevesFromCovenant = 0; // # of Duplicate sleeves purchased from the covenan;
//bitnode
this.bitNodeN = 1;
this.entropy = 0;
//Used to store the last update time.
this.lastUpdate = 0;
this.lastSave = 0;
@ -483,11 +493,11 @@ export class PlayerObject implements IPlayer {
// Let's get a hash of some semi-random stuff so we have something unique.
this.identifier = cyrb53(
"I-" +
new Date().getTime() +
navigator.userAgent +
window.innerWidth +
window.innerHeight +
getRandomInt(100, 999),
new Date().getTime() +
navigator.userAgent +
window.innerWidth +
window.innerHeight +
getRandomInt(100, 999),
);
this.init = generalMethods.init;
@ -541,6 +551,9 @@ export class PlayerObject implements IPlayer {
this.startCreateProgramWork = generalMethods.startCreateProgramWork;
this.createProgramWork = generalMethods.createProgramWork;
this.finishCreateProgramWork = generalMethods.finishCreateProgramWork;
this.startGraftAugmentationWork = generalMethods.startGraftAugmentationWork;
this.graftAugmentationWork = generalMethods.craftAugmentationWork;
this.finishGraftAugmentationWork = generalMethods.finishGraftAugmentationWork;
this.startClass = generalMethods.startClass;
this.takeClass = generalMethods.takeClass;
this.finishClass = generalMethods.finishClass;
@ -577,7 +590,7 @@ export class PlayerObject implements IPlayer {
this.gainCodingContractReward = generalMethods.gainCodingContractReward;
this.travel = generalMethods.travel;
this.gotoLocation = generalMethods.gotoLocation;
this.canAccessResleeving = generalMethods.canAccessResleeving;
this.canAccessGrafting = generalMethods.canAccessGrafting;
this.giveExploit = generalMethods.giveExploit;
this.giveAchievement = generalMethods.giveAchievement;
this.getIntelligenceBonus = generalMethods.getIntelligenceBonus;
@ -611,6 +624,8 @@ export class PlayerObject implements IPlayer {
this.canAccessCotMG = generalMethods.canAccessCotMG;
this.sourceFileLvl = generalMethods.sourceFileLvl;
this.applyEntropy = augmentationMethods.applyEntropy;
}
/**

@ -5,6 +5,8 @@ import { IPlayer } from "../IPlayer";
import { Augmentation } from "../../Augmentation/Augmentation";
import { calculateEntropy } from "../Grafting/EntropyAccumulation";
export function hasAugmentation(this: IPlayer, aug: string | Augmentation, installed = false): boolean {
const augName: string = aug instanceof Augmentation ? aug.name : aug;
@ -24,3 +26,14 @@ export function hasAugmentation(this: IPlayer, aug: string | Augmentation, insta
return false;
}
export function applyEntropy(this: IPlayer, stacks = 1): void {
// Re-apply all multipliers
this.reapplyAllAugmentations();
this.reapplyAllSourceFiles();
const newMultipliers = calculateEntropy(this, stacks);
for (const [mult, val] of Object.entries(newMultipliers)) {
this.setMult(mult, val);
}
}

@ -121,8 +121,6 @@ export function prestigeAugmentation(this: PlayerObject): void {
this.queuedAugmentations = [];
this.resleeves = [];
const numSleeves = Math.min(3, SourceFileFlags[10] + (this.bitNodeN === 10 ? 1 : 0)) + this.sleevesFromCovenant;
if (this.sleeves.length > numSleeves) this.sleeves.length = numSleeves;
for (let i = this.sleeves.length; i < numSleeves; i++) {
@ -182,6 +180,7 @@ export function prestigeAugmentation(this: PlayerObject): void {
}
export function prestigeSourceFile(this: IPlayer): void {
this.entropy = 0;
this.prestigeAugmentation();
this.karma = 0;
// Duplicate sleeves are reset to level 1 every Bit Node (but the number of sleeves you have persists)
@ -529,10 +528,12 @@ export function resetWorkStatus(this: IPlayer, generalType?: string, group?: str
this.timeWorked = 0;
this.timeWorkedCreateProgram = 0;
this.timeWorkedGraftAugmentation = 0;
this.currentWorkFactionName = "";
this.currentWorkFactionDescription = "";
this.createProgramName = "";
this.graftAugmentationName = "";
this.className = "";
this.workType = "";
}
@ -609,6 +610,10 @@ export function process(this: IPlayer, router: IRouter, numCycles = 1): void {
if (this.workPartTime(numCycles)) {
router.toCity();
}
} else if (this.workType === CONSTANTS.WorkTypeGraftAugmentation) {
if (this.graftAugmentationWork(numCycles)) {
router.toGrafting();
}
} else if (this.work(numCycles)) {
router.toCity();
}
@ -1329,6 +1334,59 @@ export function finishCreateProgramWork(this: IPlayer, cancelled: boolean): stri
this.resetWorkStatus();
return "You've finished creating " + programName + "! The new program can be found on your home computer.";
}
export function startGraftAugmentationWork(this: IPlayer, augmentationName: string, time: number): void {
this.resetWorkStatus();
this.isWorking = true;
this.workType = CONSTANTS.WorkTypeGraftAugmentation;
this.timeNeededToCompleteWork = time;
this.graftAugmentationName = augmentationName;
}
export function craftAugmentationWork(this: IPlayer, numCycles: number): boolean {
let focusBonus = 1;
if (!this.hasAugmentation(AugmentationNames.NeuroreceptorManager)) {
focusBonus = this.focus ? 1 : CONSTANTS.BaseFocusBonus;
}
let skillMult = 1 + (this.getIntelligenceBonus(3) - 1) / 3;
skillMult *= focusBonus;
this.timeWorked += CONSTANTS._idleSpeed * numCycles;
this.timeWorkedGraftAugmentation += CONSTANTS._idleSpeed * numCycles * skillMult;
if (this.timeWorkedGraftAugmentation >= this.timeNeededToCompleteWork) {
this.finishGraftAugmentationWork(false);
return true;
}
return false;
}
export function finishGraftAugmentationWork(this: IPlayer, cancelled: boolean): string {
const augName = this.graftAugmentationName;
if (cancelled === false) {
dialogBoxCreate(
`You've finished crafting ${augName}.<br>The augmentation has been grafted to your body, but you feel a bit off.`,
);
applyAugmentation(Augmentations[augName]);
this.entropy += 1;
this.applyEntropy(this.entropy);
} else {
dialogBoxCreate(`You cancelled the crafting of ${augName}.<br>Your money was not returned to you.`);
}
// Intelligence gain
if (!cancelled) {
this.gainIntelligenceExp((CONSTANTS.IntelligenceGraftBaseExpGain * this.timeWorked) / 10000);
}
this.isWorking = false;
this.resetWorkStatus();
return `Grafting of ${augName} has ended.`;
}
/* Studying/Taking Classes */
export function startClass(this: IPlayer, costMult: number, expMult: number, className: string): void {
this.resetWorkStatus();
@ -2640,7 +2698,7 @@ export function gotoLocation(this: IPlayer, to: LocationName): boolean {
return true;
}
export function canAccessResleeving(this: IPlayer): boolean {
export function canAccessGrafting(this: IPlayer): boolean {
return this.bitNodeN === 10 || SourceFileFlags[10] > 0;
}

@ -1,10 +0,0 @@
Implements the Re-sleeving feature, which allows players to purchase a new body
that comes with pre-existing Augmentations and experience. Note that purchasing
a new body causes you to lose all of your old Augmentations and experience
This feature is introduced in BitNode-10, and destroying BitNode-10 allows
the user to use it in other BitNodes (provided that they purchase the required
cortical stack Augmentation)
While they are based on the same concept, this feature is different than the
"Duplicate Sleeve" mechanic (which is referred to as just "Sleeve" in the source code).

@ -1,63 +0,0 @@
/**
* Implements the Resleeve class, which defines a new body
* that the player can "re-sleeve" into.
*/
import { Person } from "../Person";
import { Augmentation } from "../../Augmentation/Augmentation";
import { Augmentations } from "../../Augmentation/Augmentations";
import { Generic_fromJSON, Generic_toJSON, Reviver } from "../../utils/JSONReviver";
export class Resleeve extends Person {
constructor() {
super();
}
getCost(): number {
// Each experience point adds this to the cost
const CostPerExp = 25e3;
// Final cost is multiplied by this constant ^ # Augs
const NumAugsExponent = 1.2;
// Get total exp in this re-sleeve
const totalExp: number =
this.hacking_exp +
this.strength_exp +
this.defense_exp +
this.dexterity_exp +
this.agility_exp +
this.charisma_exp;
// Get total base Augmentation cost for this re-sleeve
let totalAugmentationCost = 0;
for (let i = 0; i < this.augmentations.length; ++i) {
const aug: Augmentation | null = Augmentations[this.augmentations[i].name];
if (aug == null) {
console.error(`Could not find Augmentation ${this.augmentations[i].name}`);
continue;
}
totalAugmentationCost += aug.startingCost;
}
return totalExp * CostPerExp + totalAugmentationCost * Math.pow(NumAugsExponent, this.augmentations.length);
}
/**
* Serialize the current object to a JSON save state.
*/
toJSON(): any {
return Generic_toJSON("Resleeve", this);
}
/**
* Initiatizes a Resleeve object from a JSON save state.
*/
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
static fromJSON(value: any): Resleeve {
return Generic_fromJSON(Resleeve, value.data);
}
}
Reviver.constructors.Resleeve = Resleeve;

@ -1,133 +0,0 @@
/**
* Implements the Re-sleeving mechanic for BitNode-10.
* This allows the player to purchase and "use" new sleeves at VitaLife.
* These new sleeves come with different starting experience and Augmentations
* The cost of these new sleeves scales based on the exp and Augs.
*
* Note that this is different from the "Sleeve mechanic". The "Sleeve" mechanic
* provides new sleeves, essentially clones. This Re-sleeving mechanic lets
* the player purchase a new body with pre-existing Augmentations and experience
*
* As of right now, this feature is only available in BitNode 10
*/
import { Resleeve } from "./Resleeve";
import { IPlayer } from "../IPlayer";
import { Augmentation } from "../../Augmentation/Augmentation";
import { Augmentations } from "../../Augmentation/Augmentations";
import { IPlayerOwnedAugmentation, PlayerOwnedAugmentation } from "../../Augmentation/PlayerOwnedAugmentation";
import { AugmentationNames } from "../../Augmentation/data/AugmentationNames";
import { getRandomInt } from "../../utils/helpers/getRandomInt";
// Executes the actual re-sleeve when one is purchased
export function purchaseResleeve(r: Resleeve, p: IPlayer): boolean {
const cost: number = r.getCost();
if (!p.canAfford(cost)) {
return false;
}
p.loseMoney(cost, "other");
// Set the player's exp
p.hacking_exp = r.hacking_exp;
p.strength_exp = r.strength_exp;
p.defense_exp = r.defense_exp;
p.dexterity_exp = r.dexterity_exp;
p.agility_exp = r.agility_exp;
p.charisma_exp = r.charisma_exp;
// Reset Augmentation "owned" data
for (const augKey of Object.keys(Augmentations)) {
Augmentations[augKey].owned = false;
}
// Clear all of the player's augmentations, except the NeuroFlux Governor
// which is kept
for (let i = p.augmentations.length - 1; i >= 0; --i) {
if (p.augmentations[i].name !== AugmentationNames.NeuroFluxGovernor) {
p.augmentations.splice(i, 1);
} else {
// NeuroFlux Governor
Augmentations[AugmentationNames.NeuroFluxGovernor].owned = true;
}
}
for (let i = 0; i < r.augmentations.length; ++i) {
p.augmentations.push(new PlayerOwnedAugmentation(r.augmentations[i].name));
Augmentations[r.augmentations[i].name].owned = true;
}
// The player's purchased Augmentations should remain the same, but any purchased
// Augmentations that are given by the resleeve should be removed so there are no duplicates
for (let i = p.queuedAugmentations.length - 1; i >= 0; --i) {
const name: string = p.queuedAugmentations[i].name;
if (
p.augmentations.filter((e: IPlayerOwnedAugmentation) => {
return e.name !== AugmentationNames.NeuroFluxGovernor && e.name === name;
}).length >= 1
) {
p.queuedAugmentations.splice(i, 1);
}
}
p.reapplyAllAugmentations(true);
p.reapplyAllSourceFiles(); //Multipliers get reset, so have to re-process source files too
return true;
}
// Creates all of the Re-sleeves that will be available for purchase at VitaLife
export function generateResleeves(): Resleeve[] {
const NumResleeves = 40; // Total number of Resleeves to generate
const ret: Resleeve[] = [];
for (let i = 0; i < NumResleeves; ++i) {
// i will be a number indicating how "powerful" the Re-sleeve should be
const r: Resleeve = new Resleeve();
// Generate experience
const expMult: number = 5 * i + 1;
r.hacking_exp = expMult * getRandomInt(1000, 5000);
r.strength_exp = expMult * getRandomInt(1000, 5000);
r.defense_exp = expMult * getRandomInt(1000, 5000);
r.dexterity_exp = expMult * getRandomInt(1000, 5000);
r.agility_exp = expMult * getRandomInt(1000, 5000);
r.charisma_exp = expMult * getRandomInt(1000, 5000);
// Generate Augs
// Augmentation prequisites will be ignored for this
const baseNumAugs: number = Math.max(2, Math.ceil((i + 3) / 2));
const numAugs: number = getRandomInt(baseNumAugs, baseNumAugs + 2);
const augKeys: string[] = Object.keys(Augmentations);
for (let a = 0; a < numAugs; ++a) {
// Get a random aug
const randIndex: number = getRandomInt(0, augKeys.length - 1);
const randKey: string = augKeys[randIndex];
// Forbidden augmentations
const forbidden = [
AugmentationNames.TheRedPill,
AugmentationNames.NeuroFluxGovernor,
AugmentationNames.StaneksGift1,
AugmentationNames.StaneksGift2,
AugmentationNames.StaneksGift3,
];
if (forbidden.includes(randKey)) {
continue;
}
const randAug: Augmentation | null = Augmentations[randKey];
if (randAug === null) throw new Error(`null augmentation: ${randKey}`);
r.augmentations.push({ name: randAug.name, level: 1 });
r.applyAugmentation(Augmentations[randKey]);
r.updateStatLevels();
// Remove Augmentation so that there are no duplicates
augKeys.splice(randIndex, 1);
}
ret.push(r);
}
return ret;
}

@ -1,166 +0,0 @@
import React, { useState } from "react";
import { IPlayer } from "../../IPlayer";
import { Resleeve } from "../Resleeve";
import { Augmentations } from "../../../Augmentation/Augmentations";
import { purchaseResleeve } from "../Resleeving";
import { Money } from "../../../ui/React/Money";
import { numeralWrapper } from "../../../ui/numeralFormat";
import { dialogBoxCreate } from "../../../ui/React/DialogBox";
import Typography from "@mui/material/Typography";
import Paper from "@mui/material/Paper";
import Button from "@mui/material/Button";
import Select, { SelectChangeEvent } from "@mui/material/Select";
import MenuItem from "@mui/material/MenuItem";
import Grid from "@mui/material/Grid";
interface IProps {
resleeve: Resleeve;
player: IPlayer;
}
export function ResleeveElem(props: IProps): React.ReactElement {
const [aug, setAug] = useState(props.resleeve.augmentations[0].name);
function openStats(): void {
dialogBoxCreate(
<>
<Typography variant="h5" color="primary">
Total Multipliers:
</Typography>
<Typography>
Hacking Level multiplier: {numeralWrapper.formatPercentage(props.resleeve.hacking_mult)}
<br />
Hacking Experience multiplier: {numeralWrapper.formatPercentage(props.resleeve.hacking_exp_mult)}
<br />
Strength Level multiplier: {numeralWrapper.formatPercentage(props.resleeve.strength_mult)}
<br />
Strength Experience multiplier: {numeralWrapper.formatPercentage(props.resleeve.strength_exp_mult)}
<br />
Defense Level multiplier: {numeralWrapper.formatPercentage(props.resleeve.defense_mult)}
<br />
Defense Experience multiplier: {numeralWrapper.formatPercentage(props.resleeve.defense_exp_mult)}
<br />
Dexterity Level multiplier: {numeralWrapper.formatPercentage(props.resleeve.dexterity_mult)}
<br />
Dexterity Experience multiplier: {numeralWrapper.formatPercentage(props.resleeve.dexterity_exp_mult)}
<br />
Agility Level multiplier: {numeralWrapper.formatPercentage(props.resleeve.agility_mult)}
<br />
Agility Experience multiplier: {numeralWrapper.formatPercentage(props.resleeve.agility_exp_mult)}
<br />
Charisma Level multiplier: {numeralWrapper.formatPercentage(props.resleeve.charisma_mult)}
<br />
Charisma Experience multiplier: {numeralWrapper.formatPercentage(props.resleeve.charisma_exp_mult)}
<br />
Hacking Chance multiplier: {numeralWrapper.formatPercentage(props.resleeve.hacking_chance_mult)}
<br />
Hacking Speed multiplier: {numeralWrapper.formatPercentage(props.resleeve.hacking_speed_mult)}
<br />
Hacking Money multiplier: {numeralWrapper.formatPercentage(props.resleeve.hacking_money_mult)}
<br />
Hacking Growth multiplier: {numeralWrapper.formatPercentage(props.resleeve.hacking_grow_mult)}
<br />
Salary multiplier: {numeralWrapper.formatPercentage(props.resleeve.work_money_mult)}
<br />
Company Reputation Gain multiplier: {numeralWrapper.formatPercentage(props.resleeve.company_rep_mult)}
<br />
Faction Reputation Gain multiplier: {numeralWrapper.formatPercentage(props.resleeve.faction_rep_mult)}
<br />
Crime Money multiplier: {numeralWrapper.formatPercentage(props.resleeve.crime_money_mult)}
<br />
Crime Success multiplier: {numeralWrapper.formatPercentage(props.resleeve.crime_success_mult)}
<br />
Hacknet Income multiplier: {numeralWrapper.formatPercentage(props.resleeve.hacknet_node_money_mult)}
<br />
Hacknet Purchase Cost multiplier:
{numeralWrapper.formatPercentage(props.resleeve.hacknet_node_purchase_cost_mult)}
<br />
Hacknet Level Upgrade Cost multiplier:
{numeralWrapper.formatPercentage(props.resleeve.hacknet_node_level_cost_mult)}
<br />
Hacknet Ram Upgrade Cost multiplier:
{numeralWrapper.formatPercentage(props.resleeve.hacknet_node_ram_cost_mult)}
<br />
Hacknet Core Upgrade Cost multiplier:
{numeralWrapper.formatPercentage(props.resleeve.hacknet_node_core_cost_mult)}
<br />
Bladeburner Max Stamina multiplier:
{numeralWrapper.formatPercentage(props.resleeve.bladeburner_max_stamina_mult)}
<br />
Bladeburner Stamina Gain multiplier:
{numeralWrapper.formatPercentage(props.resleeve.bladeburner_stamina_gain_mult)}
<br />
Bladeburner Field Analysis multiplier:
{numeralWrapper.formatPercentage(props.resleeve.bladeburner_analysis_mult)}
<br />
Bladeburner Success Chance multiplier:
{numeralWrapper.formatPercentage(props.resleeve.bladeburner_success_chance_mult)}
</Typography>
</>,
);
}
function onAugChange(event: SelectChangeEvent<string>): void {
setAug(event.target.value);
}
const currentAug = Augmentations[aug];
const cost = props.resleeve.getCost();
function purchase(): void {
if (!purchaseResleeve(props.resleeve, props.player)) return;
dialogBoxCreate(
<>
You re-sleeved for <Money money={cost} />!
</>,
);
}
return (
<Paper sx={{ my: 1 }}>
<Grid container>
<Grid item xs={3}>
<Typography>
Hacking: {numeralWrapper.formatSkill(props.resleeve.hacking)} (
{numeralWrapper.formatExp(props.resleeve.hacking_exp)} exp)
<br />
Strength: {numeralWrapper.formatSkill(props.resleeve.strength)} (
{numeralWrapper.formatExp(props.resleeve.strength_exp)} exp)
<br />
Defense: {numeralWrapper.formatSkill(props.resleeve.defense)} (
{numeralWrapper.formatExp(props.resleeve.defense_exp)} exp)
<br />
Dexterity: {numeralWrapper.formatSkill(props.resleeve.dexterity)} (
{numeralWrapper.formatExp(props.resleeve.dexterity_exp)} exp)
<br />
Agility: {numeralWrapper.formatSkill(props.resleeve.agility)} (
{numeralWrapper.formatExp(props.resleeve.agility_exp)} exp)
<br />
Charisma: {numeralWrapper.formatSkill(props.resleeve.charisma)} (
{numeralWrapper.formatExp(props.resleeve.charisma_exp)} exp)
<br /># Augmentations: {props.resleeve.augmentations.length}
</Typography>
<Button onClick={openStats}>Multipliers</Button>
</Grid>
<Grid item xs={6}>
<Select value={aug} onChange={onAugChange}>
{props.resleeve.augmentations.map((aug) => (
<MenuItem key={aug.name} value={aug.name}>
{aug.name}
</MenuItem>
))}
</Select>
<Typography>{currentAug !== undefined && currentAug.info}</Typography>
</Grid>
<Grid item xs={3}>
<Typography>
It costs <Money money={cost} player={props.player} /> to purchase this Sleeve.
</Typography>
<Button onClick={purchase}>Purchase</Button>
</Grid>
</Grid>
</Paper>
);
}

@ -1,124 +0,0 @@
import React, { useState } from "react";
import { generateResleeves } from "../Resleeving";
import { Resleeve } from "../Resleeve";
import { ResleeveElem } from "./ResleeveElem";
import { use } from "../../../ui/Context";
import Typography from "@mui/material/Typography";
import Select, { SelectChangeEvent } from "@mui/material/Select";
import MenuItem from "@mui/material/MenuItem";
import Box from "@mui/material/Box";
const SortOption: {
[key: string]: string | undefined;
Cost: string;
Hacking: string;
Strength: string;
Defense: string;
Dexterity: string;
Agility: string;
Charisma: string;
AverageCombatStats: string;
AverageAllStats: string;
TotalNumAugmentations: string;
} = {
Cost: "Cost",
Hacking: "Hacking Level",
Strength: "Strength Level",
Defense: "Defense Level",
Dexterity: "Dexterity Level",
Agility: "Agility Level",
Charisma: "Charisma Level",
AverageCombatStats: "Average Combat Stats",
AverageAllStats: "Average Stats",
TotalNumAugmentations: "Number of Augmentations",
};
// Helper function for averaging
function getAverage(...values: number[]): number {
let sum = 0;
for (let i = 0; i < values.length; ++i) {
sum += values[i];
}
return sum / values.length;
}
const SortFunctions: {
[key: string]: ((a: Resleeve, b: Resleeve) => number) | undefined;
Cost: (a: Resleeve, b: Resleeve) => number;
Hacking: (a: Resleeve, b: Resleeve) => number;
Strength: (a: Resleeve, b: Resleeve) => number;
Defense: (a: Resleeve, b: Resleeve) => number;
Dexterity: (a: Resleeve, b: Resleeve) => number;
Agility: (a: Resleeve, b: Resleeve) => number;
Charisma: (a: Resleeve, b: Resleeve) => number;
AverageCombatStats: (a: Resleeve, b: Resleeve) => number;
AverageAllStats: (a: Resleeve, b: Resleeve) => number;
TotalNumAugmentations: (a: Resleeve, b: Resleeve) => number;
} = {
Cost: (a: Resleeve, b: Resleeve): number => a.getCost() - b.getCost(),
Hacking: (a: Resleeve, b: Resleeve): number => a.hacking - b.hacking,
Strength: (a: Resleeve, b: Resleeve): number => a.strength - b.strength,
Defense: (a: Resleeve, b: Resleeve): number => a.defense - b.defense,
Dexterity: (a: Resleeve, b: Resleeve): number => a.dexterity - b.dexterity,
Agility: (a: Resleeve, b: Resleeve): number => a.agility - b.agility,
Charisma: (a: Resleeve, b: Resleeve): number => a.charisma - b.charisma,
AverageCombatStats: (a: Resleeve, b: Resleeve): number =>
getAverage(a.strength, a.defense, a.dexterity, a.agility) -
getAverage(b.strength, b.defense, b.dexterity, b.agility),
AverageAllStats: (a: Resleeve, b: Resleeve): number =>
getAverage(a.hacking, a.strength, a.defense, a.dexterity, a.agility, a.charisma) -
getAverage(b.hacking, b.strength, b.defense, b.dexterity, b.agility, b.charisma),
TotalNumAugmentations: (a: Resleeve, b: Resleeve): number => a.augmentations.length - b.augmentations.length,
};
export function ResleeveRoot(): React.ReactElement {
const player = use.Player();
const [sort, setSort] = useState(SortOption.Cost);
// Randomly create all Resleeves if they dont already exist
if (player.resleeves.length === 0) {
player.resleeves = generateResleeves();
}
function onSortChange(event: SelectChangeEvent<string>): void {
setSort(event.target.value);
}
const sortFunction = SortFunctions[sort];
if (sortFunction === undefined) throw new Error(`sort function '${sort}' is undefined`);
player.resleeves.sort(sortFunction);
return (
<>
<Typography>
Re-sleeving is the process of digitizing and transferring your consciousness into a new human body, or 'sleeve'.
Here at VitaLife, you can purchase new specially-engineered bodies for the re-sleeve process. Many of these
bodies even come with genetic and cybernetic Augmentations!
<br />
<br />
Re-sleeving will change your experience for every stat. It will also REMOVE all of your currently-installed
Augmentations, and replace them with the ones provided by the purchased sleeve. However, Augmentations that you
have purchased but not installed will NOT be removed. If you have purchased an Augmentation and then re-sleeve
into a body which already has that Augmentation, it will be removed (since you cannot have duplicate
Augmentations).
<br />
<br />
NOTE: The stats and multipliers displayed on this page do NOT include your bonuses from Source-File.
</Typography>
<Box display="flex" alignItems="center">
<Typography>Sort By: </Typography>
<Select value={sort} onChange={onSortChange}>
{Object.keys(SortOption).map((opt) => (
<MenuItem key={opt} value={opt}>
{SortOption[opt]}
</MenuItem>
))}
</Select>
</Box>
{player.resleeves.map((resleeve, i) => (
<ResleeveElem key={i} player={player} resleeve={resleeve} />
))}
</>
);
}

@ -108,6 +108,9 @@ export function prestigeAugmentation(): void {
// Messages
initMessages();
// Apply entropy from grafting
Player.applyEntropy(Player.entropy);
// Gang
const gang = Player.gang;
if (Player.inGang() && gang !== null) {

@ -393,6 +393,11 @@ function evaluateVersionCompatibility(ver: string | number): void {
}
}
}
if (ver < 12) {
if (anyPlayer.resleeves !== undefined) {
delete anyPlayer.resleeves;
}
}
}
}

@ -225,6 +225,9 @@ async function parseOnlyRamCalculate(
} else if (ref in workerScript.env.vars.ui) {
func = workerScript.env.vars.ui[ref];
refDetail = `ui.${ref}`;
} else if (ref in workerScript.env.vars.grafting) {
func = workerScript.env.vars.grafting[ref];
refDetail = `grafting.${ref}`;
} else {
func = workerScript.env.vars[ref];
refDetail = `${ref}`;

@ -95,6 +95,7 @@ interface Player {
tor: boolean;
hasCorporation: boolean;
inBladeburner: boolean;
entropy: number;
}
/**
@ -1597,7 +1598,7 @@ export interface Singularity {
* purchasing a TOR router using this function is the same as if you were to
* manually purchase one.
*
* @returns True if actions is successful, false otherwise.
* @returns True if actions is successful or you already own TOR router, false otherwise.
*/
purchaseTor(): boolean;
@ -3719,6 +3720,43 @@ export interface Sleeve {
purchaseSleeveAug(sleeveNumber: number, augName: string): boolean;
}
export interface Grafting {
/**
* Retrieve the grafting cost of an aug.
* @remarks
* RAM cost: 3.75 GB
*
* @param augName - Name of the aug to check the price of. Must be an exact match.
* @returns The cost required to graft the named augmentation.
* @throws Will error if an invalid Augmentation name is provided.
*/
getAugmentationGraftPrice(augName: string): number;
/**
* Retrieves the time required to graft an aug.
* @remarks
* RAM cost: 3.75 GB
*
* @param augName - Name of the aug to check the grafting time of. Must be an exact match.
* @returns The time required, in millis, to graft the named augmentation.
* @throws Will error if an invalid Augmentation name is provided.
*/
getAugmentationGraftTime(augName: string): number;
/**
* Begins grafting the named aug. You must be in New Tokyo to use this.
* @remarks
* RAM cost: 7.5 GB
*
* @param augName - The name of the aug to begin grafting. Must be an exact match.
* @param focus - Acquire player focus on this Augmentation grafting. Optional. Defaults to true.
* @returns True if the aug successfully began grafting, false otherwise (e.g. not enough money, or
* invalid Augmentation name provided).
* @throws Will error if called while you are not in New Tokyo.
*/
graftAugmentation(augName: string, focus?: boolean): boolean;
}
/**
* Skills formulas
* @public
@ -4028,14 +4066,14 @@ interface Stanek {
* RAM cost: 0.4 GB
* @returns The width of the gift.
*/
width(): number;
giftWidth(): number;
/**
* Stanek's Gift height.
* @remarks
* RAM cost: 0.4 GB
* @returns The height of the gift.
*/
height(): number;
giftHeight(): number;
/**
* Charge a fragment, increasing its power.
@ -4045,7 +4083,7 @@ interface Stanek {
* @param rootY - rootY Root Y against which to align the top left of the fragment.
* @returns Promise that lasts until the charge action is over.
*/
charge(rootX: number, rootY: number): Promise<void>;
chargeFragment(rootX: number, rootY: number): Promise<void>;
/**
* List possible fragments.
@ -4070,7 +4108,7 @@ interface Stanek {
* @remarks
* RAM cost: 0 GB
*/
clear(): void;
clearGift(): void;
/**
* Check if fragment can be placed at specified location.
@ -4083,7 +4121,7 @@ interface Stanek {
* @param fragmentId - fragmentId ID of the fragment to place.
* @returns true if the fragment can be placed at that position. false otherwise.
*/
canPlace(rootX: number, rootY: number, rotation: number, fragmentId: number): boolean;
canPlaceFragment(rootX: number, rootY: number, rotation: number, fragmentId: number): boolean;
/**
* Place fragment on Stanek's Gift.
* @remarks
@ -4095,7 +4133,7 @@ interface Stanek {
* @param fragmentId - ID of the fragment to place.
* @returns true if the fragment can be placed at that position. false otherwise.
*/
place(rootX: number, rootY: number, rotation: number, fragmentId: number): boolean;
placeFragment(rootX: number, rootY: number, rotation: number, fragmentId: number): boolean;
/**
* Get placed fragment at location.
* @remarks
@ -4105,7 +4143,7 @@ interface Stanek {
* @param rootY - Y against which to align the top left of the fragment.
* @returns The fragment at [rootX, rootY], if any.
*/
get(rootX: number, rootY: number): ActiveFragment | undefined;
getFragment(rootX: number, rootY: number): ActiveFragment | undefined;
/**
* Remove fragment at location.
@ -4116,7 +4154,7 @@ interface Stanek {
* @param rootY - Y against which to align the top left of the fragment.
* @returns The fragment at [rootX, rootY], if any.
*/
remove(rootX: number, rootY: number): boolean;
removeFragment(rootX: number, rootY: number): boolean;
}
/**
@ -4280,6 +4318,13 @@ export interface NS extends Singularity {
*/
readonly ui: UserInterface;
/**
* Namespace for grafting functions.
* @remarks
* RAM cost: 0 GB
*/
readonly grafting: Grafting;
/**
* Arguments passed into the script.
*

@ -118,6 +118,11 @@ interface IDefaultSettings {
*/
SuppressSavedGameToast: boolean;
/**
* Whether the user should be displayed a toast warning when the autosave is disabled.
*/
SuppressAutosaveDisabledWarnings: boolean;
/*
* Whether the game should skip saving the running scripts for late game
*/
@ -197,6 +202,7 @@ export const defaultSettings: IDefaultSettings = {
SuppressBladeburnerPopup: false,
SuppressTIXPopup: false,
SuppressSavedGameToast: false,
SuppressAutosaveDisabledWarnings: false,
UseIEC60027_2: false,
ExcludeRunningScriptsFromSave: false,
IsSidebarOpened: true,
@ -235,6 +241,7 @@ export const Settings: ISettings & ISelfInitializer & ISelfLoading = {
SuppressBladeburnerPopup: defaultSettings.SuppressBladeburnerPopup,
SuppressTIXPopup: defaultSettings.SuppressTIXPopup,
SuppressSavedGameToast: defaultSettings.SuppressSavedGameToast,
SuppressAutosaveDisabledWarnings: defaultSettings.SuppressAutosaveDisabledWarnings,
UseIEC60027_2: defaultSettings.UseIEC60027_2,
ExcludeRunningScriptsFromSave: defaultSettings.ExcludeRunningScriptsFromSave,
IsSidebarOpened: defaultSettings.IsSidebarOpened,

@ -617,7 +617,7 @@ export function SidebarRoot(props: IProps): React.ReactElement {
key={"City"}
className={clsx({
[classes.active]:
props.page === Page.City || props.page === Page.Resleeves || props.page === Page.Location,
props.page === Page.City || props.page === Page.Grafting || props.page === Page.Location,
})}
onClick={clickCity}
>

@ -167,8 +167,8 @@ 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
This Source-File unlocks Sleeve technology, and the Grafting API in other BitNodes.
Each level of this Source-File also grants you a Duplicate Sleeve
</>
),
);

@ -36,7 +36,7 @@ export function connect(
terminal.connectToServer(player, hostname);
return;
}
terminal.error(`Cannot directly connect to ${hostname}`);
terminal.error(`Cannot directly connect to ${hostname}. Make sure the server is backdoored or adjacent to your current Server`);
} else {
terminal.error("Host not found");
}

@ -15,6 +15,7 @@ import { Factions, initFactions } from "./Faction/Factions";
import { staneksGift } from "./CotMG/Helper";
import { processPassiveFactionRepGain, inviteToFaction } from "./Faction/FactionHelpers";
import { Router } from "./ui/GameRoot";
import { Page } from "./ui/Router";
import { SetupTextEditor } from "./ScriptEditor/ui/ScriptEditorRoot";
import {
@ -48,7 +49,8 @@ import { calculateAchievements } from "./Achievements/Achievements";
import React from "react";
import { setupUncaughtPromiseHandler } from "./UncaughtPromiseHandler";
import { Typography } from "@mui/material";
import { Button, Typography } from "@mui/material";
import { SnackbarEvents } from "./ui/React/Snackbar";
const Engine: {
_lastUpdate: number;
@ -187,7 +189,8 @@ const Engine: {
Settings.AutosaveInterval = 60;
}
if (Settings.AutosaveInterval === 0) {
Engine.Counters.autoSaveCounter = Infinity;
warnAutosaveDisabled();
Engine.Counters.autoSaveCounter = 60 * 5; // Let's check back in a bit
} else {
Engine.Counters.autoSaveCounter = Settings.AutosaveInterval * 5;
saveObject.saveGame(!Settings.SuppressSavedGameToast);
@ -260,6 +263,9 @@ const Engine: {
initSymbolToStockMap();
}
// Apply penalty for entropy accumulation
Player.applyEntropy(Player.entropy);
// Calculate the number of cycles have elapsed while offline
Engine._lastUpdate = new Date().getTime();
const lastUpdate = Player.lastUpdate;
@ -299,6 +305,8 @@ const Engine: {
Player.commitCrime(numCyclesOffline);
} else if (Player.workType == CONSTANTS.WorkTypeCompanyPartTime) {
Player.workPartTime(numCyclesOffline);
} else if (Player.workType === CONSTANTS.WorkTypeGraftAugmentation) {
Player.graftAugmentationWork(numCyclesOffline);
} else {
Player.work(numCyclesOffline);
}
@ -459,4 +467,35 @@ const Engine: {
},
};
/**
* Shows a toast warning that lets the player know that auto-saves are disabled, with an button to re-enable them.
*/
function warnAutosaveDisabled(): void {
// If the player has suppressed those warnings let's just exit right away.
if (Settings.SuppressAutosaveDisabledWarnings) return;
// We don't want this warning to show up on certain pages.
// When in recovery or importing we want to keep autosave disabled.
const ignoredPages = [Page.Recovery, Page.ImportSave];
if (ignoredPages.includes(Router.page())) return;
const warningToast = (
<>
Auto-saves are <strong>disabled</strong>!
<Button
sx={{ ml: 1 }}
color="warning"
size="small"
onClick={() => {
// We reset the value to a default
Settings.AutosaveInterval = 60;
}}
>
Enable
</Button>
</>
);
SnackbarEvents.emit(warningToast, "warning", 5000);
}
export { Engine };

@ -1,33 +0,0 @@
// These should really be imported with the module that is presenting that UI, but because they very much depend on the
// cascade order, we'll pull them all in here.
import "normalize.css";
import "../css/styles.scss";
import "../css/tooltips.scss";
import "../css/buttons.scss";
import "../css/mainmenu.scss";
import "../css/characteroverview.scss";
import "../css/scripteditor.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";
import "../css/popupboxes.scss";
import "../css/gameoptions.scss";
import "../css/interactivetutorial.scss";
import "../css/loader.scss";
import "../css/missions.scss";
import "../css/companymanagement.scss";
import "../css/bladeburner.scss";
import "../css/gang.scss";
import "../css/sleeves.scss";
import "../css/resleeving.scss";
import "../css/treant.css";
import "../css/grid.min.css";
import "../css/dev-menu.css";
import "../css/casino.scss";
import "../css/milestones.scss";
import "../css/infiltration.scss";
import "../css/staneksgift.scss";

@ -42,7 +42,7 @@ import { BladeburnerRoot } from "../Bladeburner/ui/BladeburnerRoot";
import { GangRoot } from "../Gang/ui/GangRoot";
import { CorporationRoot } from "../Corporation/ui/CorporationRoot";
import { InfiltrationRoot } from "../Infiltration/ui/InfiltrationRoot";
import { ResleeveRoot } from "../PersonObjects/Resleeving/ui/ResleeveRoot";
import { GraftingRoot } from "../PersonObjects/Grafting/ui/GraftingRoot";
import { WorkInProgressRoot } from "./WorkInProgressRoot";
import { GameOptionsRoot } from "./React/GameOptionsRoot";
import { SleeveRoot } from "../PersonObjects/Sleeve/ui/SleeveRoot";
@ -135,7 +135,7 @@ export let Router: IRouter = {
toInfiltration: uninitialized,
toJob: uninitialized,
toMilestones: uninitialized,
toResleeves: uninitialized,
toGrafting: uninitialized,
toScriptEditor: uninitialized,
toSleeves: uninitialized,
toStockMarket: uninitialized,
@ -226,7 +226,7 @@ export function GameRoot({ player, engine, terminal }: IProps): React.ReactEleme
toGang: () => setPage(Page.Gang),
toHacknetNodes: () => setPage(Page.Hacknet),
toMilestones: () => setPage(Page.Milestones),
toResleeves: () => setPage(Page.Resleeves),
toGrafting: () => setPage(Page.Grafting),
toScriptEditor: (files: Record<string, string>, options?: ScriptEditorRouteOptions) => {
setEditorOptions({
files,
@ -429,8 +429,8 @@ export function GameRoot({ player, engine, terminal }: IProps): React.ReactEleme
mainPage = <BladeburnerRoot />;
break;
}
case Page.Resleeves: {
mainPage = <ResleeveRoot />;
case Page.Grafting: {
mainPage = <GraftingRoot />;
break;
}
case Page.Travel: {

@ -66,7 +66,7 @@ export function AlertManager(): React.ReactElement {
{alerts.length > 0 && (
<Modal open={true} onClose={close}>
<Box overflow="scroll" sx={{ overflowWrap: "break-word", whiteSpace: "pre-line" }}>
<Typography>{alerts[0].text}</Typography>
<Typography component={'span'}>{alerts[0].text}</Typography>
</Box>
</Modal>
)}

@ -143,51 +143,66 @@ function Work(): React.ReactElement {
let details = <></>;
let header = <></>;
let innerText = <></>;
if (player.workType === CONSTANTS.WorkTypeCompanyPartTime || player.workType === CONSTANTS.WorkTypeCompany) {
details = (
<>
{player.jobs[player.companyName]} at <strong>{player.companyName}</strong>
</>
);
header = (
<>
Working at <strong>{player.companyName}</strong>
</>
);
innerText = (
<>
+<Reputation reputation={player.workRepGained} /> rep
</>
);
} else if (player.workType === CONSTANTS.WorkTypeFaction) {
details = (
<>
{player.factionWorkType} for <strong>{player.currentWorkFactionName}</strong>
</>
);
header = (
<>
Working for <strong>{player.currentWorkFactionName}</strong>
</>
);
innerText = (
<>
+<Reputation reputation={player.workRepGained} /> rep
</>
);
} else if (player.workType === CONSTANTS.WorkTypeStudyClass) {
details = <>{player.workType}</>;
header = <>You are {player.className}</>;
innerText = <>{convertTimeMsToTimeElapsedString(player.timeWorked)}</>;
} else if (player.workType === CONSTANTS.WorkTypeCreateProgram) {
details = <>Coding {player.createProgramName}</>;
header = <>Creating a program</>;
innerText = (
<>
{player.createProgramName}{" "}
{((player.timeWorkedCreateProgram / player.timeNeededToCompleteWork) * 100).toFixed(2)}%
</>
);
switch (player.workType) {
case CONSTANTS.WorkTypeCompanyPartTime:
case CONSTANTS.WorkTypeCompany:
details = (
<>
{player.jobs[player.companyName]} at <strong>{player.companyName}</strong>
</>
);
header = (
<>
Working at <strong>{player.companyName}</strong>
</>
);
innerText = (
<>
+<Reputation reputation={player.workRepGained} /> rep
</>
);
break;
case CONSTANTS.WorkTypeFaction:
details = (
<>
{player.factionWorkType} for <strong>{player.currentWorkFactionName}</strong>
</>
);
header = (
<>
Working for <strong>{player.currentWorkFactionName}</strong>
</>
);
innerText = (
<>
+<Reputation reputation={player.workRepGained} /> rep
</>
);
break;
case CONSTANTS.WorkTypeStudyClass:
details = <>{player.workType}</>;
header = <>You are {player.className}</>;
innerText = <>{convertTimeMsToTimeElapsedString(player.timeWorked)}</>;
break;
case CONSTANTS.WorkTypeCreateProgram:
details = <>Coding {player.createProgramName}</>;
header = <>Creating a program</>;
innerText = (
<>
{player.createProgramName}{" "}
{((player.timeWorkedCreateProgram / player.timeNeededToCompleteWork) * 100).toFixed(2)}%
</>
);
break;
case CONSTANTS.WorkTypeGraftAugmentation:
details = <>Grafting {player.graftAugmentationName}</>;
header = <>Grafting an Augmentation</>;
innerText = (
<>
<strong>{((player.timeWorkedGraftAugmentation / player.timeNeededToCompleteWork) * 100).toFixed(2)}%</strong>{" "}
done
</>
);
}
return (
@ -465,7 +480,7 @@ export function CharacterOverview({ save, killScripts }: IProps): React.ReactEle
<Box sx={{ display: "flex", borderTop: `1px solid ${Settings.theme.welllight}` }}>
<Box sx={{ display: "flex", flex: 1, justifyContent: "flex-start", alignItems: "center" }}>
<IconButton aria-label="save game" onClick={save}>
<Tooltip title="Save game">
<Tooltip title={Settings.AutosaveInterval !== 0 ? "Save game" : "Save game (auto-saves are disabled!)"}>
<SaveIcon color={Settings.AutosaveInterval !== 0 ? "primary" : "error"} />
</Tooltip>
</IconButton>

@ -328,6 +328,14 @@ export function GameOptionsRoot(props: IProps): React.ReactElement {
tooltip={<>If this is set, there will be no "Game Saved!" toast appearing after an auto-save.</>}
/>
</ListItem>
<ListItem>
<OptionSwitch
checked={Settings.SuppressAutosaveDisabledWarnings}
onChange={(newValue) => (Settings.SuppressAutosaveDisabledWarnings = newValue)}
text="Suppress Auto-Save Disabled Warning"
tooltip={<>If this is set, there will be no warning triggered when auto-save is disabled (at 0).</>}
/>
</ListItem>
<ListItem>
<OptionSwitch
checked={Settings.DisableHotkeys}

@ -18,6 +18,7 @@ import { Theme } from "@mui/material";
import { findRunningScript } from "../../Script/ScriptHelpers";
import { Player } from "../../Player";
import { debounce } from "lodash";
import { WorkerScriptStartStopEventEmitter } from "../../Netscript/WorkerScriptStartStopEventEmitter";
let layerCounter = 0;
@ -128,6 +129,23 @@ function LogWindow(props: IProps): React.ReactElement {
setRerender((old) => !old);
}
useEffect(
() =>
WorkerScriptStartStopEventEmitter.subscribe(() => {
setTimeout(() => {
const server = GetServer(script.server);
if (server === null) return;
const exisitingScript = findRunningScript(script.filename, script.args, server);
if (exisitingScript) {
exisitingScript.logs = script.logs.concat(exisitingScript.logs)
setScript(exisitingScript)
}
rerender();
}, 100)
}),
[],
);
useEffect(() => {
updateLayer();
const id = setInterval(rerender, 1000);
@ -198,7 +216,7 @@ function LogWindow(props: IProps): React.ReactElement {
const node = draggableRef?.current;
if (!node) return;
if(!isOnScreen(node)) {
if (!isOnScreen(node)) {
resetPosition();
}
}, 100);
@ -207,25 +225,25 @@ function LogWindow(props: IProps): React.ReactElement {
const bounds = node.getBoundingClientRect();
return !(bounds.right < 0 ||
bounds.bottom < 0 ||
bounds.left > innerWidth ||
bounds.top > outerWidth);
bounds.bottom < 0 ||
bounds.left > innerWidth ||
bounds.top > outerWidth);
}
const resetPosition = (): void => {
const node = rootRef?.current;
if (!node) return;
const state = node.state as {x: number; y: number};
const state = node.state as { x: number; y: number };
state.x = 0;
state.y = 0;
node.setState(state);
}
const boundToBody = (e: any): void | false => {
if(e.clientX < 0 ||
e.clientY < 0 ||
e.clientX > innerWidth ||
e.clientY > innerHeight) return false;
if (e.clientX < 0 ||
e.clientY < 0 ||
e.clientX > innerWidth ||
e.clientY > innerHeight) return false;
}
return (

@ -23,7 +23,7 @@ export enum Page {
Job,
Milestones,
Options,
Resleeves,
Grafting,
Sleeves,
Stats,
StockMarket,
@ -74,7 +74,7 @@ export interface IRouter {
toInfiltration(location: Location): void;
toJob(): void;
toMilestones(): void;
toResleeves(): void;
toGrafting(): void;
toScriptEditor(files?: Record<string, string>, options?: ScriptEditorRouteOptions): void;
toSleeves(): void;
toStockMarket(): void;

@ -41,8 +41,8 @@ export function WorkInProgressRoot(): React.ReactElement {
return (
<>
<Typography variant="h4" color="primary">
You have not joined {player.currentWorkFactionName || "(Faction not found)"} yet or cannot work at this time,
please try again if you think this should have worked
You have not joined {player.currentWorkFactionName || "(Faction not found)"} yet or cannot work at this
time, please try again if you think this should have worked
</Typography>
<Button onClick={() => router.toFactions()}>Back to Factions</Button>
</>
@ -483,6 +483,42 @@ export function WorkInProgressRoot(): React.ReactElement {
);
}
if (player.graftAugmentationName !== "") {
function cancel(): void {
player.finishGraftAugmentationWork(true);
router.toTerminal();
}
function unfocus(): void {
router.toTerminal();
player.stopFocusing();
}
return (
<Grid container direction="column" justifyContent="center" alignItems="center" style={{ minHeight: "100vh" }}>
<Grid item>
<Typography>
You are currently working on crafting {player.graftAugmentationName}.
<br />
<br />
You have been working for {convertTimeMsToTimeElapsedString(player.timeWorked)}
<br />
<br />
The augmentation is{" "}
{((player.timeWorkedGraftAugmentation / player.timeNeededToCompleteWork) * 100).toFixed(2)}% done being
crafted.
<br />
If you cancel, your work will <b>not</b> be saved, and the money you spent will <b>not</b> be returned.
</Typography>
</Grid>
<Grid item>
<Button sx={{ mx: 2 }} onClick={cancel}>
Cancel work on crafting Augmentation
</Button>
<Button onClick={unfocus}>Do something else simultaneously</Button>
</Grid>
</Grid>
);
}
if (!player.workType) router.toTerminal();
return <></>;

@ -16,7 +16,6 @@ import "numeral/locales/ru";
import { Settings } from "../Settings/Settings";
const extraFormats = [1e15, 1e18, 1e21, 1e24, 1e27, 1e30];
const extraNotations = ["q", "Q", "s", "S", "o", "n"];
const gigaMultiplier = { standard: 1e9, iec60027_2: 2 ** 30 };
@ -52,7 +51,7 @@ class NumeralFormatter {
}
formatBigNumber(n: number): string {
return this.format(n, "0.[000]a");
return this.format(n, "0.000a");
}
// TODO: leverage numeral.js to do it. This function also implies you can
@ -67,10 +66,10 @@ class NumeralFormatter {
}
}
if (nAbs < 1000) {
return this.format(n, "0.[" + "0".repeat(decimalPlaces) + "]");
return this.format(n, "0." + "0".repeat(decimalPlaces));
}
const str = this.format(n, "0.[" + "0".repeat(decimalPlaces) + "]a");
if (str === "NaNt") return this.format(n, "0.[" + " ".repeat(decimalPlaces) + "]e+0");
const str = this.format(n, "0." + "0".repeat(decimalPlaces) + "a");
if (str === "NaNt") return this.format(n, "0." + " ".repeat(decimalPlaces) + "e+0");
return str;
}
@ -113,8 +112,7 @@ class NumeralFormatter {
}
formatRAM(n: number): string {
if(Settings.UseIEC60027_2)
{
if (Settings.UseIEC60027_2) {
return this.format(n * gigaMultiplier.iec60027_2, "0.00ib");
}
return this.format(n * gigaMultiplier.standard, "0.00b");
@ -189,7 +187,7 @@ class NumeralFormatter {
}
parseCustomLargeNumber(str: string): number {
const numericRegExp = new RegExp('^(\-?\\d+\\.?\\d*)([' + extraNotations.join("") + ']?)$');
const numericRegExp = new RegExp("^(-?\\d+\\.?\\d*)([" + extraNotations.join("") + "]?)$");
const match = str.match(numericRegExp);
if (match == null) {
return NaN;
@ -203,14 +201,17 @@ class NumeralFormatter {
}
largestAbsoluteNumber(n1: number, n2 = 0, n3 = 0): number {
if(isNaN(n1)) n1=0;
if(isNaN(n2)) n2=0;
if(isNaN(n3)) n3=0;
if (isNaN(n1)) n1 = 0;
if (isNaN(n2)) n2 = 0;
if (isNaN(n3)) n3 = 0;
const largestAbsolute = Math.max(Math.abs(n1), Math.abs(n2), Math.abs(n3));
switch(largestAbsolute) {
case Math.abs(n1): return n1;
case Math.abs(n2): return n2;
case Math.abs(n3): return n3;
switch (largestAbsolute) {
case Math.abs(n1):
return n1;
case Math.abs(n2):
return n2;
case Math.abs(n3):
return n3;
}
return 0;
}
@ -222,21 +223,29 @@ class NumeralFormatter {
const parsed = parseFloat(s);
const selfParsed = this.parseCustomLargeNumber(s);
// Check for one or more NaN values
if (isNaN(parsed) && numeralValue === null && isNaN(selfParsed)) { // 3x NaN
if (isNaN(parsed) && numeralValue === null && isNaN(selfParsed)) {
// 3x NaN
return NaN;
} else if (isNaN(parsed) && isNaN(selfParsed)) { // 2x NaN
} else if (isNaN(parsed) && isNaN(selfParsed)) {
// 2x NaN
return numeralValue;
} else if (numeralValue === null && isNaN(selfParsed)) { // 2x NaN
} else if (numeralValue === null && isNaN(selfParsed)) {
// 2x NaN
return parsed;
} else if (isNaN(parsed) && numeralValue === null) { // 2x NaN
} else if (isNaN(parsed) && numeralValue === null) {
// 2x NaN
return selfParsed;
} else if (isNaN(parsed)) { // 1x NaN
} else if (isNaN(parsed)) {
// 1x NaN
return this.largestAbsoluteNumber(numeralValue, selfParsed);
} else if (numeralValue === null) { // 1x NaN
} else if (numeralValue === null) {
// 1x NaN
return this.largestAbsoluteNumber(parsed, selfParsed);
} else if (isNaN(selfParsed)) { // 1x NaN
} else if (isNaN(selfParsed)) {
// 1x NaN
return this.largestAbsoluteNumber(numeralValue, parsed);
} else { // no NaN
} else {
// no NaN
return this.largestAbsoluteNumber(numeralValue, parsed, selfParsed);
}
}

@ -1,303 +0,0 @@
module.exports = {
plugins: [
"stylelint-order" /*,
"stylelint-scss" */,
],
rules: {
"at-rule-blacklist": [],
// "at-rule-empty-line-before": [
// "always",
// {
// except: [
// "inside-block", "blockless-after-same-name-blockless"
// ],
// ignore: [
// "after-comment"
// ]
// }
// ],
"at-rule-name-case": "lower",
"at-rule-name-newline-after": "always-multi-line",
"at-rule-name-space-after": "always",
// "at-rule-no-vendor-prefix": true,
"at-rule-semicolon-newline-after": "always",
"at-rule-semicolon-space-before": "never",
// "at-rule-whitelist": [
// "content",
// "else",
// "function",
// "if",
// "import",
// "include",
// "keyframes",
// "mixin",
// "return"
// ],
"block-closing-brace-empty-line-before": "never",
"block-closing-brace-newline-after": "always",
"block-closing-brace-newline-before": "always-multi-line",
"block-no-empty": true,
// "block-opening-brace-newline-after": "always",
// "block-opening-brace-newline-before": "never-single-line",
// "block-opening-brace-space-before": "always",
"color-hex-case": "lower",
"color-hex-length": "short",
// "color-named": "never",
//"color-no-hex": true,
"color-no-invalid-hex": true,
// "comment-empty-line-before": "always",
"comment-no-empty": true,
"comment-whitespace-inside": "always",
"comment-word-blacklist": [],
"custom-media-pattern": ".+",
"custom-property-empty-line-before": "never",
"custom-property-pattern": "my-.+",
"declaration-bang-space-after": "never",
"declaration-bang-space-before": "always",
// "declaration-block-no-duplicate-properties": true,
"declaration-block-no-redundant-longhand-properties": true,
"declaration-block-no-shorthand-property-overrides": true,
// "declaration-block-semicolon-newline-after": "always",
"declaration-block-semicolon-newline-before": "never-multi-line",
"declaration-block-semicolon-space-before": "never",
// "declaration-block-single-line-max-declarations": 1,
"declaration-block-trailing-semicolon": "always",
"declaration-colon-newline-after": "always-multi-line",
"declaration-colon-space-after": "always-single-line",
"declaration-colon-space-before": "never",
// "declaration-empty-line-before": "never",
//"declaration-no-important": true,
"declaration-property-unit-blacklist": {},
"declaration-property-unit-whitelist": {},
"declaration-property-value-blacklist": {},
"declaration-property-value-whitelist": {},
// "font-family-name-quotes": "always-where-recommended",
"font-family-no-duplicate-names": true,
"font-family-no-missing-generic-family-keyword": true,
// "font-weight-notation": "numeric",
"function-blacklist": [],
"function-calc-no-unspaced-operator": true,
"function-comma-newline-after": "always-multi-line",
"function-comma-newline-before": "never-multi-line",
"function-comma-space-after": "always-single-line",
"function-comma-space-before": "never",
"function-linear-gradient-no-nonstandard-direction": true,
"function-max-empty-lines": 1,
"function-name-case": "lower",
"function-parentheses-newline-inside": "never-multi-line",
"function-parentheses-space-inside": "never",
"function-url-no-scheme-relative": true,
"function-url-quotes": "always",
"function-url-scheme-blacklist": [],
"function-url-scheme-whitelist": [],
// "function-whitelist": [
// "box-shadow-args",
// "map-get",
// "rgba",
// "skew",
// "var"
// ],
"keyframe-declaration-no-important": true,
"length-zero-no-unit": true,
"max-empty-lines": 1,
"max-line-length": 160,
"max-nesting-depth": 99,
"media-feature-colon-space-after": "always",
"media-feature-colon-space-before": "never",
"media-feature-name-blacklist": [],
"media-feature-name-case": "lower",
"media-feature-name-no-unknown": true,
"media-feature-name-no-vendor-prefix": true,
"media-feature-name-whitelist": [],
"media-feature-parentheses-space-inside": "never",
"media-feature-range-operator-space-after": "always",
"media-feature-range-operator-space-before": "always",
"media-query-list-comma-newline-after": "always-multi-line",
"media-query-list-comma-newline-before": "never-multi-line",
"media-query-list-comma-space-after": "always-single-line",
"media-query-list-comma-space-before": "never",
// "no-descending-specificity": true,
"no-duplicate-at-import-rules": true,
// "no-duplicate-selectors": true,
"no-empty-source": true,
"no-eol-whitespace": true,
"no-extra-semicolons": true,
"no-invalid-double-slash-comments": true,
"no-missing-end-of-source-newline": true,
"no-unknown-animations": true,
"number-leading-zero": "always",
"number-max-precision": [4, { ignoreUnits: ["%"] }],
// "number-no-trailing-zeros": true,
"order/order": [
[
"dollar-variables",
"at-variables",
"custom-properties",
{
type: "at-rule",
name: "extend",
},
{
type: "at-rule",
name: "include",
},
"declarations",
"rules",
"at-rules",
"less-mixins",
],
{
unspecified: "bottom",
},
],
// "order/properties-order": [
// []
// ],
// "order/properties-alphabetical-order": true,
"property-blacklist": ["grid-area", "grid-template", "grid-column", "grid-row"],
"property-case": "lower",
"property-no-unknown": true,
// "property-no-vendor-prefix": true,
/*"property-whitelist": [
"/animation$/",
"/box-shadow$/",
"/keyframes$/",
"/transform$/",
"display",
"font-size"
], */
// "rule-empty-line-before": ["always", { except: [ "after-single-line-comment" ] }],
/*"scss/at-else-closing-brace-newline-after": "always-last-in-chain",
"scss/at-else-empty-line-before": "never",
"scss/at-else-if-parentheses-space-before": "always",
"scss/at-extend-no-missing-placeholder": true,
"scss/at-function-named-arguments": "never",
"scss/at-function-parentheses-space-before": "never",
"scss/at-function-pattern": /.+/,
"scss/at-if-closing-brace-newline-after": "always-last-in-chain",
"scss/at-import-no-partial-leading-underscore": true,
"scss/at-import-partial-extension-blacklist": [
[]
],
"scss/at-import-partial-extension-whitelist": [
[]
],
"scss/at-mixin-argumentless-call-parentheses": "always",
"scss/at-mixin-named-arguments": "never",
"scss/at-mixin-parentheses-space-before": "never",
"scss/at-mixin-pattern": /.+/,
"scss/at-rule-no-unknown": [
true,
{
ignoreAtRules: []
}
],
"scss/dollar-variable-colon-newline-after": "always-multi-line",
"scss/dollar-variable-colon-space-after": "always-single-line",
"scss/dollar-variable-colon-space-before": "never",
"scss/dollar-variable-default": [
true,
{
ignore: "local"
}
],
"scss/dollar-variable-empty-line-before": "never",
"scss/dollar-variable-no-missing-interpolation": true,
"scss/dollar-variable-pattern": /.+/,
"scss/percent-placeholder-pattern": /.+/,
"scss/double-slash-comment-inline": [
"always",
{
ignore: [
"stylelint-commands"
]
}
],
"scss/double-slash-comment-whitespace-inside": "always",
"scss/declaration-nested-properties": "never",
"scss/media-feature-value-dollar-variable": "always",
"scss/operator-no-newline-after": true,
"scss/operator-no-newline-before": true,
"scss/operator-no-unspaced": true,
"scss/partial-no-import": true,
"scss/selector-no-redundant-nesting-selector": true,*/
"selector-attribute-brackets-space-inside": "never",
"selector-attribute-operator-blacklist": [],
"selector-attribute-operator-space-after": "never",
"selector-attribute-operator-space-before": "never",
"selector-attribute-operator-whitelist": ["="],
"selector-attribute-quotes": "always",
"selector-class-pattern": ".+",
//"selector-combinator-blacklist": [],
"selector-combinator-space-after": "always",
"selector-combinator-space-before": "always",
//"selector-combinator-whitelist": [],
"selector-descendant-combinator-no-non-space": true,
"selector-id-pattern": ".+",
"selector-list-comma-newline-after": "always-multi-line",
"selector-list-comma-newline-before": "never-multi-line",
"selector-list-comma-space-after": "always-single-line",
"selector-list-comma-space-before": "never",
"selector-max-attribute": 99,
"selector-max-class": 99,
"selector-max-combinators": 99,
"selector-max-compound-selectors": 99,
"selector-max-empty-lines": 1,
"selector-max-id": 1,
//"selector-max-specificity": "0,0,0",
"selector-max-type": 99,
"selector-max-universal": 1,
"selector-nested-pattern": ".+",
"selector-no-qualifying-type": [
true,
{
ignore: ["attribute", "class"],
},
],
"selector-no-vendor-prefix": true,
"selector-pseudo-class-blacklist": [],
"selector-pseudo-class-case": "lower",
"selector-pseudo-class-no-unknown": true,
"selector-pseudo-class-parentheses-space-inside": "never",
"selector-pseudo-class-whitelist": [
"active",
"after",
"before",
"checked",
"disabled",
"focus",
"hover",
"link",
"not",
"last-child",
"root",
"target",
"visited",
],
//"selector-pseudo-element-blacklist": [],
"selector-pseudo-element-case": "lower",
// "selector-pseudo-element-colon-notation": "double",
"selector-pseudo-element-no-unknown": true,
//"selector-pseudo-element-whitelist": [],
"selector-type-case": "lower",
"selector-type-no-unknown": true,
// "shorthand-property-no-redundant-values": true,
// "sh-waqar/declaration-use-variable": [
// [
// "color",
// "background-color",
// "font-family"
// ]
// ],
// "string-quotes": "double",
"time-min-milliseconds": 50,
"unit-blacklist": [],
"unit-case": "lower",
"unit-no-unknown": true,
"unit-whitelist": ["deg", "fr", "px", "rem", "ms", "s", "vw", "%"],
// "value-keyword-case": "lower",
"value-list-max-empty-lines": 0,
"value-no-vendor-prefix": true,
},
};

@ -1,91 +0,0 @@
/**
* Webpack configuration for building unit tests
*/
/* eslint-disable @typescript-eslint/no-var-requires */
var path = require("path");
var webpack = require("webpack");
module.exports = () => {
const statsConfig = {
builtAt: true,
children: false,
chunks: false,
chunkGroups: false,
chunkModules: false,
chunkOrigins: false,
colors: true,
entrypoints: true,
};
return {
plugins: [
new webpack.DefinePlugin({
"process.env.NODE_ENV": '"development"',
}),
new webpack.ProvidePlugin({
// Automtically detect jQuery and $ as free var in modules
// and inject the jquery library
// This is required by many jquery plugins
// http://stackoverflow.com/questions/29080148/expose-jquery-to-real-window-object-with-webpack
jquery: "jquery",
jQuery: "jquery",
$: "jquery",
}),
],
entry: "./test/index.js",
target: "web",
devtool: "source-map",
output: {
path: path.resolve(__dirname, "./"),
filename: "test/test.bundle.js",
},
module: {
rules: [
{
test: /\.tsx?$/,
loader: "ts-loader",
exclude: /node_modules/,
},
{
test: /\.(jsx)$/,
exclude: /node_modules/,
use: {
loader: "babel-loader",
},
},
{
test: /\.s?css$/,
loader: "null-loader",
},
],
},
optimization: {
removeAvailableModules: true,
removeEmptyChunks: true,
mergeDuplicateChunks: true,
flagIncludedChunks: true,
occurrenceOrder: true,
sideEffects: true,
providedExports: true,
usedExports: true,
concatenateModules: false,
namedModules: false,
namedChunks: false,
minimize: false,
portableRecords: true,
splitChunks: {
cacheGroups: {
vendor: {
test: /[\\/]node_modules[\\/]/,
name: `tests/vendor`,
chunks: "all",
},
},
},
},
resolve: {
extensions: [".tsx", ".ts", ".js", ".jsx"],
},
stats: statsConfig,
};
};