Compare commits

...

23 Commits

Author SHA1 Message Date
catloversg
45bce6e45e MISC: Reduce achievements check interval (#2650) 2026-04-15 18:47:29 -07:00
catloversg
c21d1f44b2 UI: Add button to open Faction page from Gang UI (#2655) 2026-04-14 15:53:40 -07:00
catloversg
956e00f789 BUGFIX: Intelligence data is incorrectly migrated when Intelligence is not unlocked (#2660) 2026-04-14 15:20:01 -07:00
catloversg
c5536d252b MISC: Update "Last updated" date in changelog (#2658) 2026-04-13 13:10:59 +07:00
catloversg
a99ca64455 MISC: Update changelog (#2657) 2026-04-13 11:32:39 +07:00
catloversg
cb14655325 MISC: Update description of "cat" command (#2654) 2026-04-12 17:06:27 -07:00
catloversg
9ab3e0bcb4 DOCUMENTATION: Update tutorial script for buying cloud servers (#2653) 2026-04-12 17:05:56 -07:00
catloversg
cc9144c01b UI: Use exponential notation when formatting very small HP or thread values (#2656) 2026-04-12 16:49:30 -07:00
Lee Stutzman
fb3fa00b3d API: Add weakenEffect to formulas.hacking namespace (#2626) 2026-04-10 16:36:45 -07:00
Lee Stutzman
8cbd6ff9e1 BUGFIX: Fix tab completion for multi-word quoted autocomplete options (#2612) 2026-04-10 16:36:11 -07:00
Michael Ficocelli
00a1bc2f6e DNET: Remove bonus time effect on authentication and heartbleed speed; fix ram rounding (#2627) 2026-04-10 16:04:05 -07:00
Lee Stutzman
be6fcd206f API: Rename ns.gang.getOtherGangInformation to getAllGangInformation (#2635) 2026-04-10 15:59:39 -07:00
catloversg
a6a112198e WORKFLOW: Allow specifying commit hash id when building artifacts (#2652) 2026-04-10 15:56:51 -07:00
catloversg
732aadb2d6 UI: Add hooks to sidebar for players to attach custom content (#2651) 2026-04-10 15:54:54 -07:00
catloversg
85c9ac0181 TOOL: Remove redundant "$" from JS/TS regex in webpack config (#2649) 2026-04-10 15:50:52 -07:00
catloversg
e232f37550 BLADEBURNER: Add tooltips explaining why skill upgrades are disabled (#2648) 2026-04-10 15:50:07 -07:00
catloversg
6074721c59 MISC: Update description of "BN9: Challenge" achievement (#2647) 2026-04-10 15:46:47 -07:00
catloversg
09e46d757b CLI: Add "hidden" mkdir command (#2646) 2026-04-10 15:45:51 -07:00
catloversg
5cb0d559df UI: Consistently calculate BitNode "level" (#2645) 2026-04-10 15:45:18 -07:00
catloversg
54287e5f7f DOCUMENTATION: Update RAM cost of hacknet APIs and remove unnecessary RAM cost docs (#2639) 2026-04-09 17:27:17 -07:00
catloversg
d25b1676ab MISC: Apply SF override to charisma calculations (#2642) 2026-04-09 17:25:51 -07:00
Lee Stutzman
d6299becd6 DOCUMENTATION: Clarify scp and exec darknet permissions in API docs (#2634) 2026-04-09 17:24:37 -07:00
catloversg
19b137e2fb UI: Remove unnecessary max-width of tab list in in-game editor (#2643) 2026-04-09 17:22:53 -07:00
93 changed files with 534 additions and 266 deletions

View File

@@ -2,7 +2,17 @@ name: Build artifacts
on:
workflow_dispatch:
inputs:
git-sha:
description: "Commit SHA-1 to checkout"
required: false
default: ""
workflow_call:
inputs:
git-sha:
type: string
required: false
default: ""
env:
GH_TOKEN: ${{ github.token }}
@@ -13,6 +23,8 @@ jobs:
runs-on: windows-latest
steps:
- uses: actions/checkout@v4
with:
ref: ${{ github.event.inputs.git-sha || inputs.git-sha || github.sha }}
- name: Use Node.js 24
uses: actions/setup-node@v4
with:
@@ -46,6 +58,8 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
ref: ${{ github.event.inputs.git-sha || inputs.git-sha || github.sha }}
- name: Use Node.js 24
uses: actions/setup-node@v4
with:
@@ -77,6 +91,8 @@ jobs:
runs-on: macos-latest
steps:
- uses: actions/checkout@v4
with:
ref: ${{ github.event.inputs.git-sha || inputs.git-sha || github.sha }}
- name: Use Node.js 24
uses: actions/setup-node@v4
with:

View File

@@ -1,15 +1,15 @@
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
[Home](./index.md) &gt; [bitburner](./bitburner.md) &gt; [Gang](./bitburner.gang.md) &gt; [getOtherGangInformation](./bitburner.gang.getotherganginformation.md)
[Home](./index.md) &gt; [bitburner](./bitburner.md) &gt; [Gang](./bitburner.gang.md) &gt; [getAllGangInformation](./bitburner.gang.getallganginformation.md)
## Gang.getOtherGangInformation() method
## Gang.getAllGangInformation() method
Get information about all gangs.
**Signature:**
```typescript
getOtherGangInformation(): Record<string, GangOtherInfoObject>;
getAllGangInformation(): Record<string, GangOtherInfoObject>;
```
**Returns:**

View File

@@ -61,6 +61,17 @@ Check if you can recruit a new gang member.
Create a gang.
</td></tr>
<tr><td>
[getAllGangInformation()](./bitburner.gang.getallganginformation.md)
</td><td>
Get information about all gangs.
</td></tr>
<tr><td>
@@ -182,17 +193,6 @@ Get information about a specific gang member.
List all gang members.
</td></tr>
<tr><td>
[getOtherGangInformation()](./bitburner.gang.getotherganginformation.md)
</td><td>
Get information about all gangs.
</td></tr>
<tr><td>

View File

@@ -126,6 +126,17 @@ Calculate hack percent for one thread. (Ex: 0.25 would steal 25% of the server's
Calculate hack time.
</td></tr>
<tr><td>
[weakenEffect(threads, cores)](./bitburner.hackingformulas.weakeneffect.md)
</td><td>
Calculate the security decrease from a weaken operation. Unlike other hacking formulas, weaken effect depends only on thread count and core count, not on server or player properties. The core bonus formula is .
</td></tr>
<tr><td>

View File

@@ -0,0 +1,72 @@
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
[Home](./index.md) &gt; [bitburner](./bitburner.md) &gt; [HackingFormulas](./bitburner.hackingformulas.md) &gt; [weakenEffect](./bitburner.hackingformulas.weakeneffect.md)
## HackingFormulas.weakenEffect() method
Calculate the security decrease from a weaken operation. Unlike other hacking formulas, weaken effect depends only on thread count and core count, not on server or player properties. The core bonus formula is .
**Signature:**
```typescript
weakenEffect(threads: number, cores?: number): number;
```
## Parameters
<table><thead><tr><th>
Parameter
</th><th>
Type
</th><th>
Description
</th></tr></thead>
<tbody><tr><td>
threads
</td><td>
number
</td><td>
Number of threads running weaken.
</td></tr>
<tr><td>
cores
</td><td>
number
</td><td>
_(Optional)_ Number of cores on the host server. Default 1.
</td></tr>
</tbody></table>
**Returns:**
number
The security decrease amount.

View File

@@ -72,7 +72,7 @@ Cost of upgrading the specified Hacknet Node's cache.
## Remarks
RAM cost: 0 GB
RAM cost: 0.5 GB
This function is only applicable for Hacknet Servers (the upgraded version of a Hacknet Node).

View File

@@ -72,7 +72,7 @@ Cost of upgrading the specified Hacknet Node's number of cores.
## Remarks
RAM cost: 0 GB
RAM cost: 0.5 GB
Returns the cost of upgrading the number of cores of the specified Hacknet Node by n.

View File

@@ -54,7 +54,7 @@ Level of the upgrade.
## Remarks
RAM cost: 0 GB
RAM cost: 0.5 GB
This function is only applicable for Hacknet Servers (the upgraded version of a Hacknet Node).

View File

@@ -19,7 +19,7 @@ An array containing the available upgrades
## Remarks
RAM cost: 0 GB
RAM cost: 0.5 GB
This function is only applicable for Hacknet Servers (the upgraded version of a Hacknet Node).

View File

@@ -72,7 +72,7 @@ Cost of upgrading the specified Hacknet Node.
## Remarks
RAM cost: 0 GB
RAM cost: 0.5 GB
Returns the cost of upgrading the specified Hacknet Node by n levels.

View File

@@ -56,7 +56,7 @@ Object containing a variety of stats about the specified Hacknet Node.
## Remarks
RAM cost: 0 GB
RAM cost: 0.5 GB
Returns an object containing a variety of stats about the specified Hacknet Node.

View File

@@ -19,7 +19,7 @@ Cost of purchasing a new Hacknet Node.
## Remarks
RAM cost: 0 GB
RAM cost: 0.5 GB
Returns the cost of purchasing a new Hacknet Node.

View File

@@ -72,7 +72,7 @@ Cost of upgrading the specified Hacknet Node's RAM.
## Remarks
RAM cost: 0 GB
RAM cost: 0.5 GB
Returns the cost of upgrading the RAM of the specified Hacknet Node n times.

View File

@@ -19,7 +19,7 @@ Multiplier.
## Remarks
RAM cost: 0 GB
RAM cost: 0.5 GB
This function is only applicable for Hacknet Servers (the upgraded version of a Hacknet Node).

View File

@@ -19,7 +19,7 @@ Multiplier.
## Remarks
RAM cost: 0 GB
RAM cost: 0.5 GB
This function is only applicable for Hacknet Servers (the upgraded version of a Hacknet Node).

View File

@@ -19,7 +19,7 @@ Number of hashes you can store.
## Remarks
RAM cost: 0 GB
RAM cost: 0.5 GB
This function is only applicable for Hacknet Servers (the upgraded version of a Hacknet Node).

View File

@@ -72,7 +72,7 @@ Number of hashes required for the specified upgrade.
## Remarks
RAM cost: 0 GB
RAM cost: 0.5 GB
This function is only applicable for Hacknet Servers (the upgraded version of a Hacknet Node).

View File

@@ -19,5 +19,5 @@ Maximum number of hacknet nodes.
## Remarks
RAM cost: 0 GB
RAM cost: 0.5 GB

View File

@@ -19,7 +19,7 @@ Number of hashes you have.
## Remarks
RAM cost: 0 GB
RAM cost: 0.5 GB
This function is only applicable for Hacknet Servers (the upgraded version of a Hacknet Node).

View File

@@ -19,7 +19,7 @@ Number of hacknet nodes.
## Remarks
RAM cost: 0 GB
RAM cost: 0.5 GB
Returns the number of Hacknet Nodes you own.

View File

@@ -19,7 +19,7 @@ The index of the Hacknet Node or if the player cannot afford to purchase a new H
## Remarks
RAM cost: 0 GB
RAM cost: 0.5 GB
Purchases a new Hacknet Node. Returns a number with the index of the Hacknet Node. This index is equivalent to the number at the end of the Hacknet Nodes name (e.g. The Hacknet Node named `hacknet-node-4` will have an index of 4).

View File

@@ -88,7 +88,7 @@ True if the upgrade is successfully purchased, and false otherwise.
## Remarks
RAM cost: 0 GB
RAM cost: 0.5 GB
This function is only applicable for Hacknet Servers (the upgraded version of a Hacknet Node).

View File

@@ -72,7 +72,7 @@ True if the Hacknet Nodes cache level is successfully upgraded, false otherwi
## Remarks
RAM cost: 0 GB
RAM cost: 0.5 GB
This function is only applicable for Hacknet Servers (the upgraded version of a Hacknet Node).

View File

@@ -72,7 +72,7 @@ True if the Hacknet Nodes cores are successfully purchased, false otherwise.
## Remarks
RAM cost: 0 GB
RAM cost: 0.5 GB
Tries to purchase n cores for the specified Hacknet Node.

View File

@@ -72,7 +72,7 @@ True if the Hacknet Nodes level is successfully upgraded, false otherwise.
## Remarks
RAM cost: 0 GB
RAM cost: 0.5 GB
Tries to upgrade the level of the specified Hacknet Node by n.

View File

@@ -72,7 +72,7 @@ True if the Hacknet Nodes RAM is successfully upgraded, false otherwise.
## Remarks
RAM cost: 0 GB
RAM cost: 0.5 GB
Tries to upgrade the specified Hacknet Nodes RAM n times. Note that each upgrade doubles the Nodes RAM. So this is equivalent to multiplying the Nodes RAM by 2 n.

View File

@@ -6,18 +6,14 @@
Arguments passed into the script.
These arguments can be accessed as a normal array by using the `[]` operator (`args[0]`<!-- -->, `args[1]`<!-- -->, etc...). Arguments can be string, number, or boolean. Use `args.length` to get the number of arguments that were passed into a script.
**Signature:**
```typescript
readonly args: ScriptArg[];
```
## Remarks
RAM cost: 0 GB
Arguments passed into a script can be accessed as a normal array by using the `[]` operator (`args[0]`<!-- -->, `args[1]`<!-- -->, etc...). Arguments can be string, number, or boolean. Use `args.length` to get the number of arguments that were passed into a script.
## Example
`run example.js 7 text true`

View File

@@ -11,8 +11,3 @@ Namespace for [Bladeburner](./bitburner.bladeburner.md) functions. Contains spoi
```typescript
readonly bladeburner: Bladeburner;
```
## Remarks
RAM cost: 0 GB

View File

@@ -11,8 +11,3 @@ Namespace for [cloud](./bitburner.cloud.md) functions.
```typescript
readonly cloud: Cloud;
```
## Remarks
RAM cost: 0 GB

View File

@@ -11,8 +11,3 @@ Namespace for [coding contract](./bitburner.codingcontract.md) functions.
```typescript
readonly codingcontract: CodingContract;
```
## Remarks
RAM cost: 0 GB

View File

@@ -11,8 +11,3 @@ Namespace for [corporation](./bitburner.corporation.md) functions. Contains spoi
```typescript
readonly corporation: Corporation;
```
## Remarks
RAM cost: 0 GB

View File

@@ -11,8 +11,3 @@ Namespace for darknet functions. Contains spoilers.
```typescript
readonly dnet: Darknet;
```
## Remarks
RAM cost: 0 GB

View File

@@ -132,4 +132,5 @@ ns.exec("generic-hack.js", "joesguns", {threads: 10});
// arguments to the script.
ns.exec("foo.js", "foodnstuff", 5, 1, "test");
```
For darknet servers: A session must be established with the target server, and the script must be running on a server that is directly connected to the target, or the target must have a backdoor or stasis link installed.

View File

@@ -11,8 +11,3 @@ Namespace for [formatting](./bitburner.format.md) functions.
```typescript
readonly format: Format;
```
## Remarks
RAM cost: 0 GB

View File

@@ -11,8 +11,3 @@ Namespace for [formulas](./bitburner.formulas.md) functions.
```typescript
readonly formulas: Formulas;
```
## Remarks
RAM cost: 0 GB

View File

@@ -11,8 +11,3 @@ Namespace for [gang](./bitburner.gang.md) functions. Contains spoilers.
```typescript
readonly gang: Gang;
```
## Remarks
RAM cost: 0 GB

View File

@@ -11,8 +11,3 @@ Namespace for [Go](./bitburner.go.md) functions.
```typescript
readonly go: Go;
```
## Remarks
RAM cost: 0 GB

View File

@@ -11,8 +11,3 @@ Namespace for [grafting](./bitburner.grafting.md) functions. Contains spoilers.
```typescript
readonly grafting: Grafting;
```
## Remarks
RAM cost: 0 GB

View File

@@ -11,8 +11,3 @@ Namespace for [hacknet](./bitburner.hacknet.md) functions. Some of this API cont
```typescript
readonly hacknet: Hacknet;
```
## Remarks
RAM cost: 4 GB.

View File

@@ -11,8 +11,3 @@ Namespace for [infiltration](./bitburner.infiltration.md) functions.
```typescript
readonly infiltration: Infiltration;
```
## Remarks
RAM cost: 0 GB

View File

@@ -69,6 +69,8 @@ Description
Arguments passed into the script.
These arguments can be accessed as a normal array by using the `[]` operator (`args[0]`<!-- -->, `args[1]`<!-- -->, etc...). Arguments can be string, number, or boolean. Use `args.length` to get the number of arguments that were passed into a script.
</td></tr>
<tr><td>

View File

@@ -112,5 +112,5 @@ const server = ns.args[0];
const files = ["hack.js", "weaken.js", "grow.js"];
ns.scp(files, server, "home");
```
For password-protected servers (such as darknet servers), a session must be established with the destination server before using this function. (The source server does not require a session.)
For darknet servers: The destination requires a session, but unlike [exec](./bitburner.ns.exec.md)<!-- -->, does not require a direct connection — scp works at any distance. The source server has no darknet requirements (no session or connection needed). Use [dnet.authenticate](./bitburner.darknet.authenticate.md) (requires direct connection) or [dnet.connectToSession](./bitburner.darknet.connecttosession.md) (at any distance) to establish a session.

View File

@@ -11,8 +11,3 @@ Namespace for [singularity](./bitburner.singularity.md) functions. Contains spoi
```typescript
readonly singularity: Singularity;
```
## Remarks
RAM cost: 0 GB

View File

@@ -11,8 +11,3 @@ Namespace for [sleeve](./bitburner.sleeve.md) functions. Contains spoilers.
```typescript
readonly sleeve: Sleeve;
```
## Remarks
RAM cost: 0 GB

View File

@@ -11,8 +11,3 @@ Namespace for [Stanek](./bitburner.stanek.md) functions. Contains spoilers.
```typescript
readonly stanek: Stanek;
```
## Remarks
RAM cost: 0 GB

View File

@@ -11,8 +11,3 @@ Namespace for [stock](./bitburner.stock.md) functions.
```typescript
readonly stock: Stock;
```
## Remarks
RAM cost: 0 GB

View File

@@ -11,8 +11,3 @@ Namespace for [user interface](./bitburner.userinterface.md) functions.
```typescript
readonly ui: UserInterface;
```
## Remarks
RAM cost: 0 GB

View File

@@ -454,7 +454,7 @@
"CHALLENGE_BN9": {
"ID": "CHALLENGE_BN9",
"Name": "BN9: Challenge",
"Description": "Destroy BN9 without using hacknet servers."
"Description": "Destroy BN9 without using hacknet servers or hacknet nodes."
},
"CHALLENGE_BN10": {
"ID": "CHALLENGE_BN10",

View File

@@ -4,19 +4,10 @@ import { AchievementList } from "./AchievementList";
import { achievements } from "./Achievements";
import { Box, Typography } from "@mui/material";
import { Player } from "@player";
import { makeStyles } from "tss-react/mui";
const useStyles = makeStyles()({
root: {
width: 50,
userSelect: "none",
},
});
export function AchievementsRoot(): JSX.Element {
const { classes } = useStyles();
return (
<div className={classes.root} style={{ width: "100%" }}>
<div style={{ width: "100%" }}>
<Typography variant="h4">Achievements</Typography>
<Box mx={2}>
<Typography>

View File

@@ -93,3 +93,12 @@ export function finishBitNode() {
}
wd.backdoorInstalled = true;
}
/**
* BitNode level is not something that is stored, but rather calculated from the current BN and SF level. The concept
* appeared because saying "Enter BN1.2" is shorter than saying "Enter BN1 with SF1.1". This is how we display it in the
* BitVerse UI and other places. This function is used to consistently calculate this "level".
*/
export function getBitNodeLevel(bn = Player.bitNodeN, sfLevel = Player.activeSourceFileLvl(bn)): number {
return Math.min(sfLevel + 1, bn === 12 ? Number.MAX_VALUE : 3);
}

View File

@@ -20,7 +20,7 @@ import { StatsRow } from "../../ui/React/StatsRow";
import { defaultMultipliers, getBitNodeMultipliers } from "../BitNode";
import { BitNodeMultipliers } from "../BitNodeMultipliers";
import { PartialRecord, getRecordEntries } from "../../Types/Record";
import { canAccessBitNodeFeature } from "../BitNodeUtils";
import { canAccessBitNodeFeature, getBitNodeLevel } from "../BitNodeUtils";
interface IProps {
n: number;
@@ -56,8 +56,7 @@ export const BitNodeMultipliersDisplay = ({ n, level, hideMultsIfCannotAccessFea
// If not, then we have to assume that we want the next level up from the
// current node's source file, so we get the min of that, the SF's max level,
// or if it's BN12, ∞
const maxSfLevel = n === 12 ? Number.MAX_VALUE : 3;
const mults = getBitNodeMultipliers(n, level ?? Math.min(Player.activeSourceFileLvl(n) + 1, maxSfLevel));
const mults = getBitNodeMultipliers(n, level ?? getBitNodeLevel(n));
return (
<Box sx={{ columnCount: 2, columnGap: 1, mb: n === 1 ? 0 : -2 }}>

View File

@@ -10,6 +10,7 @@ import Button from "@mui/material/Button";
import { BitNodeMultiplierDescription } from "./BitnodeMultipliersDescription";
import { BitNodeAdvancedOptions } from "./BitNodeAdvancedOptions";
import { JSONMap } from "../../Types/Jsonable";
import { getBitNodeLevel } from "../BitNodeUtils";
interface IProps {
open: boolean;
@@ -37,7 +38,7 @@ export function PortalModal(props: IProps): React.ReactElement {
const bitNode = BitNodes[bitNodeKey];
if (bitNode == null) throw new Error(`Could not find BitNode object for number: ${props.n}`);
const maxSourceFileLevel = props.n === 12 ? "∞" : "3";
const newLevel = Math.min(props.level + 1, props.n === 12 ? Number.MAX_VALUE : 3);
const newLevel = getBitNodeLevel(props.n, props.level);
let currentSourceFiles = new Map(Player.sourceFiles);
if (!props.flume) {

View File

@@ -3,7 +3,7 @@ import type { Bladeburner } from "../Bladeburner";
import React, { useMemo } from "react";
import { CopyableText } from "../../ui/React/CopyableText";
import { formatBigNumber } from "../../ui/formatNumber";
import { Box, IconButton, Paper, Typography } from "@mui/material";
import { Box, IconButton, Paper, Tooltip, Typography } from "@mui/material";
import AddIcon from "@mui/icons-material/Add";
import CloseIcon from "@mui/icons-material/Close";
import { Skill } from "../Skill";
@@ -18,10 +18,8 @@ export function SkillElem({ skill, bladeburner, onUpgrade }: SkillElemProps): Re
const skillName = skill.name;
const skillLevel = bladeburner.getSkillLevel(skillName);
const pointCost = useMemo(() => skill.calculateCost(skillLevel), [skill, skillLevel]);
// No need to support "+1" button when the skill level reaches Number.MAX_SAFE_INTEGER.
const isSupported = skillLevel < Number.MAX_SAFE_INTEGER;
// Use skill.canUpgrade() instead of reimplementing all conditional checks.
const canLevel = isSupported && skill.canUpgrade(bladeburner, 1).available;
const check = skill.canUpgrade(bladeburner, 1);
/**
* maxLvl is only useful when we check if we should show "MAX LEVEL". For the check of the icon button, we don't need
* it. This condition is checked in skill.canUpgrade().
@@ -37,10 +35,14 @@ export function SkillElem({ skill, bladeburner, onUpgrade }: SkillElemProps): Re
<Paper sx={{ my: 1, p: 1 }}>
<Box display="flex" flexDirection="row" alignItems="center">
<CopyableText variant="h6" color="primary" value={skillName} />
{!canLevel ? (
<IconButton disabled>
<CloseIcon />
</IconButton>
{!check.available ? (
<Tooltip title={check.error}>
<span>
<IconButton disabled>
<CloseIcon />
</IconButton>
</span>
</Tooltip>
) : (
<IconButton onClick={onClick}>
<AddIcon />
@@ -51,7 +53,7 @@ export function SkillElem({ skill, bladeburner, onUpgrade }: SkillElemProps): Re
{maxLvl ? (
<Typography>MAX LEVEL</Typography>
) : (
<Typography>Skill Points required: {isSupported ? formatBigNumber(pointCost) : "N/A"}</Typography>
<Typography>Skill Points required: {formatBigNumber(pointCost)}</Typography>
)}
<Typography>{skill.desc}</Typography>
</Paper>

View File

@@ -7,7 +7,7 @@ export const CONSTANTS = {
VersionString: "3.0.0dev",
isDevBranch: true,
isInTestEnvironment: globalThis.process?.env?.JEST_WORKER_ID !== undefined,
VersionNumber: 48,
VersionNumber: 49,
/** Max level for any skill, assuming no multipliers. Determined by max numerical value in javascript for experience
* and the skill level formula in Player.js. Note that all this means it that when experience hits MAX_INT, then
@@ -111,7 +111,7 @@ export const CONSTANTS = {
// Also update Documentation/doc/en/changelog.md when appropriate (when doing a release)
LatestUpdate: `
## v3.0.0 development version: last updated 18 February 2026
## v3.0.0 development version: last updated 13 April 2026
### BREAKING CHANGES
@@ -143,6 +143,7 @@ export const CONSTANTS = {
- Cancel sleeve's current task when calling ns.sleeve.travel() (#2559) (@catloversg)
- Make ns.cloud.purchaseServer() and ns.cloud.deleteServer() use hostname as provided (#2560) (@catloversg)
- Make implicit string conversion consistent across all coding contracts (#2608) (@catloversg)
- Rename ns.gang.getOtherGangInformation to getAllGangInformation (#2635) (@lstutzman)
### MAJOR CHANGES
@@ -224,6 +225,10 @@ export const CONSTANTS = {
- Fix: Import save comparison popup shows wrong BN level (#2595) (@catloversg)
- Fix: Cannot type in text boxes rendered by players' scripts when terminal tab is shown (#2615, #2622) (@lstutzman, @catloversg)
- Navigate to gym/university instead of city when stopping focusing on gym/class work (#2613) (@lstutzman)
- Ensure prompts shown by ns.prompt do not lose focus in the terminal tab (#2631) (@catloversg)
- Remove unnecessary max-width of tab list in in-game editor (#2643) (@catloversg)
- Add hooks to sidebar for players to attach custom content (#2651) (@catloversg)
- Use exponential notation when formatting very small HP or thread values (#2656) (@catloversg)
### MISC
@@ -325,6 +330,12 @@ export const CONSTANTS = {
- Dnet: Remove packet capture (#2594) (@ficocelliguy)
- Generate more frequent and lower-reward coding contracts (#2603) (@ficocelliguy)
- Electron: Fix issues in edge cases of using --export-save (#2590) (@catloversg)
- Fix recursive alias detection causing infinite recursion (#2610) (@lstutzman)
- Add "hidden" mkdir command (#2646) (@catloversg)
- Dnet: Remove bonus time effect on authentication and heartbleed speed; fix ram rounding (#2627) (@ficocelliguy)
- Fix tab completion for multi-word quoted autocomplete options (#2612) (@lstutzman)
- Add weakenEffect to formulas.hacking namespace (#2626) (@lstutzman)
- Update description of "cat" in "help" command (#2654) (@catloversg)
### DOCUMENTATION
@@ -378,6 +389,9 @@ export const CONSTANTS = {
- Update mention of outdated getStockForecast API (#2578) (@catloversg)
- Fix newline issues in IPvGO docs and add missing RAM cost (#2602) (@catloversg)
- Document coding contract's generation and rewards (#2624) (@catloversg)
- Clarify scp and exec darknet permissions in API docs (#2634) (@lstutzman)
- Update RAM cost of hacknet APIs and remove unnecessary RAM cost docs (#2639) (@catloversg)
- Update tutorial script for buying cloud servers (#2653) (@catloversg)
### SPOILER CHANGES - UI
@@ -389,6 +403,8 @@ export const CONSTANTS = {
- Prevent ending BNs through reuse of Bladeburner UI event handler (#2574) (@catloversg)
- Always show Black Operations list (#2592) (@catloversg)
- Show hints of Sleeves mechanic in pre-endgame (#2605) (@catloversg)
- Consistently calculate BitNode "level" (#2645) (@catloversg)
- Add tooltips explaining why Bladeburner skill upgrades are disabled (#2648) (@catloversg)
### SPOILER CHANGES - MISC
@@ -416,6 +432,9 @@ export const CONSTANTS = {
- Add APIs to get rank gain and rank loss of an action (#2572) (@catloversg)
- Reduce RAM cost of inGang and inBladeburner APIs (#2582) (@catloversg)
- Fix skillMaxUpgradeCount returning 1 at extreme skill levels (#2611) (@lstutzman)
- API: Expose charged effects of Stanek's Gift active fragments (#2638) (@catloversg)
- Apply SF override to charisma calculations (#2642) (@catloversg)
- Update description of "BN9: Challenge" achievement (#2647) (@catloversg)
### SPOILER CHANGES - DOCUMENTATION
@@ -519,5 +538,9 @@ export const CONSTANTS = {
- Refactor and fix issues in db.ts (#2623) (@catloversg)
- Add dependency array to TerminalInput keydown useEffect (#2620) (@lstutzman)
- Add dependency array to GameRoot useEffect (#2617) (@lstutzman)
- Dev menu: Initialize dark net data when setting SF15 level (#2632) (@catloversg)
- Use type-only imports in ArrayHelpers.ts (#2630) (@catloversg)
- Remove redundant "$" from JS/TS regex in webpack config (#2649) (@catloversg)
- Allow specifying commit hash id when building artifacts (#2652) (@catloversg)
`,
} as const;

View File

@@ -210,7 +210,7 @@ export const getTimingAttackConfig = (difficulty: number): ServerConfig => {
"I spent some time on it, but that's not the password",
];
const alphanumeric = difficulty > 16 && Math.random() < 0.3;
const length = (alphanumeric ? 0 : 3) + difficulty / 4;
const length = Math.min((alphanumeric ? 0 : 3) + difficulty / 4, 8);
return {
modelId: ModelIds.TimingAttack,
password: getPassword(length, alphanumeric),

View File

@@ -79,17 +79,9 @@ export const calculateAuthenticationTime = (
const underleveledFactor = applyUnderleveledFactor ? 1.5 + (chaRequired + 50) / (person.skills.charisma + 50) : 1;
const hasBootsFactor = Player.hasAugmentation(AugmentationName.TheBoots) ? 0.8 : 1;
const hasSf15_2Factor = Player.activeSourceFileLvl(15) > 2 ? 0.8 : 1;
const bonusTimeFactor = hasDarknetBonusTime() ? 0.75 : 1;
const time =
baseTime *
skillFactor *
backdoorFactor *
underleveledFactor *
hasBootsFactor *
hasSf15_2Factor *
bonusTimeFactor *
threadsFactor;
baseTime * skillFactor * backdoorFactor * underleveledFactor * hasBootsFactor * hasSf15_2Factor * threadsFactor;
// We need to call GetServer and check if it's a dnet server later because this function can be called by formulas
// APIs (darknetServerData.hostname may be an invalid hostname).

View File

@@ -1,6 +1,6 @@
import { Player } from "@player";
import { addClue } from "./effects";
import { formatNumber } from "../../ui/formatNumber";
import { formatNumber, formatRam } from "../../ui/formatNumber";
import { logger } from "./offlineServerHandling";
import type { NetscriptContext } from "../../Netscript/APIWrapper";
import type { DarknetServer } from "../../Server/DarknetServer";
@@ -12,6 +12,7 @@ import type { DarknetServerData, Person as IPerson } from "@nsdefs";
import { clampNumber } from "../../utils/helpers/clampNumber";
import { ResponseCodeEnum } from "../Enums";
import { isLabyrinthServer } from "./labyrinth";
import { roundToTwo } from "../../utils/helpers/roundToTwo";
/*
* Handles the effects of removing some blocked RAM from a Darknet server.
@@ -21,7 +22,7 @@ export const handleRamBlockRemoved = (ctx: NetscriptContext, server: DarknetServ
const difficulty = server.difficulty + 1;
const ramBlockRemoved = getRamBlockRemoved(server, threads);
server.blockedRam -= ramBlockRemoved;
server.blockedRam = roundToTwo(server.blockedRam - ramBlockRemoved);
server.updateRamUsed(server.ramUsed - ramBlockRemoved);
if (server.blockedRam <= 0) {
@@ -30,10 +31,10 @@ export const handleRamBlockRemoved = (ctx: NetscriptContext, server: DarknetServ
const xpGained = Player.mults.charisma_exp * threads * 10 * 1.1 ** difficulty;
Player.gainCharismaExp(xpGained);
const result = `Liberated ${formatNumber(
const result = `Liberated ${formatRam(
ramBlockRemoved,
4,
)}gb of RAM from the server owner's processes. (Gained ${formatNumber(xpGained, 1)} cha xp.)`;
)} of RAM from the server owner's processes. (Gained ${formatNumber(xpGained, 1)} cha xp.)`;
logger(ctx)(result);
return {
success: true,
@@ -72,7 +73,7 @@ export const getRamBlockRemoved = (darknetServerData: DarknetServerData, threads
const charismaFactor = 1 + player.skills.charisma / 100;
const difficultyFactor = 2 * 0.92 ** (difficulty + 1);
const baseAmount = 0.02;
return clampNumber(baseAmount * difficultyFactor * threads * charismaFactor, 0, remainingRamBlock);
return roundToTwo(clampNumber(baseAmount * difficultyFactor * threads * charismaFactor, 0, remainingRamBlock));
};
/*
@@ -100,5 +101,5 @@ export const getRamBlock = (maxRam: number): number => {
return [16, 32, maxRam - 8][Math.floor(Math.random() * 3)];
}
return [maxRam, maxRam - 8, maxRam - 64, maxRam / 2][Math.floor(Math.random() * 4)];
return roundToTwo([maxRam, maxRam - 8, maxRam - 64, maxRam / 2][Math.floor(Math.random() * 4)]);
};

View File

@@ -367,12 +367,11 @@ Paste the following code into the [Script](../basic/scripts.md) editor:
/** @param {NS} ns */
export async function main(ns) {
// How much RAM each cloud server will have. In this case, it'll
// be 8GB.
// How much RAM each cloud server will have. In this case, it'll be 8GB.
const ram = 8;
// Iterator we'll use for our loop
let i = 0;
let i = ns.cloud.getServerNames().length;
// Continuously try to purchase cloud servers until we've reached the maximum
// amount of servers
@@ -381,16 +380,16 @@ Paste the following code into the [Script](../basic/scripts.md) editor:
if (ns.getServerMoneyAvailable("home") > ns.cloud.getRamLimit(ram)) {
// If we have enough money, then:
// 1. Purchase the server
// 2. Copy our hacking script onto the newly-purchased cloud server
// 3. Run our hacking script on the newly-purchased cloud server with 3 threads
// 2. Copy our hacking script onto the newly purchased cloud server
// 3. Run our hacking script on the newly purchased cloud server with 3 threads
// 4. Increment our iterator to indicate that we've bought a new server
let hostname = ns.cloud.purchaseServer("cloud-server-" + i, ram);
const hostname = ns.cloud.purchaseServer("cloud-server-" + i, ram);
ns.scp("early-hack-template.js", hostname);
ns.exec("early-hack-template.js", hostname, 3);
++i;
}
//Make the script wait for a second before looping again.
//Removing this line will cause an infinite loop and crash the game.
// Make the script wait for a second before looping again.
// Removing this line will cause an infinite loop and crash the game.
await ns.sleep(1000);
}
}

View File

@@ -33,12 +33,12 @@ Adding a sleep like in the first example, or changing the code so that the `awai
Common infinite loop when translating the server purchasing script in starting guide to scripts is to have a while loop, where the condition's change is conditional:
var ram = 8;
var i = 0;
const ram = 8;
let i = ns.cloud.getServerNames().length;
while (i < ns.cloud.getServerLimit()) {
if (ns.getServerMoneyAvailable("home") > ns.cloud.getRamLimit(ram)) {
var hostname = ns.cloud.purchaseServer("cloud-server-" + i, ram);
const hostname = ns.cloud.purchaseServer("cloud-server-" + i, ram);
ns.scp("early-hack-template.js", hostname);
ns.exec("early-hack-template.js", hostname, 3);
++i;

View File

@@ -561,6 +561,7 @@ import nsDoc_bitburner_gameinfo_versionnumber_md from "../../markdown/bitburner.
import nsDoc_bitburner_gang_ascendmember_md from "../../markdown/bitburner.gang.ascendmember.md?raw";
import nsDoc_bitburner_gang_canrecruitmember_md from "../../markdown/bitburner.gang.canrecruitmember.md?raw";
import nsDoc_bitburner_gang_creategang_md from "../../markdown/bitburner.gang.creategang.md?raw";
import nsDoc_bitburner_gang_getallganginformation_md from "../../markdown/bitburner.gang.getallganginformation.md?raw";
import nsDoc_bitburner_gang_getascensionresult_md from "../../markdown/bitburner.gang.getascensionresult.md?raw";
import nsDoc_bitburner_gang_getbonustime_md from "../../markdown/bitburner.gang.getbonustime.md?raw";
import nsDoc_bitburner_gang_getchancetowinclash_md from "../../markdown/bitburner.gang.getchancetowinclash.md?raw";
@@ -572,7 +573,6 @@ import nsDoc_bitburner_gang_getganginformation_md from "../../markdown/bitburner
import nsDoc_bitburner_gang_getinstallresult_md from "../../markdown/bitburner.gang.getinstallresult.md?raw";
import nsDoc_bitburner_gang_getmemberinformation_md from "../../markdown/bitburner.gang.getmemberinformation.md?raw";
import nsDoc_bitburner_gang_getmembernames_md from "../../markdown/bitburner.gang.getmembernames.md?raw";
import nsDoc_bitburner_gang_getotherganginformation_md from "../../markdown/bitburner.gang.getotherganginformation.md?raw";
import nsDoc_bitburner_gang_getrecruitsavailable_md from "../../markdown/bitburner.gang.getrecruitsavailable.md?raw";
import nsDoc_bitburner_gang_gettasknames_md from "../../markdown/bitburner.gang.gettasknames.md?raw";
import nsDoc_bitburner_gang_gettaskstats_md from "../../markdown/bitburner.gang.gettaskstats.md?raw";
@@ -746,6 +746,7 @@ import nsDoc_bitburner_hackingformulas_hackexp_md from "../../markdown/bitburner
import nsDoc_bitburner_hackingformulas_hackpercent_md from "../../markdown/bitburner.hackingformulas.hackpercent.md?raw";
import nsDoc_bitburner_hackingformulas_hacktime_md from "../../markdown/bitburner.hackingformulas.hacktime.md?raw";
import nsDoc_bitburner_hackingformulas_md from "../../markdown/bitburner.hackingformulas.md?raw";
import nsDoc_bitburner_hackingformulas_weakeneffect_md from "../../markdown/bitburner.hackingformulas.weakeneffect.md?raw";
import nsDoc_bitburner_hackingformulas_weakentime_md from "../../markdown/bitburner.hackingformulas.weakentime.md?raw";
import nsDoc_bitburner_hackingmultipliers_chance_md from "../../markdown/bitburner.hackingmultipliers.chance.md?raw";
import nsDoc_bitburner_hackingmultipliers_growth_md from "../../markdown/bitburner.hackingmultipliers.growth.md?raw";
@@ -2157,6 +2158,7 @@ AllPages["nsDoc/bitburner.gameinfo.versionnumber.md"] = nsDoc_bitburner_gameinfo
AllPages["nsDoc/bitburner.gang.ascendmember.md"] = nsDoc_bitburner_gang_ascendmember_md;
AllPages["nsDoc/bitburner.gang.canrecruitmember.md"] = nsDoc_bitburner_gang_canrecruitmember_md;
AllPages["nsDoc/bitburner.gang.creategang.md"] = nsDoc_bitburner_gang_creategang_md;
AllPages["nsDoc/bitburner.gang.getallganginformation.md"] = nsDoc_bitburner_gang_getallganginformation_md;
AllPages["nsDoc/bitburner.gang.getascensionresult.md"] = nsDoc_bitburner_gang_getascensionresult_md;
AllPages["nsDoc/bitburner.gang.getbonustime.md"] = nsDoc_bitburner_gang_getbonustime_md;
AllPages["nsDoc/bitburner.gang.getchancetowinclash.md"] = nsDoc_bitburner_gang_getchancetowinclash_md;
@@ -2168,7 +2170,6 @@ AllPages["nsDoc/bitburner.gang.getganginformation.md"] = nsDoc_bitburner_gang_ge
AllPages["nsDoc/bitburner.gang.getinstallresult.md"] = nsDoc_bitburner_gang_getinstallresult_md;
AllPages["nsDoc/bitburner.gang.getmemberinformation.md"] = nsDoc_bitburner_gang_getmemberinformation_md;
AllPages["nsDoc/bitburner.gang.getmembernames.md"] = nsDoc_bitburner_gang_getmembernames_md;
AllPages["nsDoc/bitburner.gang.getotherganginformation.md"] = nsDoc_bitburner_gang_getotherganginformation_md;
AllPages["nsDoc/bitburner.gang.getrecruitsavailable.md"] = nsDoc_bitburner_gang_getrecruitsavailable_md;
AllPages["nsDoc/bitburner.gang.gettasknames.md"] = nsDoc_bitburner_gang_gettasknames_md;
AllPages["nsDoc/bitburner.gang.gettaskstats.md"] = nsDoc_bitburner_gang_gettaskstats_md;
@@ -2342,6 +2343,7 @@ AllPages["nsDoc/bitburner.hackingformulas.hackexp.md"] = nsDoc_bitburner_hacking
AllPages["nsDoc/bitburner.hackingformulas.hackpercent.md"] = nsDoc_bitburner_hackingformulas_hackpercent_md;
AllPages["nsDoc/bitburner.hackingformulas.hacktime.md"] = nsDoc_bitburner_hackingformulas_hacktime_md;
AllPages["nsDoc/bitburner.hackingformulas.md"] = nsDoc_bitburner_hackingformulas_md;
AllPages["nsDoc/bitburner.hackingformulas.weakeneffect.md"] = nsDoc_bitburner_hackingformulas_weakeneffect_md;
AllPages["nsDoc/bitburner.hackingformulas.weakentime.md"] = nsDoc_bitburner_hackingformulas_weakentime_md;
AllPages["nsDoc/bitburner.hackingmultipliers.chance.md"] = nsDoc_bitburner_hackingmultipliers_chance_md;
AllPages["nsDoc/bitburner.hackingmultipliers.growth.md"] = nsDoc_bitburner_hackingmultipliers_growth_md;

View File

@@ -9,6 +9,10 @@ import Tabs from "@mui/material/Tabs";
import Tab from "@mui/material/Tab";
import { useCycleRerender } from "../../ui/React/hooks";
import Button from "@mui/material/Button";
import { Router } from "../../ui/GameRoot";
import { Page } from "../../ui/Router";
import { Factions } from "../../Faction/Factions";
/** React Component for all the gang stuff. */
export function GangRoot(): React.ReactElement {
@@ -18,7 +22,7 @@ export function GangRoot(): React.ReactElement {
})();
const [value, setValue] = React.useState(0);
function handleChange(event: React.SyntheticEvent, tab: number): void {
function handleChange(__event: React.SyntheticEvent, tab: number): void {
setValue(tab);
}
@@ -26,11 +30,26 @@ export function GangRoot(): React.ReactElement {
return (
<Context.Gang.Provider value={gang}>
<Tabs variant="fullWidth" value={value} onChange={handleChange} sx={{ minWidth: "fit-content", maxWidth: "45%" }}>
<Tab label="Management" />
<Tab label="Equipment" />
<Tab label="Territory" />
</Tabs>
<div style={{ display: "flex" }}>
<Tabs
variant="fullWidth"
value={value}
onChange={handleChange}
sx={{ minWidth: "fit-content", maxWidth: "45%" }}
>
<Tab label="Management" />
<Tab label="Equipment" />
<Tab label="Territory" />
</Tabs>
<Button
style={{ marginLeft: "20px" }}
onClick={() => {
Router.toPage(Page.Faction, { faction: Factions[gang.facName] });
}}
>
Faction
</Button>
</div>
{value === 0 && <ManagementSubpage />}
{value === 1 && <EquipmentsSubpage />}
{value === 2 && <TerritorySubpage />}

View File

@@ -275,7 +275,7 @@ const gang = {
getMemberNames: RamCostConstants.GangApiBase / 4,
renameMember: 0,
getGangInformation: RamCostConstants.GangApiBase / 2,
getOtherGangInformation: RamCostConstants.GangApiBase / 2,
getAllGangInformation: RamCostConstants.GangApiBase / 2,
getMemberInformation: RamCostConstants.GangApiBase / 2,
canRecruitMember: RamCostConstants.GangApiBase / 4,
getRecruitsAvailable: RamCostConstants.GangApiBase / 4,
@@ -692,6 +692,7 @@ export const RamCosts: RamCostTree<NSFull> = {
hackTime: 0,
growTime: 0,
weakenTime: 0,
weakenEffect: 0,
},
hacknetNodes: {
moneyGainRate: 0,

View File

@@ -1,6 +1,6 @@
import { Player } from "@player";
import { calculateServerGrowth, calculateGrowMoney } from "../Server/formulas/grow";
import { numCycleForGrowthCorrected } from "../Server/ServerHelpers";
import { getWeakenEffect, numCycleForGrowthCorrected } from "../Server/ServerHelpers";
import {
calculateMoneyGainRate,
calculateLevelUpgradeCost,
@@ -235,6 +235,14 @@ export function NetscriptFormulas(): InternalAPI<IFormulas> {
checkFormulasAccess(ctx);
return calculateWeakenTime(server, person) * 1000;
},
weakenEffect:
(ctx) =>
(_threads, _cores = 1) => {
const threads = helpers.number(ctx, "threads", _threads);
const cores = helpers.number(ctx, "cores", _cores);
checkFormulasAccess(ctx);
return getWeakenEffect(threads, cores);
},
},
hacknetNodes: {
moneyGainRate:

View File

@@ -2,7 +2,7 @@ import type { Gang as IGang, EquipmentStats, GangOtherInfoObject } from "@nsdefs
import type { Gang } from "../Gang/Gang";
import type { GangMember } from "../Gang/GangMember";
import type { GangMemberTask } from "../Gang/GangMemberTask";
import type { InternalAPI, NetscriptContext } from "../Netscript/APIWrapper";
import { type InternalAPI, type NetscriptContext, setRemovedFunctions } from "../Netscript/APIWrapper";
import { GangPromise, RecruitmentResult } from "../Gang/Gang";
import { Player } from "@player";
@@ -37,7 +37,7 @@ export function NetscriptGang(): InternalAPI<IGang> {
return task;
};
return {
const gangFunctions: InternalAPI<IGang> = {
createGang: (ctx) => (_faction) => {
const faction = getEnumHelper("FactionName").nsGetMember(ctx, _faction);
if (Player.gang) {
@@ -117,7 +117,7 @@ export function NetscriptGang(): InternalAPI<IGang> {
equipmentCostMult: 1 / gang.getDiscount(),
};
},
getOtherGangInformation: (ctx) => () => {
getAllGangInformation: (ctx) => () => {
getGang(ctx);
const cpy: Record<string, GangOtherInfoObject> = {};
for (const gang of Object.keys(AllGangs)) {
@@ -362,4 +362,10 @@ export function NetscriptGang(): InternalAPI<IGang> {
return GangPromise.promise;
},
};
// Removed functions
setRemovedFunctions(gangFunctions, {
getOtherGangInformation: { version: "3.0.0", replacement: "gang.getAllGangInformation" },
});
return gangFunctions;
}

View File

@@ -147,6 +147,10 @@ export abstract class Person implements IPerson {
}
overrideIntelligence(): void {
// Do not set anything if the player has not unlocked Intelligence.
if (Player.sourceFileLvl(5) === 0 && Player.bitNodeN !== 5) {
return;
}
const persistentIntelligenceSkill = this.calculateSkill(this.persistentIntelligenceData.exp, 1);
// Reset exp and skill to the persistent values if there is no limit (intelligenceOverride) or the limit is greater
// than or equal to the persistent skill.
@@ -172,7 +176,7 @@ export abstract class Person implements IPerson {
* Don't change sourceFileLvl to activeSourceFileLvl. When the player has int level, the ability to gain more int is
* a permanent benefit.
*/
if (Player.sourceFileLvl(5) > 0 || this.skills.intelligence > 0 || Player.bitNodeN === 5) {
if (Player.sourceFileLvl(5) > 0 || Player.bitNodeN === 5) {
this.exp.intelligence += exp;
this.skills.intelligence = Math.floor(this.calculateSkill(this.exp.intelligence, 1));
this.persistentIntelligenceData.exp += exp;

View File

@@ -602,6 +602,10 @@ export function canAccessCotMG(this: PlayerObject): boolean {
return canAccessBitNodeFeature(13);
}
/**
* To ensure the "SF override" option work properly, this function should only be used in special cases. In most cases,
* activeSourceFileLvl should be used instead.
*/
export function sourceFileLvl(this: PlayerObject, n: number): number {
return this.sourceFiles.get(n) ?? 0;
}

View File

@@ -52,7 +52,7 @@ export function getFactionFieldWorkRepGain(p: IPerson, favor: number): number {
}
function getDarknetCharismaBonus(p: IPerson, scalar: number = 1): number {
if (Player.sourceFileLvl(15) >= 3) {
if (Player.activeSourceFileLvl(15) >= 3) {
return p.skills.charisma * scalar;
}
return 0;

View File

@@ -310,7 +310,7 @@ export function prestigeSourceFile(isFlume: boolean): void {
}
// BitNode 12: The Recursion
if (Player.bitNodeN === 12 && Player.activeSourceFileLvl(12) > 100) {
if (Player.bitNodeN === 12 && Player.sourceFileLvl(12) > 100) {
delayedDialog("Saynt_Garmo is watching you");
}

View File

@@ -36,6 +36,7 @@ import { loadInfiltrations } from "./Infiltration/SaveLoadInfiltration";
import { InfiltrationState } from "./Infiltration/formulas/game";
import { hasDarknetAccess } from "./DarkNet/utils/darknetAuthUtils";
import { loadSettings } from "./Settings/SettingsUtils";
import { getBitNodeLevel } from "./BitNode/BitNodeUtils";
/* SaveObject.js
* Defines the object used to save/load games
@@ -270,7 +271,7 @@ class BitburnerSaveObject implements BitburnerSaveObjectType {
* - Base64 format: save file uses .json extension. Save data is the base64-encoded json save string.
*/
const extension = canUseBinaryFormat() ? "json.gz" : "json";
return `bitburnerSave_${epochTime}_BN${bn}x${Player.sourceFileLvl(bn) + 1}.${extension}`;
return `bitburnerSave_${epochTime}_BN${bn}x${getBitNodeLevel()}.${extension}`;
}
async exportGame(): Promise<void> {
@@ -430,7 +431,10 @@ class BitburnerSaveObject implements BitburnerSaveObjectType {
achievements: importedPlayer.achievements?.length ?? 0,
bitNode: importedPlayer.bitNodeN,
bitNodeLevel: importedPlayer.sourceFileLvl(importedPlayer.bitNodeN) + 1,
bitNodeLevel: getBitNodeLevel(
importedPlayer.bitNodeN,
importedPlayer.activeSourceFileLvl(importedPlayer.bitNodeN),
),
sourceFiles: [...importedPlayer.sourceFiles].reduce<number>((total, [__bn, lvl]) => (total += lvl), 0),
exploits: importedPlayer.exploits.length,

View File

@@ -2925,7 +2925,7 @@ export interface Hacknet {
/**
* Get the number of hacknet nodes you own.
* @remarks
* RAM cost: 0 GB
* RAM cost: 0.5 GB
*
* Returns the number of Hacknet Nodes you own.
*
@@ -2936,7 +2936,7 @@ export interface Hacknet {
/**
* Get the maximum number of hacknet nodes.
* @remarks
* RAM cost: 0 GB
* RAM cost: 0.5 GB
*
* @returns Maximum number of hacknet nodes.
*/
@@ -2945,7 +2945,7 @@ export interface Hacknet {
/**
* Purchase a new hacknet node.
* @remarks
* RAM cost: 0 GB
* RAM cost: 0.5 GB
*
* Purchases a new Hacknet Node. Returns a number with the index of the
* Hacknet Node. This index is equivalent to the number at the end of
@@ -2961,7 +2961,7 @@ export interface Hacknet {
/**
* Get the price of the next hacknet node.
* @remarks
* RAM cost: 0 GB
* RAM cost: 0.5 GB
*
* Returns the cost of purchasing a new Hacknet Node.
*
@@ -2972,7 +2972,7 @@ export interface Hacknet {
/**
* Get the stats of a hacknet node.
* @remarks
* RAM cost: 0 GB
* RAM cost: 0.5 GB
*
* Returns an object containing a variety of stats about the specified Hacknet Node.
*
@@ -2988,7 +2988,7 @@ export interface Hacknet {
/**
* Upgrade the level of a hacknet node.
* @remarks
* RAM cost: 0 GB
* RAM cost: 0.5 GB
*
* Tries to upgrade the level of the specified Hacknet Node by n.
*
@@ -3006,7 +3006,7 @@ export interface Hacknet {
/**
* Upgrade the RAM of a hacknet node.
* @remarks
* RAM cost: 0 GB
* RAM cost: 0.5 GB
*
* Tries to upgrade the specified Hacknet Nodes RAM n times.
* Note that each upgrade doubles the Nodes RAM.
@@ -3026,7 +3026,7 @@ export interface Hacknet {
/**
* Upgrade the core of a hacknet node.
* @remarks
* RAM cost: 0 GB
* RAM cost: 0.5 GB
*
* Tries to purchase n cores for the specified Hacknet Node.
*
@@ -3044,7 +3044,7 @@ export interface Hacknet {
/**
* Upgrade the cache of a hacknet node.
* @remarks
* RAM cost: 0 GB
* RAM cost: 0.5 GB
*
* This function is only applicable for Hacknet Servers (the upgraded version of a Hacknet Node).
*
@@ -3064,7 +3064,7 @@ export interface Hacknet {
/**
* Calculate the cost of upgrading hacknet node levels.
* @remarks
* RAM cost: 0 GB
* RAM cost: 0.5 GB
*
* Returns the cost of upgrading the specified Hacknet Node by n levels.
*
@@ -3080,7 +3080,7 @@ export interface Hacknet {
/**
* Calculate the cost of upgrading hacknet node RAM.
* @remarks
* RAM cost: 0 GB
* RAM cost: 0.5 GB
*
* Returns the cost of upgrading the RAM of the specified Hacknet Node n times.
*
@@ -3096,7 +3096,7 @@ export interface Hacknet {
/**
* Calculate the cost of upgrading hacknet node cores.
* @remarks
* RAM cost: 0 GB
* RAM cost: 0.5 GB
*
* Returns the cost of upgrading the number of cores of the specified Hacknet Node by n.
*
@@ -3112,7 +3112,7 @@ export interface Hacknet {
/**
* Calculate the cost of upgrading hacknet node cache.
* @remarks
* RAM cost: 0 GB
* RAM cost: 0.5 GB
*
* This function is only applicable for Hacknet Servers (the upgraded version of a Hacknet Node).
*
@@ -3130,7 +3130,7 @@ export interface Hacknet {
/**
* Get the total number of hashes stored.
* @remarks
* RAM cost: 0 GB
* RAM cost: 0.5 GB
*
* This function is only applicable for Hacknet Servers (the upgraded version of a Hacknet Node).
*
@@ -3143,7 +3143,7 @@ export interface Hacknet {
/**
* Get the maximum number of hashes you can store.
* @remarks
* RAM cost: 0 GB
* RAM cost: 0.5 GB
*
* This function is only applicable for Hacknet Servers (the upgraded version of a Hacknet Node).
*
@@ -3156,7 +3156,7 @@ export interface Hacknet {
/**
* Get the cost of a hash upgrade.
* @remarks
* RAM cost: 0 GB
* RAM cost: 0.5 GB
*
* This function is only applicable for Hacknet Servers (the upgraded version of a Hacknet Node).
*
@@ -3178,7 +3178,7 @@ export interface Hacknet {
/**
* Purchase a hash upgrade.
* @remarks
* RAM cost: 0 GB
* RAM cost: 0.5 GB
*
* This function is only applicable for Hacknet Servers (the upgraded version of a Hacknet Node).
*
@@ -3207,7 +3207,7 @@ export interface Hacknet {
/**
* Get the list of hash upgrades
* @remarks
* RAM cost: 0 GB
* RAM cost: 0.5 GB
*
* This function is only applicable for Hacknet Servers (the upgraded version of a Hacknet Node).
*
@@ -3223,7 +3223,7 @@ export interface Hacknet {
/**
* Get the level of a hash upgrade.
* @remarks
* RAM cost: 0 GB
* RAM cost: 0.5 GB
*
* This function is only applicable for Hacknet Servers (the upgraded version of a Hacknet Node).
*
@@ -3234,7 +3234,7 @@ export interface Hacknet {
/**
* Get the multiplier to study.
* @remarks
* RAM cost: 0 GB
* RAM cost: 0.5 GB
*
* This function is only applicable for Hacknet Servers (the upgraded version of a Hacknet Node).
*
@@ -3245,7 +3245,7 @@ export interface Hacknet {
/**
* Get the multiplier to training.
* @remarks
* RAM cost: 0 GB
* RAM cost: 0.5 GB
*
* This function is only applicable for Hacknet Servers (the upgraded version of a Hacknet Node).
*
@@ -4879,7 +4879,7 @@ export interface Gang {
*
* @returns Object containing territory and power information about all gangs, including the player's gang, if any.
*/
getOtherGangInformation(): Record<string, GangOtherInfoObject>;
getAllGangInformation(): Record<string, GangOtherInfoObject>;
/**
* Get information about a specific gang member.
@@ -6299,6 +6299,16 @@ interface HackingFormulas {
* @returns The calculated weaken time, in milliseconds.
*/
weakenTime(server: Server, player: Person): number;
/**
* Calculate the security decrease from a weaken operation.
* Unlike other hacking formulas, weaken effect depends only on thread count and
* core count, not on server or player properties. The core bonus formula is
* {@code 1 + (cores - 1) / 16}.
* @param threads - Number of threads running weaken.
* @param cores - Number of cores on the host server. Default 1.
* @returns The security decrease amount.
*/
weakenEffect(threads: number, cores?: number): number;
}
/**
@@ -7014,113 +7024,93 @@ interface UserInterface {
export interface NS {
/**
* Namespace for {@link Hacknet | hacknet} functions. Some of this API contains spoilers.
* @remarks RAM cost: 4 GB.
*/
readonly hacknet: Hacknet;
/**
* Namespace for {@link Bladeburner | Bladeburner} functions. Contains spoilers.
* @remarks RAM cost: 0 GB
*/
readonly bladeburner: Bladeburner;
/**
* Namespace for {@link CodingContract | coding contract} functions.
* @remarks RAM cost: 0 GB
*/
readonly codingcontract: CodingContract;
/**
* Namespace for {@link Cloud | cloud} functions.
* @remarks RAM cost: 0 GB
*/
readonly cloud: Cloud;
/**
* Namespace for darknet functions. Contains spoilers.
* @remarks RAM cost: 0 GB
*/
readonly dnet: Darknet;
/**
* Namespace for {@link Format | formatting} functions.
* @remarks RAM cost: 0 GB
*/
readonly format: Format;
/**
* Namespace for {@link Gang | gang} functions. Contains spoilers.
* @remarks RAM cost: 0 GB
*/
readonly gang: Gang;
/**
* Namespace for {@link Go | Go} functions.
* @remarks RAM cost: 0 GB
*/
readonly go: Go;
/**
* Namespace for {@link Sleeve | sleeve} functions. Contains spoilers.
* @remarks RAM cost: 0 GB
*/
readonly sleeve: Sleeve;
/**
* Namespace for {@link Stock | stock} functions.
* @remarks RAM cost: 0 GB
*/
readonly stock: Stock;
/**
* Namespace for {@link Formulas | formulas} functions.
* @remarks RAM cost: 0 GB
*/
readonly formulas: Formulas;
/**
* Namespace for {@link Stanek | Stanek} functions. Contains spoilers.
* @remarks RAM cost: 0 GB
*/
readonly stanek: Stanek;
/**
* Namespace for {@link Infiltration | infiltration} functions.
* @remarks RAM cost: 0 GB
*/
readonly infiltration: Infiltration;
/**
* Namespace for {@link Corporation | corporation} functions. Contains spoilers.
* @remarks RAM cost: 0 GB
*/
readonly corporation: Corporation;
/**
* Namespace for {@link UserInterface | user interface} functions.
* @remarks RAM cost: 0 GB
*/
readonly ui: UserInterface;
/**
* Namespace for {@link Singularity | singularity} functions. Contains spoilers.
* @remarks RAM cost: 0 GB
*/
readonly singularity: Singularity;
/**
* Namespace for {@link Grafting | grafting} functions. Contains spoilers.
* @remarks RAM cost: 0 GB
*/
readonly grafting: Grafting;
/**
* Arguments passed into the script.
*
* @remarks
* RAM cost: 0 GB
*
* Arguments passed into a script can be accessed as a normal array by using the `[]` operator
* These arguments can be accessed as a normal array by using the `[]` operator
* (`args[0]`, `args[1]`, etc...).
* Arguments can be string, number, or boolean.
* Use `args.length` to get the number of arguments that were passed into a script.
@@ -7920,6 +7910,11 @@ export interface NS {
* // arguments to the script.
* ns.exec("foo.js", "foodnstuff", 5, 1, "test");
* ```
*
* For darknet servers: A session must be established with the target server, and the script must be
* running on a server that is directly connected to the target, or the target must have a backdoor or
* stasis link installed.
*
* @param script - Filename of script to execute. This file must already exist on the target server.
* @param host - Hostname/IP of the target server on which to execute the script.
* @param threadOrOptions - Either an integer number of threads for new script, or a {@link RunOptions} object. Threads defaults to 1.
@@ -8057,7 +8052,11 @@ export interface NS {
* ns.scp(files, server, "home");
* ```
*
* For password-protected servers (such as darknet servers), a session must be established with the destination server before using this function. (The source server does not require a session.)
* For darknet servers: The destination requires a session, but unlike {@link NS.exec | exec}, does not
* require a direct connection — scp works at any distance. The source server has no darknet requirements
* (no session or connection needed). Use {@link Darknet.authenticate | dnet.authenticate} (requires direct
* connection) or {@link Darknet.connectToSession | dnet.connectToSession} (at any distance) to
* establish a session.
*
* @param files - Filename or an array of filenames of script/literature files to copy. Note that if a file is located in a subdirectory, the filename must include the leading `/`.
* @param destination - Hostname/IP of the destination server, which is the server to which the file will be copied.

View File

@@ -18,7 +18,6 @@ import { OpenScript } from "./OpenScript";
import { Tab } from "./Tab";
import { SpecialServers } from "../../Server/data/SpecialServers";
const tabsMaxWidth = 1640;
const searchWidth = 180;
interface IProps {
@@ -101,7 +100,6 @@ export function Tabs({ scripts, currentScript, onTabClick, onTabClose, onTabUpda
<Droppable droppableId="tabs" direction="horizontal">
{(provided, snapshot) => (
<Box
maxWidth={`${tabsMaxWidth}px`}
display="flex"
flexGrow="1"
flexDirection="row"

View File

@@ -26,6 +26,7 @@ import { Settings } from "../Settings/Settings";
import type { ScriptKey } from "../utils/helpers/scriptKey";
import { assertObject } from "../utils/TypeAssertion";
import { clampNumber } from "../utils/helpers/clampNumber";
import { roundToTwo } from "../utils/helpers/roundToTwo";
export interface BaseServerConstructorParams {
adminRights?: boolean;
@@ -233,7 +234,7 @@ export abstract class BaseServer implements IServer {
}
updateRamUsed(ram: number): void {
this.ramUsed = clampNumber(ram, 0, this.maxRam);
this.ramUsed = roundToTwo(clampNumber(ram, 0, this.maxRam));
}
pushProgram(program: ProgramFilePath | CompletedProgramName): void {

View File

@@ -359,6 +359,7 @@ export function SidebarRoot(props: { page: Page }): React.ReactElement {
canStaneksGift && { key_: Page.StaneksGift, icon: DeveloperBoardIcon },
]}
/>
<Typography id="sidebar-extra-hook-0"></Typography>
<Divider />
<SidebarAccordion
key_="Character"
@@ -386,6 +387,7 @@ export function SidebarRoot(props: { page: Page }): React.ReactElement {
canOpenGrafting && { key_: Page.Grafting, icon: BiotechIcon },
]}
/>
<Typography id="sidebar-extra-hook-1"></Typography>
<Divider />
<SidebarAccordion
key_="World"
@@ -411,6 +413,7 @@ export function SidebarRoot(props: { page: Page }): React.ReactElement {
canDarkNet && { key_: Page.DarkNet, icon: ShareIcon },
]}
/>
<Typography id="sidebar-extra-hook-2"></Typography>
<Divider />
<SidebarAccordion
key_="Help"
@@ -428,6 +431,7 @@ export function SidebarRoot(props: { page: Page }): React.ReactElement {
process.env.NODE_ENV === "development" && { key_: Page.DevMenu, icon: DeveloperBoardIcon },
]}
/>
<Typography id="sidebar-extra-hook-3"></Typography>
</List>
</Drawer>
);

View File

@@ -5,7 +5,7 @@ export const TerminalHelpText: string[] = [
" analyze Get information about the current machine ",
" backdoor Install a backdoor on the current machine ",
" buy [-l/-a/program] Purchase a program through the Dark Web",
" cat [file] Display a .msg, .lit, or text file",
" cat [file] Display the contents of a file",
" cd [dir] Change to a new directory",
" changelog Display changelog",
" check [script] [args...] Print a script's logs to Terminal",

View File

@@ -78,6 +78,7 @@ import { commitHash } from "../utils/helpers/commitHash";
import { apr1 } from "./commands/apr1";
import { changelog } from "./commands/changelog";
import { clear } from "./commands/clear";
import { mkdir } from "./commands/mkdir";
import { currentNodeMults } from "../BitNode/BitNodeMultipliers";
import { Engine } from "../engine";
import { Directory, resolveDirectory, root } from "../Paths/Directory";
@@ -134,8 +135,12 @@ export const TerminalCommands: Record<string, (args: (string | number | boolean)
vim: vim,
weaken: weaken,
wget: wget,
mkdir: mkdir,
};
// "mkdir" is a "hidden" command; i.e., it is not shown in help text or autocomplete.
export const supportedCommands = Object.keys(TerminalCommands).filter((command) => command !== "mkdir");
export class Terminal {
// Flags to determine whether the player is currently running a hack or an analyze
action: TTimer | null = null;
@@ -877,8 +882,7 @@ export class Terminal {
}
function findSimilarCommands(command: string): string[] {
const commands = Object.keys(TerminalCommands);
const offByOneLetter = commands.filter((c) => {
const offByOneLetter = supportedCommands.filter((c) => {
if (c.length !== command.length) return false;
let diff = 0;
for (let i = 0; i < c.length; i++) {
@@ -886,6 +890,6 @@ function findSimilarCommands(command: string): string[] {
}
return diff === 1;
});
const subset = commands.filter((c) => c.includes(command)).sort((a, b) => a.length - b.length);
const subset = supportedCommands.filter((c) => c.includes(command)).sort((a, b) => a.length - b.length);
return Array.from(new Set([...offByOneLetter, ...subset])).slice(0, 3);
}

View File

@@ -0,0 +1,8 @@
import { Terminal } from "../../Terminal";
export function mkdir(): void {
Terminal.error(
"Directories do not exist in the Bitburner filesystem. They are simply part of the file path.\n" +
`For example, with "/foo/bar.txt", there is no actual "/foo" directory.`,
);
}

View File

@@ -11,20 +11,27 @@ import libarg from "arg";
import { getAllDirectories, resolveDirectory, root } from "../Paths/Directory";
import { isLegacyScript, resolveScriptFilePath } from "../Paths/ScriptFilePath";
import { enums } from "../NetscriptFunctions";
import { TerminalCommands } from "./Terminal";
import { supportedCommands } from "./Terminal";
import { Terminal } from "../Terminal";
import { parseUnknownError } from "../utils/ErrorHelper";
import { DarknetServer } from "../Server/DarknetServer";
import { CompletedProgramName } from "@enums";
/** Extract the text being autocompleted, handling unclosed double quotes as a single token */
export function extractCurrentText(terminalText: string): string {
const quoteCount = (terminalText.match(/"/g) || []).length;
if (quoteCount % 2 === 1) return terminalText.substring(terminalText.lastIndexOf('"'));
return /[^ ]*$/.exec(terminalText)?.[0] ?? "";
}
/** Suggest all completion possibilities for the last argument in the last command being typed
* @param terminalText The current full text entered in the terminal
* @param baseDir The current working directory.
* @returns Array of possible string replacements for the current text being autocompleted.
*/
export async function getTabCompletionPossibilities(terminalText: string, baseDir = root): Promise<string[]> {
// Get the current command text
const currentText = /[^ ]*$/.exec(terminalText)?.[0] ?? "";
// Get the current command text, treating unclosed quotes as a single token
const currentText = extractCurrentText(terminalText);
// Remove the current text from the commands string
const valueWithoutCurrent = terminalText.substring(0, terminalText.length - currentText.length);
// Parse the commands string, this handles alias replacement as well.
@@ -84,7 +91,7 @@ export async function getTabCompletionPossibilities(terminalText: string, baseDi
const addAliases = () => addGeneric({ iterable: Aliases.keys() });
const addGlobalAliases = () => addGeneric({ iterable: GlobalAliases.keys() });
const addCommands = () => addGeneric({ iterable: Object.keys(TerminalCommands) });
const addCommands = () => addGeneric({ iterable: supportedCommands });
const addDarkwebItems = () => addGeneric({ iterable: Object.values(DarkWebItems).map((item) => item.program) });
const addServerNames = () =>
addGeneric({

View File

@@ -6,7 +6,7 @@ import { Paper, Popper, TextField, Typography } from "@mui/material";
import { KEY } from "../../utils/KeyboardEventKey";
import { Terminal } from "../../Terminal";
import { Player } from "@player";
import { getTabCompletionPossibilities } from "../getTabCompletionPossibilities";
import { extractCurrentText, getTabCompletionPossibilities } from "../getTabCompletionPossibilities";
import { Settings } from "../../Settings/Settings";
import { longestCommonStart } from "../../utils/StringHelperFunctions";
import { exceptionAlert } from "../../utils/helpers/exceptionAlert";
@@ -266,13 +266,16 @@ export function TerminalInput(): React.ReactElement {
if (possibilities.length === 0) return;
setSearchResults([]);
// Use quote-aware replacement: if mid-quote, replace from the opening quote
const currentText = extractCurrentText(value);
const replacePattern = currentText.startsWith('"') ? /"[^"]*$/ : /[^ ]*$/;
if (possibilities.length === 1) {
saveValue(value.replace(/[^ ]*$/, possibilities[0]) + " ");
saveValue(value.replace(replacePattern, possibilities[0]) + " ");
return;
}
// More than one possibility, check to see if there is a longer common string than currentText.
const longestMatch = longestCommonStart(possibilities);
saveValue(value.replace(/[^ ]*$/, longestMatch));
saveValue(value.replace(replacePattern, longestMatch));
setPossibilities(possibilities);
}

View File

@@ -130,7 +130,7 @@ export const calculateCompanyWorkStats = (
// If player has SF-11, calculate salary multiplier from favor
const favorMult = isNaN(favor) ? 1 : 1 + favor / 100;
const bn11Mult = Player.activeSourceFileLvl(11) > 0 ? favorMult : 1;
const sf15Mult = Player.sourceFileLvl(15) > 1 ? getMultiplierFromCharisma(1.5) : 1;
const sf15Mult = Player.activeSourceFileLvl(15) > 1 ? getMultiplierFromCharisma(1.5) : 1;
const gains = scaleWorkStats(
multWorkStats(

View File

@@ -157,7 +157,7 @@ const Engine = {
messages: 150,
mechanicProcess: 5, // Process Bladeburner
contractGeneration: 3000, // Generate Coding Contracts
achievementsCounter: 60, // Check if we have new achievements
achievementsCounter: 5, // Check if we have new achievements
},
decrementAllCounters: function (numCycles = 1) {
@@ -215,7 +215,7 @@ const Engine = {
if (Engine.Counters.achievementsCounter <= 0) {
calculateAchievements();
Engine.Counters.achievementsCounter = 300;
Engine.Counters.achievementsCounter = 5;
}
// This **MUST** remain the last block in the function!

View File

@@ -17,7 +17,7 @@ import { StatsRow } from "./React/StatsRow";
import { StatsTable } from "./React/StatsTable";
import { useCycleRerender } from "./React/hooks";
import { getMaxRep } from "../Go/effects/effect";
import { canAccessBitNodeFeature, knowAboutBitverse } from "../BitNode/BitNodeUtils";
import { canAccessBitNodeFeature, getBitNodeLevel, knowAboutBitverse } from "../BitNode/BitNodeUtils";
interface EmployersModalProps {
open: boolean;
@@ -103,11 +103,10 @@ function MultiplierTable(props: MultTableProps): React.ReactElement {
function CurrentBitNode(): React.ReactElement {
if (knowAboutBitverse()) {
const index = "BitNode" + Player.bitNodeN;
const lvl = Math.min(Player.sourceFileLvl(Player.bitNodeN) + 1, Player.bitNodeN === 12 ? Number.MAX_VALUE : 3);
return (
<Paper sx={{ mb: 1, p: 1 }}>
<Typography variant="h5">
BitNode {Player.bitNodeN}: {BitNodes[index].name} (Level {lvl})
BitNode {Player.bitNodeN}: {BitNodes[index].name} (Level {getBitNodeLevel()})
</Typography>
<Typography component="div" sx={{ whiteSpace: "pre-wrap", overflowWrap: "break-word" }}>
{BitNodes[index].info}

View File

@@ -193,8 +193,18 @@ export const formatInt = (n: number) => formatNumber(n, 3, 1000, true);
export const formatSleeveMemory = formatInt;
export const formatShares = formatInt;
/** Display an integer up to 999,999 before collapsing to suffixed form with 3 fractional digits */
export const formatHp = (n: number) => formatNumber(n, 3, 1e6, true);
/**
* Format a number using basicFormatter for values below 1e6, and a suffixed form with up to 3 fractional digits for
* values at or above 1e6. This uses formatNumber, so check that function for nuanced details.
*
* Values in the range (0, 0.001) are displayed in exponential notation.
*/
export const formatHp = (n: number) => {
if (n > 0 && n < 0.001) {
return formatExponential(n);
}
return formatNumber(n, 3, 1e6, true);
};
export const formatThreads = formatHp;
/** Display an integer up to 999,999,999 before collapsing to suffixed form with 3 fractional digits */

View File

@@ -624,5 +624,20 @@ export const breakingChanges300: VersionBreakingChange = {
`- Read the "General rules", "String conversion", and "Tips" sections on the "Coding Contracts" page carefully.`,
showWarning: false,
},
{
brokenAPIs: [
{
name: "ns.gang.getOtherGangInformation",
migration: {
searchValue: "getOtherGangInformation",
replaceValue: "getAllGangInformation",
},
},
],
info:
"ns.gang.getOtherGangInformation() was renamed to ns.gang.getAllGangInformation().\n" +
"The function was renamed because it returns information about all gangs, including the player's own gang.",
showWarning: false,
},
],
};

View File

@@ -637,4 +637,11 @@ Error: ${e}`,
if (ver < 48) {
showAPIBreaks("3.0.0", breakingChanges300);
}
if (ver < 49 && Player.sourceFileLvl(5) === 0 && Player.bitNodeN !== 5) {
for (const person of [Player, ...Player.sleeves]) {
person.persistentIntelligenceData.exp = 0;
person.exp.intelligence = 0;
person.skills.intelligence = 0;
}
}
}

View File

@@ -105,4 +105,31 @@ describe("v3", () => {
`bitburnerSave_backup_2.8.1_${Math.round(lastUpdate / 1000)}.json.gz`,
);
});
describe("Intelligence migration bug", () => {
test("No change in exp and skill level", async () => {
const saveData = new Uint8Array(fs.readFileSync("test/jest/Migration/save-files/v2.8.1_SF1.1_SF10.3.gz"));
const mockedDownload = await loadGameFromSaveData(saveData);
for (const person of [Player, ...Player.sleeves]) {
expect(person.persistentIntelligenceData.exp).toStrictEqual(0);
expect(person.exp.intelligence).toStrictEqual(0);
expect(person.skills.intelligence).toStrictEqual(0);
}
expect(mockedDownload).toHaveBeenCalledWith(saveData, "bitburnerSave_backup_2.8.1_1776173824.json.gz");
});
test("Reset wrong exp and skill level", async () => {
const saveData = new Uint8Array(fs.readFileSync("test/jest/Migration/save-files/v3.0.0_int_migration_bug.gz"));
const mockedDownload = await loadGameFromSaveData(saveData);
for (const person of [Player, ...Player.sleeves]) {
expect(person.persistentIntelligenceData.exp).toStrictEqual(0);
expect(person.exp.intelligence).toStrictEqual(0);
expect(person.skills.intelligence).toStrictEqual(0);
}
expect(mockedDownload).not.toHaveBeenCalled();
});
});
});

Binary file not shown.

View File

@@ -34,6 +34,7 @@ import {
import { getMostRecentAuthLog } from "../../../src/DarkNet/models/packetSniffing";
import type { Result } from "@nsdefs";
import { assertNonNullish } from "../../../src/utils/TypeAssertion";
import { roundToTwo } from "../../../src/utils/helpers/roundToTwo";
const hostnameOfNonExistentServer = "fake-server";
const errorMessageForNonExistentServer = `Invalid host: '${hostnameOfNonExistentServer}'`;
@@ -53,7 +54,9 @@ beforeEach(() => {
getDarkscapeNavigator();
Player.getHomeComputer().programs.push(CompletedProgramName.formulas);
Player.mults.charisma = 1e10;
Player.mults.hacking = 1e10;
Player.gainCharismaExp(1e100);
Player.gainHackingExp(1e100);
getNsOnServerNearLabyrinth();
});
@@ -1223,9 +1226,13 @@ describe("Use IP instead of hostname", () => {
server.ramUsed = server.blockedRam = 1;
const ns = getNS(server.hostname);
const initialBlockedRam = server.blockedRam;
const result3 = await ns.dnet.memoryReallocation(ns.getIP());
const updatedBlockedRam = getDarknetServerOrThrow(server.hostname).blockedRam;
expect(result3.success).toStrictEqual(true);
expect(result3.code).toStrictEqual(ResponseCodeEnum.Success);
expect(updatedBlockedRam).toBeLessThan(initialBlockedRam);
expect(updatedBlockedRam).toEqual(roundToTwo(updatedBlockedRam));
});
test("getBlockedRam", () => {
const ns = getNsOnNonDarkwebDarknetServer();

View File

@@ -0,0 +1,46 @@
import { FactionName } from "@enums";
import { Player } from "@player";
import { Gang } from "../../../src/Gang/Gang";
import { AllGangs } from "../../../src/Gang/AllGangs";
import { getNS, initGameEnvironment, setupBasicTestingEnvironment } from "../Utilities";
beforeAll(() => {
initGameEnvironment();
});
beforeEach(() => {
setupBasicTestingEnvironment();
// Give the player a gang so gang API is accessible
Player.gang = new Gang(FactionName.SlumSnakes, false);
});
describe("ns.gang.getAllGangInformation", () => {
it("should return territory and power info for all gangs including the player's", () => {
const ns = getNS();
const info = ns.gang.getAllGangInformation();
const gangNames = Object.keys(info);
// Should include all 7 gangs
expect(gangNames).toHaveLength(Object.keys(AllGangs).length);
// Should include the player's own gang
expect(info[FactionName.SlumSnakes]).toBeDefined();
// Each entry should have power and territory
for (const name of gangNames) {
expect(info[name]).toHaveProperty("power");
expect(info[name]).toHaveProperty("territory");
expect(typeof info[name].power).toBe("number");
expect(typeof info[name].territory).toBe("number");
}
});
it("should return copies, not references to the original AllGangs data", () => {
const ns = getNS();
const info = ns.gang.getAllGangInformation();
// Mutating the returned data should not affect AllGangs
info[FactionName.SlumSnakes].power = 999999;
expect(AllGangs[FactionName.SlumSnakes].power).not.toBe(999999);
});
});

View File

@@ -55,8 +55,12 @@ function testIntelligenceOverride(
setUpBeforePrestige = () => {},
): void {
Player.sourceFiles.set(5, 1);
// The intelligence skill level starts at 0.
expect(Player.skills.intelligence).toStrictEqual(0);
prestigeSourceFile(true);
// Start without exp.
expect(Player.exp.intelligence).toStrictEqual(0);
// When having SF5 and the skill level is 0, it's set to 1.
expect(Player.skills.intelligence).toStrictEqual(1);
expect(Player.persistentIntelligenceData.exp).toStrictEqual(0);
// Gain 1e6 exp (skill = 242).

View File

@@ -0,0 +1,30 @@
import { getWeakenEffect } from "../../../src/Server/ServerHelpers";
describe("getWeakenEffect (formulas.hacking.weakenEffect)", () => {
it("returns 0.05 per thread with single core", () => {
expect(getWeakenEffect(1, 1)).toBe(0.05);
expect(getWeakenEffect(100, 1)).toBe(5.0);
});
it("applies core bonus correctly", () => {
// Core bonus: 1 + (cores - 1) / 16
// 8 cores: 1 + 7/16 = 1.4375
expect(getWeakenEffect(1, 8)).toBeCloseTo(0.071875);
expect(getWeakenEffect(100, 8)).toBeCloseTo(7.1875);
});
it("returns 0 for 0 threads", () => {
expect(getWeakenEffect(0, 1)).toBe(0);
});
it("handles single core (no bonus)", () => {
// Core bonus with 1 core: 1 + 0/16 = 1.0
expect(getWeakenEffect(50, 1)).toBe(2.5);
});
it("handles max cores (8)", () => {
// 8 cores: 1 + 7/16 = 1.4375
// 10 threads * 0.05 * 1.4375 = 0.71875
expect(getWeakenEffect(10, 8)).toBeCloseTo(0.71875);
});
});

View File

@@ -1,7 +1,7 @@
/* eslint-disable no-await-in-loop */
import { Player } from "../../../src/Player";
import { getTabCompletionPossibilities } from "../../../src/Terminal/getTabCompletionPossibilities";
import { getTabCompletionPossibilities, extractCurrentText } from "../../../src/Terminal/getTabCompletionPossibilities";
import { Server } from "../../../src/Server/Server";
import { AddToAllServers, prestigeAllServers } from "../../../src/Server/AllServers";
import { LocationName } from "../../../src/Enums";
@@ -187,6 +187,27 @@ describe("getTabCompletionPossibilities", function () {
});
});
describe("extractCurrentText", () => {
it("returns last word for unquoted input", () => {
expect(extractCurrentText("run myscript.js foo")).toBe("foo");
});
it("returns empty string for input ending with space", () => {
expect(extractCurrentText("run myscript.js ")).toBe("");
});
it("returns text from opening quote for unclosed double quote", () => {
expect(extractCurrentText('run myscript.js "nonunique se')).toBe('"nonunique se');
});
it("returns last word when all quotes are closed", () => {
expect(extractCurrentText('run myscript.js "arg1" foo')).toBe("foo");
});
it("handles empty input", () => {
expect(extractCurrentText("")).toBe("");
});
it("returns text from opening quote with one word inside", () => {
expect(extractCurrentText('run "partial')).toBe('"partial');
});
});
function asDirectory(dir: string): Directory {
if (!isAbsolutePath(dir) || !isDirectoryPath(dir)) throw new Error(`Directory ${dir} failed typechecking`);
return dir;

View File

@@ -134,7 +134,7 @@ module.exports = (env, argv) => {
module: {
rules: [
{
test: /\.(js$|jsx|ts|tsx)$/,
test: /\.(js|jsx|ts|tsx)$/,
exclude: /node_modules/,
resourceQuery: { not: /raw/ },
use: {