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 | | [AutocompleteData](./bitburner.autocompletedata.md) | Used for autocompletion |
| [BackdoorRequirement](./bitburner.backdoorrequirement.md) | Player must have installed a backdoor on this server. | | [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)<!-- -->. | | [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. | | [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. | | [BitNodeRequirement](./bitburner.bitnoderequirement.md) | Player must be located in this BitNode. |
| [Bladeburner](./bitburner.bladeburner.md) | Bladeburner API | | [Bladeburner](./bitburner.bladeburner.md) | Bladeburner API |
| [BladeburnerCurAction](./bitburner.bladeburnercuraction.md) | Bladeburner current action. | | [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 | | 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 | | [currentNode](./bitburner.resetinfo.currentnode.md) | | number | The current bitnode |
| [lastAugReset](./bitburner.resetinfo.lastaugreset.md) | | number | Numeric timestamp (from Date.now()) of last augmentation reset | | [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 | | [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:** **Signature:**
```typescript ```typescript
b1tflum3(nextBN: number, callbackScript?: string): void; b1tflum3(nextBN: number, callbackScript?: string, bitNodeOptions?: BitNodeOptions): void;
``` ```
## Parameters ## Parameters
@ -18,6 +18,7 @@ b1tflum3(nextBN: number, callbackScript?: string): void;
| --- | --- | --- | | --- | --- | --- |
| nextBN | number | BN number to jump to | | nextBN | number | BN number to jump to |
| callbackScript | string | _(Optional)_ Name of the script to launch in the next BN. | | 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:** **Returns:**

@ -9,7 +9,7 @@ Destroy the w0r1d\_d43m0n and move on to the next BN.
**Signature:** **Signature:**
```typescript ```typescript
destroyW0r1dD43m0n(nextBN: number, callbackScript?: string): void; destroyW0r1dD43m0n(nextBN: number, callbackScript?: string, bitNodeOptions?: BitNodeOptions): void;
``` ```
## Parameters ## Parameters
@ -18,6 +18,7 @@ destroyW0r1dD43m0n(nextBN: number, callbackScript?: string): void;
| --- | --- | --- | | --- | --- | --- |
| nextBN | number | BN number to jump to | | nextBN | number | BN number to jump to |
| callbackScript | string | _(Optional)_ Name of the script to launch in the next BN. | | 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:** **Returns:**

@ -21,12 +21,12 @@ This API requires Source-File 4 to use. The RAM cost of all these functions is m
| Method | Description | | Method | Description |
| --- | --- | | --- | --- |
| [applyToCompany(companyName, field)](./bitburner.singularity.applytocompany.md) | Apply for a job at a company. | | [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. | | [checkFactionInvitations()](./bitburner.singularity.checkfactioninvitations.md) | List all current faction invitations. |
| [commitCrime(crime, focus)](./bitburner.singularity.commitcrime.md) | Commit a crime. | | [commitCrime(crime, focus)](./bitburner.singularity.commitcrime.md) | Commit a crime. |
| [connect(hostname)](./bitburner.singularity.connect.md) | Connect to a server. | | [connect(hostname)](./bitburner.singularity.connect.md) | Connect to a server. |
| [createProgram(program, focus)](./bitburner.singularity.createprogram.md) | Create a program. | | [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. | | [donateToFaction(faction, amount)](./bitburner.singularity.donatetofaction.md) | Donate to a faction. |
| [exportGame()](./bitburner.singularity.exportgame.md) | Backup game save. | | [exportGame()](./bitburner.singularity.exportgame.md) | Backup game save. |
| [exportGameBonus()](./bitburner.singularity.exportgamebonus.md) | Returns Backup save bonus availability. | | [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 { getRecordValues } from "../Types/Record";
import { ServerConstants } from "../Server/data/Constants"; 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... // Unable to correctly cast the JSON data into AchievementDataJson type otherwise...
const achievementData = (<AchievementDataJson>(<unknown>data)).achievements; const achievementData = (<AchievementDataJson>(<unknown>data)).achievements;
@ -60,14 +60,6 @@ export interface AchievementData {
Description: string; 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> { function sfAchievements(): Record<string, Achievement> {
const achs: Record<string, Achievement> = {}; const achs: Record<string, Achievement> = {};
for (let i = 1; i <= 12; i++) { for (let i = 1; i <= 12; i++) {
@ -75,7 +67,7 @@ function sfAchievements(): Record<string, Achievement> {
achs[ID] = { achs[ID] = {
...achievementData[ID], ...achievementData[ID],
Icon: ID, Icon: ID,
Visible: knowsAboutBitverse, Visible: knowAboutBitverse,
Condition: () => Player.sourceFileLvl(i) >= 1, Condition: () => Player.sourceFileLvl(i) >= 1,
}; };
} }
@ -498,7 +490,7 @@ export const achievements: Record<string, Achievement> = {
INDECISIVE: { INDECISIVE: {
...achievementData.INDECISIVE, ...achievementData.INDECISIVE,
Icon: "1H", Icon: "1H",
Visible: knowsAboutBitverse, Visible: knowAboutBitverse,
Condition: (function () { Condition: (function () {
let c = 0; let c = 0;
setInterval(() => { setInterval(() => {
@ -514,13 +506,13 @@ export const achievements: Record<string, Achievement> = {
FAST_BN: { FAST_BN: {
...achievementData.FAST_BN, ...achievementData.FAST_BN,
Icon: "2DAYS", Icon: "2DAYS",
Visible: knowsAboutBitverse, Visible: knowAboutBitverse,
Condition: () => isBitNodeFinished() && Player.playtimeSinceLastBitnode < 1000 * 60 * 60 * 24 * 2, Condition: () => isBitNodeFinished() && Player.playtimeSinceLastBitnode < 1000 * 60 * 60 * 24 * 2,
}, },
CHALLENGE_BN1: { CHALLENGE_BN1: {
...achievementData.CHALLENGE_BN1, ...achievementData.CHALLENGE_BN1,
Icon: "BN1+", Icon: "BN1+",
Visible: knowsAboutBitverse, Visible: knowAboutBitverse,
Condition: () => Condition: () =>
Player.bitNodeN === 1 && Player.bitNodeN === 1 &&
isBitNodeFinished() && isBitNodeFinished() &&

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

@ -26,7 +26,7 @@ const soaAugmentationNames = [
]; ];
export function getBaseAugmentationPriceMultiplier(): number { 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 { export function getGenericAugmentationPriceMultiplier(): number {
const queuedNonSoAAugmentationList = Player.queuedAugmentations.filter((augmentation) => { const queuedNonSoAAugmentationList = Player.queuedAugmentations.filter((augmentation) => {

@ -7,6 +7,7 @@ import { Player } from "@player";
import { Settings } from "../../Settings/Settings"; import { Settings } from "../../Settings/Settings";
import { formatPercent } from "../../ui/formatNumber"; import { formatPercent } from "../../ui/formatNumber";
import { Augmentations } from "../Augmentations"; import { Augmentations } from "../Augmentations";
import { canAccessBitNodeFeature } from "../../BitNode/BitNodeUtils";
function calculateAugmentedStats(): Multipliers { function calculateAugmentedStats(): Multipliers {
let augP: Multipliers = defaultMultipliers(); let augP: Multipliers = defaultMultipliers();
@ -29,7 +30,7 @@ function customFormatPercent(value: number): string {
function BitNodeModifiedStats(props: IBitNodeModifiedStatsProps): React.ReactElement { 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 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>; 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 List from "@mui/material/List";
import Typography from "@mui/material/Typography"; import Typography from "@mui/material/Typography";
import React, { useState } from "react"; import React, { useState } from "react";
import { Exploit, ExploitName } from "../../Exploits/Exploit"; import { Exploit, ExploitDescription } from "../../Exploits/Exploit";
import { Player } from "@player"; import { Player } from "@player";
import { OwnedAugmentationsOrderSetting } from "../../Settings/SettingEnums"; import { OwnedAugmentationsOrderSetting } from "../../Settings/SettingEnums";
import { Settings } from "../../Settings/Settings"; import { Settings } from "../../Settings/Settings";
import { SourceFile } from "../../SourceFile/SourceFile";
import { SourceFiles } from "../../SourceFile/SourceFiles"; import { SourceFiles } from "../../SourceFile/SourceFiles";
interface SfMinus1 { interface SourceFileData {
info: React.ReactElement;
n: number; n: number;
level: number;
maxLevel: number;
activeLevel: number;
name: string; name: string;
lvl: number; info: JSX.Element;
} }
const safeGetSf = (sfNum: number): SourceFile | SfMinus1 | null => { const getSourceFileData = (sfNumber: number): SourceFileData | null => {
if (sfNum === -1) { let maxLevel: number;
const sfMinus1: SfMinus1 = { 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: ( 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 />
<br /> <br />
It increases all of the player's multipliers by 0.1% It increases all of the player's multipliers by 0.1%
<br /> <br />
<br /> <br />
You have found the following exploits: You have found the following exploits:
<br /> <ul>
<br /> {Player.exploits.map((c) => (
{Player.exploits.map((c) => ( <li key={c}>
<React.Fragment key={c}> {c}: {ExploitDescription[c]}
* {ExploitName(c)} </li>
<br /> ))}
</React.Fragment> </ul>
))}
</> </>
), ),
lvl: Player.exploits.length, });
n: -1, }
name: "Source-File -1: Exploits in the BitNodes", for (const sfNumber of Player.sourceFiles.keys()) {
}; const sourceFileData = getSourceFileData(sfNumber);
return sfMinus1; if (!sourceFileData) {
continue;
}
sourceFileList.push(sourceFileData);
} }
const srcFileKey = "SourceFile" + sfNum;
const sfObj = SourceFiles[srcFileKey];
if (sfObj == null) {
console.error(`Invalid source file number: ${sfNum}`);
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) { if (Settings.OwnedAugmentationsOrder === OwnedAugmentationsOrderSetting.Alphabetically) {
sfList.sort(([n1, __lvl1], [n2, __lvl2]) => n1 - n2); sourceFileList.sort((a, b) => a.n - b.n);
} }
const [selectedSf, setSelectedSf] = useState(() => { const [selectedSfData, setSelectedSfData] = useState<SourceFileData | null>(() => {
if (sfList.length === 0) return null; if (sourceFileList.length === 0) {
const [n, lvl] = sfList[0]; return null;
return { n, lvl }; }
return sourceFileList[0];
}); });
if (!selectedSf) { if (!selectedSfData) {
return <></>; return <></>;
} }
@ -104,26 +112,26 @@ export function SourceFilesElement(): React.ReactElement {
sx={{ height: 400, overflowY: "scroll", borderRight: `1px solid ${Settings.theme.welllight}` }} sx={{ height: 400, overflowY: "scroll", borderRight: `1px solid ${Settings.theme.welllight}` }}
disablePadding disablePadding
> >
{sfList.map(([n, lvl], i) => { {sourceFileList.map((sourceFileData, i) => {
const sfObj = safeGetSf(n);
if (!sfObj) return;
const maxLevel = getMaxLevel(sfObj);
return ( return (
<ListItemButton <ListItemButton
key={i + 1} key={i + 1}
onClick={() => setSelectedSf({ n, lvl })} onClick={() => setSelectedSfData(sourceFileData)}
selected={selectedSf.n === n} selected={selectedSfData.n === sourceFileData.n}
sx={{ py: 0 }} sx={{ py: 0 }}
> >
<ListItemText <ListItemText
disableTypography disableTypography
primary={<Typography>{sfObj.name}</Typography>} primary={<Typography>{sourceFileData.name}</Typography>}
secondary={ secondary={
<Typography> <>
Level {lvl} / {maxLevel} <Typography>
</Typography> Level: {sourceFileData.level} / {sourceFileData.maxLevel}
</Typography>
{sourceFileData.activeLevel < sourceFileData.level && (
<Typography>Active level: {sourceFileData.activeLevel}</Typography>
)}
</>
} }
/> />
</ListItemButton> </ListItemButton>
@ -131,28 +139,25 @@ export function SourceFilesElement(): React.ReactElement {
})} })}
</List> </List>
</Box> </Box>
<Box sx={{ m: 1 }}> {selectedSfData !== null && (
<Typography variant="h6" sx={{ display: "flex", alignItems: "center", flexWrap: "wrap" }}> <Box sx={{ m: 1 }}>
{safeGetSf(selectedSf.n)?.name} <Typography variant="h6" sx={{ display: "flex", alignItems: "center", flexWrap: "wrap" }}>
</Typography> {selectedSfData.name}
<Typography component="div" sx={{ maxHeight: 350, overflowY: "scroll" }}> </Typography>
{(() => { <Typography component="div" sx={{ maxHeight: 350, overflowY: "scroll" }}>
const sfObj = safeGetSf(selectedSf.n); Level: {selectedSfData.level} / {selectedSfData.maxLevel}
if (!sfObj) return; <br />
{selectedSfData.activeLevel < selectedSfData.level && (
const maxLevel = getMaxLevel(sfObj);
return (
<> <>
Level {selectedSf.lvl} / {maxLevel} Active level: {selectedSfData.activeLevel}
<br /> <br />
<br />
{sfObj.info}
</> </>
); )}
})()} <br />
</Typography> {selectedSfData.info}
</Box> </Typography>
</Box>
)}
</Paper> </Paper>
</Box> </Box>
); );

@ -962,5 +962,5 @@ export function getBitNodeMultipliers(n: number, lvl: number): BitNodeMultiplier
} }
export function initBitNodeMultipliers(): void { 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 { GetServer } from "../Server/AllServers";
import { Server } from "../Server/Server"; import { Server } from "../Server/Server";
import { SpecialServers } from "../Server/data/SpecialServers"; 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 { export function isBitNodeFinished(): boolean {
const wd = GetServer(SpecialServers.WorldDaemon); const wd = GetServer(SpecialServers.WorldDaemon);
@ -9,3 +14,70 @@ export function isBitNodeFinished(): boolean {
} }
return wd.backdoorInstalled; 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 { defaultMultipliers, getBitNodeMultipliers } from "../BitNode";
import { BitNodeMultipliers } from "../BitNodeMultipliers"; import { BitNodeMultipliers } from "../BitNodeMultipliers";
import { PartialRecord, getRecordEntries } from "../../Types/Record"; import { PartialRecord, getRecordEntries } from "../../Types/Record";
import { canAccessBitNodeFeature } from "../BitNodeUtils";
interface IProps { interface IProps {
n: number; n: number;
@ -23,7 +24,7 @@ export function BitnodeMultiplierDescription({ n, level }: IProps): React.ReactE
return ( return (
<Box component={Paper} sx={{ mt: 1, p: 1 }}> <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>} /> <ListItemText primary={<Typography variant="h6">Bitnode Multipliers</Typography>} />
{open ? <ExpandLess color="primary" /> : <ExpandMore color="primary" />} {open ? <ExpandLess color="primary" /> : <ExpandMore color="primary" />}
</ListItemButton> </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 // 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, // current node's source file, so we get the min of that, the SF's max level,
// or if it's BN12, ∞ // or if it's BN12, ∞
const maxSfLevel = n === 12 ? Infinity : 3; const maxSfLevel = n === 12 ? Number.MAX_VALUE : 3;
const mults = getBitNodeMultipliers(n, level ?? Math.min(Player.sourceFileLvl(n) + 1, maxSfLevel)); const mults = getBitNodeMultipliers(n, level ?? Math.min(Player.activeSourceFileLvl(n) + 1, maxSfLevel));
return ( return (
<Box sx={{ columnCount: 2, columnGap: 1, mb: n === 1 ? 0 : -2 }}> <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 { function GangMults({ mults }: IMultsProps): React.ReactElement {
if (Player.bitNodeN !== 2 && Player.sourceFileLvl(2) <= 0) return <></>; if (!canAccessBitNodeFeature(2)) return <></>;
const rows: IBNMultRows = { const rows: IBNMultRows = {
GangSoftcap: { GangSoftcap: {

@ -172,7 +172,7 @@ export function BitverseRoot(props: IProps): React.ReactElement {
if (n !== destroyed) { if (n !== destroyed) {
return lvl; 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 // If accessed via flume, display the current BN level, else the next
return Math.min(max, lvl + Number(!props.flume)); return Math.min(max, lvl + Number(!props.flume));

@ -1,11 +1,15 @@
import React from "react"; import React from "react";
import { Player } from "@player";
import { type BitNodeBooleanOptions } from "@nsdefs";
import { enterBitNode } from "../../RedPill"; import { enterBitNode } from "../../RedPill";
import { BitNodes } from "../BitNode"; import { BitNodes } from "../BitNode";
import { Modal } from "../../ui/React/Modal"; import { Modal } from "../../ui/React/Modal";
import Typography from "@mui/material/Typography"; import Typography from "@mui/material/Typography";
import Button from "@mui/material/Button"; import Button from "@mui/material/Button";
import { BitnodeMultiplierDescription } from "./BitnodeMultipliersDescription"; import { BitnodeMultiplierDescription } from "./BitnodeMultipliersDescription";
import { BitNodeAdvancedOptions } from "./BitNodeAdvancedOptions";
import { JSONMap } from "../../Types/Jsonable";
interface IProps { interface IProps {
open: boolean; open: boolean;
@ -17,14 +21,77 @@ interface IProps {
} }
export function PortalModal(props: IProps): React.ReactElement { 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 bitNodeKey = "BitNode" + props.n;
const bitNode = BitNodes[bitNodeKey]; const bitNode = BitNodes[bitNodeKey];
if (bitNode == null) throw new Error(`Could not find BitNode object for number: ${props.n}`); if (bitNode == null) throw new Error(`Could not find BitNode object for number: ${props.n}`);
const maxSourceFileLevel = props.n === 12 ? "∞" : "3"; 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 ( return (
<Modal open={props.open} onClose={props.onClose}> <Modal open={props.open} onClose={onClose}>
<Typography variant="h4"> <Typography variant="h4">
BitNode-{props.n}: {bitNode.name} BitNode-{props.n}: {bitNode.name}
</Typography> </Typography>
@ -39,12 +106,25 @@ export function PortalModal(props: IProps): React.ReactElement {
<br /> <br />
<Typography component="div">{bitNode.info}</Typography> <Typography component="div">{bitNode.info}</Typography>
<BitnodeMultiplierDescription n={props.n} level={newLevel} /> <BitnodeMultiplierDescription n={props.n} level={newLevel} />
<BitNodeAdvancedOptions
targetBitNode={props.n}
currentSourceFiles={currentSourceFiles}
sourceFileOverrides={sourceFileOverrides}
intelligenceOverride={intelligenceOverride}
bitNodeBooleanOptions={bitNodeBooleanOptions}
callbacks={callbacks}
></BitNodeAdvancedOptions>
<br /> <br />
<Button <Button
aria-label={`enter-bitnode-${bitNode.number.toString()}`} aria-label={`enter-bitnode-${bitNode.number.toString()}`}
autoFocus={true} autoFocus={true}
onClick={() => { onClick={() => {
enterBitNode(props.flume, props.destroyedBitNode, props.n); const bitNodeOptions = {
sourceFileOverrides,
intelligenceOverride,
...bitNodeBooleanOptions,
};
enterBitNode(props.flume, props.destroyedBitNode, props.n, bitNodeOptions);
props.onClose(); props.onClose();
}} }}
> >

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

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

@ -29,24 +29,20 @@ export enum Exploit {
EditSaveFile = "EditSaveFile", EditSaveFile = "EditSaveFile",
} }
const names: Record<Exploit, string> = { export const ExploitDescription: Record<Exploit, string> = {
Bypass: "by circumventing the ram cost of document.", [Exploit.Bypass]: "by circumventing the ram cost of document.",
EditSaveFile: "by editing your save file.", [Exploit.EditSaveFile]: "by editing your save file.",
PrototypeTampering: "by tampering with Numbers prototype.", [Exploit.PrototypeTampering]: "by tampering with Numbers prototype.",
TimeCompression: "by compressing time.", [Exploit.TimeCompression]: "by compressing time.",
Unclickable: "by clicking the unclickable.", [Exploit.Unclickable]: "by clicking the unclickable.",
UndocumentedFunctionCall: "by looking beyond the documentation.", [Exploit.UndocumentedFunctionCall]: "by looking beyond the documentation.",
RealityAlteration: "by altering reality to suit your whims.", [Exploit.RealityAlteration]: "by altering reality to suit your whims.",
N00dles: "by harnessing the power of the n00dles.", [Exploit.N00dles]: "by harnessing the power of the n00dles.",
YoureNotMeantToAccessThis: "by accessing the dev menu.", [Exploit.YoureNotMeantToAccessThis]: "by accessing the dev menu.",
TrueRecursion: "by truly recursing.", [Exploit.TrueRecursion]: "by truly recursing.",
INeedARainbow: "by using the power of the rainbow.", [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 // Needed in case player edits save file poorly
export function sanitizeExploits(exploits: Exploit[]): Exploit[] { export function sanitizeExploits(exploits: Exploit[]): Exploit[] {
exploits = exploits.filter((e: Exploit) => Object.values(Exploit).includes(e)); 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]); 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 // Remove faction-unique augs that don't belong to this faction
const uniqueFilter = (a: Augmentation): boolean => { const uniqueFilter = (a: Augmentation): boolean => {
// Keep all the non-unique one // Keep all the non-unique one

@ -302,7 +302,7 @@ export const haveSourceFile = (n: number): PlayerCondition => ({
}; };
}, },
isSatisfied(p: PlayerObject): boolean { 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 // 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) { if (ramUsage <= minRam) {
return ( return (
<Tooltip <Tooltip

@ -829,5 +829,5 @@ function waitCycle(useOfflineCycles = true): Promise<void> {
} }
export function showWorldDemon() { 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 { export function CalculateEffect(nodes: number, faction: GoOpponent): number {
const power = getEffectPowerForFaction(faction); const power = getEffectPowerForFaction(faction);
const sourceFileBonus = Player.sourceFileLvl(14) ? 2 : 1; const sourceFileBonus = Player.activeSourceFileLvl(14) ? 2 : 1;
return ( return (
1 + Math.log(nodes + 1) * Math.pow(nodes + 1, 0.3) * 0.002 * power * currentNodeMults.GoPower * sourceFileBonus 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 * for factions you are a member of
*/ */
export function getMaxFavor() { export function getMaxFavor() {
const sourceFileLevel = Player.sourceFileLvl(14); const sourceFileLevel = Player.activeSourceFileLvl(14);
if (sourceFileLevel === 1) { if (sourceFileLevel === 1) {
return 80; return 80;

@ -323,8 +323,8 @@ export function getStats() {
/** Validate singularity access by throwing an error if the player does not have access. */ /** Validate singularity access by throwing an error if the player does not have access. */
export function checkCheatApiAccess(error: (s: string) => void): void { export function checkCheatApiAccess(error: (s: string) => void): void {
const hasSourceFile = Player.sourceFileLvl(14) > 1; const hasSourceFile = Player.activeSourceFileLvl(14) > 1;
const isBitnodeFourteenTwo = Player.sourceFileLvl(14) === 1 && Player.bitNodeN === 14; const isBitnodeFourteenTwo = Player.activeSourceFileLvl(14) === 1 && Player.bitNodeN === 14;
if (!hasSourceFile && !isBitnodeFourteenTwo) { if (!hasSourceFile && !isBitnodeFourteenTwo) {
error( error(
`The go.cheat API requires Source-File 14.2 to run, a power up you obtain later in the game. `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% * 15: +31,358,645%
*/ */
export function cheatSuccessChance(cheatCount: number) { 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; const cheatCountScalar = (0.7 - 0.02 * cheatCount) ** cheatCount;
return Math.max(Math.min(0.6 * cheatCountScalar * Player.mults.crime_success + sourceFileBonus, 1), 0); 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 { Server } from "../Server/Server";
import { Companies } from "../Company/Companies"; import { Companies } from "../Company/Companies";
import { isMember } from "../utils/EnumHelper"; import { isMember } from "../utils/EnumHelper";
import { canAccessBitNodeFeature } from "../BitNode/BitNodeUtils";
// Returns a boolean indicating whether the player has Hacknet Servers // Returns a boolean indicating whether the player has Hacknet Servers
// (the upgraded form of Hacknet Nodes) // (the upgraded form of Hacknet Nodes)
export function hasHacknetServers(): boolean { export function hasHacknetServers(): boolean {
return Player.bitNodeN === 9 || Player.sourceFileLvl(9) > 0; return canAccessBitNodeFeature(9) && !Player.bitNodeOptions.disableHacknetServer;
} }
export function purchaseHacknet(): number { export function purchaseHacknet(): number {

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

@ -19,7 +19,10 @@ interface IProps {
export function RamButton(props: IProps): React.ReactElement { export function RamButton(props: IProps): React.ReactElement {
const homeComputer = Player.getHomeComputer(); 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>; return <Button>Upgrade 'home' RAM - MAX</Button>;
} }

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

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

@ -1,5 +1,11 @@
import type { NetscriptContext } from "./APIWrapper"; 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 type { WorkerScript } from "./WorkerScript";
import React from "react"; import React from "react";
@ -49,6 +55,12 @@ import { CustomBoundary } from "../ui/Components/CustomBoundary";
import { ServerConstants } from "../Server/data/Constants"; import { ServerConstants } from "../Server/data/Constants";
import { basicErrorMessage, errorMessage, log } from "./ErrorMessages"; import { basicErrorMessage, errorMessage, log } from "./ErrorMessages";
import { assertString, debugType } from "./TypeAssertion"; import { assertString, debugType } from "./TypeAssertion";
import {
canAccessBitNodeFeature,
getDefaultBitNodeOptions,
validateSourceFileOverrides,
} from "../BitNode/BitNodeUtils";
import { JSONMap } from "../Types/Jsonable";
export const helpers = { export const helpers = {
string, string,
@ -83,6 +95,7 @@ export const helpers = {
getCannotFindRunningScriptErrorMessage, getCannotFindRunningScriptErrorMessage,
createPublicRunningScript, createPublicRunningScript,
failOnHacknetServer, failOnHacknetServer,
validateBitNodeOptions,
}; };
/** RunOptions with non-optional, type-validated members, for passing between internal functions. */ /** 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. */ /** Validate singularity access by throwing an error if the player does not have access. */
function checkSingularityAccess(ctx: NetscriptContext): void { function checkSingularityAccess(ctx: NetscriptContext): void {
if (Player.bitNodeN !== 4 && Player.sourceFileLvl(4) === 0) { if (!canAccessBitNodeFeature(4)) {
throw errorMessage( throw errorMessage(
ctx, ctx,
`This singularity function requires Source-File 4 to run. A power up you obtain later in the game. `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) { export function wrapUserNode(value: unknown) {
return <CustomBoundary key={`PlayerContent${customElementKey++}`}>{value}</CustomBoundary>; 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 { function SF4Cost(cost: number): () => number {
return () => { return () => {
if (Player.bitNodeN === 4) return cost; if (Player.bitNodeN === 4) {
const sf4 = Player.sourceFileLvl(4); return cost;
if (sf4 <= 1) return cost * 16; }
if (sf4 === 2) return cost * 4; const sf4 = Player.activeSourceFileLvl(4);
if (sf4 <= 1) {
return cost * 16;
}
if (sf4 === 2) {
return cost * 4;
}
return cost; return cost;
}; };
} }

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

@ -12,6 +12,7 @@ import { Skills } from "../Bladeburner/data/Skills";
import { assertString } from "../Netscript/TypeAssertion"; import { assertString } from "../Netscript/TypeAssertion";
import { BlackOperations, blackOpsArray } from "../Bladeburner/data/BlackOperations"; import { BlackOperations, blackOpsArray } from "../Bladeburner/data/BlackOperations";
import { checkSleeveAPIAccess, checkSleeveNumber } from "../NetscriptFunctions/Sleeve"; import { checkSleeveAPIAccess, checkSleeveNumber } from "../NetscriptFunctions/Sleeve";
import { canAccessBitNodeFeature } from "../BitNode/BitNodeUtils";
export function NetscriptBladeburner(): InternalAPI<INetscriptBladeburner> { export function NetscriptBladeburner(): InternalAPI<INetscriptBladeburner> {
const checkBladeburnerAccess = function (ctx: NetscriptContext): void { const checkBladeburnerAccess = function (ctx: NetscriptContext): void {
@ -19,7 +20,7 @@ export function NetscriptBladeburner(): InternalAPI<INetscriptBladeburner> {
return; return;
}; };
const getBladeburner = function (ctx: NetscriptContext): Bladeburner { const getBladeburner = function (ctx: NetscriptContext): Bladeburner {
const apiAccess = Player.bitNodeN === 7 || Player.sourceFileLvl(7) > 0; const apiAccess = canAccessBitNodeFeature(7);
if (!apiAccess) { if (!apiAccess) {
throw helpers.errorMessage(ctx, "You have not unlocked the Bladeburner API.", "API ACCESS"); 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; return !!attempt.success;
}, },
joinBladeburnerDivision: (ctx) => () => { joinBladeburnerDivision: (ctx) => () => {
if (Player.bitNodeN === 7 || Player.sourceFileLvl(7) > 0) { if (!canAccessBitNodeFeature(7) || Player.bitNodeOptions.disableBladeburner) {
if (currentNodeMults.BladeburnerRank === 0) { return false;
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
) {
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; if (currentNodeMults.BladeburnerRank === 0) {
return false; // Disabled in this bitnode
}
if (Player.bladeburner) {
return true; // Already member
}
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;
}, },
getBonusTime: (ctx) => () => { getBonusTime: (ctx) => () => {
const bladeburner = getBladeburner(ctx); const bladeburner = getBladeburner(ctx);

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

@ -15,6 +15,7 @@ import { helpers } from "../Netscript/NetscriptHelpers";
import { getAugCost } from "../Augmentation/AugmentationHelpers"; import { getAugCost } from "../Augmentation/AugmentationHelpers";
import { Factions } from "../Faction/Factions"; import { Factions } from "../Faction/Factions";
import { SleeveWorkType } from "../PersonObjects/Sleeve/Work/Work"; import { SleeveWorkType } from "../PersonObjects/Sleeve/Work/Work";
import { canAccessBitNodeFeature } from "../BitNode/BitNodeUtils";
export const checkSleeveAPIAccess = function (ctx: NetscriptContext) { export const checkSleeveAPIAccess = function (ctx: NetscriptContext) {
if (Player.bitNodeN !== 10 && !Player.sourceFileLvl(10)) { if (Player.bitNodeN !== 10 && !Player.sourceFileLvl(10)) {
@ -33,6 +34,23 @@ export const checkSleeveNumber = function (ctx: NetscriptContext, sleeveNumber:
}; };
export function NetscriptSleeve(): InternalAPI<NetscriptSleeve> { 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> = { const sleeveFunctions: InternalAPI<NetscriptSleeve> = {
getNumSleeves: (ctx) => () => { getNumSleeves: (ctx) => () => {
checkSleeveAPIAccess(ctx); checkSleeveAPIAccess(ctx);

@ -167,10 +167,8 @@ export function NetscriptStockMarket(): InternalAPI<TIX> {
const symbol = helpers.string(ctx, "symbol", _symbol); const symbol = helpers.string(ctx, "symbol", _symbol);
const shares = helpers.number(ctx, "shares", _shares); const shares = helpers.number(ctx, "shares", _shares);
checkTixApiAccess(ctx); checkTixApiAccess(ctx);
if (Player.bitNodeN !== 8) { if (Player.bitNodeN !== 8 && Player.activeSourceFileLvl(8) <= 1) {
if (Player.sourceFileLvl(8) <= 1) { throw helpers.errorMessage(ctx, "You must either be in BitNode-8 or you must have Source-File 8 Level 2.");
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 stock = getStockFromSymbol(ctx, symbol);
const res = shortStock(stock, shares, ctx, {}); const res = shortStock(stock, shares, ctx, {});
@ -181,10 +179,8 @@ export function NetscriptStockMarket(): InternalAPI<TIX> {
const symbol = helpers.string(ctx, "symbol", _symbol); const symbol = helpers.string(ctx, "symbol", _symbol);
const shares = helpers.number(ctx, "shares", _shares); const shares = helpers.number(ctx, "shares", _shares);
checkTixApiAccess(ctx); checkTixApiAccess(ctx);
if (Player.bitNodeN !== 8) { if (Player.bitNodeN !== 8 && Player.activeSourceFileLvl(8) <= 1) {
if (Player.sourceFileLvl(8) <= 1) { throw helpers.errorMessage(ctx, "You must either be in BitNode-8 or you must have Source-File 8 Level 2.");
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 stock = getStockFromSymbol(ctx, symbol);
const res = sellShort(stock, shares, ctx, {}); const res = sellShort(stock, shares, ctx, {});
@ -198,10 +194,8 @@ export function NetscriptStockMarket(): InternalAPI<TIX> {
const type = helpers.string(ctx, "type", _type); const type = helpers.string(ctx, "type", _type);
const pos = helpers.string(ctx, "pos", _pos); const pos = helpers.string(ctx, "pos", _pos);
checkTixApiAccess(ctx); checkTixApiAccess(ctx);
if (Player.bitNodeN !== 8) { if (Player.bitNodeN !== 8 && Player.activeSourceFileLvl(8) <= 2) {
if (Player.sourceFileLvl(8) <= 2) { throw helpers.errorMessage(ctx, "You must either be in BitNode-8 or you must have Source-File 8 Level 3.");
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); const stock = getStockFromSymbol(ctx, symbol);
@ -238,10 +232,8 @@ export function NetscriptStockMarket(): InternalAPI<TIX> {
const type = helpers.string(ctx, "type", _type); const type = helpers.string(ctx, "type", _type);
const pos = helpers.string(ctx, "pos", _pos); const pos = helpers.string(ctx, "pos", _pos);
checkTixApiAccess(ctx); checkTixApiAccess(ctx);
if (Player.bitNodeN !== 8) { if (Player.bitNodeN !== 8 && Player.activeSourceFileLvl(8) <= 2) {
if (Player.sourceFileLvl(8) <= 2) { throw helpers.errorMessage(ctx, "You must either be in BitNode-8 or you must have Source-File 8 Level 3.");
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); const stock = getStockFromSymbol(ctx, symbol);
if (isNaN(shares) || isNaN(price)) { if (isNaN(shares) || isNaN(price)) {
@ -281,10 +273,8 @@ export function NetscriptStockMarket(): InternalAPI<TIX> {
}, },
getOrders: (ctx) => () => { getOrders: (ctx) => () => {
checkTixApiAccess(ctx); checkTixApiAccess(ctx);
if (Player.bitNodeN !== 8) { if (Player.bitNodeN !== 8 && Player.activeSourceFileLvl(8) <= 2) {
if (Player.sourceFileLvl(8) <= 2) { throw helpers.errorMessage(ctx, "You must either be in BitNode-8 or have Source-File 8 Level 3.");
throw helpers.errorMessage(ctx, "You must either be in BitNode-8 or have Source-File 8 Level 3.");
}
} }
const orders: StockOrder = {}; const orders: StockOrder = {};
@ -328,6 +318,11 @@ export function NetscriptStockMarket(): InternalAPI<TIX> {
return forecast / 100; // Convert from percentage to decimal return forecast / 100; // Convert from percentage to decimal
}, },
purchase4SMarketData: (ctx) => () => { purchase4SMarketData: (ctx) => () => {
if (Player.bitNodeOptions.disable4SData) {
helpers.log(ctx, () => "4S Market Data is disabled.");
return false;
}
if (Player.has4SData) { if (Player.has4SData) {
helpers.log(ctx, () => "Already purchased 4S Market Data."); helpers.log(ctx, () => "Already purchased 4S Market Data.");
return true; return true;
@ -344,6 +339,11 @@ export function NetscriptStockMarket(): InternalAPI<TIX> {
return true; return true;
}, },
purchase4SMarketDataTixApi: (ctx) => () => { purchase4SMarketDataTixApi: (ctx) => () => {
if (Player.bitNodeOptions.disable4SData) {
helpers.log(ctx, () => "4S Market Data is disabled.");
return false;
}
checkTixApiAccess(ctx); checkTixApiAccess(ctx);
if (Player.has4SDataTixApi) { 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 { PlayerAchievement } from "../../Achievements/Achievements";
import type { Bladeburner } from "../../Bladeburner/Bladeburner"; import type { Bladeburner } from "../../Bladeburner/Bladeburner";
import type { Corporation } from "../../Corporation/Corporation"; import type { Corporation } from "../../Corporation/Corporation";
@ -74,6 +74,22 @@ export class PlayerObject extends Person implements IPlayer {
entropy = 0; 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 // Player-specific methods
init = generalMethods.init; init = generalMethods.init;
startWork = workMethods.startWork; startWork = workMethods.startWork;
@ -129,6 +145,7 @@ export class PlayerObject extends Person implements IPlayer {
setBitNodeNumber = generalMethods.setBitNodeNumber; setBitNodeNumber = generalMethods.setBitNodeNumber;
canAccessCotMG = generalMethods.canAccessCotMG; canAccessCotMG = generalMethods.canAccessCotMG;
sourceFileLvl = generalMethods.sourceFileLvl; sourceFileLvl = generalMethods.sourceFileLvl;
activeSourceFileLvl = generalMethods.activeSourceFileLvl;
applyEntropy = augmentationMethods.applyEntropy; applyEntropy = augmentationMethods.applyEntropy;
focusPenalty = generalMethods.focusPenalty; focusPenalty = generalMethods.focusPenalty;

@ -1,9 +1,10 @@
import { canAccessBitNodeFeature } from "../../BitNode/BitNodeUtils";
import { Bladeburner } from "../../Bladeburner/Bladeburner"; import { Bladeburner } from "../../Bladeburner/Bladeburner";
import type { PlayerObject } from "./PlayerObject"; import type { PlayerObject } from "./PlayerObject";
export function canAccessBladeburner(this: PlayerObject): boolean { 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 { export function startBladeburner(this: PlayerObject): void {

@ -3,9 +3,10 @@ import { resetIndustryResearchTrees } from "../../Corporation/data/IndustryData"
import { Corporation } from "../../Corporation/Corporation"; import { Corporation } from "../../Corporation/Corporation";
import type { PlayerObject } from "./PlayerObject"; import type { PlayerObject } from "./PlayerObject";
import { canAccessBitNodeFeature } from "../../BitNode/BitNodeUtils";
export function canAccessCorporation(this: PlayerObject): boolean { 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 { 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 //reset the research tree in case the corporation was restarted
resetIndustryResearchTrees(); 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.WarehouseAPI);
this.corporation.unlocks.add(CorpUnlockName.OfficeAPI); this.corporation.unlocks.add(CorpUnlockName.OfficeAPI);
} }

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

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

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

@ -25,6 +25,7 @@ import { isSleeveClassWork } from "../Work/SleeveClassWork";
import { isSleeveFactionWork } from "../Work/SleeveFactionWork"; import { isSleeveFactionWork } from "../Work/SleeveFactionWork";
import { isSleeveCompanyWork } from "../Work/SleeveCompanyWork"; import { isSleeveCompanyWork } from "../Work/SleeveCompanyWork";
import { isSleeveCrimeWork } from "../Work/SleeveCrimeWork"; import { isSleeveCrimeWork } from "../Work/SleeveCrimeWork";
import { canAccessBitNodeFeature } from "../../../BitNode/BitNodeUtils";
const CYCLES_PER_SEC = 1000 / CONSTANTS.MilliPerCycle; const CYCLES_PER_SEC = 1000 / CONSTANTS.MilliPerCycle;
@ -76,7 +77,7 @@ export function StatsElement(props: IProps): React.ReactElement {
color={Settings.theme.cha} color={Settings.theme.cha}
data={{ level: props.sleeve.skills.charisma, exp: props.sleeve.exp.charisma }} data={{ level: props.sleeve.skills.charisma, exp: props.sleeve.exp.charisma }}
/> />
{(Player.sourceFileLvl(5) > 0 || Player.bitNodeN === 5) && ( {canAccessBitNodeFeature(5) && (
<StatsRow <StatsRow
name="Intelligence" name="Intelligence"
color={Settings.theme.int} color={Settings.theme.int}
@ -111,7 +112,7 @@ export function StatsElement(props: IProps): React.ReactElement {
export function EarningsElement(props: IProps): React.ReactElement { export function EarningsElement(props: IProps): React.ReactElement {
const { classes } = useStyles(); const { classes } = useStyles();
let data: (string | JSX.Element)[][] = []; let data: [string, string | JSX.Element][] = [];
if (isSleeveCrimeWork(props.sleeve.currentWork)) { if (isSleeveCrimeWork(props.sleeve.currentWork)) {
const gains = props.sleeve.currentWork.getExp(props.sleeve); const gains = props.sleeve.currentWork.getExp(props.sleeve);
data = [ data = [
@ -150,20 +151,21 @@ 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]; const job = Player.jobs[props.sleeve.currentWork.companyName];
if (!job) break companyWork; if (job) {
const rates = props.sleeve.currentWork.getGainRates(props.sleeve, job); const rates = props.sleeve.currentWork.getGainRates(props.sleeve, job);
data = [ data = [
[`Money:`, <MoneyRate key="money-rate" money={CYCLES_PER_SEC * rates.money} />], [`Money:`, <MoneyRate key="money-rate" money={CYCLES_PER_SEC * rates.money} />],
[`Hacking Exp:`, `${formatExp(CYCLES_PER_SEC * rates.hackExp)} / sec`], [`Hacking Exp:`, `${formatExp(CYCLES_PER_SEC * rates.hackExp)} / sec`],
[`Strength Exp:`, `${formatExp(CYCLES_PER_SEC * rates.strExp)} / sec`], [`Strength Exp:`, `${formatExp(CYCLES_PER_SEC * rates.strExp)} / sec`],
[`Defense Exp:`, `${formatExp(CYCLES_PER_SEC * rates.defExp)} / sec`], [`Defense Exp:`, `${formatExp(CYCLES_PER_SEC * rates.defExp)} / sec`],
[`Dexterity Exp:`, `${formatExp(CYCLES_PER_SEC * rates.dexExp)} / sec`], [`Dexterity Exp:`, `${formatExp(CYCLES_PER_SEC * rates.dexExp)} / sec`],
[`Agility Exp:`, `${formatExp(CYCLES_PER_SEC * rates.agiExp)} / sec`], [`Agility Exp:`, `${formatExp(CYCLES_PER_SEC * rates.agiExp)} / sec`],
[`Charisma Exp:`, `${formatExp(CYCLES_PER_SEC * rates.chaExp)} / sec`], [`Charisma Exp:`, `${formatExp(CYCLES_PER_SEC * rates.chaExp)} / sec`],
[`Reputation:`, <ReputationRate key="reputation-rate" reputation={CYCLES_PER_SEC * rates.reputation} />], [`Reputation:`, <ReputationRate key="reputation-rate" reputation={CYCLES_PER_SEC * rates.reputation} />],
]; ];
}
} }
return ( return (

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

@ -13,6 +13,7 @@ import { calculateHackingTime, calculateGrowTime, calculateWeakenTime } from "..
import { CompletedProgramName, FactionName } from "@enums"; import { CompletedProgramName, FactionName } from "@enums";
import { Router } from "../ui/GameRoot"; import { Router } from "../ui/GameRoot";
import { Page } from "../ui/Router"; import { Page } from "../ui/Router";
import { knowAboutBitverse } from "../BitNode/BitNodeUtils";
function requireHackingLevel(lvl: number) { function requireHackingLevel(lvl: number) {
return function () { return function () {
@ -22,7 +23,7 @@ function requireHackingLevel(lvl: number) {
function bitFlumeRequirements() { function bitFlumeRequirements() {
return function () { 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 */ /** Implementation for what happens when you destroy a BitNode */
import React from "react"; import React from "react";
import { Player } from "@player"; import { Player } from "@player";
import { type BitNodeOptions } from "@nsdefs";
import { SourceFiles } from "./SourceFile/SourceFiles"; import { SourceFiles } from "./SourceFile/SourceFiles";
import { dialogBoxCreate } from "./ui/React/DialogBox"; import { dialogBoxCreate } from "./ui/React/DialogBox";
import { Router } from "./ui/GameRoot"; import { Router } from "./ui/GameRoot";
import { Page } from "./ui/Router"; import { Page } from "./ui/Router";
import { prestigeSourceFile } from "./Prestige"; import { prestigeSourceFile } from "./Prestige";
import { setBitNodeOptions } from "./BitNode/BitNodeUtils";
function giveSourceFile(bitNodeNumber: number): void { function giveSourceFile(bitNodeNumber: number): void {
const sourceFileKey = "SourceFile" + bitNodeNumber.toString(); 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) { if (!isFlume) {
giveSourceFile(destroyedBitNode); giveSourceFile(destroyedBitNode);
} else if (Player.sourceFileLvl(5) === 0 && newBitNode !== 5) { } else if (Player.sourceFileLvl(5) === 0 && newBitNode !== 5) {
@ -61,6 +68,9 @@ export function enterBitNode(isFlume: boolean, destroyedBitNode: number, newBitN
// Set new Bit Node // Set new Bit Node
Player.bitNodeN = newBitNode; Player.bitNodeN = newBitNode;
// Set BitNode options
setBitNodeOptions(bitNodeOptions);
if (newBitNode === 6) { if (newBitNode === 6) {
Router.toPage(Page.BladeburnerCinematic); Router.toPage(Page.BladeburnerCinematic);
} else { } else {

@ -76,6 +76,8 @@ interface ResetInfo {
ownedAugs: Map<string, number>; ownedAugs: Map<string, number>;
/** A map of owned SF to their levels. Keyed by the SF number. Map values are the SF level. */ /** A map of owned SF to their levels. Keyed by the SF number. Map values are the SF level. */
ownedSF: Map<number, number>; ownedSF: Map<number, number>;
/** Current BitNode options */
bitNodeOptions: BitNodeOptions;
} }
/** @public */ /** @public */
@ -1701,6 +1703,36 @@ export interface GraftingTask {
*/ */
export type Task = StudyTask | CompanyWorkTask | CreateProgramWorkTask | CrimeTask | FactionWorkTask | 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 * Singularity API
* @remarks * @remarks
@ -2615,8 +2647,9 @@ export interface Singularity {
* *
* @param nextBN - BN number to jump to * @param nextBN - BN number to jump to
* @param callbackScript - Name of the script to launch in the next BN. * @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. * 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 nextBN - BN number to jump to
* @param callbackScript - Name of the script to launch in the next BN. * @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. * Get the current work the player is doing.

@ -164,7 +164,10 @@ export function purchaseRamForHomeComputer(): void {
} }
const homeComputer = Player.getHomeComputer(); 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`); dialogBoxCreate(`You cannot upgrade your home computer RAM because it is at its maximum possible value`);
return; return;
} }

@ -56,6 +56,7 @@ import { hash } from "../../hash/hash";
import { Locations } from "../../Locations/Locations"; import { Locations } from "../../Locations/Locations";
import { useRerender } from "../../ui/React/hooks"; import { useRerender } from "../../ui/React/hooks";
import { playerHasDiscoveredGo } from "../../Go/effects/effect"; import { playerHasDiscoveredGo } from "../../Go/effects/effect";
import { knowAboutBitverse } from "../../BitNode/BitNodeUtils";
const RotatedDoubleArrowIcon = React.forwardRef(function RotatedDoubleArrowIcon( const RotatedDoubleArrowIcon = React.forwardRef(function RotatedDoubleArrowIcon(
props: { color: "primary" | "secondary" | "error" }, props: { color: "primary" | "secondary" | "error" },
@ -141,12 +142,12 @@ export function SidebarRoot(props: { page: Page }): React.ReactElement {
Player.factions.length > 0 || Player.factions.length > 0 ||
Player.augmentations.length > 0 || Player.augmentations.length > 0 ||
Player.queuedAugmentations.length > 0 || Player.queuedAugmentations.length > 0 ||
Player.sourceFiles.size > 0; knowAboutBitverse();
const canOpenAugmentations = const canOpenAugmentations =
Player.augmentations.length > 0 || Player.augmentations.length > 0 ||
Player.queuedAugmentations.length > 0 || Player.queuedAugmentations.length > 0 ||
Player.sourceFiles.size > 0 || knowAboutBitverse() ||
Player.exploits.length > 0; Player.exploits.length > 0;
const canOpenSleeves = Player.sleeves.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 PurchaseTixApiAccessButton(props: IProps): React.ReactElement {
function purchaseTixApiAccess(): void { function purchaseTixApiAccess(): void {
if (Player.bitNodeOptions.disable4SData) {
return;
}
if (Player.hasTixApiAccess) { if (Player.hasTixApiAccess) {
return; return;
} }
@ -135,6 +138,9 @@ function PurchaseTixApiAccessButton(props: IProps): React.ReactElement {
function Purchase4SMarketDataButton(props: IProps): React.ReactElement { function Purchase4SMarketDataButton(props: IProps): React.ReactElement {
function purchase4SMarketData(): void { function purchase4SMarketData(): void {
if (Player.bitNodeOptions.disable4SData) {
return;
}
if (Player.has4SData) { if (Player.has4SData) {
return; 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. the TIX API lets you write code to create your own algorithmic/automated trading strategies.
</Typography> </Typography>
<PurchaseTixApiAccessButton {...props} /> <PurchaseTixApiAccessButton {...props} />
<Typography variant="h5" color="primary"> {!Player.bitNodeOptions.disable4SData && (
{FactionName.FourSigma} (4S) Market Data Feed <>
</Typography> <Typography variant="h5" color="primary">
<Typography> {FactionName.FourSigma} (4S) Market Data Feed
{FactionName.FourSigma}'s (4S) Market Data Feed provides information about stocks that will help your trading </Typography>
strategies. <Typography>
<IconButton onClick={() => setHelpOpen(true)}> {FactionName.FourSigma}'s (4S) Market Data Feed provides information about stocks that will help your
<HelpIcon /> trading strategies.
</IconButton> <IconButton onClick={() => setHelpOpen(true)}>
</Typography> <HelpIcon />
<Purchase4SMarketDataTixApiAccessButton {...props} /> </IconButton>
<Purchase4SMarketDataButton {...props} /> </Typography>
<Purchase4SMarketDataTixApiAccessButton {...props} />
<Purchase4SMarketDataButton {...props} />
</>
)}
<Typography> <Typography>
Commission Fees: Every transaction you make has a{" "} Commission Fees: Every transaction you make has a{" "}
<Money money={StockMarketConstants.StockMarketCommission} forPurchase={true} /> commission fee. <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) // Whether the player has access to orders besides market orders (limit/stop)
function hasOrderAccess(): boolean { 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 // Whether the player has access to shorting stocks
function hasShortAccess(): boolean { function hasShortAccess(): boolean {
return Player.bitNodeN === 8 || Player.sourceFileLvl(8) >= 2; return Player.bitNodeN === 8 || Player.activeSourceFileLvl(8) >= 2;
} }
return ( return (

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

@ -20,6 +20,20 @@ import { Company } from "../Company/Company";
import { CompanyPosition } from "../Company/CompanyPosition"; import { CompanyPosition } from "../Company/CompanyPosition";
import { isMember } from "../utils/EnumHelper"; 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 const gameCPS = 1000 / CONSTANTS.MilliPerCycle; // 5 cycles per second
export const FactionWorkStats: Record<FactionWorkType, WorkStats> = { export const FactionWorkStats: Record<FactionWorkType, WorkStats> = {
[FactionWorkType.hacking]: newWorkStats({ hackExp: 2 }), [FactionWorkType.hacking]: newWorkStats({ hackExp: 2 }),
@ -60,7 +74,7 @@ export function calculateCrimeWorkStats(person: IPerson, crime: Crime): WorkStat
currentNodeMults.CrimeExpGain, currentNodeMults.CrimeExpGain,
false, false,
); );
return gains; return processWorkStats(person, gains);
} }
/** @returns faction rep rate per cycle */ /** @returns faction rep rate per cycle */
@ -75,9 +89,9 @@ export const calculateFactionRep = (person: IPerson, type: FactionWorkType, favo
/** @returns per-cycle WorkStats */ /** @returns per-cycle WorkStats */
export function calculateFactionExp(person: IPerson, type: FactionWorkType): WorkStats { export function calculateFactionExp(person: IPerson, type: FactionWorkType): WorkStats {
return scaleWorkStats( return processWorkStats(
multWorkStats(FactionWorkStats[type], person.mults), person,
currentNodeMults.FactionWorkExpGain / gameCPS, scaleWorkStats(multWorkStats(FactionWorkStats[type], person.mults), currentNodeMults.FactionWorkExpGain / gameCPS),
); );
} }
@ -102,7 +116,7 @@ export function calculateClassEarnings(person: IPerson, type: ClassType, locatio
person.mults, person.mults,
); );
earnings.money = calculateCost(classs, location) / gameCPS; earnings.money = calculateCost(classs, location) / gameCPS;
return earnings; return processWorkStats(person, earnings);
} }
/** @returns per-cycle WorkStats */ /** @returns per-cycle WorkStats */
@ -114,7 +128,7 @@ export const calculateCompanyWorkStats = (
): WorkStats => { ): WorkStats => {
// If player has SF-11, calculate salary multiplier from favor // If player has SF-11, calculate salary multiplier from favor
const favorMult = isNaN(favor) ? 1 : 1 + favor / 100; 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( const gains = scaleWorkStats(
multWorkStats( multWorkStats(
@ -138,5 +152,5 @@ export const calculateCompanyWorkStats = (
gains.reputation = jobPerformance * worker.mults.company_rep * favorMult * currentNodeMults.CompanyWorkRepGain; 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 { StatsTable } from "./React/StatsTable";
import { useRerender } from "./React/hooks"; import { useRerender } from "./React/hooks";
import { getMaxFavor } from "../Go/effects/effect"; import { getMaxFavor } from "../Go/effects/effect";
import { canAccessBitNodeFeature, knowAboutBitverse } from "../BitNode/BitNodeUtils";
interface EmployersModalProps { interface EmployersModalProps {
open: boolean; open: boolean;
@ -68,7 +69,7 @@ function MultiplierTable(props: MultTableProps): React.ReactElement {
{props.rows.map((data) => { {props.rows.map((data) => {
const { mult, value, effValue = null, color = props.color } = 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 ( return (
<StatsRow key={mult} name={mult} color={color} data={{}}> <StatsRow key={mult} name={mult} color={color} data={{}}>
<> <>
@ -100,9 +101,9 @@ function MultiplierTable(props: MultTableProps): React.ReactElement {
} }
function CurrentBitNode(): React.ReactElement { function CurrentBitNode(): React.ReactElement {
if (Player.sourceFiles.size > 0) { if (knowAboutBitverse()) {
const index = "BitNode" + Player.bitNodeN; 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 ( return (
<Paper sx={{ mb: 1, p: 1 }}> <Paper sx={{ mb: 1, p: 1 }}>
<Typography variant="h5"> <Typography variant="h5">
@ -194,7 +195,7 @@ function MoneyModal({ open, onClose }: IMoneyModalProps): React.ReactElement {
{convertMoneySourceTrackerToString(Player.moneySourceA)} {convertMoneySourceTrackerToString(Player.moneySourceA)}
</> </>
); );
if (Player.sourceFiles.size > 0) { if (knowAboutBitverse()) {
content = ( content = (
<> <>
{content} {content}
@ -224,7 +225,7 @@ export function CharacterStats(): React.ReactElement {
const timeRows = [ const timeRows = [
["Since last Augmentation installation", convertTimeMsToTimeElapsedString(Player.playtimeSinceLastAug)], ["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(["Since last Bitnode destroyed", convertTimeMsToTimeElapsedString(Player.playtimeSinceLastBitnode)]);
} }
timeRows.push(["Total", convertTimeMsToTimeElapsedString(Player.totalPlaytime)]); timeRows.push(["Total", convertTimeMsToTimeElapsedString(Player.totalPlaytime)]);
@ -265,13 +266,11 @@ export function CharacterStats(): React.ReactElement {
data={{ content: `${Player.purchasedServers.length} / ${getPurchaseServerLimit()}` }} data={{ content: `${Player.purchasedServers.length} / ${getPurchaseServerLimit()}` }}
/> />
<StatsRow <StatsRow
name={`Hacknet ${Player.bitNodeN === 9 || Player.sourceFileLvl(9) > 0 ? "Servers" : "Nodes"} owned`} name={`Hacknet ${canAccessBitNodeFeature(9) ? "Servers" : "Nodes"} owned`}
color={Settings.theme.primary} color={Settings.theme.primary}
data={{ data={{
content: `${Player.hacknetNodes.length}${ content: `${Player.hacknetNodes.length}${
Player.bitNodeN === 9 || Player.sourceFileLvl(9) > 0 canAccessBitNodeFeature(9) ? ` / ${HacknetServerConstants.MaxServers}` : ""
? ` / ${HacknetServerConstants.MaxServers}`
: ""
}`, }`,
}} }}
/> />
@ -317,7 +316,7 @@ export function CharacterStats(): React.ReactElement {
color={Settings.theme.cha} color={Settings.theme.cha}
data={{ level: Player.skills.charisma, exp: Player.exp.charisma }} 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 <StatsRow
name="Intelligence" name="Intelligence"
color={Settings.theme.int} color={Settings.theme.int}
@ -332,7 +331,7 @@ export function CharacterStats(): React.ReactElement {
<Paper sx={{ p: 1, mb: 1 }}> <Paper sx={{ p: 1, mb: 1 }}>
<Typography variant="h5" color="primary" sx={{ display: "flex", alignItems: "center", flexWrap: "wrap" }}> <Typography variant="h5" color="primary" sx={{ display: "flex", alignItems: "center", flexWrap: "wrap" }}>
Multipliers Multipliers
{Player.sourceFileLvl(5) > 0 && ( {canAccessBitNodeFeature(5) && (
<Tooltip <Tooltip
title={ title={
<Typography> <Typography>
@ -553,15 +552,15 @@ export function CharacterStats(): React.ReactElement {
}, },
]} ]}
color={Settings.theme.primary} color={Settings.theme.primary}
noMargin={!Player.sourceFileLvl(14) && Player.bitNodeN !== 14} noMargin={!canAccessBitNodeFeature(14)}
/> />
)} )}
{(Player.sourceFileLvl(14) || Player.bitNodeN === 14) && ( {canAccessBitNodeFeature(14) && (
<MultiplierTable <MultiplierTable
rows={[ rows={[
{ {
mult: "IPvGO Node Power bonus", 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", mult: "IPvGO Max Favor",
@ -590,7 +589,7 @@ export function CharacterStats(): React.ReactElement {
<CurrentBitNode /> <CurrentBitNode />
{(Player.bitNodeN === 5 || Player.sourceFileLvl(5) > 0) && ( {canAccessBitNodeFeature(5) && (
<Paper sx={{ p: 1, mb: 1 }}> <Paper sx={{ p: 1, mb: 1 }}>
<Typography variant="h5">BitNode Multipliers</Typography> <Typography variant="h5">BitNode Multipliers</Typography>
<BitNodeMultipliersDisplay n={Player.bitNodeN} /> <BitNodeMultipliersDisplay n={Player.bitNodeN} />

@ -118,6 +118,20 @@ export function Val({ name, color }: ValProps): React.ReactElement {
return clearSubscription; return clearSubscription;
}, [name]); }, [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>; return <Typography color={color}>{formattedVals[name]()}</Typography>;
} }

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

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