FEATURE: BitNode options (#1411)

This commit is contained in:
catloversg 2024-07-15 04:30:30 +07:00 committed by GitHub
parent 0e1e8a9862
commit 783120c886
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
71 changed files with 1315 additions and 308 deletions

@ -0,0 +1,11 @@
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
[Home](./index.md) &gt; [bitburner](./bitburner.md) &gt; [BitNodeBooleanOptions](./bitburner.bitnodebooleanoptions.md) &gt; [disable4SData](./bitburner.bitnodebooleanoptions.disable4sdata.md)
## BitNodeBooleanOptions.disable4SData property
**Signature:**
```typescript
disable4SData: boolean;
```

@ -0,0 +1,11 @@
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
[Home](./index.md) &gt; [bitburner](./bitburner.md) &gt; [BitNodeBooleanOptions](./bitburner.bitnodebooleanoptions.md) &gt; [disableBladeburner](./bitburner.bitnodebooleanoptions.disablebladeburner.md)
## BitNodeBooleanOptions.disableBladeburner property
**Signature:**
```typescript
disableBladeburner: boolean;
```

@ -0,0 +1,11 @@
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
[Home](./index.md) &gt; [bitburner](./bitburner.md) &gt; [BitNodeBooleanOptions](./bitburner.bitnodebooleanoptions.md) &gt; [disableCorporation](./bitburner.bitnodebooleanoptions.disablecorporation.md)
## BitNodeBooleanOptions.disableCorporation property
**Signature:**
```typescript
disableCorporation: boolean;
```

@ -0,0 +1,11 @@
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
[Home](./index.md) &gt; [bitburner](./bitburner.md) &gt; [BitNodeBooleanOptions](./bitburner.bitnodebooleanoptions.md) &gt; [disableGang](./bitburner.bitnodebooleanoptions.disablegang.md)
## BitNodeBooleanOptions.disableGang property
**Signature:**
```typescript
disableGang: boolean;
```

@ -0,0 +1,11 @@
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
[Home](./index.md) &gt; [bitburner](./bitburner.md) &gt; [BitNodeBooleanOptions](./bitburner.bitnodebooleanoptions.md) &gt; [disableHacknetServer](./bitburner.bitnodebooleanoptions.disablehacknetserver.md)
## BitNodeBooleanOptions.disableHacknetServer property
**Signature:**
```typescript
disableHacknetServer: boolean;
```

@ -0,0 +1,11 @@
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
[Home](./index.md) &gt; [bitburner](./bitburner.md) &gt; [BitNodeBooleanOptions](./bitburner.bitnodebooleanoptions.md) &gt; [disableSleeveExpAndAugmentation](./bitburner.bitnodebooleanoptions.disablesleeveexpandaugmentation.md)
## BitNodeBooleanOptions.disableSleeveExpAndAugmentation property
**Signature:**
```typescript
disableSleeveExpAndAugmentation: boolean;
```

@ -0,0 +1,28 @@
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
[Home](./index.md) &gt; [bitburner](./bitburner.md) &gt; [BitNodeBooleanOptions](./bitburner.bitnodebooleanoptions.md)
## BitNodeBooleanOptions interface
restrictHomePCUpgrade: The home computer's maximum RAM and number of cores are lower than normal. Max RAM: 128GB. Max core: 1.
disableSleeveExpAndAugmentation: Your Sleeves do not gain experience when they perform action. You also cannot buy augmentations for them.
**Signature:**
```typescript
export interface BitNodeBooleanOptions
```
## Properties
| Property | Modifiers | Type | Description |
| --- | --- | --- | --- |
| [disable4SData](./bitburner.bitnodebooleanoptions.disable4sdata.md) | | boolean | |
| [disableBladeburner](./bitburner.bitnodebooleanoptions.disablebladeburner.md) | | boolean | |
| [disableCorporation](./bitburner.bitnodebooleanoptions.disablecorporation.md) | | boolean | |
| [disableGang](./bitburner.bitnodebooleanoptions.disablegang.md) | | boolean | |
| [disableHacknetServer](./bitburner.bitnodebooleanoptions.disablehacknetserver.md) | | boolean | |
| [disableSleeveExpAndAugmentation](./bitburner.bitnodebooleanoptions.disablesleeveexpandaugmentation.md) | | boolean | |
| [restrictHomePCUpgrade](./bitburner.bitnodebooleanoptions.restricthomepcupgrade.md) | | boolean | |

@ -0,0 +1,11 @@
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
[Home](./index.md) &gt; [bitburner](./bitburner.md) &gt; [BitNodeBooleanOptions](./bitburner.bitnodebooleanoptions.md) &gt; [restrictHomePCUpgrade](./bitburner.bitnodebooleanoptions.restricthomepcupgrade.md)
## BitNodeBooleanOptions.restrictHomePCUpgrade property
**Signature:**
```typescript
restrictHomePCUpgrade: boolean;
```

@ -0,0 +1,11 @@
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
[Home](./index.md) &gt; [bitburner](./bitburner.md) &gt; [BitNodeOptions](./bitburner.bitnodeoptions.md) &gt; [intelligenceOverride](./bitburner.bitnodeoptions.intelligenceoverride.md)
## BitNodeOptions.intelligenceOverride property
**Signature:**
```typescript
intelligenceOverride: number | undefined;
```

@ -0,0 +1,24 @@
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
[Home](./index.md) &gt; [bitburner](./bitburner.md) &gt; [BitNodeOptions](./bitburner.bitnodeoptions.md)
## BitNodeOptions interface
Default value: - sourceFileOverrides: an empty Map - intelligenceOverride: undefined - All boolean options: false
If you specify intelligenceOverride, it must be a non-negative integer.
**Signature:**
```typescript
export interface BitNodeOptions extends BitNodeBooleanOptions
```
**Extends:** [BitNodeBooleanOptions](./bitburner.bitnodebooleanoptions.md)
## Properties
| Property | Modifiers | Type | Description |
| --- | --- | --- | --- |
| [intelligenceOverride](./bitburner.bitnodeoptions.intelligenceoverride.md) | | number \| undefined | |
| [sourceFileOverrides](./bitburner.bitnodeoptions.sourcefileoverrides.md) | | Map&lt;number, number&gt; | |

@ -0,0 +1,11 @@
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
[Home](./index.md) &gt; [bitburner](./bitburner.md) &gt; [BitNodeOptions](./bitburner.bitnodeoptions.md) &gt; [sourceFileOverrides](./bitburner.bitnodeoptions.sourcefileoverrides.md)
## BitNodeOptions.sourceFileOverrides property
**Signature:**
```typescript
sourceFileOverrides: Map<number, number>;
```

@ -30,7 +30,9 @@
| [AutocompleteData](./bitburner.autocompletedata.md) | Used for autocompletion |
| [BackdoorRequirement](./bitburner.backdoorrequirement.md) | Player must have installed a backdoor on this server. |
| [BasicHGWOptions](./bitburner.basichgwoptions.md) | Options to affect the behavior of [hack](./bitburner.ns.hack.md)<!-- -->, [grow](./bitburner.ns.grow.md)<!-- -->, and [weaken](./bitburner.ns.weaken.md)<!-- -->. |
| [BitNodeBooleanOptions](./bitburner.bitnodebooleanoptions.md) | <p>restrictHomePCUpgrade: The home computer's maximum RAM and number of cores are lower than normal. Max RAM: 128GB. Max core: 1.</p><p>disableSleeveExpAndAugmentation: Your Sleeves do not gain experience when they perform action. You also cannot buy augmentations for them.</p> |
| [BitNodeMultipliers](./bitburner.bitnodemultipliers.md) | All multipliers affecting the difficulty of the current challenge. |
| [BitNodeOptions](./bitburner.bitnodeoptions.md) | <p>Default value: - sourceFileOverrides: an empty Map - intelligenceOverride: undefined - All boolean options: false</p><p>If you specify intelligenceOverride, it must be a non-negative integer.</p> |
| [BitNodeRequirement](./bitburner.bitnoderequirement.md) | Player must be located in this BitNode. |
| [Bladeburner](./bitburner.bladeburner.md) | Bladeburner API |
| [BladeburnerCurAction](./bitburner.bladeburnercuraction.md) | Bladeburner current action. |

@ -0,0 +1,13 @@
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
[Home](./index.md) &gt; [bitburner](./bitburner.md) &gt; [ResetInfo](./bitburner.resetinfo.md) &gt; [bitNodeOptions](./bitburner.resetinfo.bitnodeoptions.md)
## ResetInfo.bitNodeOptions property
Current BitNode options
**Signature:**
```typescript
bitNodeOptions: BitNodeOptions;
```

@ -16,6 +16,7 @@ interface ResetInfo
| Property | Modifiers | Type | Description |
| --- | --- | --- | --- |
| [bitNodeOptions](./bitburner.resetinfo.bitnodeoptions.md) | | [BitNodeOptions](./bitburner.bitnodeoptions.md) | Current BitNode options |
| [currentNode](./bitburner.resetinfo.currentnode.md) | | number | The current bitnode |
| [lastAugReset](./bitburner.resetinfo.lastaugreset.md) | | number | Numeric timestamp (from Date.now()) of last augmentation reset |
| [lastNodeReset](./bitburner.resetinfo.lastnodereset.md) | | number | Numeric timestamp (from Date.now()) of last bitnode reset |

@ -9,7 +9,7 @@ b1t\_flum3 into a different BN.
**Signature:**
```typescript
b1tflum3(nextBN: number, callbackScript?: string): void;
b1tflum3(nextBN: number, callbackScript?: string, bitNodeOptions?: BitNodeOptions): void;
```
## Parameters
@ -18,6 +18,7 @@ b1tflum3(nextBN: number, callbackScript?: string): void;
| --- | --- | --- |
| nextBN | number | BN number to jump to |
| callbackScript | string | _(Optional)_ Name of the script to launch in the next BN. |
| bitNodeOptions | [BitNodeOptions](./bitburner.bitnodeoptions.md) | _(Optional)_ BitNode options for the next BN. |
**Returns:**

@ -9,7 +9,7 @@ Destroy the w0r1d\_d43m0n and move on to the next BN.
**Signature:**
```typescript
destroyW0r1dD43m0n(nextBN: number, callbackScript?: string): void;
destroyW0r1dD43m0n(nextBN: number, callbackScript?: string, bitNodeOptions?: BitNodeOptions): void;
```
## Parameters
@ -18,6 +18,7 @@ destroyW0r1dD43m0n(nextBN: number, callbackScript?: string): void;
| --- | --- | --- |
| nextBN | number | BN number to jump to |
| callbackScript | string | _(Optional)_ Name of the script to launch in the next BN. |
| bitNodeOptions | [BitNodeOptions](./bitburner.bitnodeoptions.md) | _(Optional)_ BitNode options for the next BN. |
**Returns:**

@ -21,12 +21,12 @@ This API requires Source-File 4 to use. The RAM cost of all these functions is m
| Method | Description |
| --- | --- |
| [applyToCompany(companyName, field)](./bitburner.singularity.applytocompany.md) | Apply for a job at a company. |
| [b1tflum3(nextBN, callbackScript)](./bitburner.singularity.b1tflum3.md) | b1t\_flum3 into a different BN. |
| [b1tflum3(nextBN, callbackScript, bitNodeOptions)](./bitburner.singularity.b1tflum3.md) | b1t\_flum3 into a different BN. |
| [checkFactionInvitations()](./bitburner.singularity.checkfactioninvitations.md) | List all current faction invitations. |
| [commitCrime(crime, focus)](./bitburner.singularity.commitcrime.md) | Commit a crime. |
| [connect(hostname)](./bitburner.singularity.connect.md) | Connect to a server. |
| [createProgram(program, focus)](./bitburner.singularity.createprogram.md) | Create a program. |
| [destroyW0r1dD43m0n(nextBN, callbackScript)](./bitburner.singularity.destroyw0r1dd43m0n.md) | Destroy the w0r1d\_d43m0n and move on to the next BN. |
| [destroyW0r1dD43m0n(nextBN, callbackScript, bitNodeOptions)](./bitburner.singularity.destroyw0r1dd43m0n.md) | Destroy the w0r1d\_d43m0n and move on to the next BN. |
| [donateToFaction(faction, amount)](./bitburner.singularity.donatetofaction.md) | Donate to a faction. |
| [exportGame()](./bitburner.singularity.exportgame.md) | Backup game save. |
| [exportGameBonus()](./bitburner.singularity.exportgamebonus.md) | Returns Backup save bonus availability. |

@ -29,7 +29,7 @@ import { workerScripts } from "../Netscript/WorkerScripts";
import { getRecordValues } from "../Types/Record";
import { ServerConstants } from "../Server/data/Constants";
import { isBitNodeFinished } from "../BitNode/BitNodeUtils";
import { canAccessBitNodeFeature, isBitNodeFinished, knowAboutBitverse } from "../BitNode/BitNodeUtils";
// Unable to correctly cast the JSON data into AchievementDataJson type otherwise...
const achievementData = (<AchievementDataJson>(<unknown>data)).achievements;
@ -60,14 +60,6 @@ export interface AchievementData {
Description: string;
}
function canAccessBitNodeFeature(bitNode: number): boolean {
return Player.bitNodeN === bitNode || Player.sourceFileLvl(bitNode) > 0;
}
function knowsAboutBitverse(): boolean {
return Player.sourceFiles.size > 0;
}
function sfAchievements(): Record<string, Achievement> {
const achs: Record<string, Achievement> = {};
for (let i = 1; i <= 12; i++) {
@ -75,7 +67,7 @@ function sfAchievements(): Record<string, Achievement> {
achs[ID] = {
...achievementData[ID],
Icon: ID,
Visible: knowsAboutBitverse,
Visible: knowAboutBitverse,
Condition: () => Player.sourceFileLvl(i) >= 1,
};
}
@ -498,7 +490,7 @@ export const achievements: Record<string, Achievement> = {
INDECISIVE: {
...achievementData.INDECISIVE,
Icon: "1H",
Visible: knowsAboutBitverse,
Visible: knowAboutBitverse,
Condition: (function () {
let c = 0;
setInterval(() => {
@ -514,13 +506,13 @@ export const achievements: Record<string, Achievement> = {
FAST_BN: {
...achievementData.FAST_BN,
Icon: "2DAYS",
Visible: knowsAboutBitverse,
Visible: knowAboutBitverse,
Condition: () => isBitNodeFinished() && Player.playtimeSinceLastBitnode < 1000 * 60 * 60 * 24 * 2,
},
CHALLENGE_BN1: {
...achievementData.CHALLENGE_BN1,
Icon: "BN1+",
Visible: knowsAboutBitverse,
Visible: knowAboutBitverse,
Condition: () =>
Player.bitNodeN === 1 &&
isBitNodeFinished() &&

@ -14,7 +14,7 @@ export function ArcadeRoot(): React.ReactElement {
const [page, setPage] = useState(Page.None);
function mbBurner2000(): void {
if (Player.sourceFileLvl(1) === 0) {
if (Player.activeSourceFileLvl(1) === 0) {
AlertEvents.emit("This machine is broken.");
} else {
setPage(Page.Megabyteburner2000);

@ -26,7 +26,7 @@ const soaAugmentationNames = [
];
export function getBaseAugmentationPriceMultiplier(): number {
return CONSTANTS.MultipleAugMultiplier * [1, 0.96, 0.94, 0.93][Player.sourceFileLvl(11)];
return CONSTANTS.MultipleAugMultiplier * [1, 0.96, 0.94, 0.93][Player.activeSourceFileLvl(11)];
}
export function getGenericAugmentationPriceMultiplier(): number {
const queuedNonSoAAugmentationList = Player.queuedAugmentations.filter((augmentation) => {

@ -7,6 +7,7 @@ import { Player } from "@player";
import { Settings } from "../../Settings/Settings";
import { formatPercent } from "../../ui/formatNumber";
import { Augmentations } from "../Augmentations";
import { canAccessBitNodeFeature } from "../../BitNode/BitNodeUtils";
function calculateAugmentedStats(): Multipliers {
let augP: Multipliers = defaultMultipliers();
@ -29,7 +30,7 @@ function customFormatPercent(value: number): string {
function BitNodeModifiedStats(props: IBitNodeModifiedStatsProps): React.ReactElement {
// If the player doesn't have access to SF5 feature or if the property isn't affected by BitNode mults
if (props.mult === 1 || (Player.bitNodeN !== 5 && Player.sourceFileLvl(5) === 0)) {
if (props.mult === 1 || !canAccessBitNodeFeature(5)) {
return <Typography color={props.color}>{customFormatPercent(props.base)}</Typography>;
}

@ -3,93 +3,101 @@ import Box from "@mui/material/Box";
import List from "@mui/material/List";
import Typography from "@mui/material/Typography";
import React, { useState } from "react";
import { Exploit, ExploitName } from "../../Exploits/Exploit";
import { Exploit, ExploitDescription } from "../../Exploits/Exploit";
import { Player } from "@player";
import { OwnedAugmentationsOrderSetting } from "../../Settings/SettingEnums";
import { Settings } from "../../Settings/Settings";
import { SourceFile } from "../../SourceFile/SourceFile";
import { SourceFiles } from "../../SourceFile/SourceFiles";
interface SfMinus1 {
info: React.ReactElement;
interface SourceFileData {
n: number;
level: number;
maxLevel: number;
activeLevel: number;
name: string;
lvl: number;
info: JSX.Element;
}
const safeGetSf = (sfNum: number): SourceFile | SfMinus1 | null => {
if (sfNum === -1) {
const sfMinus1: SfMinus1 = {
const getSourceFileData = (sfNumber: number): SourceFileData | null => {
let maxLevel: number;
switch (sfNumber) {
case -1:
maxLevel = Object.keys(Exploit).length;
break;
case 12:
maxLevel = Infinity;
break;
default:
maxLevel = 3;
}
const sourceFile = SourceFiles["SourceFile" + sfNumber];
if (sourceFile === undefined) {
console.error(`Invalid source file number: ${sfNumber}`);
return null;
}
return {
n: sfNumber,
level: Player.sourceFileLvl(sfNumber),
maxLevel: maxLevel,
activeLevel: Player.activeSourceFileLvl(sfNumber),
name: sourceFile.name,
info: sourceFile.info,
};
};
export function SourceFilesElement(): React.ReactElement {
const sourceFileList: SourceFileData[] = [];
const exploits = Player.exploits;
// Create a fake SF for -1, if "owned"
if (exploits.length > 0) {
sourceFileList.push({
n: -1,
level: Player.exploits.length,
maxLevel: Object.keys(Exploit).length,
activeLevel: Player.exploits.length,
name: "Source-File -1: Exploits in the BitNodes",
info: (
<>
This Source-File can only be acquired with obscure knowledge of the game, javascript, and the web ecosystem.
This Source-File can only be acquired with obscure knowledge of the game, Javascript, and the web ecosystem.
<br />
<br />
It increases all of the player's multipliers by 0.1%
<br />
<br />
You have found the following exploits:
<br />
<br />
<ul>
{Player.exploits.map((c) => (
<React.Fragment key={c}>
* {ExploitName(c)}
<br />
</React.Fragment>
<li key={c}>
{c}: {ExploitDescription[c]}
</li>
))}
</ul>
</>
),
lvl: Player.exploits.length,
n: -1,
name: "Source-File -1: Exploits in the BitNodes",
};
return sfMinus1;
});
}
for (const sfNumber of Player.sourceFiles.keys()) {
const sourceFileData = getSourceFileData(sfNumber);
if (!sourceFileData) {
continue;
}
sourceFileList.push(sourceFileData);
}
const srcFileKey = "SourceFile" + sfNum;
const sfObj = SourceFiles[srcFileKey];
if (sfObj == null) {
console.error(`Invalid source file number: ${sfNum}`);
if (Settings.OwnedAugmentationsOrder === OwnedAugmentationsOrderSetting.Alphabetically) {
sourceFileList.sort((a, b) => a.n - b.n);
}
const [selectedSfData, setSelectedSfData] = useState<SourceFileData | null>(() => {
if (sourceFileList.length === 0) {
return null;
}
return sfObj;
};
const getMaxLevel = (sfObj: SourceFile | SfMinus1): string | number => {
let maxLevel;
switch (sfObj.n) {
case 12:
maxLevel = "∞";
break;
case -1:
maxLevel = Object.keys(Exploit).length;
break;
default:
maxLevel = "3";
}
return maxLevel;
};
export function SourceFilesElement(): React.ReactElement {
const sourceFilesCopy = new Map(Player.sourceFiles);
const exploits = Player.exploits;
// Create a fake SF for -1, if "owned"
if (exploits.length > 0) {
sourceFilesCopy.set(-1, exploits.length);
}
const sfList = [...sourceFilesCopy];
if (Settings.OwnedAugmentationsOrder === OwnedAugmentationsOrderSetting.Alphabetically) {
sfList.sort(([n1, __lvl1], [n2, __lvl2]) => n1 - n2);
}
const [selectedSf, setSelectedSf] = useState(() => {
if (sfList.length === 0) return null;
const [n, lvl] = sfList[0];
return { n, lvl };
return sourceFileList[0];
});
if (!selectedSf) {
if (!selectedSfData) {
return <></>;
}
@ -104,26 +112,26 @@ export function SourceFilesElement(): React.ReactElement {
sx={{ height: 400, overflowY: "scroll", borderRight: `1px solid ${Settings.theme.welllight}` }}
disablePadding
>
{sfList.map(([n, lvl], i) => {
const sfObj = safeGetSf(n);
if (!sfObj) return;
const maxLevel = getMaxLevel(sfObj);
{sourceFileList.map((sourceFileData, i) => {
return (
<ListItemButton
key={i + 1}
onClick={() => setSelectedSf({ n, lvl })}
selected={selectedSf.n === n}
onClick={() => setSelectedSfData(sourceFileData)}
selected={selectedSfData.n === sourceFileData.n}
sx={{ py: 0 }}
>
<ListItemText
disableTypography
primary={<Typography>{sfObj.name}</Typography>}
primary={<Typography>{sourceFileData.name}</Typography>}
secondary={
<>
<Typography>
Level {lvl} / {maxLevel}
Level: {sourceFileData.level} / {sourceFileData.maxLevel}
</Typography>
{sourceFileData.activeLevel < sourceFileData.level && (
<Typography>Active level: {sourceFileData.activeLevel}</Typography>
)}
</>
}
/>
</ListItemButton>
@ -131,28 +139,25 @@ export function SourceFilesElement(): React.ReactElement {
})}
</List>
</Box>
{selectedSfData !== null && (
<Box sx={{ m: 1 }}>
<Typography variant="h6" sx={{ display: "flex", alignItems: "center", flexWrap: "wrap" }}>
{safeGetSf(selectedSf.n)?.name}
{selectedSfData.name}
</Typography>
<Typography component="div" sx={{ maxHeight: 350, overflowY: "scroll" }}>
{(() => {
const sfObj = safeGetSf(selectedSf.n);
if (!sfObj) return;
const maxLevel = getMaxLevel(sfObj);
return (
Level: {selectedSfData.level} / {selectedSfData.maxLevel}
<br />
{selectedSfData.activeLevel < selectedSfData.level && (
<>
Level {selectedSf.lvl} / {maxLevel}
Active level: {selectedSfData.activeLevel}
<br />
<br />
{sfObj.info}
</>
);
})()}
)}
<br />
{selectedSfData.info}
</Typography>
</Box>
)}
</Paper>
</Box>
);

@ -962,5 +962,5 @@ export function getBitNodeMultipliers(n: number, lvl: number): BitNodeMultiplier
}
export function initBitNodeMultipliers(): void {
replaceCurrentNodeMults(getBitNodeMultipliers(Player.bitNodeN, Player.sourceFileLvl(Player.bitNodeN) + 1));
replaceCurrentNodeMults(getBitNodeMultipliers(Player.bitNodeN, Player.activeSourceFileLvl(Player.bitNodeN) + 1));
}

@ -1,6 +1,11 @@
import { Player } from "@player";
import { type BitNodeOptions } from "@nsdefs";
import { GetServer } from "../Server/AllServers";
import { Server } from "../Server/Server";
import { SpecialServers } from "../Server/data/SpecialServers";
import { JSONMap } from "../Types/Jsonable";
export const validBitNodes = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14];
export function isBitNodeFinished(): boolean {
const wd = GetServer(SpecialServers.WorldDaemon);
@ -9,3 +14,70 @@ export function isBitNodeFinished(): boolean {
}
return wd.backdoorInstalled;
}
export function canAccessBitNodeFeature(bitNode: number): boolean {
return Player.bitNodeN === bitNode || Player.activeSourceFileLvl(bitNode) > 0;
}
export function knowAboutBitverse(): boolean {
for (const sfActiveLevel of Player.activeSourceFiles.values()) {
if (sfActiveLevel > 0) {
return true;
}
}
return false;
}
export function getDefaultBitNodeOptions(): BitNodeOptions {
return {
sourceFileOverrides: new Map<number, number>(),
intelligenceOverride: undefined,
restrictHomePCUpgrade: false,
disableGang: false,
disableCorporation: false,
disableBladeburner: false,
disable4SData: false,
disableHacknetServer: false,
disableSleeveExpAndAugmentation: false,
};
}
export function validateSourceFileOverrides(
sourceFileOverrides: Map<number, number>,
isDataFromPlayer: boolean,
): {
valid: boolean;
message?: string;
} {
if (!isDataFromPlayer && !(sourceFileOverrides instanceof JSONMap)) {
return { valid: false, message: `It must be a JSONMap.` };
}
for (const [sfNumber, sfLevel] of sourceFileOverrides.entries()) {
if (!validBitNodes.includes(sfNumber)) {
return { valid: false, message: `Invalid BitNode: ${sfNumber}.` };
}
if (!Number.isFinite(sfLevel)) {
return { valid: false, message: `Invalid SF level: ${sfLevel}.` };
}
const maxSfLevel = Player.sourceFileLvl(sfNumber);
if (sfLevel > maxSfLevel) {
return { valid: false, message: `Invalid SF level: ${sfLevel}. Max level: ${maxSfLevel}.` };
}
}
return { valid: true };
}
export function setBitNodeOptions(bitNodeOptions: BitNodeOptions): void {
const validationResultForSourceFileOverrides = validateSourceFileOverrides(bitNodeOptions.sourceFileOverrides, false);
if (!validationResultForSourceFileOverrides.valid) {
throw new Error(`sourceFileOverrides is invalid. Reason: ${validationResultForSourceFileOverrides.message}`);
}
if (
bitNodeOptions.intelligenceOverride !== undefined &&
(!Number.isInteger(bitNodeOptions.intelligenceOverride) || bitNodeOptions.intelligenceOverride < 0)
) {
throw new Error(`intelligenceOverride is invalid. It must be a non-negative integer.`);
}
Object.assign(Player.bitNodeOptions, bitNodeOptions);
}

@ -0,0 +1,433 @@
import { type BitNodeBooleanOptions } from "@nsdefs";
import React from "react";
import {
Box,
Button,
Collapse,
ListItemButton,
ListItemText,
MenuItem,
Paper,
Select,
TextField,
Tooltip,
Typography,
} from "@mui/material";
import { OptionSwitch } from "../../ui/React/OptionSwitch";
import { ButtonWithTooltip } from "../../ui/Components/ButtonWithTooltip";
import { ExpandLess, ExpandMore } from "@mui/icons-material";
import { JSONMap } from "../../Types/Jsonable";
import { Settings } from "../../Settings/Settings";
import { Player } from "@player";
interface SourceFileButtonRowProps {
sfNumber: number;
sfLevel: number;
sfActiveLevel: number;
callbacks: BitNodeAdvancedOptionsProps["callbacks"];
}
function SourceFileButtonRow({
sfNumber,
sfLevel,
sfActiveLevel,
callbacks,
}: SourceFileButtonRowProps): React.ReactElement {
const title = `SF-${sfNumber}`;
const sourceFileLevelTool =
sfNumber !== 12 ? (
[...Array(sfLevel + 1).keys()].map((level) => (
<Button
key={level}
onClick={() => {
callbacks.setSfActiveLevel(sfNumber, level);
}}
sx={{
marginRight: "0.5rem !important",
border: level === sfActiveLevel ? `1px solid ${Settings.theme.info}` : "",
minWidth: "40px",
}}
>
{level}
</Button>
))
) : (
// The usage of TextField instead of NumberInput is intentional.
<TextField
sx={{ maxWidth: "185px" }}
value={sfActiveLevel}
onChange={(event) => {
// Empty string will be automatically changed to "0".
if (event.target.value === "") {
callbacks.setSfActiveLevel(sfNumber, 0);
return;
}
const level = Number.parseInt(event.target.value);
if (!Number.isFinite(level) || level < 0 || level > sfLevel) {
return;
}
callbacks.setSfActiveLevel(sfNumber, level);
}}
></TextField>
);
const extraInfo =
sfNumber === 12 ? (
<td>
<Typography marginLeft="1rem">Max level: {sfLevel}</Typography>
</td>
) : null;
return (
<tr>
<td>
<Typography>{title}</Typography>
</td>
<td>{sourceFileLevelTool}</td>
{extraInfo}
</tr>
);
}
function SourceFileOverrides({
currentSourceFiles,
sourceFileOverrides,
callbacks,
getSfLevel,
}: {
currentSourceFiles: BitNodeAdvancedOptionsProps["currentSourceFiles"];
sourceFileOverrides: BitNodeAdvancedOptionsProps["sourceFileOverrides"];
callbacks: BitNodeAdvancedOptionsProps["callbacks"];
getSfLevel: (sfNumber: number) => number;
}): React.ReactElement {
const firstSourceFile = React.useMemo(
() => (currentSourceFiles.size > 0 ? [...currentSourceFiles.keys()][0] : null),
[currentSourceFiles],
);
const [selectElementValue, setSelectElementValue] = React.useState<number | null>(firstSourceFile);
const getMenuItemList = (data: typeof sourceFileOverrides): number[] => {
return [...currentSourceFiles.keys()].filter((sfNumber) => ![...data.keys()].includes(sfNumber));
};
const menuItemList = getMenuItemList(sourceFileOverrides);
React.useEffect(() => {
if (sourceFileOverrides.size === 0) {
setSelectElementValue(firstSourceFile);
}
}, [sourceFileOverrides, firstSourceFile]);
const basicNote = `Changing the active level of a SF is temporary; you still permanently own that SF level. For example, if
you enter BN 1.3 while having SF 1.2 but with the active level set to 0, you will not get the bonuses from SF
1.1 or SF 1.2, but you will still earn SF 1.3 when destroying the BN.`;
const note = currentSourceFiles.has(10) ? (
<>
<Typography>Note:</Typography>
<ul style={{ marginTop: 0 }}>
<li>{basicNote}</li>
<li>
Changing the active level of SF 10 does not affect your current sleeves or the maximum number of sleeves.
</li>
</ul>
</>
) : (
<>
<Typography>Note: {basicNote}</Typography>
<br />
</>
);
return (
<>
<Typography>Override active level of Source-File:</Typography>
<br />
<Typography component="div">{note}</Typography>
<div>
<Select
disabled={menuItemList.length === 0}
value={selectElementValue ?? ""}
onChange={(event) => {
setSelectElementValue(Number(event.target.value));
}}
sx={{ minWidth: "80px" }}
>
{menuItemList.map((sfNumber) => (
<MenuItem key={sfNumber} value={sfNumber}>
SF-{sfNumber}
</MenuItem>
))}
</Select>
<Button
disabled={menuItemList.length === 0}
onClick={() => {
if (!selectElementValue) {
return;
}
const newSfOverrides = new JSONMap(sourceFileOverrides);
newSfOverrides.set(selectElementValue, getSfLevel(selectElementValue));
const newMenuItemList = getMenuItemList(newSfOverrides);
if (newMenuItemList.length > 0) {
setSelectElementValue(newMenuItemList[0]);
} else {
setSelectElementValue(null);
}
callbacks.setSfOverrides(newSfOverrides);
}}
sx={{ marginLeft: "1rem" }}
>
Add
</Button>
<ButtonWithTooltip
normalTooltip="Remove all overridden SF"
disabledTooltip={sourceFileOverrides.size === 0 ? "No overridden SF" : ""}
onClick={() => {
callbacks.setSfOverrides(new JSONMap());
}}
buttonProps={{ sx: { marginLeft: "1rem" } }}
>
Remove all
</ButtonWithTooltip>
</div>
<br />
{sourceFileOverrides.size > 0 && (
<>
<table>
<tbody>
<tr>
<td>
<Tooltip title="Set active level for all chosen SF">
<Typography minWidth="7rem">Set all SF</Typography>
</Tooltip>
</td>
<td>
{[0, 1, 2, 3].map((level) => (
<ButtonWithTooltip
key={level}
onClick={() => {
const newSfOverrides = new JSONMap(sourceFileOverrides);
for (const [sfNumber] of newSfOverrides) {
newSfOverrides.set(sfNumber, Math.min(level, getSfLevel(sfNumber)));
}
callbacks.setSfOverrides(newSfOverrides);
}}
buttonProps={{ sx: { marginRight: "0.5rem", minWidth: "40px" } }}
>
{level}
</ButtonWithTooltip>
))}
</td>
</tr>
{[...sourceFileOverrides.keys()].map((sfNumber) => (
<SourceFileButtonRow
key={sfNumber}
sfNumber={sfNumber}
sfLevel={getSfLevel(sfNumber)}
sfActiveLevel={sourceFileOverrides.get(sfNumber) ?? 0}
callbacks={callbacks}
></SourceFileButtonRow>
))}
</tbody>
</table>
<br />
</>
)}
</>
);
}
function IntelligenceOverride({
intelligenceOverride,
callbacks,
}: {
intelligenceOverride: BitNodeAdvancedOptionsProps["intelligenceOverride"];
callbacks: BitNodeAdvancedOptionsProps["callbacks"];
}): React.ReactElement {
const [enabled, setEnabled] = React.useState<boolean>(false);
const [lastValueOfIntelligenceOverride, setLastValueOfIntelligenceOverride] = React.useState<number | undefined>(
Player.skills.intelligence,
);
return (
<OptionSwitch
disabled={Player.skills.intelligence <= 0}
checked={false}
onChange={(value) => {
setEnabled(value);
if (!value) {
// If this option is disabled, save last value and reset data.
setLastValueOfIntelligenceOverride(intelligenceOverride);
callbacks.setIntelligenceOverride(undefined);
return;
} else {
// If this option is enabled, load last value.
callbacks.setIntelligenceOverride(lastValueOfIntelligenceOverride);
}
}}
text={
<>
<Typography component="div" display="flex" gap="1rem">
<Typography display="flex" alignItems="center">
Override Intelligence:
</Typography>
<TextField
sx={{ maxWidth: "4rem" }}
disabled={!enabled}
value={intelligenceOverride !== undefined ? intelligenceOverride : ""}
onChange={(event) => {
// Empty string will be automatically changed to "0".
if (event.target.value === "") {
callbacks.setIntelligenceOverride(0);
return;
}
const value = Number.parseInt(event.target.value);
if (!Number.isInteger(value) || value < 0) {
return;
}
callbacks.setIntelligenceOverride(value);
}}
></TextField>
</Typography>
</>
}
tooltip={
<>
<Typography component="div">
The Intelligence bonuses for you and your Sleeves will be limited by this value. For example:
<ul>
<li>
If your Intelligence is 1000 and you set this value to 500, the "effective" Intelligence, which is used
for bonus calculation, is only 500.
</li>
<li>
If a Sleeve's Intelligence is 200 and you set this value to 500, the "effective" Intelligence, which is
used for bonus calculation, is still 200.
</li>
</ul>
</Typography>
<Typography>
You will still gain Intelligence experience as normal. Intelligence Override only affects the Intelligence
bonus.
</Typography>
<br />
<Typography>
The "effective" Intelligence will be shown in the character overview. If the effective value is different
from the original value, you can hover your mouse over it to see the original value.
</Typography>
</>
}
/>
);
}
interface BitNodeAdvancedOptionsProps {
targetBitNode: number;
currentSourceFiles: Map<number, number>;
sourceFileOverrides: JSONMap<number, number>;
intelligenceOverride: number | undefined;
bitNodeBooleanOptions: BitNodeBooleanOptions;
callbacks: {
setSfOverrides: (value: JSONMap<number, number>) => void;
setSfActiveLevel: (sfNumber: number, sfLevel: number) => void;
setIntelligenceOverride: (value: number | undefined) => void;
setBooleanOption: (key: keyof BitNodeBooleanOptions, value: boolean) => void;
};
}
export function BitNodeAdvancedOptions({
targetBitNode,
currentSourceFiles,
sourceFileOverrides,
intelligenceOverride,
bitNodeBooleanOptions,
callbacks,
}: BitNodeAdvancedOptionsProps): React.ReactElement {
const [open, setOpen] = React.useState(false);
const getSfLevel = React.useCallback(
(sfNumber: number): number => {
return currentSourceFiles.get(sfNumber) ?? 0;
},
[currentSourceFiles],
);
return (
<Box component={Paper} sx={{ mt: 1, p: 1 }}>
<ListItemButton disableGutters onClick={() => setOpen((old) => !old)} sx={{ padding: "4px 8px" }}>
<ListItemText primary={<Typography variant="h6">Advanced options</Typography>} />
{open ? <ExpandLess color="primary" /> : <ExpandMore color="primary" />}
</ListItemButton>
<Collapse in={open}>
<Box sx={{ padding: "0 1rem" }}>
<OptionSwitch
checked={bitNodeBooleanOptions.restrictHomePCUpgrade}
onChange={(value) => {
callbacks.setBooleanOption("restrictHomePCUpgrade", value);
}}
text="Restrict max RAM and core of Home PC"
tooltip="The home computer's maximum RAM and number of cores are lower than normal. Max RAM: 128GB. Max core: 1."
/>
<OptionSwitch
disabled={getSfLevel(2) === 0 && targetBitNode !== 2}
checked={bitNodeBooleanOptions.disableGang}
onChange={(value) => {
callbacks.setBooleanOption("disableGang", value);
}}
text="Disable Gang"
tooltip="Disable Gang"
/>
<OptionSwitch
disabled={getSfLevel(3) === 0 && targetBitNode !== 3}
checked={bitNodeBooleanOptions.disableCorporation}
onChange={(value) => {
callbacks.setBooleanOption("disableCorporation", value);
}}
text="Disable Corporation"
tooltip="Disable Corporation"
/>
<OptionSwitch
disabled={getSfLevel(6) === 0 && getSfLevel(7) === 0 && targetBitNode !== 6 && targetBitNode !== 7}
checked={bitNodeBooleanOptions.disableBladeburner}
onChange={(value) => {
callbacks.setBooleanOption("disableBladeburner", value);
}}
text="Disable Bladeburner"
tooltip="Disable Bladeburner"
/>
<OptionSwitch
checked={bitNodeBooleanOptions.disable4SData}
onChange={(value) => {
callbacks.setBooleanOption("disable4SData", value);
}}
text="Disable 4S Market Data"
tooltip="Disable 4S Market Data"
/>
<OptionSwitch
disabled={getSfLevel(9) === 0 && targetBitNode !== 9}
checked={bitNodeBooleanOptions.disableHacknetServer}
onChange={(value) => {
callbacks.setBooleanOption("disableHacknetServer", value);
}}
text="Disable Hacknet Server"
tooltip="Hacknet Node is re-enabled in place of Hacknet Server."
/>
<OptionSwitch
disabled={getSfLevel(10) === 0 && targetBitNode !== 10}
checked={bitNodeBooleanOptions.disableSleeveExpAndAugmentation}
onChange={(value) => {
callbacks.setBooleanOption("disableSleeveExpAndAugmentation", value);
}}
text="Disable Sleeves' experience and augmentation"
tooltip="Sleeves cannot gain experience or install augmentations"
/>
<IntelligenceOverride
intelligenceOverride={intelligenceOverride}
callbacks={callbacks}
></IntelligenceOverride>
<br />
<SourceFileOverrides
currentSourceFiles={currentSourceFiles}
sourceFileOverrides={sourceFileOverrides}
callbacks={callbacks}
getSfLevel={getSfLevel}
></SourceFileOverrides>
</Box>
</Collapse>
</Box>
);
}

@ -11,6 +11,7 @@ import { StatsRow } from "../../ui/React/StatsRow";
import { defaultMultipliers, getBitNodeMultipliers } from "../BitNode";
import { BitNodeMultipliers } from "../BitNodeMultipliers";
import { PartialRecord, getRecordEntries } from "../../Types/Record";
import { canAccessBitNodeFeature } from "../BitNodeUtils";
interface IProps {
n: number;
@ -23,7 +24,7 @@ export function BitnodeMultiplierDescription({ n, level }: IProps): React.ReactE
return (
<Box component={Paper} sx={{ mt: 1, p: 1 }}>
<ListItemButton disableGutters onClick={() => setOpen((old) => !old)}>
<ListItemButton disableGutters onClick={() => setOpen((old) => !old)} sx={{ padding: "4px 8px" }}>
<ListItemText primary={<Typography variant="h6">Bitnode Multipliers</Typography>} />
{open ? <ExpandLess color="primary" /> : <ExpandMore color="primary" />}
</ListItemButton>
@ -39,8 +40,8 @@ export const BitNodeMultipliersDisplay = ({ n, level }: IProps): React.ReactElem
// If not, then we have to assume that we want the next level up from the
// current node's source file, so we get the min of that, the SF's max level,
// or if it's BN12, ∞
const maxSfLevel = n === 12 ? Infinity : 3;
const mults = getBitNodeMultipliers(n, level ?? Math.min(Player.sourceFileLvl(n) + 1, maxSfLevel));
const maxSfLevel = n === 12 ? Number.MAX_VALUE : 3;
const mults = getBitNodeMultipliers(n, level ?? Math.min(Player.activeSourceFileLvl(n) + 1, maxSfLevel));
return (
<Box sx={{ columnCount: 2, columnGap: 1, mb: n === 1 ? 0 : -2 }}>
@ -314,7 +315,7 @@ function StanekMults({ mults }: IMultsProps): React.ReactElement {
}
function GangMults({ mults }: IMultsProps): React.ReactElement {
if (Player.bitNodeN !== 2 && Player.sourceFileLvl(2) <= 0) return <></>;
if (!canAccessBitNodeFeature(2)) return <></>;
const rows: IBNMultRows = {
GangSoftcap: {

@ -172,7 +172,7 @@ export function BitverseRoot(props: IProps): React.ReactElement {
if (n !== destroyed) {
return lvl;
}
const max = n === 12 ? Infinity : 3;
const max = n === 12 ? Number.MAX_VALUE : 3;
// If accessed via flume, display the current BN level, else the next
return Math.min(max, lvl + Number(!props.flume));

@ -1,11 +1,15 @@
import React from "react";
import { Player } from "@player";
import { type BitNodeBooleanOptions } from "@nsdefs";
import { enterBitNode } from "../../RedPill";
import { BitNodes } from "../BitNode";
import { Modal } from "../../ui/React/Modal";
import Typography from "@mui/material/Typography";
import Button from "@mui/material/Button";
import { BitnodeMultiplierDescription } from "./BitnodeMultipliersDescription";
import { BitNodeAdvancedOptions } from "./BitNodeAdvancedOptions";
import { JSONMap } from "../../Types/Jsonable";
interface IProps {
open: boolean;
@ -17,14 +21,77 @@ interface IProps {
}
export function PortalModal(props: IProps): React.ReactElement {
const [sourceFileOverrides, setSourceFileOverrides] = React.useState<JSONMap<number, number>>(new JSONMap());
const [intelligenceOverride, setIntelligenceOverride] = React.useState<number | undefined>();
const [bitNodeBooleanOptions, setBitNodeBooleanOptions] = React.useState<BitNodeBooleanOptions>({
restrictHomePCUpgrade: false,
disableGang: false,
disableCorporation: false,
disableBladeburner: false,
disable4SData: false,
disableHacknetServer: false,
disableSleeveExpAndAugmentation: false,
});
const bitNodeKey = "BitNode" + props.n;
const bitNode = BitNodes[bitNodeKey];
if (bitNode == null) throw new Error(`Could not find BitNode object for number: ${props.n}`);
const maxSourceFileLevel = props.n === 12 ? "∞" : "3";
const newLevel = Math.min(props.level + 1, props.n === 12 ? Number.MAX_VALUE : 3);
let currentSourceFiles = new Map(Player.sourceFiles);
if (!props.flume) {
const currentSourceFileLevel = Player.sourceFileLvl(props.destroyedBitNode);
if (currentSourceFileLevel < 3 || props.destroyedBitNode === 12) {
currentSourceFiles.set(props.destroyedBitNode, currentSourceFileLevel + 1);
}
}
currentSourceFiles = new Map([...currentSourceFiles].sort((a, b) => a[0] - b[0]));
const callbacks = {
setSfOverrides: (value: JSONMap<number, number>) => {
setSourceFileOverrides(value);
},
setSfActiveLevel: (sfNumber: number, sfLevel: number) => {
setSourceFileOverrides((old) => {
const newValue = new JSONMap(old);
newValue.set(sfNumber, sfLevel);
return newValue;
});
},
setIntelligenceOverride: (value: number | undefined) => {
setIntelligenceOverride(value);
},
setBooleanOption: (key: keyof BitNodeBooleanOptions, value: boolean) => {
if (!(key in bitNodeBooleanOptions)) {
throw new Error(`Invalid key of booleanOptions: ${key}`);
}
setBitNodeBooleanOptions((old) => {
return {
...old,
[key]: value,
};
});
},
};
function onClose() {
setSourceFileOverrides(new JSONMap());
setIntelligenceOverride(undefined);
setBitNodeBooleanOptions({
restrictHomePCUpgrade: false,
disableGang: false,
disableCorporation: false,
disableBladeburner: false,
disable4SData: false,
disableHacknetServer: false,
disableSleeveExpAndAugmentation: false,
});
props.onClose();
}
const newLevel = Math.min(props.level + 1, props.n === 12 ? Infinity : 3);
return (
<Modal open={props.open} onClose={props.onClose}>
<Modal open={props.open} onClose={onClose}>
<Typography variant="h4">
BitNode-{props.n}: {bitNode.name}
</Typography>
@ -39,12 +106,25 @@ export function PortalModal(props: IProps): React.ReactElement {
<br />
<Typography component="div">{bitNode.info}</Typography>
<BitnodeMultiplierDescription n={props.n} level={newLevel} />
<BitNodeAdvancedOptions
targetBitNode={props.n}
currentSourceFiles={currentSourceFiles}
sourceFileOverrides={sourceFileOverrides}
intelligenceOverride={intelligenceOverride}
bitNodeBooleanOptions={bitNodeBooleanOptions}
callbacks={callbacks}
></BitNodeAdvancedOptions>
<br />
<Button
aria-label={`enter-bitnode-${bitNode.number.toString()}`}
autoFocus={true}
onClick={() => {
enterBitNode(props.flume, props.destroyedBitNode, props.n);
const bitNodeOptions = {
sourceFileOverrides,
intelligenceOverride,
...bitNodeBooleanOptions,
};
enterBitNode(props.flume, props.destroyedBitNode, props.n, bitNodeOptions);
props.onClose();
}}
>

@ -23,7 +23,7 @@ export class StaneksGift extends BaseGift {
}
baseSize(): number {
return StanekConstants.BaseSize + currentNodeMults.StaneksGiftExtraSize + Player.sourceFileLvl(13);
return StanekConstants.BaseSize + currentNodeMults.StaneksGiftExtraSize + Player.activeSourceFileLvl(13);
}
width(): number {

@ -32,12 +32,18 @@ export function SourceFilesDev({ parentRerender }: { parentRerender: () => void
}
if (sfLvl === 0) {
Player.sourceFiles.delete(sfN);
if (sfN === 10) Sleeve.recalculateNumOwned();
Player.bitNodeOptions.sourceFileOverrides.delete(sfN);
if (sfN === 10) {
Sleeve.recalculateNumOwned();
}
parentRerender();
return;
}
Player.sourceFiles.set(sfN, sfLvl);
if (sfN === 10) Sleeve.recalculateNumOwned();
Player.bitNodeOptions.sourceFileOverrides.set(sfN, sfLvl);
if (sfN === 10) {
Sleeve.recalculateNumOwned();
}
parentRerender();
},
[parentRerender],
@ -113,6 +119,8 @@ export function SourceFilesDev({ parentRerender }: { parentRerender: () => void
<Typography>Source-Files</Typography>
</AccordionSummary>
<AccordionDetails>
<Typography>Note: This tool sets both the owned level and the overridden level.</Typography>
<br />
<table>
<tbody>
<tr>

@ -29,24 +29,20 @@ export enum Exploit {
EditSaveFile = "EditSaveFile",
}
const names: Record<Exploit, string> = {
Bypass: "by circumventing the ram cost of document.",
EditSaveFile: "by editing your save file.",
PrototypeTampering: "by tampering with Numbers prototype.",
TimeCompression: "by compressing time.",
Unclickable: "by clicking the unclickable.",
UndocumentedFunctionCall: "by looking beyond the documentation.",
RealityAlteration: "by altering reality to suit your whims.",
N00dles: "by harnessing the power of the n00dles.",
YoureNotMeantToAccessThis: "by accessing the dev menu.",
TrueRecursion: "by truly recursing.",
INeedARainbow: "by using the power of the rainbow.",
export const ExploitDescription: Record<Exploit, string> = {
[Exploit.Bypass]: "by circumventing the ram cost of document.",
[Exploit.EditSaveFile]: "by editing your save file.",
[Exploit.PrototypeTampering]: "by tampering with Numbers prototype.",
[Exploit.TimeCompression]: "by compressing time.",
[Exploit.Unclickable]: "by clicking the unclickable.",
[Exploit.UndocumentedFunctionCall]: "by looking beyond the documentation.",
[Exploit.RealityAlteration]: "by altering reality to suit your whims.",
[Exploit.N00dles]: "by harnessing the power of the n00dles.",
[Exploit.YoureNotMeantToAccessThis]: "by accessing the dev menu.",
[Exploit.TrueRecursion]: "by truly recursing.",
[Exploit.INeedARainbow]: "by using the power of the rainbow.",
};
export function ExploitName(exploit: Exploit): string {
return names[exploit];
}
// Needed in case player edits save file poorly
export function sanitizeExploits(exploits: Exploit[]): Exploit[] {
exploits = exploits.filter((e: Exploit) => Object.values(Exploit).includes(e));

@ -149,7 +149,7 @@ export const getFactionAugmentationsFiltered = (faction: Faction): AugmentationN
augs.push(Augmentations[AugmentationName.TheRedPill]);
}
const rng = SFC32RNG(`BN${Player.bitNodeN}.${Player.sourceFileLvl(Player.bitNodeN)}`);
const rng = SFC32RNG(`BN${Player.bitNodeN}.${Player.activeSourceFileLvl(Player.bitNodeN)}`);
// Remove faction-unique augs that don't belong to this faction
const uniqueFilter = (a: Augmentation): boolean => {
// Keep all the non-unique one

@ -302,7 +302,7 @@ export const haveSourceFile = (n: number): PlayerCondition => ({
};
},
isSatisfied(p: PlayerObject): boolean {
return p.bitNodeN == n || p.sourceFileLvl(n) > 0;
return p.bitNodeN == n || p.activeSourceFileLvl(n) > 0;
},
});

@ -60,7 +60,7 @@ export const AutoexecInput = (props: IProps): React.ReactElement => {
);
}
// Stolen from Prestige.ts
const minRam = Player.sourceFileLvl(9) >= 2 ? 128 : Player.sourceFileLvl(1) > 0 ? 32 : 8;
const minRam = Player.activeSourceFileLvl(9) >= 2 ? 128 : Player.activeSourceFileLvl(1) > 0 ? 32 : 8;
if (ramUsage <= minRam) {
return (
<Tooltip

@ -829,5 +829,5 @@ function waitCycle(useOfflineCycles = true): Promise<void> {
}
export function showWorldDemon() {
return Player.hasAugmentation(AugmentationName.TheRedPill, true) && Player.sourceFileLvl(1);
return Player.hasAugmentation(AugmentationName.TheRedPill, true) && Player.activeSourceFileLvl(1);
}

@ -15,7 +15,7 @@ import { getRecordEntries, getRecordValues } from "../../Types/Record";
*/
export function CalculateEffect(nodes: number, faction: GoOpponent): number {
const power = getEffectPowerForFaction(faction);
const sourceFileBonus = Player.sourceFileLvl(14) ? 2 : 1;
const sourceFileBonus = Player.activeSourceFileLvl(14) ? 2 : 1;
return (
1 + Math.log(nodes + 1) * Math.pow(nodes + 1, 0.3) * 0.002 * power * currentNodeMults.GoPower * sourceFileBonus
);
@ -26,7 +26,7 @@ export function CalculateEffect(nodes: number, faction: GoOpponent): number {
* for factions you are a member of
*/
export function getMaxFavor() {
const sourceFileLevel = Player.sourceFileLvl(14);
const sourceFileLevel = Player.activeSourceFileLvl(14);
if (sourceFileLevel === 1) {
return 80;

@ -323,8 +323,8 @@ export function getStats() {
/** Validate singularity access by throwing an error if the player does not have access. */
export function checkCheatApiAccess(error: (s: string) => void): void {
const hasSourceFile = Player.sourceFileLvl(14) > 1;
const isBitnodeFourteenTwo = Player.sourceFileLvl(14) === 1 && Player.bitNodeN === 14;
const hasSourceFile = Player.activeSourceFileLvl(14) > 1;
const isBitnodeFourteenTwo = Player.activeSourceFileLvl(14) === 1 && Player.bitNodeN === 14;
if (!hasSourceFile && !isBitnodeFourteenTwo) {
error(
`The go.cheat API requires Source-File 14.2 to run, a power up you obtain later in the game.
@ -387,7 +387,7 @@ export async function determineCheatSuccess(
* 15: +31,358,645%
*/
export function cheatSuccessChance(cheatCount: number) {
const sourceFileBonus = Player.sourceFileLvl(14) === 3 ? 0.25 : 0;
const sourceFileBonus = Player.activeSourceFileLvl(14) === 3 ? 0.25 : 0;
const cheatCountScalar = (0.7 - 0.02 * cheatCount) ** cheatCount;
return Math.max(Math.min(0.6 * cheatCountScalar * Player.mults.crime_success + sourceFileBonus, 1), 0);
}

@ -23,11 +23,12 @@ import { GetServer } from "../Server/AllServers";
import { Server } from "../Server/Server";
import { Companies } from "../Company/Companies";
import { isMember } from "../utils/EnumHelper";
import { canAccessBitNodeFeature } from "../BitNode/BitNodeUtils";
// Returns a boolean indicating whether the player has Hacknet Servers
// (the upgraded form of Hacknet Nodes)
export function hasHacknetServers(): boolean {
return Player.bitNodeN === 9 || Player.sourceFileLvl(9) > 0;
return canAccessBitNodeFeature(9) && !Player.bitNodeOptions.disableHacknetServer;
}
export function purchaseHacknet(): number {

@ -14,7 +14,7 @@ interface IProps {
export function CoresButton(props: IProps): React.ReactElement {
const homeComputer = Player.getHomeComputer();
const maxCores = homeComputer.cpuCores >= 8;
const maxCores = Player.bitNodeOptions.restrictHomePCUpgrade || homeComputer.cpuCores >= 8;
if (maxCores) {
return <Button>Upgrade 'home' cores - MAX</Button>;
}

@ -19,7 +19,10 @@ interface IProps {
export function RamButton(props: IProps): React.ReactElement {
const homeComputer = Player.getHomeComputer();
if (homeComputer.maxRam >= ServerConstants.HomeComputerMaxRam) {
if (
(Player.bitNodeOptions.restrictHomePCUpgrade && homeComputer.maxRam >= 128) ||
homeComputer.maxRam >= ServerConstants.HomeComputerMaxRam
) {
return <Button>Upgrade 'home' RAM - MAX</Button>;
}

@ -35,6 +35,7 @@ import { HacknetServer } from "../../Hacknet/HacknetServer";
import { GetServer } from "../../Server/AllServers";
import { ArcadeRoot } from "../../Arcade/ui/ArcadeRoot";
import { currentNodeMults } from "../../BitNode/BitNodeMultipliers";
import { canAccessBitNodeFeature, knowAboutBitverse } from "../../BitNode/BitNodeUtils";
interface SpecialLocationProps {
loc: Location;
@ -91,8 +92,10 @@ export function SpecialLocation(props: SpecialLocationProps): React.ReactElement
function EatNoodles(): void {
SnackbarEvents.emit("You ate some delicious noodles and feel refreshed", ToastVariant.SUCCESS, 2000);
N00dles(); // This is the true power of the noodles.
if (Player.sourceFiles.size > 0) Player.giveExploit(Exploit.N00dles);
if (Player.sourceFileLvl(5) > 0 || Player.bitNodeN === 5) {
if (knowAboutBitverse()) {
Player.giveExploit(Exploit.N00dles);
}
if (canAccessBitNodeFeature(5)) {
Player.exp.intelligence *= 1.0000000000000002;
}
Player.exp.hacking *= 1.0000000000000002;

@ -8,6 +8,7 @@ import { SpecialServers } from "../Server/data/SpecialServers";
import { Settings } from "../Settings/Settings";
import { dialogBoxCreate } from "../ui/React/DialogBox";
import { Server } from "../Server/Server";
import { knowAboutBitverse } from "../BitNode/BitNodeUtils";
//Sends message to player, including a pop up
function sendMessage(name: MessageFilename, forced = false): void {
@ -63,9 +64,9 @@ function checkForMessagesToSend(): void {
//If the daemon can be hacked, send the player icarus.msg
if (
Player.skills.hacking >= worldDaemon.requiredHackingSkill &&
(Player.sourceFiles.size === 0 || !recvd(MessageFilename.RedPill))
(!knowAboutBitverse() || !recvd(MessageFilename.RedPill))
) {
sendMessage(MessageFilename.RedPill, Player.sourceFiles.size === 0);
sendMessage(MessageFilename.RedPill, !knowAboutBitverse());
}
//If the daemon cannot be hacked, send the player truthgazer.msg a single time.
else if (!recvd(MessageFilename.TruthGazer)) {

@ -1,5 +1,11 @@
import type { NetscriptContext } from "./APIWrapper";
import type { RunningScript as IRunningScript, Person as IPerson, Server as IServer, ScriptArg } from "@nsdefs";
import type {
RunningScript as IRunningScript,
Person as IPerson,
Server as IServer,
ScriptArg,
BitNodeOptions,
} from "@nsdefs";
import type { WorkerScript } from "./WorkerScript";
import React from "react";
@ -49,6 +55,12 @@ import { CustomBoundary } from "../ui/Components/CustomBoundary";
import { ServerConstants } from "../Server/data/Constants";
import { basicErrorMessage, errorMessage, log } from "./ErrorMessages";
import { assertString, debugType } from "./TypeAssertion";
import {
canAccessBitNodeFeature,
getDefaultBitNodeOptions,
validateSourceFileOverrides,
} from "../BitNode/BitNodeUtils";
import { JSONMap } from "../Types/Jsonable";
export const helpers = {
string,
@ -83,6 +95,7 @@ export const helpers = {
getCannotFindRunningScriptErrorMessage,
createPublicRunningScript,
failOnHacknetServer,
validateBitNodeOptions,
};
/** RunOptions with non-optional, type-validated members, for passing between internal functions. */
@ -279,7 +292,7 @@ function validateHGWOptions(ctx: NetscriptContext, opts: unknown): CompleteHGWOp
/** Validate singularity access by throwing an error if the player does not have access. */
function checkSingularityAccess(ctx: NetscriptContext): void {
if (Player.bitNodeN !== 4 && Player.sourceFileLvl(4) === 0) {
if (!canAccessBitNodeFeature(4)) {
throw errorMessage(
ctx,
`This singularity function requires Source-File 4 to run. A power up you obtain later in the game.
@ -732,3 +745,40 @@ let customElementKey = 0;
export function wrapUserNode(value: unknown) {
return <CustomBoundary key={`PlayerContent${customElementKey++}`}>{value}</CustomBoundary>;
}
function validateBitNodeOptions(ctx: NetscriptContext, bitNodeOptions: unknown): BitNodeOptions {
const result = getDefaultBitNodeOptions();
if (bitNodeOptions == null) {
return result;
}
if (typeof bitNodeOptions !== "object") {
throw errorMessage(ctx, `bitNodeOptions must be an object if it's specified. It was ${bitNodeOptions}.`);
}
const options = bitNodeOptions as Unknownify<BitNodeOptions>;
if (!(options.sourceFileOverrides instanceof Map)) {
throw errorMessage(ctx, `sourceFileOverrides must be a Map.`);
}
const validationResultForSourceFileOverrides = validateSourceFileOverrides(options.sourceFileOverrides, true);
if (!validationResultForSourceFileOverrides.valid) {
throw errorMessage(
ctx,
`sourceFileOverrides is invalid. Reason: ${validationResultForSourceFileOverrides.message}`,
);
}
result.sourceFileOverrides = new JSONMap(options.sourceFileOverrides);
if (options.intelligenceOverride !== undefined) {
result.intelligenceOverride = number(ctx, "intelligenceOverride", options.intelligenceOverride);
} else {
result.intelligenceOverride = undefined;
}
result.restrictHomePCUpgrade = !!options.restrictHomePCUpgrade;
result.disableGang = !!options.disableGang;
result.disableCorporation = !!options.disableCorporation;
result.disableBladeburner = !!options.disableBladeburner;
result.disable4SData = !!options.disable4SData;
result.disableHacknetServer = !!options.disableHacknetServer;
result.disableSleeveExpAndAugmentation = !!options.disableSleeveExpAndAugmentation;
return result;
}

@ -86,10 +86,16 @@ export const RamCostConstants = {
function SF4Cost(cost: number): () => number {
return () => {
if (Player.bitNodeN === 4) return cost;
const sf4 = Player.sourceFileLvl(4);
if (sf4 <= 1) return cost * 16;
if (sf4 === 2) return cost * 4;
if (Player.bitNodeN === 4) {
return cost;
}
const sf4 = Player.activeSourceFileLvl(4);
if (sf4 <= 1) {
return cost * 16;
}
if (sf4 === 2) {
return cost * 4;
}
return cost;
};
}

@ -111,6 +111,7 @@ import { ServerConstants } from "./Server/data/Constants";
import { assertFunction } from "./Netscript/TypeAssertion";
import { Router } from "./ui/GameRoot";
import { Page } from "./ui/Router";
import { canAccessBitNodeFeature, validBitNodes } from "./BitNode/BitNodeUtils";
export const enums: NSEnums = {
CityName,
@ -972,13 +973,19 @@ export const ns: InternalAPI<NSFull> = {
},
getBitNodeMultipliers:
(ctx) =>
(_n = Player.bitNodeN, _lvl = Player.sourceFileLvl(Player.bitNodeN) + 1) => {
if (Player.sourceFileLvl(5) <= 0 && Player.bitNodeN !== 5)
(_n = Player.bitNodeN, _lvl = Player.activeSourceFileLvl(Player.bitNodeN) + 1) => {
if (!canAccessBitNodeFeature(5)) {
throw helpers.errorMessage(ctx, "Requires Source-File 5 to run.");
}
// TODO v3.0: check n and lvl with helpers.number() and Number.isInteger().
const n = Math.round(helpers.number(ctx, "n", _n));
const lvl = Math.round(helpers.number(ctx, "lvl", _lvl));
if (n < 1 || n > 14) throw new Error("n must be between 1 and 14");
if (lvl < 1) throw new Error("lvl must be >= 1");
if (!validBitNodes.includes(n)) {
throw new Error(`Invalid BitNode: ${n}.`);
}
if (lvl < 1) {
throw new Error("SF level must be greater than or equal to 1.");
}
return Object.assign({}, getBitNodeMultipliers(n, lvl));
},
@ -1800,6 +1807,10 @@ export const ns: InternalAPI<NSFull> = {
currentNode: Player.bitNodeN,
ownedAugs: new Map(Player.augmentations.map((aug) => [aug.name, aug.level])),
ownedSF: new Map(Player.sourceFiles),
bitNodeOptions: {
...Player.bitNodeOptions,
sourceFileOverrides: new Map(Player.bitNodeOptions.sourceFileOverrides),
},
}),
getFunctionRamCost: (ctx) => (_name) => {
const name = helpers.string(ctx, "name", _name);

@ -12,6 +12,7 @@ import { Skills } from "../Bladeburner/data/Skills";
import { assertString } from "../Netscript/TypeAssertion";
import { BlackOperations, blackOpsArray } from "../Bladeburner/data/BlackOperations";
import { checkSleeveAPIAccess, checkSleeveNumber } from "../NetscriptFunctions/Sleeve";
import { canAccessBitNodeFeature } from "../BitNode/BitNodeUtils";
export function NetscriptBladeburner(): InternalAPI<INetscriptBladeburner> {
const checkBladeburnerAccess = function (ctx: NetscriptContext): void {
@ -19,7 +20,7 @@ export function NetscriptBladeburner(): InternalAPI<INetscriptBladeburner> {
return;
};
const getBladeburner = function (ctx: NetscriptContext): Bladeburner {
const apiAccess = Player.bitNodeN === 7 || Player.sourceFileLvl(7) > 0;
const apiAccess = canAccessBitNodeFeature(7);
if (!apiAccess) {
throw helpers.errorMessage(ctx, "You have not unlocked the Bladeburner API.", "API ACCESS");
}
@ -298,28 +299,28 @@ export function NetscriptBladeburner(): InternalAPI<INetscriptBladeburner> {
return !!attempt.success;
},
joinBladeburnerDivision: (ctx) => () => {
if (Player.bitNodeN === 7 || Player.sourceFileLvl(7) > 0) {
if (!canAccessBitNodeFeature(7) || Player.bitNodeOptions.disableBladeburner) {
return false;
}
if (currentNodeMults.BladeburnerRank === 0) {
return false; // Disabled in this bitnode
}
if (Player.bladeburner) {
return true; // Already member
} else if (
Player.skills.strength >= 100 &&
Player.skills.defense >= 100 &&
Player.skills.dexterity >= 100 &&
Player.skills.agility >= 100
}
if (
Player.skills.strength < 100 ||
Player.skills.defense < 100 ||
Player.skills.dexterity < 100 ||
Player.skills.agility < 100
) {
helpers.log(ctx, () => "You do not meet the requirements for joining the Bladeburner division");
return false;
}
Player.startBladeburner();
helpers.log(ctx, () => "You have been accepted into the Bladeburner division");
return true;
} else {
helpers.log(ctx, () => "You do not meet the requirements for joining the Bladeburner division");
return false;
}
}
return false;
},
getBonusTime: (ctx) => () => {
const bladeburner = getBladeburner(ctx);

@ -59,6 +59,7 @@ import { ServerConstants } from "../Server/data/Constants";
import { blackOpsArray } from "../Bladeburner/data/BlackOperations";
import { calculateEffectiveRequiredReputation } from "../Company/utils";
import { calculateFavorAfterResetting } from "../Faction/formulas/favor";
import { validBitNodes } from "../BitNode/BitNodeUtils";
export function NetscriptSingularity(): InternalAPI<ISingularity> {
const runAfterReset = function (cbScript: ScriptFilePath) {
@ -628,7 +629,7 @@ export function NetscriptSingularity(): InternalAPI<ISingularity> {
// Check if we're at max cores
const homeComputer = Player.getHomeComputer();
if (homeComputer.cpuCores >= 8) {
if (Player.bitNodeOptions.restrictHomePCUpgrade || homeComputer.cpuCores >= 8) {
helpers.log(ctx, () => `Your home computer is at max cores.`);
return false;
}
@ -659,7 +660,10 @@ export function NetscriptSingularity(): InternalAPI<ISingularity> {
// Check if we're at max RAM
const homeComputer = Player.getHomeComputer();
if (homeComputer.maxRam >= ServerConstants.HomeComputerMaxRam) {
if (
(Player.bitNodeOptions.restrictHomePCUpgrade && homeComputer.maxRam >= 128) ||
homeComputer.maxRam >= ServerConstants.HomeComputerMaxRam
) {
helpers.log(ctx, () => `Your home computer is at max RAM.`);
return false;
}
@ -1137,38 +1141,47 @@ export function NetscriptSingularity(): InternalAPI<ISingularity> {
}
return item.price;
},
b1tflum3: (ctx) => (_nextBN, _cbScript) => {
b1tflum3: (ctx) => (_nextBN, _cbScript, _bitNodeOptions) => {
helpers.checkSingularityAccess(ctx);
const nextBN = helpers.number(ctx, "nextBN", _nextBN);
const cbScript = _cbScript
? resolveScriptFilePath(helpers.string(ctx, "cbScript", _cbScript), ctx.workerScript.name)
: false;
if (cbScript === null) throw helpers.errorMessage(ctx, `Could not resolve file path: ${_cbScript}`);
enterBitNode(true, Player.bitNodeN, nextBN);
if (cbScript) setTimeout(() => runAfterReset(cbScript), 500);
if (cbScript === null) {
throw helpers.errorMessage(ctx, `Could not resolve file path: ${_cbScript}`);
}
enterBitNode(true, Player.bitNodeN, nextBN, helpers.validateBitNodeOptions(ctx, _bitNodeOptions));
if (cbScript) {
setTimeout(() => runAfterReset(cbScript), 500);
}
},
destroyW0r1dD43m0n: (ctx) => (_nextBN, _cbScript) => {
destroyW0r1dD43m0n: (ctx) => (_nextBN, _cbScript, _bitNodeOptions) => {
helpers.checkSingularityAccess(ctx);
const nextBN = helpers.number(ctx, "nextBN", _nextBN);
if (nextBN > 14 || nextBN < 1 || !Number.isInteger(nextBN)) {
throw new Error(`Invalid bitnode specified: ${_nextBN}`);
if (!validBitNodes.includes(nextBN)) {
throw new Error(`Invalid BitNode: ${_nextBN}.`);
}
const cbScript = _cbScript
? resolveScriptFilePath(helpers.string(ctx, "cbScript", _cbScript), ctx.workerScript.name)
: false;
if (cbScript === null) throw helpers.errorMessage(ctx, `Could not resolve file path: ${_cbScript}`);
if (cbScript === null) {
throw helpers.errorMessage(ctx, `Could not resolve file path: ${_cbScript}`);
}
const wd = GetServer(SpecialServers.WorldDaemon);
if (!(wd instanceof Server)) {
throw new Error("WorldDaemon is not a normal server. This is a bug. Please contact developers.");
}
const hackingRequirements = () => {
if (Player.skills.hacking < wd.requiredHackingSkill) return false;
if (!wd.hasAdminRights) return false;
if (Player.skills.hacking < wd.requiredHackingSkill || !wd.hasAdminRights) {
return false;
}
return true;
};
const bladeburnerRequirements = () => {
if (!Player.bladeburner) return false;
if (!Player.bladeburner) {
return false;
}
return Player.bladeburner.numBlackOpsComplete >= blackOpsArray.length;
};
@ -1179,8 +1192,10 @@ export function NetscriptSingularity(): InternalAPI<ISingularity> {
wd.backdoorInstalled = true;
calculateAchievements();
enterBitNode(false, Player.bitNodeN, nextBN);
if (cbScript) setTimeout(() => runAfterReset(cbScript), 500);
enterBitNode(false, Player.bitNodeN, nextBN, helpers.validateBitNodeOptions(ctx, _bitNodeOptions));
if (cbScript) {
setTimeout(() => runAfterReset(cbScript), 500);
}
},
getCurrentWork: (ctx) => () => {
helpers.checkSingularityAccess(ctx);

@ -15,6 +15,7 @@ import { helpers } from "../Netscript/NetscriptHelpers";
import { getAugCost } from "../Augmentation/AugmentationHelpers";
import { Factions } from "../Faction/Factions";
import { SleeveWorkType } from "../PersonObjects/Sleeve/Work/Work";
import { canAccessBitNodeFeature } from "../BitNode/BitNodeUtils";
export const checkSleeveAPIAccess = function (ctx: NetscriptContext) {
if (Player.bitNodeN !== 10 && !Player.sourceFileLvl(10)) {
@ -33,6 +34,23 @@ export const checkSleeveNumber = function (ctx: NetscriptContext, sleeveNumber:
};
export function NetscriptSleeve(): InternalAPI<NetscriptSleeve> {
const checkSleeveAPIAccess = function (ctx: NetscriptContext) {
if (!canAccessBitNodeFeature(10)) {
throw helpers.errorMessage(
ctx,
"You do not have access to the Sleeve API. This is either because you are not in BitNode-10 or because you do not have Source-File 10.",
);
}
};
const checkSleeveNumber = function (ctx: NetscriptContext, sleeveNumber: number) {
if (sleeveNumber >= Player.sleeves.length || sleeveNumber < 0) {
const msg = `Invalid sleeve number: ${sleeveNumber}`;
helpers.log(ctx, () => msg);
throw helpers.errorMessage(ctx, msg);
}
};
const sleeveFunctions: InternalAPI<NetscriptSleeve> = {
getNumSleeves: (ctx) => () => {
checkSleeveAPIAccess(ctx);

@ -167,11 +167,9 @@ export function NetscriptStockMarket(): InternalAPI<TIX> {
const symbol = helpers.string(ctx, "symbol", _symbol);
const shares = helpers.number(ctx, "shares", _shares);
checkTixApiAccess(ctx);
if (Player.bitNodeN !== 8) {
if (Player.sourceFileLvl(8) <= 1) {
if (Player.bitNodeN !== 8 && Player.activeSourceFileLvl(8) <= 1) {
throw helpers.errorMessage(ctx, "You must either be in BitNode-8 or you must have Source-File 8 Level 2.");
}
}
const stock = getStockFromSymbol(ctx, symbol);
const res = shortStock(stock, shares, ctx, {});
@ -181,11 +179,9 @@ export function NetscriptStockMarket(): InternalAPI<TIX> {
const symbol = helpers.string(ctx, "symbol", _symbol);
const shares = helpers.number(ctx, "shares", _shares);
checkTixApiAccess(ctx);
if (Player.bitNodeN !== 8) {
if (Player.sourceFileLvl(8) <= 1) {
if (Player.bitNodeN !== 8 && Player.activeSourceFileLvl(8) <= 1) {
throw helpers.errorMessage(ctx, "You must either be in BitNode-8 or you must have Source-File 8 Level 2.");
}
}
const stock = getStockFromSymbol(ctx, symbol);
const res = sellShort(stock, shares, ctx, {});
@ -198,11 +194,9 @@ export function NetscriptStockMarket(): InternalAPI<TIX> {
const type = helpers.string(ctx, "type", _type);
const pos = helpers.string(ctx, "pos", _pos);
checkTixApiAccess(ctx);
if (Player.bitNodeN !== 8) {
if (Player.sourceFileLvl(8) <= 2) {
if (Player.bitNodeN !== 8 && Player.activeSourceFileLvl(8) <= 2) {
throw helpers.errorMessage(ctx, "You must either be in BitNode-8 or you must have Source-File 8 Level 3.");
}
}
const stock = getStockFromSymbol(ctx, symbol);
let orderType;
@ -238,11 +232,9 @@ export function NetscriptStockMarket(): InternalAPI<TIX> {
const type = helpers.string(ctx, "type", _type);
const pos = helpers.string(ctx, "pos", _pos);
checkTixApiAccess(ctx);
if (Player.bitNodeN !== 8) {
if (Player.sourceFileLvl(8) <= 2) {
if (Player.bitNodeN !== 8 && Player.activeSourceFileLvl(8) <= 2) {
throw helpers.errorMessage(ctx, "You must either be in BitNode-8 or you must have Source-File 8 Level 3.");
}
}
const stock = getStockFromSymbol(ctx, symbol);
if (isNaN(shares) || isNaN(price)) {
throw helpers.errorMessage(ctx, `Invalid shares or price. Must be numeric. shares=${shares}, price=${price}`);
@ -281,11 +273,9 @@ export function NetscriptStockMarket(): InternalAPI<TIX> {
},
getOrders: (ctx) => () => {
checkTixApiAccess(ctx);
if (Player.bitNodeN !== 8) {
if (Player.sourceFileLvl(8) <= 2) {
if (Player.bitNodeN !== 8 && Player.activeSourceFileLvl(8) <= 2) {
throw helpers.errorMessage(ctx, "You must either be in BitNode-8 or have Source-File 8 Level 3.");
}
}
const orders: StockOrder = {};
@ -328,6 +318,11 @@ export function NetscriptStockMarket(): InternalAPI<TIX> {
return forecast / 100; // Convert from percentage to decimal
},
purchase4SMarketData: (ctx) => () => {
if (Player.bitNodeOptions.disable4SData) {
helpers.log(ctx, () => "4S Market Data is disabled.");
return false;
}
if (Player.has4SData) {
helpers.log(ctx, () => "Already purchased 4S Market Data.");
return true;
@ -344,6 +339,11 @@ export function NetscriptStockMarket(): InternalAPI<TIX> {
return true;
},
purchase4SMarketDataTixApi: (ctx) => () => {
if (Player.bitNodeOptions.disable4SData) {
helpers.log(ctx, () => "4S Market Data is disabled.");
return false;
}
checkTixApiAccess(ctx);
if (Player.has4SDataTixApi) {

@ -1,4 +1,4 @@
import type { Player as IPlayer } from "@nsdefs";
import type { BitNodeOptions, Player as IPlayer } from "@nsdefs";
import type { PlayerAchievement } from "../../Achievements/Achievements";
import type { Bladeburner } from "../../Bladeburner/Bladeburner";
import type { Corporation } from "../../Corporation/Corporation";
@ -74,6 +74,22 @@ export class PlayerObject extends Person implements IPlayer {
entropy = 0;
bitNodeOptions: BitNodeOptions = {
sourceFileOverrides: new JSONMap<number, number>(),
intelligenceOverride: undefined,
restrictHomePCUpgrade: false,
disableGang: false,
disableCorporation: false,
disableBladeburner: false,
disable4SData: false,
disableHacknetServer: false,
disableSleeveExpAndAugmentation: false,
};
get activeSourceFiles(): JSONMap<number, number> {
return new JSONMap([...this.sourceFiles, ...this.bitNodeOptions.sourceFileOverrides]);
}
// Player-specific methods
init = generalMethods.init;
startWork = workMethods.startWork;
@ -129,6 +145,7 @@ export class PlayerObject extends Person implements IPlayer {
setBitNodeNumber = generalMethods.setBitNodeNumber;
canAccessCotMG = generalMethods.canAccessCotMG;
sourceFileLvl = generalMethods.sourceFileLvl;
activeSourceFileLvl = generalMethods.activeSourceFileLvl;
applyEntropy = augmentationMethods.applyEntropy;
focusPenalty = generalMethods.focusPenalty;

@ -1,9 +1,10 @@
import { canAccessBitNodeFeature } from "../../BitNode/BitNodeUtils";
import { Bladeburner } from "../../Bladeburner/Bladeburner";
import type { PlayerObject } from "./PlayerObject";
export function canAccessBladeburner(this: PlayerObject): boolean {
return this.bitNodeN === 6 || this.bitNodeN === 7 || this.sourceFileLvl(6) > 0 || this.sourceFileLvl(7) > 0;
return (canAccessBitNodeFeature(6) || canAccessBitNodeFeature(7)) && !this.bitNodeOptions.disableBladeburner;
}
export function startBladeburner(this: PlayerObject): void {

@ -3,9 +3,10 @@ import { resetIndustryResearchTrees } from "../../Corporation/data/IndustryData"
import { Corporation } from "../../Corporation/Corporation";
import type { PlayerObject } from "./PlayerObject";
import { canAccessBitNodeFeature } from "../../BitNode/BitNodeUtils";
export function canAccessCorporation(this: PlayerObject): boolean {
return this.bitNodeN === 3 || this.sourceFileLvl(3) > 0;
return canAccessBitNodeFeature(3) && !this.bitNodeOptions.disableCorporation;
}
export function startCorporation(this: PlayerObject, corpName: string, seedFunded: boolean): void {
@ -17,7 +18,7 @@ export function startCorporation(this: PlayerObject, corpName: string, seedFunde
//reset the research tree in case the corporation was restarted
resetIndustryResearchTrees();
if (this.bitNodeN === 3 || this.sourceFileLvl(3) === 3) {
if (this.bitNodeN === 3 || this.activeSourceFileLvl(3) === 3) {
this.corporation.unlocks.add(CorpUnlockName.WarehouseAPI);
this.corporation.unlocks.add(CorpUnlockName.OfficeAPI);
}

@ -6,12 +6,16 @@ import { Factions } from "../../Faction/Factions";
import { Gang } from "../../Gang/Gang";
import { GangConstants } from "../../Gang/data/Constants";
import { isFactionWork } from "../../Work/FactionWork";
import { canAccessBitNodeFeature } from "../../BitNode/BitNodeUtils";
export function canAccessGang(this: PlayerObject): boolean {
if (this.bitNodeOptions.disableGang) {
return false;
}
if (this.bitNodeN === 2) {
return true;
}
if (this.sourceFileLvl(2) <= 0) {
if (this.activeSourceFileLvl(2) === 0) {
return false;
}
@ -19,7 +23,7 @@ export function canAccessGang(this: PlayerObject): boolean {
}
export function isAwareOfGang(this: PlayerObject): boolean {
return this.bitNodeN === 2 || this.sourceFileLvl(2) >= 1;
return canAccessBitNodeFeature(2) && !this.bitNodeOptions.disableGang;
}
export function getGangFaction(this: PlayerObject): Faction {

@ -49,6 +49,7 @@ import { achievements } from "../../Achievements/Achievements";
import { isCompanyWork } from "../../Work/CompanyWork";
import { isMember } from "../../utils/EnumHelper";
import { canAccessBitNodeFeature } from "../../BitNode/BitNodeUtils";
export function init(this: PlayerObject): void {
/* Initialize Player's home computer */
@ -415,7 +416,7 @@ export function reapplyAllSourceFiles(this: PlayerObject): void {
//Will always be called after reapplyAllAugmentations() so multipliers do not have to be reset
//this.resetMultipliers();
for (const [bn, lvl] of this.sourceFiles) {
for (const [bn, lvl] of this.activeSourceFiles) {
const srcFileKey = "SourceFile" + bn;
const sourceFileObject = SourceFiles[srcFileKey];
if (!sourceFileObject) {
@ -536,7 +537,7 @@ export function gotoLocation(this: PlayerObject, to: LocationName): boolean {
}
export function canAccessGrafting(this: PlayerObject): boolean {
return this.bitNodeN === 10 || this.sourceFileLvl(10) > 0;
return canAccessBitNodeFeature(10);
}
export function giveExploit(this: PlayerObject, exploit: Exploit): void {
@ -560,13 +561,20 @@ export function getCasinoWinnings(this: PlayerObject): number {
}
export function canAccessCotMG(this: PlayerObject): boolean {
return this.bitNodeN === 13 || this.sourceFileLvl(13) > 0;
return canAccessBitNodeFeature(13);
}
export function sourceFileLvl(this: PlayerObject, n: number): number {
return this.sourceFiles.get(n) ?? 0;
}
export function activeSourceFileLvl(this: PlayerObject, n: number): number {
if (this.bitNodeOptions.sourceFileOverrides.has(n)) {
return this.bitNodeOptions.sourceFileOverrides.get(n) ?? 0;
}
return this.sourceFiles.get(n) ?? 0;
}
export function focusPenalty(this: PlayerObject): number {
let focus = 1;
if (!this.hasAugmentation(AugmentationName.NeuroreceptorManager, true)) {

@ -1,5 +1,4 @@
import { Sleeve } from "../Sleeve";
import { Player } from "@player";
import { formatExp, formatPercent } from "../../../ui/formatNumber";
import { convertTimeMsToTimeElapsedString } from "../../../utils/StringHelperFunctions";
import { CONSTANTS } from "../../../Constants";
@ -7,6 +6,7 @@ import { Typography } from "@mui/material";
import { StatsTable } from "../../../ui/React/StatsTable";
import { Modal } from "../../../ui/React/Modal";
import React from "react";
import { canAccessBitNodeFeature } from "../../../BitNode/BitNodeUtils";
interface IProps {
open: boolean;
@ -30,7 +30,7 @@ export function MoreStatsModal(props: IProps): React.ReactElement {
[<>Agility:&nbsp;</>, props.sleeve.skills.agility, <>&nbsp;({formatExp(props.sleeve.exp.agility)} exp)</>],
[<>Charisma:&nbsp;</>, props.sleeve.skills.charisma, <>&nbsp;({formatExp(props.sleeve.exp.charisma)} exp)</>],
[
...(Player.sourceFileLvl(5) > 0 || Player.bitNodeN === 5
...(canAccessBitNodeFeature(5)
? [
<>Intelligence:&nbsp;</>,
props.sleeve.skills.intelligence,

@ -25,6 +25,7 @@ import { isSleeveClassWork } from "../Work/SleeveClassWork";
import { isSleeveFactionWork } from "../Work/SleeveFactionWork";
import { isSleeveCompanyWork } from "../Work/SleeveCompanyWork";
import { isSleeveCrimeWork } from "../Work/SleeveCrimeWork";
import { canAccessBitNodeFeature } from "../../../BitNode/BitNodeUtils";
const CYCLES_PER_SEC = 1000 / CONSTANTS.MilliPerCycle;
@ -76,7 +77,7 @@ export function StatsElement(props: IProps): React.ReactElement {
color={Settings.theme.cha}
data={{ level: props.sleeve.skills.charisma, exp: props.sleeve.exp.charisma }}
/>
{(Player.sourceFileLvl(5) > 0 || Player.bitNodeN === 5) && (
{canAccessBitNodeFeature(5) && (
<StatsRow
name="Intelligence"
color={Settings.theme.int}
@ -111,7 +112,7 @@ export function StatsElement(props: IProps): React.ReactElement {
export function EarningsElement(props: IProps): React.ReactElement {
const { classes } = useStyles();
let data: (string | JSX.Element)[][] = [];
let data: [string, string | JSX.Element][] = [];
if (isSleeveCrimeWork(props.sleeve.currentWork)) {
const gains = props.sleeve.currentWork.getExp(props.sleeve);
data = [
@ -150,9 +151,9 @@ export function EarningsElement(props: IProps): React.ReactElement {
];
}
companyWork: if (isSleeveCompanyWork(props.sleeve.currentWork)) {
if (isSleeveCompanyWork(props.sleeve.currentWork)) {
const job = Player.jobs[props.sleeve.currentWork.companyName];
if (!job) break companyWork;
if (job) {
const rates = props.sleeve.currentWork.getGainRates(props.sleeve, job);
data = [
[`Money:`, <MoneyRate key="money-rate" money={CYCLES_PER_SEC * rates.money} />],
@ -165,6 +166,7 @@ export function EarningsElement(props: IProps): React.ReactElement {
[`Reputation:`, <ReputationRate key="reputation-rate" reputation={CYCLES_PER_SEC * rates.reputation} />],
];
}
}
return (
<Table sx={{ display: "table", mb: 1, width: "100%", lineHeight: 0 }}>

@ -1,3 +1,9 @@
import { Player } from "@player";
export function calculateIntelligenceBonus(intelligence: number, weight = 1): number {
return 1 + (weight * Math.pow(intelligence, 0.8)) / 600;
const effectiveIntelligence =
Player.bitNodeOptions.intelligenceOverride !== undefined
? Math.min(Player.bitNodeOptions.intelligenceOverride, intelligence)
: intelligence;
return 1 + (weight * Math.pow(effectiveIntelligence, 0.8)) / 600;
}

@ -28,6 +28,7 @@ import { initCircadianModulator } from "./Augmentation/Augmentations";
import { Go } from "./Go/Go";
import { calculateExp } from "./PersonObjects/formulas/skill";
import { currentNodeMults } from "./BitNode/BitNodeMultipliers";
import { canAccessBitNodeFeature } from "./BitNode/BitNodeUtils";
const BitNode8StartingMoney = 250e6;
function delayedDialog(message: string) {
@ -82,7 +83,7 @@ export function prestigeAugmentation(): void {
homeComp.programs.push(program);
}
}
if (Player.sourceFileLvl(5) > 0 || Player.bitNodeN === 5) {
if (canAccessBitNodeFeature(5)) {
homeComp.programs.push(CompletedProgramName.formulas);
}
@ -146,7 +147,7 @@ export function prestigeAugmentation(): void {
if (Player.bitNodeN === 8) {
Player.money = BitNode8StartingMoney;
}
if (Player.bitNodeN === 8 || Player.sourceFileLvl(8) > 0) {
if (canAccessBitNodeFeature(8)) {
Player.hasWseAccount = true;
Player.hasTixApiAccess = true;
}
@ -169,7 +170,7 @@ export function prestigeAugmentation(): void {
// Bitnode 13: Church of the Machine God
if (Player.hasAugmentation(AugmentationName.StaneksGift1, true)) {
joinFaction(Factions[FactionName.ChurchOfTheMachineGod]);
} else if (Player.bitNodeN != 13) {
} else if (Player.bitNodeN !== 13) {
if (Player.augmentations.some((a) => a.name !== AugmentationName.NeuroFluxGovernor)) {
Factions[FactionName.ChurchOfTheMachineGod].isBanned = true;
}
@ -215,9 +216,9 @@ export function prestigeSourceFile(isFlume: boolean): void {
// Re-create foreign servers
initForeignServers(Player.getHomeComputer());
if (Player.sourceFileLvl(9) >= 2) {
if (Player.activeSourceFileLvl(9) >= 2) {
homeComp.setMaxRam(128);
} else if (Player.sourceFileLvl(1) > 0) {
} else if (Player.activeSourceFileLvl(1) > 0) {
homeComp.setMaxRam(32);
} else {
homeComp.setMaxRam(8);
@ -234,10 +235,10 @@ export function prestigeSourceFile(isFlume: boolean): void {
}
// Give levels of NeuroFluxGovernor for Source-File 12. Must be done here before Augmentations are recalculated
if (Player.sourceFileLvl(12) > 0) {
if (Player.activeSourceFileLvl(12) > 0) {
Player.augmentations.push({
name: AugmentationName.NeuroFluxGovernor,
level: Player.sourceFileLvl(12),
level: Player.activeSourceFileLvl(12),
});
}
@ -246,7 +247,7 @@ export function prestigeSourceFile(isFlume: boolean): void {
Player.reapplyAllAugmentations();
Player.reapplyAllSourceFiles();
if (Player.sourceFileLvl(5) > 0 || Player.bitNodeN === 5) {
if (canAccessBitNodeFeature(5)) {
homeComp.programs.push(CompletedProgramName.formulas);
}
@ -269,7 +270,7 @@ export function prestigeSourceFile(isFlume: boolean): void {
if (Player.bitNodeN === 8) {
Player.money = BitNode8StartingMoney;
}
if (Player.bitNodeN === 8 || Player.sourceFileLvl(8) > 0) {
if (Player.bitNodeN === 8 || Player.activeSourceFileLvl(8) > 0) {
Player.hasWseAccount = true;
Player.hasTixApiAccess = true;
}
@ -282,7 +283,7 @@ export function prestigeSourceFile(isFlume: boolean): void {
}
// BitNode 12: The Recursion
if (Player.bitNodeN === 12 && Player.sourceFileLvl(12) > 100) {
if (Player.bitNodeN === 12 && Player.activeSourceFileLvl(12) > 100) {
delayedDialog("Saynt_Garmo is watching you");
}
@ -301,7 +302,7 @@ export function prestigeSourceFile(isFlume: boolean): void {
// Source-File 9 (level 3) effect
// also now applies when entering bn9 until install
if (Player.sourceFileLvl(9) >= 3 || Player.bitNodeN === 9) {
if ((Player.activeSourceFileLvl(9) >= 3 || Player.bitNodeN === 9) && !Player.bitNodeOptions.disableHacknetServer) {
const hserver = Player.createHacknetServer();
hserver.level = 100;
@ -319,7 +320,9 @@ export function prestigeSourceFile(isFlume: boolean): void {
staneksGift.prestigeSourceFile();
// Gain int exp
if (Player.sourceFileLvl(5) !== 0 && !isFlume) Player.gainIntelligenceExp(300);
if (Player.activeSourceFileLvl(5) !== 0 && !isFlume) {
Player.gainIntelligenceExp(300);
}
// Clear recent scripts
recentScripts.splice(0, recentScripts.length);

@ -13,6 +13,7 @@ import { calculateHackingTime, calculateGrowTime, calculateWeakenTime } from "..
import { CompletedProgramName, FactionName } from "@enums";
import { Router } from "../ui/GameRoot";
import { Page } from "../ui/Router";
import { knowAboutBitverse } from "../BitNode/BitNodeUtils";
function requireHackingLevel(lvl: number) {
return function () {
@ -22,7 +23,7 @@ function requireHackingLevel(lvl: number) {
function bitFlumeRequirements() {
return function () {
return Player.sourceFiles.size > 0 && Player.skills.hacking >= 1;
return knowAboutBitverse() && Player.skills.hacking >= 1;
};
}

@ -1,12 +1,14 @@
/** Implementation for what happens when you destroy a BitNode */
import React from "react";
import { Player } from "@player";
import { type BitNodeOptions } from "@nsdefs";
import { SourceFiles } from "./SourceFile/SourceFiles";
import { dialogBoxCreate } from "./ui/React/DialogBox";
import { Router } from "./ui/GameRoot";
import { Page } from "./ui/Router";
import { prestigeSourceFile } from "./Prestige";
import { setBitNodeOptions } from "./BitNode/BitNodeUtils";
function giveSourceFile(bitNodeNumber: number): void {
const sourceFileKey = "SourceFile" + bitNodeNumber.toString();
@ -48,7 +50,12 @@ function giveSourceFile(bitNodeNumber: number): void {
}
}
export function enterBitNode(isFlume: boolean, destroyedBitNode: number, newBitNode: number): void {
export function enterBitNode(
isFlume: boolean,
destroyedBitNode: number,
newBitNode: number,
bitNodeOptions: BitNodeOptions,
): void {
if (!isFlume) {
giveSourceFile(destroyedBitNode);
} else if (Player.sourceFileLvl(5) === 0 && newBitNode !== 5) {
@ -61,6 +68,9 @@ export function enterBitNode(isFlume: boolean, destroyedBitNode: number, newBitN
// Set new Bit Node
Player.bitNodeN = newBitNode;
// Set BitNode options
setBitNodeOptions(bitNodeOptions);
if (newBitNode === 6) {
Router.toPage(Page.BladeburnerCinematic);
} else {

@ -76,6 +76,8 @@ interface ResetInfo {
ownedAugs: Map<string, number>;
/** A map of owned SF to their levels. Keyed by the SF number. Map values are the SF level. */
ownedSF: Map<number, number>;
/** Current BitNode options */
bitNodeOptions: BitNodeOptions;
}
/** @public */
@ -1701,6 +1703,36 @@ export interface GraftingTask {
*/
export type Task = StudyTask | CompanyWorkTask | CreateProgramWorkTask | CrimeTask | FactionWorkTask | GraftingTask;
/**
* Default value:
* - sourceFileOverrides: an empty Map
* - intelligenceOverride: undefined
* - All boolean options: false
*
* If you specify intelligenceOverride, it must be a non-negative integer.
*/
export interface BitNodeOptions extends BitNodeBooleanOptions {
sourceFileOverrides: Map<number, number>;
intelligenceOverride: number | undefined;
}
/**
* restrictHomePCUpgrade: The home computer's maximum RAM and number of cores are lower than normal. Max RAM: 128GB. Max
* core: 1.
*
* disableSleeveExpAndAugmentation: Your Sleeves do not gain experience when they perform action. You also cannot buy
* augmentations for them.
*/
export interface BitNodeBooleanOptions {
restrictHomePCUpgrade: boolean;
disableGang: boolean;
disableCorporation: boolean;
disableBladeburner: boolean;
disable4SData: boolean;
disableHacknetServer: boolean;
disableSleeveExpAndAugmentation: boolean;
}
/**
* Singularity API
* @remarks
@ -2615,8 +2647,9 @@ export interface Singularity {
*
* @param nextBN - BN number to jump to
* @param callbackScript - Name of the script to launch in the next BN.
* @param bitNodeOptions - BitNode options for the next BN.
*/
b1tflum3(nextBN: number, callbackScript?: string): void;
b1tflum3(nextBN: number, callbackScript?: string, bitNodeOptions?: BitNodeOptions): void;
/**
* Destroy the w0r1d_d43m0n and move on to the next BN.
@ -2629,8 +2662,9 @@ export interface Singularity {
*
* @param nextBN - BN number to jump to
* @param callbackScript - Name of the script to launch in the next BN.
* @param bitNodeOptions - BitNode options for the next BN.
*/
destroyW0r1dD43m0n(nextBN: number, callbackScript?: string): void;
destroyW0r1dD43m0n(nextBN: number, callbackScript?: string, bitNodeOptions?: BitNodeOptions): void;
/**
* Get the current work the player is doing.

@ -164,7 +164,10 @@ export function purchaseRamForHomeComputer(): void {
}
const homeComputer = Player.getHomeComputer();
if (homeComputer.maxRam >= ServerConstants.HomeComputerMaxRam) {
if (
(Player.bitNodeOptions.restrictHomePCUpgrade && homeComputer.maxRam >= 128) ||
homeComputer.maxRam >= ServerConstants.HomeComputerMaxRam
) {
dialogBoxCreate(`You cannot upgrade your home computer RAM because it is at its maximum possible value`);
return;
}

@ -56,6 +56,7 @@ import { hash } from "../../hash/hash";
import { Locations } from "../../Locations/Locations";
import { useRerender } from "../../ui/React/hooks";
import { playerHasDiscoveredGo } from "../../Go/effects/effect";
import { knowAboutBitverse } from "../../BitNode/BitNodeUtils";
const RotatedDoubleArrowIcon = React.forwardRef(function RotatedDoubleArrowIcon(
props: { color: "primary" | "secondary" | "error" },
@ -141,12 +142,12 @@ export function SidebarRoot(props: { page: Page }): React.ReactElement {
Player.factions.length > 0 ||
Player.augmentations.length > 0 ||
Player.queuedAugmentations.length > 0 ||
Player.sourceFiles.size > 0;
knowAboutBitverse();
const canOpenAugmentations =
Player.augmentations.length > 0 ||
Player.queuedAugmentations.length > 0 ||
Player.sourceFiles.size > 0 ||
knowAboutBitverse() ||
Player.exploits.length > 0;
const canOpenSleeves = Player.sleeves.length > 0;

@ -105,6 +105,9 @@ function PurchaseWseAccountButton(props: IProps): React.ReactElement {
function PurchaseTixApiAccessButton(props: IProps): React.ReactElement {
function purchaseTixApiAccess(): void {
if (Player.bitNodeOptions.disable4SData) {
return;
}
if (Player.hasTixApiAccess) {
return;
}
@ -135,6 +138,9 @@ function PurchaseTixApiAccessButton(props: IProps): React.ReactElement {
function Purchase4SMarketDataButton(props: IProps): React.ReactElement {
function purchase4SMarketData(): void {
if (Player.bitNodeOptions.disable4SData) {
return;
}
if (Player.has4SData) {
return;
}
@ -184,18 +190,22 @@ export function InfoAndPurchases(props: IProps): React.ReactElement {
the TIX API lets you write code to create your own algorithmic/automated trading strategies.
</Typography>
<PurchaseTixApiAccessButton {...props} />
{!Player.bitNodeOptions.disable4SData && (
<>
<Typography variant="h5" color="primary">
{FactionName.FourSigma} (4S) Market Data Feed
</Typography>
<Typography>
{FactionName.FourSigma}'s (4S) Market Data Feed provides information about stocks that will help your trading
strategies.
{FactionName.FourSigma}'s (4S) Market Data Feed provides information about stocks that will help your
trading strategies.
<IconButton onClick={() => setHelpOpen(true)}>
<HelpIcon />
</IconButton>
</Typography>
<Purchase4SMarketDataTixApiAccessButton {...props} />
<Purchase4SMarketDataButton {...props} />
</>
)}
<Typography>
Commission Fees: Every transaction you make has a{" "}
<Money money={StockMarketConstants.StockMarketCommission} forPurchase={true} /> commission fee.

@ -270,12 +270,12 @@ export function StockTicker(props: IProps): React.ReactElement {
// Whether the player has access to orders besides market orders (limit/stop)
function hasOrderAccess(): boolean {
return Player.bitNodeN === 8 || Player.sourceFileLvl(8) >= 3;
return Player.bitNodeN === 8 || Player.activeSourceFileLvl(8) >= 3;
}
// Whether the player has access to shorting stocks
function hasShortAccess(): boolean {
return Player.bitNodeN === 8 || Player.sourceFileLvl(8) >= 2;
return Player.bitNodeN === 8 || Player.activeSourceFileLvl(8) >= 2;
}
return (

@ -65,7 +65,7 @@ function ShortPosition(props: IProps): React.ReactElement {
percentageGains = 0;
}
if (Player.bitNodeN === 8 || Player.sourceFileLvl(8) >= 2) {
if (Player.bitNodeN === 8 || Player.activeSourceFileLvl(8) >= 2) {
return (
<>
<Box display="flex">

@ -20,6 +20,20 @@ import { Company } from "../Company/Company";
import { CompanyPosition } from "../Company/CompanyPosition";
import { isMember } from "../utils/EnumHelper";
function processWorkStats(person: IPerson, workStats: WorkStats): WorkStats {
// "person" can be a normal object that the player passes to NS APIs, so we cannot use `person instanceof Sleeve`.
if (Player.bitNodeOptions.disableSleeveExpAndAugmentation && "shock" in person) {
workStats.hackExp = 0;
workStats.strExp = 0;
workStats.defExp = 0;
workStats.dexExp = 0;
workStats.agiExp = 0;
workStats.chaExp = 0;
workStats.intExp = 0;
}
return workStats;
}
const gameCPS = 1000 / CONSTANTS.MilliPerCycle; // 5 cycles per second
export const FactionWorkStats: Record<FactionWorkType, WorkStats> = {
[FactionWorkType.hacking]: newWorkStats({ hackExp: 2 }),
@ -60,7 +74,7 @@ export function calculateCrimeWorkStats(person: IPerson, crime: Crime): WorkStat
currentNodeMults.CrimeExpGain,
false,
);
return gains;
return processWorkStats(person, gains);
}
/** @returns faction rep rate per cycle */
@ -75,9 +89,9 @@ export const calculateFactionRep = (person: IPerson, type: FactionWorkType, favo
/** @returns per-cycle WorkStats */
export function calculateFactionExp(person: IPerson, type: FactionWorkType): WorkStats {
return scaleWorkStats(
multWorkStats(FactionWorkStats[type], person.mults),
currentNodeMults.FactionWorkExpGain / gameCPS,
return processWorkStats(
person,
scaleWorkStats(multWorkStats(FactionWorkStats[type], person.mults), currentNodeMults.FactionWorkExpGain / gameCPS),
);
}
@ -102,7 +116,7 @@ export function calculateClassEarnings(person: IPerson, type: ClassType, locatio
person.mults,
);
earnings.money = calculateCost(classs, location) / gameCPS;
return earnings;
return processWorkStats(person, earnings);
}
/** @returns per-cycle WorkStats */
@ -114,7 +128,7 @@ export const calculateCompanyWorkStats = (
): WorkStats => {
// If player has SF-11, calculate salary multiplier from favor
const favorMult = isNaN(favor) ? 1 : 1 + favor / 100;
const bn11Mult = Player.sourceFileLvl(11) > 0 ? favorMult : 1;
const bn11Mult = Player.activeSourceFileLvl(11) > 0 ? favorMult : 1;
const gains = scaleWorkStats(
multWorkStats(
@ -138,5 +152,5 @@ export const calculateCompanyWorkStats = (
gains.reputation = jobPerformance * worker.mults.company_rep * favorMult * currentNodeMults.CompanyWorkRepGain;
return gains;
return processWorkStats(worker, gains);
};

@ -17,6 +17,7 @@ import { StatsRow } from "./React/StatsRow";
import { StatsTable } from "./React/StatsTable";
import { useRerender } from "./React/hooks";
import { getMaxFavor } from "../Go/effects/effect";
import { canAccessBitNodeFeature, knowAboutBitverse } from "../BitNode/BitNodeUtils";
interface EmployersModalProps {
open: boolean;
@ -68,7 +69,7 @@ function MultiplierTable(props: MultTableProps): React.ReactElement {
{props.rows.map((data) => {
const { mult, value, effValue = null, color = props.color } = data;
if (effValue !== null && effValue !== value && Player.sourceFileLvl(5) > 0) {
if (effValue !== null && effValue !== value && canAccessBitNodeFeature(5)) {
return (
<StatsRow key={mult} name={mult} color={color} data={{}}>
<>
@ -100,9 +101,9 @@ function MultiplierTable(props: MultTableProps): React.ReactElement {
}
function CurrentBitNode(): React.ReactElement {
if (Player.sourceFiles.size > 0) {
if (knowAboutBitverse()) {
const index = "BitNode" + Player.bitNodeN;
const lvl = Math.min(Player.sourceFileLvl(Player.bitNodeN) + 1, Player.bitNodeN === 12 ? Infinity : 3);
const lvl = Math.min(Player.sourceFileLvl(Player.bitNodeN) + 1, Player.bitNodeN === 12 ? Number.MAX_VALUE : 3);
return (
<Paper sx={{ mb: 1, p: 1 }}>
<Typography variant="h5">
@ -194,7 +195,7 @@ function MoneyModal({ open, onClose }: IMoneyModalProps): React.ReactElement {
{convertMoneySourceTrackerToString(Player.moneySourceA)}
</>
);
if (Player.sourceFiles.size > 0) {
if (knowAboutBitverse()) {
content = (
<>
{content}
@ -224,7 +225,7 @@ export function CharacterStats(): React.ReactElement {
const timeRows = [
["Since last Augmentation installation", convertTimeMsToTimeElapsedString(Player.playtimeSinceLastAug)],
];
if (Player.sourceFiles.size > 0) {
if (knowAboutBitverse()) {
timeRows.push(["Since last Bitnode destroyed", convertTimeMsToTimeElapsedString(Player.playtimeSinceLastBitnode)]);
}
timeRows.push(["Total", convertTimeMsToTimeElapsedString(Player.totalPlaytime)]);
@ -265,13 +266,11 @@ export function CharacterStats(): React.ReactElement {
data={{ content: `${Player.purchasedServers.length} / ${getPurchaseServerLimit()}` }}
/>
<StatsRow
name={`Hacknet ${Player.bitNodeN === 9 || Player.sourceFileLvl(9) > 0 ? "Servers" : "Nodes"} owned`}
name={`Hacknet ${canAccessBitNodeFeature(9) ? "Servers" : "Nodes"} owned`}
color={Settings.theme.primary}
data={{
content: `${Player.hacknetNodes.length}${
Player.bitNodeN === 9 || Player.sourceFileLvl(9) > 0
? ` / ${HacknetServerConstants.MaxServers}`
: ""
canAccessBitNodeFeature(9) ? ` / ${HacknetServerConstants.MaxServers}` : ""
}`,
}}
/>
@ -317,7 +316,7 @@ export function CharacterStats(): React.ReactElement {
color={Settings.theme.cha}
data={{ level: Player.skills.charisma, exp: Player.exp.charisma }}
/>
{Player.skills.intelligence > 0 && (Player.bitNodeN === 5 || Player.sourceFileLvl(5) > 0) && (
{Player.skills.intelligence > 0 && canAccessBitNodeFeature(5) && (
<StatsRow
name="Intelligence"
color={Settings.theme.int}
@ -332,7 +331,7 @@ export function CharacterStats(): React.ReactElement {
<Paper sx={{ p: 1, mb: 1 }}>
<Typography variant="h5" color="primary" sx={{ display: "flex", alignItems: "center", flexWrap: "wrap" }}>
Multipliers
{Player.sourceFileLvl(5) > 0 && (
{canAccessBitNodeFeature(5) && (
<Tooltip
title={
<Typography>
@ -553,15 +552,15 @@ export function CharacterStats(): React.ReactElement {
},
]}
color={Settings.theme.primary}
noMargin={!Player.sourceFileLvl(14) && Player.bitNodeN !== 14}
noMargin={!canAccessBitNodeFeature(14)}
/>
)}
{(Player.sourceFileLvl(14) || Player.bitNodeN === 14) && (
{canAccessBitNodeFeature(14) && (
<MultiplierTable
rows={[
{
mult: "IPvGO Node Power bonus",
value: Player.sourceFileLvl(14) ? 2 * currentNodeMults.GoPower : currentNodeMults.GoPower,
value: Player.activeSourceFileLvl(14) ? 2 * currentNodeMults.GoPower : currentNodeMults.GoPower,
},
{
mult: "IPvGO Max Favor",
@ -590,7 +589,7 @@ export function CharacterStats(): React.ReactElement {
<CurrentBitNode />
{(Player.bitNodeN === 5 || Player.sourceFileLvl(5) > 0) && (
{canAccessBitNodeFeature(5) && (
<Paper sx={{ p: 1, mb: 1 }}>
<Typography variant="h5">BitNode Multipliers</Typography>
<BitNodeMultipliersDisplay n={Player.bitNodeN} />

@ -118,6 +118,20 @@ export function Val({ name, color }: ValProps): React.ReactElement {
return clearSubscription;
}, [name]);
if (
name === "Int" &&
Player.bitNodeOptions.intelligenceOverride !== undefined &&
Player.bitNodeOptions.intelligenceOverride < Player.skills.intelligence
) {
return (
<Tooltip title={`Intelligence: ${formatSkill(Player.skills.intelligence)}`}>
<Typography color={color}>
{formatSkill(Player.bitNodeOptions.intelligenceOverride)}
<sup>*</sup>
</Typography>
</Tooltip>
);
}
return <Typography color={color}>{formattedVals[name]()}</Typography>;
}

@ -30,8 +30,8 @@ export function OptionSwitch({
disabled={disabled}
control={<Switch checked={value} onChange={handleSwitchChange} />}
label={
<Tooltip title={<Typography>{tooltip}</Typography>}>
<Typography>{text}</Typography>
<Tooltip title={<Typography component="div">{tooltip}</Typography>}>
<Typography component="div">{text}</Typography>
</Tooltip>
}
/>

@ -56,6 +56,19 @@ exports[`Check Save File Continuity PlayerSave continuity 1`] = `
"achievements": [],
"augmentations": [],
"bitNodeN": 1,
"bitNodeOptions": {
"disable4SData": false,
"disableBladeburner": false,
"disableCorporation": false,
"disableGang": false,
"disableHacknetServer": false,
"disableSleeveExpAndAugmentation": false,
"restrictHomePCUpgrade": false,
"sourceFileOverrides": {
"ctor": "JSONMap",
"data": [],
},
},
"bladeburner": {
"ctor": "Bladeburner",
"data": {