Merge pull request #3170 from nickofolas/feature/grafting

[Feature] Grafting
This commit is contained in:
hydroflame 2022-03-29 13:25:41 -04:00 committed by GitHub
commit fb1bce579f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
37 changed files with 794 additions and 642 deletions

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

@ -92,18 +92,19 @@ 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. Memory is a persistent stat, meaning it never gets reset back to 1.
The maximum possible value for a sleeve's memory is 100. The maximum possible value for a sleeve's memory is 100.
Re-sleeving Grafting
^^^^^^^^^^^ ^^^^^^^^
Re-sleeving is the process of digitizing and transferring your consciousness into a Grafting is an experimental process through which you can obtain the benefits of
new human body, or "sleeve". When you re-sleeve into a new body, your stat experience Augmentations, without needing to install them.
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 In order to graft, you must first purchase a blueprint for and craft the Augmentation.
New Tokyo. Once you purchase a body to re-sleeve into, the effects will take This can be done at VitaLife in New Tokyo, where you'll find a shady researcher with
place immediately. questionable connections. Once you purchase a blueprint, you will start crafting the
Augmentation, and it will be grafted to your body once complete.
Note that resleeving **REMOVES** all of your currently-installed Augmentations, Be warned, some who have tested grafting have reported an unidentified malware. Dubbed
and replaces them with the ones provided by the purchased sleeve. However, "Entropy", this virus seems to grow in potency as more Augmentations are grafted,
Augmentations that are purchased but not installed will **not** be removed. If you have purchased causing unpredictable affects to the victim.
an Augmentation and then re-sleeve into a body which already has that Augmentation,
it will be removed since you cannot have duplicate Augmentations. Note that when crafting an Augmentation, cancelling will **not** save your progress,
and the money spent will **not** be returned.

@ -53,6 +53,7 @@ List of all Source-Files
+-------------------------------------+--------------------------------------------------------------------------------------------------------------------------------------------------------------------------+ +-------------------------------------+--------------------------------------------------------------------------------------------------------------------------------------------------------------------------+
|| BitNode-10: Digital Carbon || * Each level of this grants a Duplicate Sleeve. | || 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. | || || * 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 grafting 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 | || 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). | || || company by 1% per favor (rather than just the reputation gain). |

@ -17,6 +17,10 @@ import { OwnedAugmentationsOrderSetting } from "../../Settings/SettingEnums";
import Button from "@mui/material/Button"; import Button from "@mui/material/Button";
import Tooltip from "@mui/material/Tooltip"; import Tooltip from "@mui/material/Tooltip";
import List from "@mui/material/List"; 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 { export function InstalledAugmentations(): React.ReactElement {
const setRerender = useState(true)[1]; const setRerender = useState(true)[1];
@ -55,6 +59,38 @@ export function InstalledAugmentations(): React.ReactElement {
</Button> </Button>
</Tooltip> </Tooltip>
<List dense> <List dense>
{player.entropyStacks > 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 ({player.entropyStacks} accumulated)
</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.entropyStacks) * 100, 3)}% (multiplicative)
</Typography>
</Box>
</Collapse>
</Box>
);
})()}
{sourceAugs.map((e) => { {sourceAugs.map((e) => {
const aug = Augmentations[e.name]; const aug = Augmentations[e.name];

@ -425,7 +425,7 @@ BitNodes["BitNode10"] = new BitNode(
This BitNode unlocks Sleeve technology. Sleeve technology allows you to: This BitNode unlocks Sleeve technology. Sleeve technology allows you to:
<br /> <br />
<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 /> <br />
2. Duplicate Sleeves: Duplicate your consciousness into Synthoids, allowing you to perform different tasks 2. Duplicate Sleeves: Duplicate your consciousness into Synthoids, allowing you to perform different tasks
synchronously synchronously

@ -41,6 +41,7 @@ export const CONSTANTS: {
IntelligenceInfiltrationWeight: number; IntelligenceInfiltrationWeight: number;
IntelligenceCrimeBaseExpGain: number; IntelligenceCrimeBaseExpGain: number;
IntelligenceProgramBaseExpGain: number; IntelligenceProgramBaseExpGain: number;
IntelligenceCraftBaseExpGain: number;
IntelligenceTerminalHackBaseExpGain: number; IntelligenceTerminalHackBaseExpGain: number;
IntelligenceSingFnBaseExpGain: number; IntelligenceSingFnBaseExpGain: number;
IntelligenceClassBaseExpGain: number; IntelligenceClassBaseExpGain: number;
@ -71,6 +72,7 @@ export const CONSTANTS: {
WorkTypeCreateProgram: string; WorkTypeCreateProgram: string;
WorkTypeStudyClass: string; WorkTypeStudyClass: string;
WorkTypeCrime: string; WorkTypeCrime: string;
WorkTypeCraftAugmentation: string;
ClassStudyComputerScience: string; ClassStudyComputerScience: string;
ClassDataStructures: string; ClassDataStructures: string;
ClassNetworks: string; ClassNetworks: string;
@ -108,6 +110,9 @@ export const CONSTANTS: {
CodingContractBaseFactionRepGain: number; CodingContractBaseFactionRepGain: number;
CodingContractBaseCompanyRepGain: number; CodingContractBaseCompanyRepGain: number;
CodingContractBaseMoneyGain: number; CodingContractBaseMoneyGain: number;
AugmentationCraftingCostMult: number;
AugmentationCraftingTimeBase: number;
EntropyEffect: number;
TotalNumBitNodes: number; TotalNumBitNodes: number;
LatestUpdate: string; LatestUpdate: string;
} = { } = {
@ -180,6 +185,7 @@ export const CONSTANTS: {
IntelligenceInfiltrationWeight: 0.1, // Weight for how much int affects infiltration success rates IntelligenceInfiltrationWeight: 0.1, // Weight for how much int affects infiltration success rates
IntelligenceCrimeBaseExpGain: 0.05, IntelligenceCrimeBaseExpGain: 0.05,
IntelligenceProgramBaseExpGain: 0.1, // Program required hack level divided by this to determine int exp gain IntelligenceProgramBaseExpGain: 0.1, // Program required hack level divided by this to determine int exp gain
IntelligenceCraftBaseExpGain: 0.05,
IntelligenceTerminalHackBaseExpGain: 200, // Hacking exp divided by this to determine int exp gain IntelligenceTerminalHackBaseExpGain: 200, // Hacking exp divided by this to determine int exp gain
IntelligenceSingFnBaseExpGain: 1.5, IntelligenceSingFnBaseExpGain: 1.5,
IntelligenceClassBaseExpGain: 0.01, IntelligenceClassBaseExpGain: 0.01,
@ -224,6 +230,7 @@ export const CONSTANTS: {
WorkTypeCreateProgram: "Working on Create a Program", WorkTypeCreateProgram: "Working on Create a Program",
WorkTypeStudyClass: "Studying or Taking a class at university", WorkTypeStudyClass: "Studying or Taking a class at university",
WorkTypeCrime: "Committing a crime", WorkTypeCrime: "Committing a crime",
WorkTypeCraftAugmentation: "Crafting an Augmentation",
ClassStudyComputerScience: "studying Computer Science", ClassStudyComputerScience: "studying Computer Science",
ClassDataStructures: "taking a Data Structures course", ClassDataStructures: "taking a Data Structures course",
@ -269,6 +276,13 @@ export const CONSTANTS: {
CodingContractBaseCompanyRepGain: 4000, CodingContractBaseCompanyRepGain: 4000,
CodingContractBaseMoneyGain: 75e6, CodingContractBaseMoneyGain: 75e6,
// Augmentation crafting multipliers
AugmentationCraftingCostMult: 1.2,
AugmentationCraftingTimeBase: 3600000,
// Value raised to the number of entropy stacks, then multiplied to player multipliers
EntropyEffect: 0.99,
// BitNode/Source-File related stuff // BitNode/Source-File related stuff
TotalNumBitNodes: 24, TotalNumBitNodes: 24,

@ -23,6 +23,7 @@ import { Sleeves } from "./DevMenu/ui/Sleeves";
import { Stanek } from "./DevMenu/ui/Stanek"; import { Stanek } from "./DevMenu/ui/Stanek";
import { TimeSkip } from "./DevMenu/ui/TimeSkip"; import { TimeSkip } from "./DevMenu/ui/TimeSkip";
import { Achievements } from "./DevMenu/ui/Achievements"; import { Achievements } from "./DevMenu/ui/Achievements";
import { Entropy } from "./DevMenu/ui/Entropy";
import Typography from "@mui/material/Typography"; import Typography from "@mui/material/Typography";
import { Exploit } from "./Exploits/Exploit"; import { Exploit } from "./Exploits/Exploit";
@ -63,6 +64,7 @@ export function DevMenuRoot(props: IProps): React.ReactElement {
<TimeSkip player={props.player} engine={props.engine} /> <TimeSkip player={props.player} engine={props.engine} />
<Achievements 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.entropyStacks += num;
props.player.applyEntropy(props.player.entropyStacks);
}}
subtract={num => {
props.player.entropyStacks -= num;
props.player.applyEntropy(props.player.entropyStacks);
}}
tons={() => {
props.player.entropyStacks += 1e12;
props.player.applyEntropy(props.player.entropyStacks);
}}
reset={() => {
props.player.entropyStacks = 0;
props.player.applyEntropy(props.player.entropyStacks);
}}
/>
</AccordionDetails>
</Accordion>
);
}

@ -45,12 +45,17 @@ export function AugmentationsPage(props: IProps): React.ReactElement {
if (isPlayersGang) { if (isPlayersGang) {
const augs: string[] = []; const augs: string[] = [];
for (const augName of Object.keys(Augmentations)) { for (const augName of Object.keys(Augmentations)) {
if (augName === AugmentationNames.NeuroFluxGovernor) continue;
if (augName === AugmentationNames.TheRedPill && player.bitNodeN !== 2) continue;
const aug = Augmentations[augName]; const aug = Augmentations[augName];
if (!aug.isSpecial) { if (
augs.push(augName); 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 && !props.faction.augmentations.includes(augName) && player.bitNodeN !== 2)
)
continue;
augs.push(augName);
} }
return augs; return augs;

@ -1,14 +1,6 @@
import React, { useEffect, useState } from "react"; import React, { useEffect, useState } from "react";
import { import { Box, Button, Container, Paper, TableBody, TableRow, Typography } from "@mui/material";
Box,
Button,
Container,
Paper,
TableBody,
TableRow,
Typography
} from "@mui/material";
import { Augmentations } from "../../Augmentation/Augmentations"; import { Augmentations } from "../../Augmentation/Augmentations";
import { AugmentationNames } from "../../Augmentation/data/AugmentationNames"; import { AugmentationNames } from "../../Augmentation/data/AugmentationNames";
@ -65,26 +57,28 @@ export function FactionsRoot(props: IProps): React.ReactElement {
if (isPlayersGang) { if (isPlayersGang) {
for (const augName of Object.keys(Augmentations)) { for (const augName of Object.keys(Augmentations)) {
const aug = Augmentations[augName];
if ( if (
augName === AugmentationNames.NeuroFluxGovernor || augName === AugmentationNames.NeuroFluxGovernor ||
augName === AugmentationNames.TheRedPill && player.bitNodeN !== 2 || (augName === AugmentationNames.TheRedPill && player.bitNodeN !== 2) ||
Augmentations[augName].isSpecial // Special augs (i.e. Bladeburner augs)
) continue; aug.isSpecial ||
augs.push(augName) // Exclusive augs (i.e. QLink)
(aug.factions.length <= 1 && !faction.augmentations.includes(augName) && player.bitNodeN !== 2)
)
continue;
augs.push(augName);
} }
} else { } else {
augs = faction.augmentations.slice(); augs = faction.augmentations.slice();
} }
return augs.filter( return augs.filter((augmentation: string) => !player.hasAugmentation(augmentation)).length;
(augmentation: string) => !player.hasAugmentation(augmentation) };
).length;
}
const allFactions = Object.values(FactionNames).map(faction => faction as string) const allFactions = Object.values(FactionNames).map((faction) => faction as string);
const allJoinedFactions = props.player.factions.slice(0); const allJoinedFactions = props.player.factions.slice(0);
allJoinedFactions.sort((a, b) => allJoinedFactions.sort((a, b) => allFactions.indexOf(a) - allFactions.indexOf(b));
allFactions.indexOf(a) - allFactions.indexOf(b));
return ( return (
<Container disableGutters maxWidth="md" sx={{ mx: 0, mb: 10 }}> <Container disableGutters maxWidth="md" sx={{ mx: 0, mb: 10 }}>
@ -116,7 +110,7 @@ export function FactionsRoot(props: IProps): React.ReactElement {
</TableCell> </TableCell>
<TableCell align="right"> <TableCell align="right">
<Box ml={1} mb={1}> <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)} Augmentations Left: {getAugsLeft(Factions[faction], props.player)}
</Button> </Button>
</Box> </Box>

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

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

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

@ -0,0 +1,85 @@
import { CityName } from "../Locations/data/CityNames";
import { Augmentations } from "../Augmentation/Augmentations";
import { getRamCost } from "../Netscript/RamCostGenerator";
import { WorkerScript } from "../Netscript/WorkerScript";
import { CraftableAugmentation } from "../PersonObjects/Grafting/CraftableAugmentation";
import { getAvailableAugs } from "../PersonObjects/Grafting/ui/GraftingRoot";
import { IPlayer } from "../PersonObjects/IPlayer";
import { Grafting as IGrafting } from "../ScriptEditor/NetscriptDefinitions";
import { SourceFileFlags } from "../SourceFile/SourceFileFlags";
import { Router } from "../ui/GameRoot";
import { INetscriptHelper } from "./INetscriptHelper";
export function NetscriptGrafting(player: IPlayer, workerScript: WorkerScript, helper: INetscriptHelper): IGrafting {
const checkGraftingAPIAccess = (func: any): 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 {
getAugmentationCraftPrice: (augName: string): number => {
helper.updateDynamicRam("getAugmentationCraftPrice", getRamCost(player, "grafting", "getAugmentationCraftPrice"));
checkGraftingAPIAccess("getAugmentationCraftPrice");
if (!Augmentations.hasOwnProperty(augName)) {
throw helper.makeRuntimeErrorMsg("grafting.getAugmentationCraftPrice", `Invalid aug: ${augName}`);
}
const craftableAug = new CraftableAugmentation(Augmentations[augName]);
return craftableAug.cost;
},
getAugmentationCraftTime: (augName: string): number => {
helper.updateDynamicRam("getAugmentationCraftTime", getRamCost(player, "grafting", "getAugmentationCraftTime"));
checkGraftingAPIAccess("getAugmentationCraftTime");
if (!Augmentations.hasOwnProperty(augName)) {
throw helper.makeRuntimeErrorMsg("grafting.getAugmentationCraftTime", `Invalid aug: ${augName}`);
}
const craftableAug = new CraftableAugmentation(Augmentations[augName]);
return craftableAug.time;
},
craftAugmentation: (augName: string, focus = true): boolean => {
helper.updateDynamicRam("craftAugmentation", getRamCost(player, "grafting", "craftAugmentation"));
checkGraftingAPIAccess("craftAugmentation");
if (player.city !== CityName.NewTokyo) {
throw helper.makeRuntimeErrorMsg(
"grafting.craftAugmentation",
"You must be in New Tokyo to begin crafting an Augmentation.",
);
}
if (!getAvailableAugs(player).includes(augName)) {
workerScript.log("grafting.craftAugmentation", () => `Invalid aug: ${augName}`);
return false;
}
const wasFocusing = player.focus;
if (player.isWorking) {
const txt = player.singularityStopWork();
workerScript.log("craftAugmentation", () => txt);
}
const craftableAug = new CraftableAugmentation(Augmentations[augName]);
if (player.money < craftableAug.cost) {
workerScript.log("grafting.craftAugmentation", () => `You don't have enough money to craft ${augName}`);
return false;
}
player.loseMoney(craftableAug.cost, "augmentations");
player.startCraftAugmentationWork(augName, craftableAug.time);
if (focus) {
player.startFocusing();
Router.toWork();
} else if (wasFocusing) {
player.stopFocusing();
Router.toTerminal();
}
workerScript.log("grafting.craftAugmentation", () => `Began crafting Augmentation ${augName}.`);
return true;
},
};
}

@ -167,12 +167,17 @@ export function NetscriptSingularity(
let augs = []; let augs = [];
if (player.hasGangWith(faction)) { if (player.hasGangWith(faction)) {
for (const augName of Object.keys(Augmentations)) { for (const augName of Object.keys(Augmentations)) {
if (augName === AugmentationNames.NeuroFluxGovernor) continue; const aug = Augmentations[augName];
if (augName === AugmentationNames.TheRedPill && player.bitNodeN !== 2) continue; if (
const tempAug = Augmentations[augName]; augName === AugmentationNames.NeuroFluxGovernor ||
if (!tempAug.isSpecial) { (augName === AugmentationNames.TheRedPill && player.bitNodeN !== 2) ||
augs.push(augName); // 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 { } else {
augs = fac.augmentations; augs = fac.augmentations;

@ -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 CraftableAugmentation {
// The augmentation that this craftable corresponds to
augmentation: Augmentation;
constructor(augmentation: Augmentation) {
this.augmentation = augmentation;
}
get cost(): number {
return this.augmentation.startingCost * CONSTANTS.AugmentationCraftingCostMult;
}
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.AugmentationCraftingTimeBase * mult + CONSTANTS.MillisecondsPerHalfHour;
}
}

@ -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,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 { CraftableAugmentation } from "../CraftableAugmentation";
const CraftableAugmentations: IMap<CraftableAugmentation> = {};
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 craftableAug = new CraftableAugmentation(aug);
CraftableAugmentations[name] = craftableAug;
}
const [selectedAug, setSelectedAug] = useState(getAvailableAugs(player)[0]);
const [craftOpen, setCraftOpen] = 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">Craft 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={() => setCraftOpen(true)}
sx={{ width: "100%" }}
disabled={player.money < CraftableAugmentations[selectedAug].cost}
>
Craft Augmentation (
<Typography color={Settings.theme.money}>
<Money money={CraftableAugmentations[selectedAug].cost} player={player} />
</Typography>
)
</Button>
<ConfirmationModal
open={craftOpen}
onClose={() => setCraftOpen(false)}
onConfirm={() => {
const craftableAug = CraftableAugmentations[selectedAug];
player.loseMoney(craftableAug.cost, "augmentations");
player.startCraftAugmentationWork(selectedAug, craftableAug.time);
player.startFocusing();
router.toWork();
}}
confirmationText={
<>
Cancelling crafting will <b>not</b> save crafting 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 Craft:</b>{" "}
{convertTimeMsToTimeElapsedString(
CraftableAugmentations[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 Accumulation</Typography>
<Paper sx={{ my: 1, p: 1, width: "fit-content" }}>
<Typography>
<b>Accumulated Entropy:</b> {player.entropyStacks}
<br />
<b>All multipliers decreased by:</b>{" "}
{formatNumber((1 - CONSTANTS.EntropyEffect ** player.entropyStacks) * 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 * Used because at the time of implementation, the PlayerObject
* cant be converted to TypeScript. * cant be converted to TypeScript.
*/ */
import { Resleeve } from "./Resleeving/Resleeve";
import { Sleeve } from "./Sleeve/Sleeve"; import { Sleeve } from "./Sleeve/Sleeve";
import { IMap } from "../types"; import { IMap } from "../types";
@ -65,7 +64,6 @@ export interface IPlayer {
playtimeSinceLastBitnode: number; playtimeSinceLastBitnode: number;
purchasedServers: any[]; purchasedServers: any[];
queuedAugmentations: IPlayerOwnedAugmentation[]; queuedAugmentations: IPlayerOwnedAugmentation[];
resleeves: Resleeve[];
scriptProdSinceLastAug: number; scriptProdSinceLastAug: number;
sleeves: Sleeve[]; sleeves: Sleeve[];
sleevesFromCovenant: number; sleevesFromCovenant: number;
@ -130,6 +128,8 @@ export interface IPlayer {
factionWorkType: string; factionWorkType: string;
createProgramName: string; createProgramName: string;
timeWorkedCreateProgram: number; timeWorkedCreateProgram: number;
craftAugmentationName: string;
timeWorkedCraftAugmentation: number;
crimeType: string; crimeType: string;
committingCrimeThruSingFn: boolean; committingCrimeThruSingFn: boolean;
singFnCrimeWorkerScript: WorkerScript | null; singFnCrimeWorkerScript: WorkerScript | null;
@ -160,6 +160,8 @@ export interface IPlayer {
workChaExpGainRate: number; workChaExpGainRate: number;
workMoneyLossRate: number; workMoneyLossRate: number;
entropyStacks: number;
// Methods // Methods
work(numCycles: number): boolean; work(numCycles: number): boolean;
workPartTime(numCycles: number): boolean; workPartTime(numCycles: number): boolean;
@ -181,7 +183,7 @@ export interface IPlayer {
canAccessBladeburner(): boolean; canAccessBladeburner(): boolean;
canAccessCorporation(): boolean; canAccessCorporation(): boolean;
canAccessGang(): boolean; canAccessGang(): boolean;
canAccessResleeving(): boolean; canAccessGrafting(): boolean;
canAfford(cost: number): boolean; canAfford(cost: number): boolean;
gainHackingExp(exp: number): void; gainHackingExp(exp: number): void;
gainStrengthExp(exp: number): void; gainStrengthExp(exp: number): void;
@ -286,4 +288,8 @@ export interface IPlayer {
setMult(name: string, mult: number): void; setMult(name: string, mult: number): void;
canAccessCotMG(): boolean; canAccessCotMG(): boolean;
sourceFileLvl(n: number): number; sourceFileLvl(n: number): number;
startCraftAugmentationWork(augmentationName: string, time: number): void;
craftAugmentationWork(numCycles: number): boolean;
finishCraftAugmentationWork(cancelled: boolean): string;
applyEntropy(stacks?: number): void;
} }

@ -6,7 +6,6 @@ import * as generalMethods from "./PlayerObjectGeneralMethods";
import * as serverMethods from "./PlayerObjectServerMethods"; import * as serverMethods from "./PlayerObjectServerMethods";
import { IMap } from "../../types"; import { IMap } from "../../types";
import { Resleeve } from "../Resleeving/Resleeve";
import { Sleeve } from "../Sleeve/Sleeve"; import { Sleeve } from "../Sleeve/Sleeve";
import { IPlayerOwnedSourceFile } from "../../SourceFile/PlayerOwnedSourceFile"; import { IPlayerOwnedSourceFile } from "../../SourceFile/PlayerOwnedSourceFile";
import { Exploit } from "../../Exploits/Exploit"; import { Exploit } from "../../Exploits/Exploit";
@ -72,7 +71,6 @@ export class PlayerObject implements IPlayer {
playtimeSinceLastBitnode: number; playtimeSinceLastBitnode: number;
purchasedServers: any[]; purchasedServers: any[];
queuedAugmentations: IPlayerOwnedAugmentation[]; queuedAugmentations: IPlayerOwnedAugmentation[];
resleeves: Resleeve[];
scriptProdSinceLastAug: number; scriptProdSinceLastAug: number;
sleeves: Sleeve[]; sleeves: Sleeve[];
sleevesFromCovenant: number; sleevesFromCovenant: number;
@ -139,6 +137,8 @@ export class PlayerObject implements IPlayer {
factionWorkType: string; factionWorkType: string;
createProgramName: string; createProgramName: string;
timeWorkedCreateProgram: number; timeWorkedCreateProgram: number;
craftAugmentationName: string;
timeWorkedCraftAugmentation: number;
crimeType: string; crimeType: string;
committingCrimeThruSingFn: boolean; committingCrimeThruSingFn: boolean;
singFnCrimeWorkerScript: WorkerScript | null; singFnCrimeWorkerScript: WorkerScript | null;
@ -169,6 +169,8 @@ export class PlayerObject implements IPlayer {
workChaExpGainRate: number; workChaExpGainRate: number;
workMoneyLossRate: number; workMoneyLossRate: number;
entropyStacks: number;
// Methods // Methods
work: (numCycles: number) => boolean; work: (numCycles: number) => boolean;
workPartTime: (numCycles: number) => boolean; workPartTime: (numCycles: number) => boolean;
@ -190,7 +192,7 @@ export class PlayerObject implements IPlayer {
canAccessBladeburner: () => boolean; canAccessBladeburner: () => boolean;
canAccessCorporation: () => boolean; canAccessCorporation: () => boolean;
canAccessGang: () => boolean; canAccessGang: () => boolean;
canAccessResleeving: () => boolean; canAccessGrafting: () => boolean;
canAfford: (cost: number) => boolean; canAfford: (cost: number) => boolean;
gainHackingExp: (exp: number) => void; gainHackingExp: (exp: number) => void;
gainStrengthExp: (exp: number) => void; gainStrengthExp: (exp: number) => void;
@ -296,6 +298,10 @@ export class PlayerObject implements IPlayer {
setMult: (name: string, mult: number) => void; setMult: (name: string, mult: number) => void;
canAccessCotMG: () => boolean; canAccessCotMG: () => boolean;
sourceFileLvl: (n: number) => number; sourceFileLvl: (n: number) => number;
startCraftAugmentationWork: (augmentationName: string, time: number) => void;
craftAugmentationWork: (numCycles: number) => boolean;
finishCraftAugmentationWork: (cancelled: boolean) => string;
applyEntropy: (stacks?: number) => void;
constructor() { constructor() {
//Skills and stats //Skills and stats
@ -419,6 +425,9 @@ export class PlayerObject implements IPlayer {
this.createProgramName = ""; this.createProgramName = "";
this.createProgramReqLvl = 0; this.createProgramReqLvl = 0;
this.craftAugmentationName = "";
this.timeWorkedCraftAugmentation = 0;
this.className = ""; this.className = "";
this.crimeType = ""; this.crimeType = "";
@ -457,11 +466,12 @@ export class PlayerObject implements IPlayer {
// Sleeves & Re-sleeving // Sleeves & Re-sleeving
this.sleeves = []; this.sleeves = [];
this.resleeves = [];
this.sleevesFromCovenant = 0; // # of Duplicate sleeves purchased from the covenan; this.sleevesFromCovenant = 0; // # of Duplicate sleeves purchased from the covenan;
//bitnode //bitnode
this.bitNodeN = 1; this.bitNodeN = 1;
this.entropyStacks = 0;
//Used to store the last update time. //Used to store the last update time.
this.lastUpdate = 0; this.lastUpdate = 0;
this.lastSave = 0; this.lastSave = 0;
@ -541,6 +551,9 @@ export class PlayerObject implements IPlayer {
this.startCreateProgramWork = generalMethods.startCreateProgramWork; this.startCreateProgramWork = generalMethods.startCreateProgramWork;
this.createProgramWork = generalMethods.createProgramWork; this.createProgramWork = generalMethods.createProgramWork;
this.finishCreateProgramWork = generalMethods.finishCreateProgramWork; this.finishCreateProgramWork = generalMethods.finishCreateProgramWork;
this.startCraftAugmentationWork = generalMethods.startCraftAugmentationWork;
this.craftAugmentationWork = generalMethods.craftAugmentationWork;
this.finishCraftAugmentationWork = generalMethods.finishCraftAugmentationWork;
this.startClass = generalMethods.startClass; this.startClass = generalMethods.startClass;
this.takeClass = generalMethods.takeClass; this.takeClass = generalMethods.takeClass;
this.finishClass = generalMethods.finishClass; this.finishClass = generalMethods.finishClass;
@ -577,7 +590,7 @@ export class PlayerObject implements IPlayer {
this.gainCodingContractReward = generalMethods.gainCodingContractReward; this.gainCodingContractReward = generalMethods.gainCodingContractReward;
this.travel = generalMethods.travel; this.travel = generalMethods.travel;
this.gotoLocation = generalMethods.gotoLocation; this.gotoLocation = generalMethods.gotoLocation;
this.canAccessResleeving = generalMethods.canAccessResleeving; this.canAccessGrafting = generalMethods.canAccessGrafting;
this.giveExploit = generalMethods.giveExploit; this.giveExploit = generalMethods.giveExploit;
this.giveAchievement = generalMethods.giveAchievement; this.giveAchievement = generalMethods.giveAchievement;
this.getIntelligenceBonus = generalMethods.getIntelligenceBonus; this.getIntelligenceBonus = generalMethods.getIntelligenceBonus;
@ -611,6 +624,8 @@ export class PlayerObject implements IPlayer {
this.canAccessCotMG = generalMethods.canAccessCotMG; this.canAccessCotMG = generalMethods.canAccessCotMG;
this.sourceFileLvl = generalMethods.sourceFileLvl; this.sourceFileLvl = generalMethods.sourceFileLvl;
this.applyEntropy = augmentationMethods.applyEntropy;
} }
/** /**

@ -5,6 +5,8 @@ import { IPlayer } from "../IPlayer";
import { Augmentation } from "../../Augmentation/Augmentation"; import { Augmentation } from "../../Augmentation/Augmentation";
import { calculateEntropy } from "../Grafting/EntropyAccumulation";
export function hasAugmentation(this: IPlayer, aug: string | Augmentation, installed = false): boolean { export function hasAugmentation(this: IPlayer, aug: string | Augmentation, installed = false): boolean {
const augName: string = aug instanceof Augmentation ? aug.name : aug; const augName: string = aug instanceof Augmentation ? aug.name : aug;
@ -24,3 +26,14 @@ export function hasAugmentation(this: IPlayer, aug: string | Augmentation, insta
return false; 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.queuedAugmentations = [];
this.resleeves = [];
const numSleeves = Math.min(3, SourceFileFlags[10] + (this.bitNodeN === 10 ? 1 : 0)) + this.sleevesFromCovenant; const numSleeves = Math.min(3, SourceFileFlags[10] + (this.bitNodeN === 10 ? 1 : 0)) + this.sleevesFromCovenant;
if (this.sleeves.length > numSleeves) this.sleeves.length = numSleeves; if (this.sleeves.length > numSleeves) this.sleeves.length = numSleeves;
for (let i = this.sleeves.length; i < numSleeves; i++) { 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 { export function prestigeSourceFile(this: IPlayer): void {
this.entropyStacks = 0;
this.prestigeAugmentation(); this.prestigeAugmentation();
this.karma = 0; this.karma = 0;
// Duplicate sleeves are reset to level 1 every Bit Node (but the number of sleeves you have persists) // 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.timeWorked = 0;
this.timeWorkedCreateProgram = 0; this.timeWorkedCreateProgram = 0;
this.timeWorkedCraftAugmentation = 0;
this.currentWorkFactionName = ""; this.currentWorkFactionName = "";
this.currentWorkFactionDescription = ""; this.currentWorkFactionDescription = "";
this.createProgramName = ""; this.createProgramName = "";
this.craftAugmentationName = "";
this.className = ""; this.className = "";
this.workType = ""; this.workType = "";
} }
@ -609,6 +610,10 @@ export function process(this: IPlayer, router: IRouter, numCycles = 1): void {
if (this.workPartTime(numCycles)) { if (this.workPartTime(numCycles)) {
router.toCity(); router.toCity();
} }
} else if (this.workType === CONSTANTS.WorkTypeCraftAugmentation) {
if (this.craftAugmentationWork(numCycles)) {
router.toGrafting();
}
} else if (this.work(numCycles)) { } else if (this.work(numCycles)) {
router.toCity(); router.toCity();
} }
@ -1329,6 +1334,61 @@ export function finishCreateProgramWork(this: IPlayer, cancelled: boolean): stri
this.resetWorkStatus(); this.resetWorkStatus();
return "You've finished creating " + programName + "! The new program can be found on your home computer."; return "You've finished creating " + programName + "! The new program can be found on your home computer.";
} }
export function startCraftAugmentationWork(
this: IPlayer,
augmentationName: string,
time: number,
): void {
this.resetWorkStatus()
this.isWorking = true;
this.workType = CONSTANTS.WorkTypeCraftAugmentation;
this.timeNeededToCompleteWork = time;
this.craftAugmentationName = 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.timeWorkedCraftAugmentation += CONSTANTS._idleSpeed * numCycles * skillMult;
if (this.timeWorkedCraftAugmentation >= this.timeNeededToCompleteWork) {
this.finishCraftAugmentationWork(false);
return true;
}
return false;
}
export function finishCraftAugmentationWork(this: IPlayer, cancelled: boolean): string {
const augName = this.craftAugmentationName;
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.entropyStacks += 1;
this.applyEntropy(this.entropyStacks);
} else {
dialogBoxCreate(`You cancelled the crafting of ${augName}.<br>Your money was not returned to you.`)
}
// Intelligence gain
if (!cancelled) {
this.gainIntelligenceExp((CONSTANTS.IntelligenceCraftBaseExpGain * this.timeWorked) / 10000);
}
this.isWorking = false;
this.resetWorkStatus();
return `Crafting of ${augName} has ended.`
}
/* Studying/Taking Classes */ /* Studying/Taking Classes */
export function startClass(this: IPlayer, costMult: number, expMult: number, className: string): void { export function startClass(this: IPlayer, costMult: number, expMult: number, className: string): void {
this.resetWorkStatus(); this.resetWorkStatus();
@ -1507,20 +1567,20 @@ export function finishCrime(this: IPlayer, cancelled: boolean): string {
if (ws.disableLogs.ALL == null && ws.disableLogs.commitCrime == null) { if (ws.disableLogs.ALL == null && ws.disableLogs.commitCrime == null) {
ws.scriptRef.log( ws.scriptRef.log(
"SUCCESS: Crime successful! Gained " + "SUCCESS: Crime successful! Gained " +
numeralWrapper.formatMoney(this.workMoneyGained) + numeralWrapper.formatMoney(this.workMoneyGained) +
", " + ", " +
numeralWrapper.formatExp(this.workHackExpGained) + numeralWrapper.formatExp(this.workHackExpGained) +
" hack exp, " + " hack exp, " +
numeralWrapper.formatExp(this.workStrExpGained) + numeralWrapper.formatExp(this.workStrExpGained) +
" str exp, " + " str exp, " +
numeralWrapper.formatExp(this.workDefExpGained) + numeralWrapper.formatExp(this.workDefExpGained) +
" def exp, " + " def exp, " +
numeralWrapper.formatExp(this.workDexExpGained) + numeralWrapper.formatExp(this.workDexExpGained) +
" dex exp, " + " dex exp, " +
numeralWrapper.formatExp(this.workAgiExpGained) + numeralWrapper.formatExp(this.workAgiExpGained) +
" agi exp, " + " agi exp, " +
numeralWrapper.formatExp(this.workChaExpGained) + numeralWrapper.formatExp(this.workChaExpGained) +
" cha exp.", " cha exp.",
); );
} }
} else { } else {
@ -1559,18 +1619,18 @@ export function finishCrime(this: IPlayer, cancelled: boolean): string {
if (ws.disableLogs.ALL == null && ws.disableLogs.commitCrime == null) { if (ws.disableLogs.ALL == null && ws.disableLogs.commitCrime == null) {
ws.scriptRef.log( ws.scriptRef.log(
"FAIL: Crime failed! Gained " + "FAIL: Crime failed! Gained " +
numeralWrapper.formatExp(this.workHackExpGained) + numeralWrapper.formatExp(this.workHackExpGained) +
" hack exp, " + " hack exp, " +
numeralWrapper.formatExp(this.workStrExpGained) + numeralWrapper.formatExp(this.workStrExpGained) +
" str exp, " + " str exp, " +
numeralWrapper.formatExp(this.workDefExpGained) + numeralWrapper.formatExp(this.workDefExpGained) +
" def exp, " + " def exp, " +
numeralWrapper.formatExp(this.workDexExpGained) + numeralWrapper.formatExp(this.workDexExpGained) +
" dex exp, " + " dex exp, " +
numeralWrapper.formatExp(this.workAgiExpGained) + numeralWrapper.formatExp(this.workAgiExpGained) +
" agi exp, " + " agi exp, " +
numeralWrapper.formatExp(this.workChaExpGained) + numeralWrapper.formatExp(this.workChaExpGained) +
" cha exp.", " cha exp.",
); );
} }
} else { } else {
@ -2640,7 +2700,7 @@ export function gotoLocation(this: IPlayer, to: LocationName): boolean {
return true; return true;
} }
export function canAccessResleeving(this: IPlayer): boolean { export function canAccessGrafting(this: IPlayer): boolean {
return this.bitNodeN === 10 || SourceFileFlags[10] > 0; 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 // Messages
initMessages(); initMessages();
// Apply entropy from grafting
Player.applyEntropy(Player.entropyStacks);
// Gang // Gang
const gang = Player.gang; const gang = Player.gang;
if (Player.inGang() && gang !== null) { if (Player.inGang() && gang !== null) {

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

@ -95,6 +95,7 @@ interface Player {
tor: boolean; tor: boolean;
hasCorporation: boolean; hasCorporation: boolean;
inBladeburner: boolean; inBladeburner: boolean;
entropyStacks: number;
} }
/** /**
@ -3719,6 +3720,43 @@ export interface Sleeve {
purchaseSleeveAug(sleeveNumber: number, augName: string): boolean; purchaseSleeveAug(sleeveNumber: number, augName: string): boolean;
} }
export interface Grafting {
/**
* Retrieve the crafting 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 craft the named augmentation.
* @throws Will error if an invalid Augmentation name is provided.
*/
getAugmentationCraftPrice(augName: string): number;
/**
* Retrieves the time required to craft an aug.
* @remarks
* RAM cost: 3.75 GB
*
* @param augName - Name of the aug to check the crafting time of. Must be an exact match.
* @returns The time required, in millis, to craft the named augmentation.
* @throws Will error if an invalid Augmentation name is provided.
*/
getAugmentationCraftTime(augName: string): number;
/**
* Begins crafting 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 crafting. Must be an exact match.
* @param focus - Acquire player focus on this Augmentation crafting. Optional. Defaults to true.
* @returns True if the aug successfully began crafting, false otherwise (e.g. not enough money, or
* invalid Augmentation name provided).
* @throws Will error if called while you are not in New Tokyo.
*/
craftAugmentation(augName: string, focus?: boolean): boolean;
}
/** /**
* Skills formulas * Skills formulas
* @public * @public
@ -4280,6 +4318,13 @@ export interface NS extends Singularity {
*/ */
readonly ui: UserInterface; readonly ui: UserInterface;
/**
* Namespace for grafting functions.
* @remarks
* RAM cost: 0 GB
*/
readonly grafting: Grafting;
/** /**
* Arguments passed into the script. * Arguments passed into the script.
* *

@ -617,7 +617,7 @@ export function SidebarRoot(props: IProps): React.ReactElement {
key={"City"} key={"City"}
className={clsx({ className={clsx({
[classes.active]: [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} onClick={clickCity}
> >

@ -167,8 +167,8 @@ SourceFiles["SourceFile10"] = new SourceFile(
10, 10,
( (
<> <>
This Source-File unlocks Sleeve technology in other BitNodes. Each level of this Source-File also grants you a This Source-File unlocks Sleeve technology, and the Grafting API in other BitNodes.
Duplicate Sleeve Each level of this Source-File also grants you a Duplicate Sleeve
</> </>
), ),
); );

@ -263,6 +263,9 @@ const Engine: {
initSymbolToStockMap(); initSymbolToStockMap();
} }
// Apply penalty for entropy accumulation
Player.applyEntropy(Player.entropyStacks);
// Calculate the number of cycles have elapsed while offline // Calculate the number of cycles have elapsed while offline
Engine._lastUpdate = new Date().getTime(); Engine._lastUpdate = new Date().getTime();
const lastUpdate = Player.lastUpdate; const lastUpdate = Player.lastUpdate;
@ -302,6 +305,8 @@ const Engine: {
Player.commitCrime(numCyclesOffline); Player.commitCrime(numCyclesOffline);
} else if (Player.workType == CONSTANTS.WorkTypeCompanyPartTime) { } else if (Player.workType == CONSTANTS.WorkTypeCompanyPartTime) {
Player.workPartTime(numCyclesOffline); Player.workPartTime(numCyclesOffline);
} else if (Player.workType === CONSTANTS.WorkTypeCraftAugmentation) {
Player.craftAugmentationWork(numCyclesOffline);
} else { } else {
Player.work(numCyclesOffline); Player.work(numCyclesOffline);
} }

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

@ -143,51 +143,66 @@ function Work(): React.ReactElement {
let details = <></>; let details = <></>;
let header = <></>; let header = <></>;
let innerText = <></>; let innerText = <></>;
if (player.workType === CONSTANTS.WorkTypeCompanyPartTime || player.workType === CONSTANTS.WorkTypeCompany) { switch (player.workType) {
details = ( case CONSTANTS.WorkTypeCompanyPartTime:
<> case CONSTANTS.WorkTypeCompany:
{player.jobs[player.companyName]} at <strong>{player.companyName}</strong> details = (
</> <>
); {player.jobs[player.companyName]} at <strong>{player.companyName}</strong>
header = ( </>
<> );
Working at <strong>{player.companyName}</strong> header = (
</> <>
); Working at <strong>{player.companyName}</strong>
innerText = ( </>
<> );
+<Reputation reputation={player.workRepGained} /> rep innerText = (
</> <>
); +<Reputation reputation={player.workRepGained} /> rep
} else if (player.workType === CONSTANTS.WorkTypeFaction) { </>
details = ( );
<> break;
{player.factionWorkType} for <strong>{player.currentWorkFactionName}</strong> case CONSTANTS.WorkTypeFaction:
</> details = (
); <>
header = ( {player.factionWorkType} for <strong>{player.currentWorkFactionName}</strong>
<> </>
Working for <strong>{player.currentWorkFactionName}</strong> );
</> header = (
); <>
innerText = ( Working for <strong>{player.currentWorkFactionName}</strong>
<> </>
+<Reputation reputation={player.workRepGained} /> rep );
</> innerText = (
); <>
} else if (player.workType === CONSTANTS.WorkTypeStudyClass) { +<Reputation reputation={player.workRepGained} /> rep
details = <>{player.workType}</>; </>
header = <>You are {player.className}</>; );
innerText = <>{convertTimeMsToTimeElapsedString(player.timeWorked)}</>; break;
} else if (player.workType === CONSTANTS.WorkTypeCreateProgram) { case CONSTANTS.WorkTypeStudyClass:
details = <>Coding {player.createProgramName}</>; details = <>{player.workType}</>;
header = <>Creating a program</>; header = <>You are {player.className}</>;
innerText = ( innerText = <>{convertTimeMsToTimeElapsedString(player.timeWorked)}</>;
<> break;
{player.createProgramName}{" "} case CONSTANTS.WorkTypeCreateProgram:
{((player.timeWorkedCreateProgram / player.timeNeededToCompleteWork) * 100).toFixed(2)}% details = <>Coding {player.createProgramName}</>;
</> header = <>Creating a program</>;
); innerText = (
<>
{player.createProgramName}{" "}
{((player.timeWorkedCreateProgram / player.timeNeededToCompleteWork) * 100).toFixed(2)}%
</>
);
break;
case CONSTANTS.WorkTypeCraftAugmentation:
details = <>Crafting {player.craftAugmentationName}</>;
header = <>Crafting an Augmentation</>;
innerText = (
<>
<strong>{((player.timeWorkedCraftAugmentation / player.timeNeededToCompleteWork) * 100).toFixed(2)}%</strong>
{" "}done
</>
);
} }
return ( return (

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

@ -41,8 +41,8 @@ export function WorkInProgressRoot(): React.ReactElement {
return ( return (
<> <>
<Typography variant="h4" color="primary"> <Typography variant="h4" color="primary">
You have not joined {player.currentWorkFactionName || "(Faction not found)"} yet or cannot work at this time, You have not joined {player.currentWorkFactionName || "(Faction not found)"} yet or cannot work at this
please try again if you think this should have worked time, please try again if you think this should have worked
</Typography> </Typography>
<Button onClick={() => router.toFactions()}>Back to Factions</Button> <Button onClick={() => router.toFactions()}>Back to Factions</Button>
</> </>
@ -483,6 +483,42 @@ export function WorkInProgressRoot(): React.ReactElement {
); );
} }
if (player.craftAugmentationName !== "") {
function cancel(): void {
player.finishCraftAugmentationWork(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.craftAugmentationName}.
<br />
<br />
You have been working for {convertTimeMsToTimeElapsedString(player.timeWorked)}
<br />
<br />
The augmentation is{" "}
{((player.timeWorkedCraftAugmentation / 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(); if (!player.workType) router.toTerminal();
return <></>; return <></>;