Compare commits

...

50 Commits

Author SHA1 Message Date
catloversg 2aa5092d85 BLADEBURNER: Store BlackOp team count in save data (#2675) 2026-04-19 12:20:08 -07:00
catloversg a7409a01cc UI: Ensure intelligence override is a positive integer (#2673) 2026-04-19 12:09:11 -07:00
catloversg a99109d9c7 BUGFIX: Follow-up to #2660 (#2666) 2026-04-19 12:06:54 -07:00
catloversg 95af138c39 BLADEBURNER: Restrict team count of Ops/BlackOps to total team size (#2672) 2026-04-17 14:32:22 -07:00
Mathekatze f5bbc26495 MISC: Clear recent scripts when installing augmentations (#2670) 2026-04-17 14:20:07 -07:00
catloversg f8ec7f4294 CODEBASE: Fix passive event listener warning (#2671) 2026-04-17 14:09:12 -07:00
catloversg c06c6590c9 BUGFIX: calculateExp throws errors in edge cases (#2667) 2026-04-17 00:57:50 -07:00
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
catloversg ee2949418f API: Expose charged effects of active fragments (#2638) 2026-04-08 14:45:17 -07:00
Lee Stutzman fbd7930ab2 BUGFIX: Fix recursive alias detection causing infinite recursion (#2610) 2026-04-04 17:01:21 -07:00
catloversg 8b3c7c13c5 CODEBASE: Use type-only imports in ArrayHelpers.ts (#2630) 2026-04-04 16:39:24 -07:00
catloversg 996bb01075 UI: Ensure prompts shown by ns.prompt do not lose focus in the terminal tab (#2631) 2026-04-04 16:39:06 -07:00
catloversg eb4e193fac DEVMENU: Initialize dark net data when setting SF15 level (#2632) 2026-04-04 16:37:26 -07:00
catloversg 0c39fc3720 MISC: Update changelog and version number (#2628) 2026-04-04 12:39:38 +07:00
catloversg 44741a7795 DOCUMENTATION: Document coding contract's generation and rewards (#2624) 2026-04-03 21:50:19 -07:00
Lee Stutzman 2818969c8a UI: Fix non-explicit GameRoot effect dependency (#2617) 2026-04-03 21:49:17 -07:00
Lee Stutzman de9311f820 UI: Add dependency array to TerminalInput keydown useEffect (#2620) 2026-04-03 15:39:05 -07:00
Lee Stutzman 15d463d583 BUGFIX: "Do something else simultaneously" navigates to gym/university instead of City (#2613) 2026-04-03 15:37:34 -07:00
catloversg 2819947378 CODEBASE: Refactor and fix issues in db.ts (#2623) 2026-04-03 15:28:48 -07:00
catloversg 48fad72b6a UI: Follow-up to #2615 (#2622) 2026-04-03 15:15:04 -07:00
Lee Stutzman 63aa4d2a45 BUGFIX: Fix prompt text input losing focus to terminal (#2615) 2026-04-03 00:10:48 -07:00
catloversg abdf3082ca ELECTRON: Fix issues in edge cases of using --export-save (#2590) 2026-04-02 23:57:25 -07:00
catloversg 8dcccdc5bb MISC: Make implicit string conversion consistent across all coding contracts (#2608) 2026-04-02 23:53:16 -07:00
Lee Stutzman d1b6acc57a CODEBASE: Replace ipExists() linear scan with O(1) Map.has() (#2621) 2026-04-02 19:13:35 -07:00
catloversg dc4ea8452c CODEBASE: Update comments to reflect changes in #2603 (#2606) 2026-04-02 19:13:21 -07:00
Lee Stutzman 5fc54809de BUGFIX: Fix skillMaxUpgradeCount returning 1 at extreme skill levels (#2611) 2026-04-02 19:06:44 -07:00
catloversg 7425d8a8fd UI: Show hints of Sleeves mechanic in pre-endgame (#2605) 2026-03-31 13:29:49 -07:00
Michael Ficocelli 3e44f08a0f CCT: Generate more frequent and lower-reward coding contracts (#2603) 2026-03-31 13:28:39 -07:00
153 changed files with 2875 additions and 1349 deletions
+16
View File
@@ -2,7 +2,17 @@ name: Build artifacts
on: on:
workflow_dispatch: workflow_dispatch:
inputs:
git-sha:
description: "Commit SHA-1 to checkout"
required: false
default: ""
workflow_call: workflow_call:
inputs:
git-sha:
type: string
required: false
default: ""
env: env:
GH_TOKEN: ${{ github.token }} GH_TOKEN: ${{ github.token }}
@@ -13,6 +23,8 @@ jobs:
runs-on: windows-latest runs-on: windows-latest
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
with:
ref: ${{ github.event.inputs.git-sha || inputs.git-sha || github.sha }}
- name: Use Node.js 24 - name: Use Node.js 24
uses: actions/setup-node@v4 uses: actions/setup-node@v4
with: with:
@@ -46,6 +58,8 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
with:
ref: ${{ github.event.inputs.git-sha || inputs.git-sha || github.sha }}
- name: Use Node.js 24 - name: Use Node.js 24
uses: actions/setup-node@v4 uses: actions/setup-node@v4
with: with:
@@ -77,6 +91,8 @@ jobs:
runs-on: macos-latest runs-on: macos-latest
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
with:
ref: ${{ github.event.inputs.git-sha || inputs.git-sha || github.sha }}
- name: Use Node.js 24 - name: Use Node.js 24
uses: actions/setup-node@v4 uses: actions/setup-node@v4
with: with:
+72
View File
@@ -25,5 +25,77 @@
<div> <div>
<h1>Close me when operation is completed.</h1> <h1>Close me when operation is completed.</h1>
</div> </div>
<!-- Use esm for top-level await -->
<script type="module">
const databaseName = "bitburnerSave";
// Check src/db.ts to see why the current max version is 2. If the database version is greater than this value, it
// means that the code in this file is outdated.
const maxDatabaseVersion = 2;
const databases = await window.indexedDB.databases();
const database = databases.find((info) => info.name === databaseName);
if (!database) {
alert("There is no save data");
// This is the simplest way to stop execution in top-level code without using a labeled block or IIFE.
throw new Error("There is no save data");
}
if (database.version === undefined || database.version > maxDatabaseVersion) {
alert(`Invalid database version: ${database.version}`);
throw new Error(`Invalid database version: ${database.version}`);
}
// Do NOT specify the version. We must open the database at the current version; otherwise, we will trigger
// onupgradeneeded.
const dbRequest = window.indexedDB.open(databaseName);
dbRequest.onerror = (event) => {
console.error(event.target.error);
alert(event.target.error);
};
dbRequest.onsuccess = () => {
const db = dbRequest.result;
try {
if (!db.objectStoreNames.contains("savestring")) {
alert("There is no save data");
return;
}
const transaction = db.transaction(["savestring"], "readonly");
const objectStore = transaction.objectStore("savestring");
const request = objectStore.get("save");
request.onsuccess = () => {
if (request.result == null) {
alert("There is no save data");
return;
}
let isBinaryFormat;
if (request.result instanceof Uint8Array) {
// All modules in the Electron folder are CommonJS, so importing them here would be really difficult. The
// isBinaryFormat function is very small, so let's inline it here.
isBinaryFormat = true;
const magicBytesOfDeflateGzip = [0x1f, 0x8b, 0x08];
for (let i = 0; i < magicBytesOfDeflateGzip.length; ++i) {
if (magicBytesOfDeflateGzip[i] !== request.result[i]) {
isBinaryFormat = false;
break;
}
}
} else {
isBinaryFormat = false;
}
const extension = isBinaryFormat ? "json.gz" : "json";
const filename = `bitburnerSave_${Date.now()}.${extension}`;
const blob = new Blob([request.result]);
const anchorElement = document.createElement("a");
const url = URL.createObjectURL(blob);
anchorElement.href = url;
anchorElement.download = filename;
anchorElement.click();
setTimeout(function () {
URL.revokeObjectURL(url);
}, 0);
};
} catch (error) {
console.error(error);
alert(error);
}
};
</script>
</body> </body>
</html> </html>
-1
View File
@@ -258,7 +258,6 @@ app.on("ready", async () => {
await window.loadFile("export.html"); await window.loadFile("export.html");
window.show(); window.show();
setStopProcessHandler(window); setStopProcessHandler(window);
await utils.exportSave(window);
} else { } else {
window = await startWindow(process.argv.includes("--no-scripts")); window = await startWindow(process.argv.includes("--no-scripts"));
if (global.steamworksError) { if (global.steamworksError) {
-31
View File
@@ -90,36 +90,6 @@ function showErrorBox(title, error) {
dialog.showErrorBox(title, `${error.name}\n\n${error.message}`); dialog.showErrorBox(title, `${error.name}\n\n${error.message}`);
} }
function exportSaveFromIndexedDb() {
return new Promise((resolve) => {
const dbRequest = indexedDB.open("bitburnerSave");
dbRequest.onsuccess = () => {
const db = dbRequest.result;
const transaction = db.transaction(["savestring"], "readonly");
const store = transaction.objectStore("savestring");
const request = store.get("save");
request.onsuccess = () => {
const file = new Blob([request.result], { type: "text/plain" });
const a = document.createElement("a");
const url = URL.createObjectURL(file);
a.href = url;
a.download = "save.json";
document.body.appendChild(a);
a.click();
setTimeout(function () {
document.body.removeChild(a);
window.URL.revokeObjectURL(url);
resolve();
}, 0);
};
};
});
}
async function exportSave(window) {
await window.webContents.executeJavaScript(`${exportSaveFromIndexedDb.toString()}; exportSaveFromIndexedDb();`, true);
}
async function writeTerminal(window, message, type = null) { async function writeTerminal(window, message, type = null) {
await window.webContents.executeJavaScript(`window.appNotifier.terminal("${message}", "${type}");`, true); await window.webContents.executeJavaScript(`window.appNotifier.terminal("${message}", "${type}");`, true);
} }
@@ -186,7 +156,6 @@ function initializeLogLevelConfig() {
module.exports = { module.exports = {
reloadAndKill, reloadAndKill,
showErrorBox, showErrorBox,
exportSave,
attachUnresponsiveAppHandler, attachUnresponsiveAppHandler,
detachUnresponsiveAppHandler, detachUnresponsiveAppHandler,
writeTerminal, writeTerminal,
@@ -0,0 +1,19 @@
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
[Home](./index.md) &gt; [bitburner](./bitburner.md) &gt; [ActiveFragment](./bitburner.activefragment.md) &gt; [chargedEffect](./bitburner.activefragment.chargedeffect.md)
## ActiveFragment.chargedEffect property
This is the raw value of the modifier used to calculate the effect on your multipliers. It may not be a multiplier.
With fragments that increase a multiplier, this value is a multiplier. For example, with "+x% hacknet production" fragment, a value of 1.25 will multiply the "hacknet\_node\_money" multiplier by 1.25. The UI will show "+25% hacknet production".
With fragments that decrease a multiplier, you need to invert this value. For example, with "-x% cheaper hacknet costs" fragment, a value of 1.25 means the "hacknet\_node\_purchase\_cost" (and other relevant cost multipliers) will be multiplied by 0.8 (1 / 1.25). The UI will show "20% cheaper hacknet costs".
With booster fragments, this value is always 1. Booster fragments only boost non-booster fragments. They don't directly boost your multipliers.
**Signature:**
```typescript
chargedEffect: number;
```
+25
View File
@@ -37,6 +37,31 @@ Description
</th></tr></thead> </th></tr></thead>
<tbody><tr><td> <tbody><tr><td>
[chargedEffect](./bitburner.activefragment.chargedeffect.md)
</td><td>
</td><td>
number
</td><td>
This is the raw value of the modifier used to calculate the effect on your multipliers. It may not be a multiplier.
With fragments that increase a multiplier, this value is a multiplier. For example, with "+x% hacknet production" fragment, a value of 1.25 will multiply the "hacknet\_node\_money" multiplier by 1.25. The UI will show "+25% hacknet production".
With fragments that decrease a multiplier, you need to invert this value. For example, with "-x% cheaper hacknet costs" fragment, a value of 1.25 means the "hacknet\_node\_purchase\_cost" (and other relevant cost multipliers) will be multiplied by 0.8 (1 / 1.25). The UI will show "20% cheaper hacknet costs".
With booster fragments, this value is always 1. Booster fragments only boost non-booster fragments. They don't directly boost your multipliers.
</td></tr>
<tr><td>
[highestCharge](./bitburner.activefragment.highestcharge.md) [highestCharge](./bitburner.activefragment.highestcharge.md)
+1 -1
View File
@@ -12,7 +12,7 @@ Default value:
- All boolean options: false - All boolean options: false
If you specify intelligenceOverride, it must be a non-negative integer. If you specify intelligenceOverride, it must be a positive integer.
**Signature:** **Signature:**
@@ -1,15 +1,15 @@
<!-- Do not edit this file. It is automatically generated by API Documenter. --> <!-- 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. Get information about all gangs.
**Signature:** **Signature:**
```typescript ```typescript
getOtherGangInformation(): Record<string, GangOtherInfoObject>; getAllGangInformation(): Record<string, GangOtherInfoObject>;
``` ```
**Returns:** **Returns:**
+11 -11
View File
@@ -61,6 +61,17 @@ Check if you can recruit a new gang member.
Create a gang. Create a gang.
</td></tr>
<tr><td>
[getAllGangInformation()](./bitburner.gang.getallganginformation.md)
</td><td>
Get information about all gangs.
</td></tr> </td></tr>
<tr><td> <tr><td>
@@ -182,17 +193,6 @@ Get information about a specific gang member.
List all gang members. List all gang members.
</td></tr>
<tr><td>
[getOtherGangInformation()](./bitburner.gang.getotherganginformation.md)
</td><td>
Get information about all gangs.
</td></tr> </td></tr>
<tr><td> <tr><td>
+11
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. 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> </td></tr>
<tr><td> <tr><td>
@@ -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.
@@ -72,7 +72,7 @@ Cost of upgrading the specified Hacknet Node's cache.
## Remarks ## 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). This function is only applicable for Hacknet Servers (the upgraded version of a Hacknet Node).
@@ -72,7 +72,7 @@ Cost of upgrading the specified Hacknet Node's number of cores.
## Remarks ## 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. Returns the cost of upgrading the number of cores of the specified Hacknet Node by n.
@@ -54,7 +54,7 @@ Level of the upgrade.
## Remarks ## 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). This function is only applicable for Hacknet Servers (the upgraded version of a Hacknet Node).
@@ -19,7 +19,7 @@ An array containing the available upgrades
## Remarks ## 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). This function is only applicable for Hacknet Servers (the upgraded version of a Hacknet Node).
@@ -72,7 +72,7 @@ Cost of upgrading the specified Hacknet Node.
## Remarks ## Remarks
RAM cost: 0 GB RAM cost: 0.5 GB
Returns the cost of upgrading the specified Hacknet Node by n levels. Returns the cost of upgrading the specified Hacknet Node by n levels.
+1 -1
View File
@@ -56,7 +56,7 @@ Object containing a variety of stats about the specified Hacknet Node.
## Remarks ## Remarks
RAM cost: 0 GB RAM cost: 0.5 GB
Returns an object containing a variety of stats about the specified Hacknet Node. Returns an object containing a variety of stats about the specified Hacknet Node.
@@ -19,7 +19,7 @@ Cost of purchasing a new Hacknet Node.
## Remarks ## Remarks
RAM cost: 0 GB RAM cost: 0.5 GB
Returns the cost of purchasing a new Hacknet Node. Returns the cost of purchasing a new Hacknet Node.
@@ -72,7 +72,7 @@ Cost of upgrading the specified Hacknet Node's RAM.
## Remarks ## Remarks
RAM cost: 0 GB RAM cost: 0.5 GB
Returns the cost of upgrading the RAM of the specified Hacknet Node n times. Returns the cost of upgrading the RAM of the specified Hacknet Node n times.
+1 -1
View File
@@ -19,7 +19,7 @@ Multiplier.
## Remarks ## 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). This function is only applicable for Hacknet Servers (the upgraded version of a Hacknet Node).
@@ -19,7 +19,7 @@ Multiplier.
## Remarks ## 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). This function is only applicable for Hacknet Servers (the upgraded version of a Hacknet Node).
+1 -1
View File
@@ -19,7 +19,7 @@ Number of hashes you can store.
## Remarks ## 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). This function is only applicable for Hacknet Servers (the upgraded version of a Hacknet Node).
+1 -1
View File
@@ -72,7 +72,7 @@ Number of hashes required for the specified upgrade.
## Remarks ## 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). This function is only applicable for Hacknet Servers (the upgraded version of a Hacknet Node).
+1 -1
View File
@@ -19,5 +19,5 @@ Maximum number of hacknet nodes.
## Remarks ## Remarks
RAM cost: 0 GB RAM cost: 0.5 GB
+1 -1
View File
@@ -19,7 +19,7 @@ Number of hashes you have.
## Remarks ## 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). This function is only applicable for Hacknet Servers (the upgraded version of a Hacknet Node).
+1 -1
View File
@@ -19,7 +19,7 @@ Number of hacknet nodes.
## Remarks ## Remarks
RAM cost: 0 GB RAM cost: 0.5 GB
Returns the number of Hacknet Nodes you own. Returns the number of Hacknet Nodes you own.
+1 -1
View File
@@ -19,7 +19,7 @@ The index of the Hacknet Node or if the player cannot afford to purchase a new H
## Remarks ## 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). 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).
+1 -1
View File
@@ -88,7 +88,7 @@ True if the upgrade is successfully purchased, and false otherwise.
## Remarks ## 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). This function is only applicable for Hacknet Servers (the upgraded version of a Hacknet Node).
+1 -1
View File
@@ -72,7 +72,7 @@ True if the Hacknet Nodes cache level is successfully upgraded, false otherwi
## Remarks ## 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). This function is only applicable for Hacknet Servers (the upgraded version of a Hacknet Node).
+1 -1
View File
@@ -72,7 +72,7 @@ True if the Hacknet Nodes cores are successfully purchased, false otherwise.
## Remarks ## Remarks
RAM cost: 0 GB RAM cost: 0.5 GB
Tries to purchase n cores for the specified Hacknet Node. Tries to purchase n cores for the specified Hacknet Node.
+1 -1
View File
@@ -72,7 +72,7 @@ True if the Hacknet Nodes level is successfully upgraded, false otherwise.
## Remarks ## Remarks
RAM cost: 0 GB RAM cost: 0.5 GB
Tries to upgrade the level of the specified Hacknet Node by n. Tries to upgrade the level of the specified Hacknet Node by n.
+1 -1
View File
@@ -72,7 +72,7 @@ True if the Hacknet Nodes RAM is successfully upgraded, false otherwise.
## Remarks ## 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. 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.
+1 -1
View File
@@ -123,7 +123,7 @@ Default value:
- All boolean options: false - All boolean options: false
If you specify intelligenceOverride, it must be a non-negative integer. If you specify intelligenceOverride, it must be a positive integer.
</td></tr> </td></tr>
+2 -6
View File
@@ -6,18 +6,14 @@
Arguments passed into the script. 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:** **Signature:**
```typescript ```typescript
readonly args: ScriptArg[]; 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 ## Example
`run example.js 7 text true` `run example.js 7 text true`
-5
View File
@@ -11,8 +11,3 @@ Namespace for [Bladeburner](./bitburner.bladeburner.md) functions. Contains spoi
```typescript ```typescript
readonly bladeburner: Bladeburner; readonly bladeburner: Bladeburner;
``` ```
## Remarks
RAM cost: 0 GB
-5
View File
@@ -11,8 +11,3 @@ Namespace for [cloud](./bitburner.cloud.md) functions.
```typescript ```typescript
readonly cloud: Cloud; readonly cloud: Cloud;
``` ```
## Remarks
RAM cost: 0 GB
-5
View File
@@ -11,8 +11,3 @@ Namespace for [coding contract](./bitburner.codingcontract.md) functions.
```typescript ```typescript
readonly codingcontract: CodingContract; readonly codingcontract: CodingContract;
``` ```
## Remarks
RAM cost: 0 GB
-5
View File
@@ -11,8 +11,3 @@ Namespace for [corporation](./bitburner.corporation.md) functions. Contains spoi
```typescript ```typescript
readonly corporation: Corporation; readonly corporation: Corporation;
``` ```
## Remarks
RAM cost: 0 GB
-5
View File
@@ -11,8 +11,3 @@ Namespace for darknet functions. Contains spoilers.
```typescript ```typescript
readonly dnet: Darknet; readonly dnet: Darknet;
``` ```
## Remarks
RAM cost: 0 GB
+1
View File
@@ -132,4 +132,5 @@ ns.exec("generic-hack.js", "joesguns", {threads: 10});
// arguments to the script. // arguments to the script.
ns.exec("foo.js", "foodnstuff", 5, 1, "test"); 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.
-5
View File
@@ -11,8 +11,3 @@ Namespace for [formatting](./bitburner.format.md) functions.
```typescript ```typescript
readonly format: Format; readonly format: Format;
``` ```
## Remarks
RAM cost: 0 GB
-5
View File
@@ -11,8 +11,3 @@ Namespace for [formulas](./bitburner.formulas.md) functions.
```typescript ```typescript
readonly formulas: Formulas; readonly formulas: Formulas;
``` ```
## Remarks
RAM cost: 0 GB
-5
View File
@@ -11,8 +11,3 @@ Namespace for [gang](./bitburner.gang.md) functions. Contains spoilers.
```typescript ```typescript
readonly gang: Gang; readonly gang: Gang;
``` ```
## Remarks
RAM cost: 0 GB
-5
View File
@@ -11,8 +11,3 @@ Namespace for [Go](./bitburner.go.md) functions.
```typescript ```typescript
readonly go: Go; readonly go: Go;
``` ```
## Remarks
RAM cost: 0 GB
-5
View File
@@ -11,8 +11,3 @@ Namespace for [grafting](./bitburner.grafting.md) functions. Contains spoilers.
```typescript ```typescript
readonly grafting: Grafting; readonly grafting: Grafting;
``` ```
## Remarks
RAM cost: 0 GB
-5
View File
@@ -11,8 +11,3 @@ Namespace for [hacknet](./bitburner.hacknet.md) functions. Some of this API cont
```typescript ```typescript
readonly hacknet: Hacknet; readonly hacknet: Hacknet;
``` ```
## Remarks
RAM cost: 4 GB.
-5
View File
@@ -11,8 +11,3 @@ Namespace for [infiltration](./bitburner.infiltration.md) functions.
```typescript ```typescript
readonly infiltration: Infiltration; readonly infiltration: Infiltration;
``` ```
## Remarks
RAM cost: 0 GB
+2
View File
@@ -69,6 +69,8 @@ Description
Arguments passed into the script. 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> </td></tr>
<tr><td> <tr><td>
+1 -1
View File
@@ -112,5 +112,5 @@ const server = ns.args[0];
const files = ["hack.js", "weaken.js", "grow.js"]; const files = ["hack.js", "weaken.js", "grow.js"];
ns.scp(files, server, "home"); 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.
-5
View File
@@ -11,8 +11,3 @@ Namespace for [singularity](./bitburner.singularity.md) functions. Contains spoi
```typescript ```typescript
readonly singularity: Singularity; readonly singularity: Singularity;
``` ```
## Remarks
RAM cost: 0 GB
-5
View File
@@ -11,8 +11,3 @@ Namespace for [sleeve](./bitburner.sleeve.md) functions. Contains spoilers.
```typescript ```typescript
readonly sleeve: Sleeve; readonly sleeve: Sleeve;
``` ```
## Remarks
RAM cost: 0 GB
-5
View File
@@ -11,8 +11,3 @@ Namespace for [Stanek](./bitburner.stanek.md) functions. Contains spoilers.
```typescript ```typescript
readonly stanek: Stanek; readonly stanek: Stanek;
``` ```
## Remarks
RAM cost: 0 GB
-5
View File
@@ -11,8 +11,3 @@ Namespace for [stock](./bitburner.stock.md) functions.
```typescript ```typescript
readonly stock: Stock; readonly stock: Stock;
``` ```
## Remarks
RAM cost: 0 GB
-5
View File
@@ -11,8 +11,3 @@ Namespace for [user interface](./bitburner.userinterface.md) functions.
```typescript ```typescript
readonly ui: UserInterface; readonly ui: UserInterface;
``` ```
## Remarks
RAM cost: 0 GB
+1 -1
View File
@@ -454,7 +454,7 @@
"CHALLENGE_BN9": { "CHALLENGE_BN9": {
"ID": "CHALLENGE_BN9", "ID": "CHALLENGE_BN9",
"Name": "BN9: Challenge", "Name": "BN9: Challenge",
"Description": "Destroy BN9 without using hacknet servers." "Description": "Destroy BN9 without using hacknet servers or hacknet nodes."
}, },
"CHALLENGE_BN10": { "CHALLENGE_BN10": {
"ID": "CHALLENGE_BN10", "ID": "CHALLENGE_BN10",
+1 -10
View File
@@ -4,19 +4,10 @@ import { AchievementList } from "./AchievementList";
import { achievements } from "./Achievements"; import { achievements } from "./Achievements";
import { Box, Typography } from "@mui/material"; import { Box, Typography } from "@mui/material";
import { Player } from "@player"; import { Player } from "@player";
import { makeStyles } from "tss-react/mui";
const useStyles = makeStyles()({
root: {
width: 50,
userSelect: "none",
},
});
export function AchievementsRoot(): JSX.Element { export function AchievementsRoot(): JSX.Element {
const { classes } = useStyles();
return ( return (
<div className={classes.root} style={{ width: "100%" }}> <div style={{ width: "100%" }}>
<Typography variant="h4">Achievements</Typography> <Typography variant="h4">Achievements</Typography>
<Box mx={2}> <Box mx={2}>
<Typography> <Typography>
+2 -2
View File
@@ -93,7 +93,7 @@ function applyAliases(origCommand: string, depth = 0, currentlyProcessingAliases
// First get non-global aliases, and recursively apply them // First get non-global aliases, and recursively apply them
// (unless there are any reference loops or the reference chain is too deep) // (unless there are any reference loops or the reference chain is too deep)
const localAlias = Aliases.get(commandArray[0]); const localAlias = Aliases.get(commandArray[0]);
if (localAlias && !currentlyProcessingAliases.includes(localAlias)) { if (localAlias && !currentlyProcessingAliases.includes(commandArray[0])) {
const appliedAlias = applyAliases(localAlias, depth + 1, [commandArray[0], ...currentlyProcessingAliases]); const appliedAlias = applyAliases(localAlias, depth + 1, [commandArray[0], ...currentlyProcessingAliases]);
commandArray.splice(0, 1, ...appliedAlias.split(" ")); commandArray.splice(0, 1, ...appliedAlias.split(" "));
} }
@@ -101,7 +101,7 @@ function applyAliases(origCommand: string, depth = 0, currentlyProcessingAliases
// Once local aliasing is complete (or if none are present) handle any global aliases // Once local aliasing is complete (or if none are present) handle any global aliases
const processedCommands = commandArray.reduce((resolvedCommandArray: string[], command) => { const processedCommands = commandArray.reduce((resolvedCommandArray: string[], command) => {
const globalAlias = GlobalAliases.get(command); const globalAlias = GlobalAliases.get(command);
if (globalAlias && !currentlyProcessingAliases.includes(globalAlias)) { if (globalAlias && !currentlyProcessingAliases.includes(command)) {
const appliedAlias = applyAliases(globalAlias, depth + 1, [command, ...currentlyProcessingAliases]); const appliedAlias = applyAliases(globalAlias, depth + 1, [command, ...currentlyProcessingAliases]);
resolvedCommandArray.push(appliedAlias); resolvedCommandArray.push(appliedAlias);
} else { } else {
+1 -1
View File
@@ -370,7 +370,7 @@ export function initBitNodes() {
<ul> <ul>
<li> <li>
Sleeve: Duplicate your consciousness into Synthoids, allowing you to perform different tasks asynchronously. Sleeve: Duplicate your consciousness into Synthoids, allowing you to perform different tasks asynchronously.
You cannot buy Sleeves outside this BitNode. You cannot buy Sleeves or upgrade them outside this BitNode.
</li> </li>
<li> <li>
Grafting: Visit VitaLife in New Tokyo to get access to this technology. It allows you to graft Grafting: Visit VitaLife in New Tokyo to get access to this technology. It allows you to graft
+9
View File
@@ -93,3 +93,12 @@ export function finishBitNode() {
} }
wd.backdoorInstalled = true; 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);
}
+3 -3
View File
@@ -271,13 +271,13 @@ function IntelligenceOverride({
disabled={!enabled} disabled={!enabled}
value={intelligenceOverride !== undefined ? intelligenceOverride : ""} value={intelligenceOverride !== undefined ? intelligenceOverride : ""}
onChange={(event) => { onChange={(event) => {
// Empty string will be automatically changed to "0". // Empty string will be automatically changed to "1".
if (event.target.value === "") { if (event.target.value === "") {
callbacks.setIntelligenceOverride(0); callbacks.setIntelligenceOverride(1);
return; return;
} }
const value = Number.parseInt(event.target.value); const value = Number.parseInt(event.target.value);
if (!Number.isInteger(value) || value < 0) { if (!Number.isInteger(value) || value < 1) {
return; return;
} }
callbacks.setIntelligenceOverride(value); callbacks.setIntelligenceOverride(value);
@@ -20,7 +20,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"; import { canAccessBitNodeFeature, getBitNodeLevel } from "../BitNodeUtils";
interface IProps { interface IProps {
n: number; 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 // 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 ? Number.MAX_VALUE : 3; const mults = getBitNodeMultipliers(n, level ?? getBitNodeLevel(n));
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 }}>
+2 -1
View File
@@ -10,6 +10,7 @@ import Button from "@mui/material/Button";
import { BitNodeMultiplierDescription } from "./BitnodeMultipliersDescription"; import { BitNodeMultiplierDescription } from "./BitnodeMultipliersDescription";
import { BitNodeAdvancedOptions } from "./BitNodeAdvancedOptions"; import { BitNodeAdvancedOptions } from "./BitNodeAdvancedOptions";
import { JSONMap } from "../../Types/Jsonable"; import { JSONMap } from "../../Types/Jsonable";
import { getBitNodeLevel } from "../BitNodeUtils";
interface IProps { interface IProps {
open: boolean; open: boolean;
@@ -37,7 +38,7 @@ export function PortalModal(props: IProps): React.ReactElement {
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); const newLevel = getBitNodeLevel(props.n, props.level);
let currentSourceFiles = new Map(Player.sourceFiles); let currentSourceFiles = new Map(Player.sourceFiles);
if (!props.flume) { if (!props.flume) {
+25 -4
View File
@@ -6,6 +6,8 @@ import { ActionClass, ActionParams } from "./Action";
import { operationSkillSuccessBonus, operationTeamSuccessBonus } from "./Operation"; import { operationSkillSuccessBonus, operationTeamSuccessBonus } from "./Operation";
import { getEnumHelper } from "../../utils/EnumHelper"; import { getEnumHelper } from "../../utils/EnumHelper";
import type { TeamActionWithCasualties } from "./TeamCasualties"; import type { TeamActionWithCasualties } from "./TeamCasualties";
import { constructorsForReviver, Generic_fromJSON, type IReviverValue } from "../../utils/JSONReviver";
import { clampInteger } from "../../utils/helpers/clampNumber";
interface BlackOpParams { interface BlackOpParams {
name: BladeburnerBlackOpName; name: BladeburnerBlackOpName;
@@ -32,11 +34,11 @@ export class BlackOperation extends ActionClass implements TeamActionWithCasualt
return getEnumHelper("BladeburnerBlackOpName").isMember(name); return getEnumHelper("BladeburnerBlackOpName").isMember(name);
} }
constructor(params: ActionParams & BlackOpParams) { constructor(params: (ActionParams & BlackOpParams) | null = null) {
super(params); super(params);
this.name = params.name; this.name = params?.name ?? BladeburnerBlackOpName.OperationTyphoon;
this.reqdRank = params.reqdRank; this.reqdRank = params?.reqdRank ?? 0;
this.n = params.n; this.n = params?.n ?? 0;
} }
getAvailability(bladeburner: Bladeburner): Availability { getAvailability(bladeburner: Bladeburner): Availability {
@@ -65,4 +67,23 @@ export class BlackOperation extends ActionClass implements TeamActionWithCasualt
getTeamSuccessBonus = operationTeamSuccessBonus; getTeamSuccessBonus = operationTeamSuccessBonus;
getActionTypeSkillSuccessBonus = operationSkillSuccessBonus; getActionTypeSkillSuccessBonus = operationSkillSuccessBonus;
toJSON(): IReviverValue {
return {
ctor: "BlackOperation",
data: {
teamCount: this.teamCount,
},
};
}
loadData(loadedObject: BlackOperation): void {
this.teamCount = clampInteger(loadedObject.teamCount, 0);
}
static fromJSON(value: IReviverValue): BlackOperation {
return Generic_fromJSON(BlackOperation, value.data);
}
} }
constructorsForReviver.BlackOperation = BlackOperation;
+10 -4
View File
@@ -44,12 +44,18 @@ export function resolveTeamCasualties(action: TeamActionWithCasualties, team: Op
*/ */
const losses = const losses =
minCasualties <= maxCasualties ? team.getTeamCasualtiesRoll(minCasualties, maxCasualties) : minCasualties; minCasualties <= maxCasualties ? team.getTeamCasualtiesRoll(minCasualties, maxCasualties) : minCasualties;
team.teamSize -= losses; // Calculate the new teamSize in a temporary variable and call the setter team.teamSize ONCE.
if (team.teamSize < team.sleeveSize) { // Note that it's important to call the setter only once; otherwise, the team count of each operation won't be reset
team.killRandomSupportingSleeves(team.sleeveSize - team.teamSize); // correctly.
// For example, if _teamSize is 9 (1 team member + 8 support sleeves) and "losses" is 9, calling the setter with
// (team.teamSize - losses) will set teamCount of ops/blackOps to 0 while it should be 8.
let newTeamSize = team.teamSize - losses;
if (newTeamSize < team.sleeveSize) {
team.killRandomSupportingSleeves(team.sleeveSize - newTeamSize);
// If this happens, all team members died and some sleeves took damage. In this case, teamSize = sleeveSize. // If this happens, all team members died and some sleeves took damage. In this case, teamSize = sleeveSize.
team.teamSize = team.sleeveSize; newTeamSize = team.sleeveSize;
} }
team.teamSize = newTeamSize;
team.teamLost += losses; team.teamLost += losses;
return losses; return losses;
+50 -9
View File
@@ -47,7 +47,7 @@ import { createContracts, loadContractsData } from "./data/Contracts";
import { createOperations, loadOperationsData } from "./data/Operations"; import { createOperations, loadOperationsData } from "./data/Operations";
import { clampInteger, clampNumber } from "../utils/helpers/clampNumber"; import { clampInteger, clampNumber } from "../utils/helpers/clampNumber";
import { parseCommand } from "../Terminal/Parser"; import { parseCommand } from "../Terminal/Parser";
import { BlackOperations } from "./data/BlackOperations"; import { createBlackOperations, loadBlackOperationsData } from "./data/BlackOperations";
import { GeneralActions } from "./data/GeneralActions"; import { GeneralActions } from "./data/GeneralActions";
import { PlayerObject } from "../PersonObjects/Player/PlayerObject"; import { PlayerObject } from "../PersonObjects/Player/PlayerObject";
import { Sleeve } from "../PersonObjects/Sleeve/Sleeve"; import { Sleeve } from "../PersonObjects/Sleeve/Sleeve";
@@ -72,7 +72,31 @@ export class Bladeburner implements OperationTeam {
skillPoints = 0; skillPoints = 0;
totalSkillPoints = 0; totalSkillPoints = 0;
teamSize = 0; /**
* Do NOT directly read and write this field. You must use the getter/setter.
* We use _teamSize instead of a private field #teamSize to reduce the complexity of saving/loading code.
*/
_teamSize = 0;
get teamSize() {
return this._teamSize;
}
set teamSize(value: number) {
// Ensure teamSize is a non-negative integer.
let newSize = value;
if (!Number.isInteger(newSize) || newSize < 0) {
newSize = 0;
}
// Early return if there is no change.
if (this._teamSize === newSize) {
return;
}
this._teamSize = newSize;
// Reduce teamCount of actions if it's greater than the team size.
for (const action of [...Object.values(this.operations), ...Object.values(this.blackOperations)]) {
action.teamCount = Math.min(action.teamCount, this._teamSize);
}
}
get sleeveSize() { get sleeveSize() {
return Player.sleevesSupportingBladeburner().length; return Player.sleevesSupportingBladeburner().length;
} }
@@ -96,9 +120,13 @@ export class Bladeburner implements OperationTeam {
staminaBonus = 0; staminaBonus = 0;
maxStamina = 1; maxStamina = 1;
stamina = 1; stamina = 1;
// Contracts and operations are stored on the Bladeburner object even though they are global so that they can utilize save/load of the main bladeburner object // Contracts, operations and blackOps are stored on the Bladeburner object even though they are global so that they
// can utilize save/load of the main bladeburner object
contracts: Record<BladeburnerContractName, Contract>; contracts: Record<BladeburnerContractName, Contract>;
operations: Record<BladeburnerOperationName, Operation>; operations: Record<BladeburnerOperationName, Operation>;
blackOperations: Record<BladeburnerBlackOpName, BlackOperation>;
// Array for quick lookup by BlackOp number
blackOperationArray: BlackOperation[];
numBlackOpsComplete = 0; numBlackOpsComplete = 0;
logging = { logging = {
general: true, general: true,
@@ -119,6 +147,11 @@ export class Bladeburner implements OperationTeam {
constructor() { constructor() {
this.contracts = createContracts(); this.contracts = createContracts();
this.operations = createOperations(); this.operations = createOperations();
this.blackOperations = createBlackOperations();
this.blackOperationArray = Object.values(this.blackOperations).sort((a, b) => (a.n < b.n ? -1 : 1));
if (!this.blackOperationArray.every((blackOp, i) => blackOp.n === i)) {
throw new Error("blackOperationArray is not initialized with correct indices");
}
} }
// Initialization code that is dependent on Player is here instead of in the constructor // Initialization code that is dependent on Player is here instead of in the constructor
@@ -1407,7 +1440,7 @@ export class Bladeburner implements OperationTeam {
case BladeburnerActionType.Operation: case BladeburnerActionType.Operation:
return this.operations[actionId.name]; return this.operations[actionId.name];
case BladeburnerActionType.BlackOp: case BladeburnerActionType.BlackOp:
return BlackOperations[actionId.name]; return this.blackOperations[actionId.name];
case BladeburnerActionType.General: case BladeburnerActionType.General:
return GeneralActions[actionId.name]; return GeneralActions[actionId.name];
} }
@@ -1426,7 +1459,7 @@ export class Bladeburner implements OperationTeam {
case BladeburnerActionType.Operation: case BladeburnerActionType.Operation:
return this.operations[name as BladeburnerOperationName]; return this.operations[name as BladeburnerOperationName];
case BladeburnerActionType.BlackOp: case BladeburnerActionType.BlackOp:
return BlackOperations[name as BladeburnerBlackOpName]; return this.blackOperations[name as BladeburnerBlackOpName];
} }
} }
@@ -1437,9 +1470,11 @@ export class Bladeburner implements OperationTeam {
return id ? this.getActionObject(id) : null; return id ? this.getActionObject(id) : null;
} }
static keysToSave = getKeyList(Bladeburner, { removedKeys: ["skillMultipliers"] }); static keysToSave = getKeyList(Bladeburner, { removedKeys: ["skillMultipliers", "blackOperationArray"] });
// Don't load contracts or operations because of the special loading method they use, see fromJSON // Don't load contracts or operations because of the special loading method they use, see fromJSON
static keysToLoad = getKeyList(Bladeburner, { removedKeys: ["skillMultipliers", "contracts", "operations"] }); static keysToLoad = getKeyList(Bladeburner, {
removedKeys: ["skillMultipliers", "contracts", "operations", "blackOperations", "blackOperationArray"],
});
/** Serialize the current object to a JSON save state. */ /** Serialize the current object to a JSON save state. */
toJSON(): IReviverValue { toJSON(): IReviverValue {
@@ -1449,9 +1484,10 @@ export class Bladeburner implements OperationTeam {
/** Initializes a Bladeburner object from a JSON save state. */ /** Initializes a Bladeburner object from a JSON save state. */
static fromJSON(value: IReviverValue): Bladeburner { static fromJSON(value: IReviverValue): Bladeburner {
assertObject(value.data); assertObject(value.data);
// operations and contracts are not loaded directly from the save, we load them in using a different method // Contracts, operations, and black ops are not loaded directly from the save; they are loaded via a different method.
const contractsData = value.data.contracts; const contractsData = value.data.contracts;
const operationsData = value.data.operations; const operationsData = value.data.operations;
const blackOperationsData = value.data.blackOperations;
const bladeburner = Generic_fromJSON(Bladeburner, value.data, Bladeburner.keysToLoad); const bladeburner = Generic_fromJSON(Bladeburner, value.data, Bladeburner.keysToLoad);
/** /**
@@ -1472,10 +1508,11 @@ export class Bladeburner implements OperationTeam {
bladeburner.automateActionLow = loadActionIdentifier(bladeburner.automateActionLow); bladeburner.automateActionLow = loadActionIdentifier(bladeburner.automateActionLow);
} }
} }
// Loading this way allows better typesafety and also allows faithfully reconstructing contracts/operations // Loading this way allows better typesafety and also allows faithfully reconstructing contracts/operations/blackOps
// even from save data that is missing a lot of static info about the objects. // even from save data that is missing a lot of static info about the objects.
loadContractsData(contractsData, bladeburner.contracts); loadContractsData(contractsData, bladeburner.contracts);
loadOperationsData(operationsData, bladeburner.operations); loadOperationsData(operationsData, bladeburner.operations);
loadBlackOperationsData(blackOperationsData, bladeburner.blackOperations);
// Regenerate skill multiplier data, which is not included in savedata // Regenerate skill multiplier data, which is not included in savedata
bladeburner.updateSkillMultipliers(); bladeburner.updateSkillMultipliers();
// If stamina or maxStamina is invalid, we set both of them to 1 and recalculate them. // If stamina or maxStamina is invalid, we set both of them to 1 and recalculate them.
@@ -1488,6 +1525,10 @@ export class Bladeburner implements OperationTeam {
bladeburner.maxStamina = 1; bladeburner.maxStamina = 1;
bladeburner.calculateMaxStamina(); bladeburner.calculateMaxStamina();
} }
// "_teamSize" was "teamSize" in pre-v3 versions.
if ("teamSize" in value.data && Number.isFinite(value.data.teamSize)) {
bladeburner.teamSize = value.data.teamSize as number;
}
return bladeburner; return bladeburner;
} }
} }
+3
View File
@@ -81,6 +81,9 @@ export class Skill {
} }
calculateMaxUpgradeCount(currentLevel: number, cost: PositiveNumber): number { calculateMaxUpgradeCount(currentLevel: number, cost: PositiveNumber): number {
// At extreme levels, floating-point precision loss makes currentLevel + 1 === currentLevel,
// causing calculateCost to return 0. No upgrade is possible in this case.
if (this.calculateCost(currentLevel, 1 as PositiveInteger) <= 0) return 0;
/** /**
* Define: * Define:
* - x = count * - x = count
File diff suppressed because it is too large Load Diff
+3 -1
View File
@@ -113,7 +113,9 @@ export function createContracts(): Record<BladeburnerContractName, Contract> {
export function loadContractsData(data: unknown, contracts: Record<BladeburnerContractName, Contract>) { export function loadContractsData(data: unknown, contracts: Record<BladeburnerContractName, Contract>) {
// loading data as "unknown" and typechecking it down is probably not necessary // loading data as "unknown" and typechecking it down is probably not necessary
// but this will prevent crashes even with malformed savedata // but this will prevent crashes even with malformed savedata
if (!data || typeof data !== "object") return; if (data == null || typeof data !== "object" || Array.isArray(data)) {
return;
}
assertLoadingType<Record<BladeburnerContractName, unknown>>(data); assertLoadingType<Record<BladeburnerContractName, unknown>>(data);
for (const contractName of Object.values(BladeburnerContractName)) { for (const contractName of Object.values(BladeburnerContractName)) {
const loadedContract = data[contractName]; const loadedContract = data[contractName];
+3 -1
View File
@@ -230,7 +230,9 @@ export function createOperations(): Record<BladeburnerOperationName, Operation>
export function loadOperationsData(data: unknown, operations: Record<BladeburnerOperationName, Operation>) { export function loadOperationsData(data: unknown, operations: Record<BladeburnerOperationName, Operation>) {
// loading data as "unknown" and typechecking it down is probably not necessary // loading data as "unknown" and typechecking it down is probably not necessary
// but this will prevent crashes even with malformed savedata // but this will prevent crashes even with malformed savedata
if (!data || typeof data !== "object") return; if (data == null || typeof data !== "object" || Array.isArray(data)) {
return;
}
assertLoadingType<Record<BladeburnerOperationName, unknown>>(data); assertLoadingType<Record<BladeburnerOperationName, unknown>>(data);
for (const operationName of Object.values(BladeburnerOperationName)) { for (const operationName of Object.values(BladeburnerOperationName)) {
const loadedOperation = data[operationName]; const loadedOperation = data[operationName];
+4 -4
View File
@@ -7,7 +7,7 @@ import { BlackOpElem } from "./BlackOpElem";
import { Router } from "../../ui/GameRoot"; import { Router } from "../../ui/GameRoot";
import { Page } from "../../ui/Router"; import { Page } from "../../ui/Router";
import { CorruptibleText } from "../../ui/React/CorruptibleText"; import { CorruptibleText } from "../../ui/React/CorruptibleText";
import { blackOpsArray } from "../data/BlackOperations"; import { numberOfBlackOperations } from "../data/BlackOperations";
import { finishBitNode } from "../../BitNode/BitNodeUtils"; import { finishBitNode } from "../../BitNode/BitNodeUtils";
import { Player } from "@player"; import { Player } from "@player";
@@ -16,7 +16,7 @@ interface BlackOpPageProps {
} }
export function BlackOpPage({ bladeburner }: BlackOpPageProps): React.ReactElement { export function BlackOpPage({ bladeburner }: BlackOpPageProps): React.ReactElement {
const blackOperations = blackOpsArray.slice(0, bladeburner.numBlackOpsComplete + 1).reverse(); const blackOperations = bladeburner.blackOperationArray.slice(0, bladeburner.numBlackOpsComplete + 1).reverse();
return ( return (
<> <>
@@ -36,11 +36,11 @@ export function BlackOpPage({ bladeburner }: BlackOpPageProps): React.ReactEleme
Unaffected by Charisma. Unaffected by Charisma.
</Typography> </Typography>
{bladeburner.numBlackOpsComplete >= blackOpsArray.length && ( {bladeburner.numBlackOpsComplete >= numberOfBlackOperations && (
<Button <Button
sx={{ my: 1, p: 1 }} sx={{ my: 1, p: 1 }}
onClick={() => { onClick={() => {
if (!Player.bladeburner || Player.bladeburner.numBlackOpsComplete < blackOpsArray.length) { if (!Player.bladeburner || Player.bladeburner.numBlackOpsComplete < numberOfBlackOperations) {
return; return;
} }
finishBitNode(); finishBitNode();
+11 -9
View File
@@ -3,7 +3,7 @@ import type { Bladeburner } from "../Bladeburner";
import React, { useMemo } from "react"; import React, { useMemo } from "react";
import { CopyableText } from "../../ui/React/CopyableText"; import { CopyableText } from "../../ui/React/CopyableText";
import { formatBigNumber } from "../../ui/formatNumber"; 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 AddIcon from "@mui/icons-material/Add";
import CloseIcon from "@mui/icons-material/Close"; import CloseIcon from "@mui/icons-material/Close";
import { Skill } from "../Skill"; import { Skill } from "../Skill";
@@ -18,10 +18,8 @@ export function SkillElem({ skill, bladeburner, onUpgrade }: SkillElemProps): Re
const skillName = skill.name; const skillName = skill.name;
const skillLevel = bladeburner.getSkillLevel(skillName); const skillLevel = bladeburner.getSkillLevel(skillName);
const pointCost = useMemo(() => skill.calculateCost(skillLevel), [skill, skillLevel]); 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. // 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 * 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(). * 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 }}> <Paper sx={{ my: 1, p: 1 }}>
<Box display="flex" flexDirection="row" alignItems="center"> <Box display="flex" flexDirection="row" alignItems="center">
<CopyableText variant="h6" color="primary" value={skillName} /> <CopyableText variant="h6" color="primary" value={skillName} />
{!canLevel ? ( {!check.available ? (
<IconButton disabled> <Tooltip title={check.error}>
<CloseIcon /> <span>
</IconButton> <IconButton disabled>
<CloseIcon />
</IconButton>
</span>
</Tooltip>
) : ( ) : (
<IconButton onClick={onClick}> <IconButton onClick={onClick}>
<AddIcon /> <AddIcon />
@@ -51,7 +53,7 @@ export function SkillElem({ skill, bladeburner, onUpgrade }: SkillElemProps): Re
{maxLvl ? ( {maxLvl ? (
<Typography>MAX LEVEL</Typography> <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> <Typography>{skill.desc}</Typography>
</Paper> </Paper>
+11 -10
View File
@@ -15,25 +15,26 @@ interface TeamSizeModalProps {
} }
export function TeamSizeModal({ bladeburner, action, open, onClose }: TeamSizeModalProps): React.ReactElement { export function TeamSizeModal({ bladeburner, action, open, onClose }: TeamSizeModalProps): React.ReactElement {
const [teamSize, setTeamSize] = useState<number | undefined>(); const [teamSize, setTeamSize] = useState(0);
function confirmTeamSize(event: React.FormEvent): void { function confirmTeamSize(event: React.FormEvent): void {
// Prevent reloading page when submitting form // Prevent reloading page when submitting form
event.preventDefault(); event.preventDefault();
if (teamSize === undefined) return; if (!Number.isInteger(teamSize) || teamSize < 0) {
const num = Math.round(teamSize); dialogBoxCreate("Invalid value entered for number of Team Members (must be a non-negative integer)");
if (isNaN(num) || num < 0) { return;
dialogBoxCreate("Invalid value entered for number of Team Members (must be numeric and non-negative)");
} else {
action.teamCount = num;
} }
action.teamCount = teamSize;
onClose(); onClose();
} }
function onTeamSize(event: React.ChangeEvent<HTMLInputElement>): void { function onTeamSize(event: React.ChangeEvent<HTMLInputElement>): void {
const x = parseFloat(event.target.value); const newTeamSize = Number(event.target.value);
if (x > bladeburner.teamSize) setTeamSize(bladeburner.teamSize); if (newTeamSize > bladeburner.teamSize) {
else setTeamSize(x); setTeamSize(bladeburner.teamSize);
} else {
setTeamSize(newTeamSize);
}
} }
return ( return (
+16 -16
View File
@@ -15,11 +15,11 @@ import { getRandomAlphanumericString } from "../utils/StringHelperFunctions";
export function tryGeneratingRandomContract(numberOfTries: number): void { export function tryGeneratingRandomContract(numberOfTries: number): void {
/** /**
* We try to generate a contract every 10 minutes. 525600 is the number of tries in 10 years. There is no reason to * We try to generate contracts three times every 10 minutes. 1576800 is the number of tries in 10 years. There is no
* support anything above that. We tested this number (525600) on a very old machine. It took only 300-350ms to * reason to support anything above that. We tested this number (1576800) on a very old machine. It took only ~300ms
* loop 525600 times and generate ~9137 contracts on that machine. * to loop 1576800 times and generate ~10103 contracts on that machine.
*/ */
numberOfTries = clampNumber(Math.floor(numberOfTries), 0, 525600); numberOfTries = clampNumber(Math.floor(numberOfTries), 0, 1576800);
if (numberOfTries < 1) { if (numberOfTries < 1) {
return; return;
} }
@@ -47,19 +47,19 @@ export function tryGeneratingRandomContract(numberOfTries: number): void {
* - If the offline time is unusually large (being offline for years, editing save file, tampering function prototype, * - If the offline time is unusually large (being offline for years, editing save file, tampering function prototype,
* etc.), the game will not hang when it tries to generate contracts. * etc.), the game will not hang when it tries to generate contracts.
* *
* These are some data for reference: * These are some data points for reference:
* - 1 month: ~1077 contracts. * - 1 month: ~3157 contracts.
* - 3 months: ~3157 contracts. * - 3 months: ~6198 contracts.
* - 6 months: ~5296 contracts. * - 6 months: ~7231 contracts.
* - 12 months: ~6678 contracts. * - 12 months: ~8003 contracts.
* - 2 years: ~7570 contracts. * - 2 years: ~8687 contracts.
* - 5 years: ~8504 contracts. * - 5 years: ~9506 contracts.
* - 10 years: ~9137 contracts. * - 10 years: ~10103 contracts.
* - 25 years: ~9936 contracts. * - 25 years: ~10879 contracts.
* - 50 years: ~10526 contracts. * - 50 years: ~11461 contracts.
* *
* Those numbers mean: If the player does not have any contracts and is online (or loads a save file with equivalent * Those numbers mean that if the player does not have any contracts and is online (or loads a save file with
* offline time) for X months/years, they will have ~Y contracts. * equivalent offline time) for X months/years, they will have ~Y contracts.
*/ */
if (random > 100 / (399 + Math.exp(0.0012 * currentNumberOfContracts))) { if (random > 100 / (399 + Math.exp(0.0012 * currentNumberOfContracts))) {
continue; continue;
+35
View File
@@ -83,6 +83,41 @@ export function removeBracketsFromArrayString(str: string): string {
return strCpy; return strCpy;
} }
/**
* This function only performs very simple checks to add the outermost optional brackets. Callers must perform other
* preprocessing steps and postprocessing validations. For example:
* - "[ [0, 1]]" will be incorrectly wrapped and converted to "[[[0,1]]]". Callers need to remove redundant whitespace.
* - "[[1,2],3]" is not an array of arrays, but this function will return it as is. Callers need to call validateAnswer.
*
* Note:
* - "" will always be converted to an empty array ([]).
* - When parsing an array of arrays (isArrayOfArray = true), "[]" will be converted to an empty array ([]), not an
* array containing an empty array ([[]]).
*/
export function parseArrayString(answer: string, isArrayOfArray = false): unknown {
let modifiedAnswer = answer.trim();
if (isArrayOfArray && modifiedAnswer === "[]") {
return [];
}
// If it doesn't start with any bracket, it's definitely "naked".
if (!modifiedAnswer.startsWith("[")) {
modifiedAnswer = `[${modifiedAnswer}]`;
} else if (isArrayOfArray && !modifiedAnswer.startsWith("[[")) {
// If it's supposed to be an array of arrays but only has one "[".
modifiedAnswer = `[${modifiedAnswer}]`;
}
try {
return JSON.parse(modifiedAnswer);
} catch (error) {
console.error(`Invalid answer: ${answer}`);
console.error(error);
return null;
}
}
export function removeQuotesFromString(str: string): string { export function removeQuotesFromString(str: string): string {
let strCpy: string = str; let strCpy: string = str;
if (strCpy.startsWith('"') || strCpy.startsWith("'")) { if (strCpy.startsWith('"') || strCpy.startsWith("'")) {
@@ -1,7 +1,6 @@
import { filterTruthy } from "../../utils/helpers/ArrayHelpers";
import { exceptionAlert } from "../../utils/helpers/exceptionAlert"; import { exceptionAlert } from "../../utils/helpers/exceptionAlert";
import { getRandomIntInclusive } from "../../utils/helpers/getRandomIntInclusive"; import { getRandomIntInclusive } from "../../utils/helpers/getRandomIntInclusive";
import { CodingContractTypes, removeBracketsFromArrayString, removeQuotesFromString } from "../ContractTypes"; import { CodingContractTypes, parseArrayString } from "../ContractTypes";
import { CodingContractName } from "@enums"; import { CodingContractName } from "@enums";
export const findAllValidMathExpressions: Pick<CodingContractTypes, CodingContractName.FindAllValidMathExpressions> = { export const findAllValidMathExpressions: Pick<CodingContractTypes, CodingContractName.FindAllValidMathExpressions> = {
@@ -107,10 +106,12 @@ export const findAllValidMathExpressions: Pick<CodingContractTypes, CodingContra
return result.every((sol) => solutions.has(sol)); return result.every((sol) => solutions.has(sol));
}, },
convertAnswer: (ans) => { convertAnswer: (ans) => {
const sanitized = removeBracketsFromArrayString(ans).split(","); const parsedAnswer = parseArrayString(ans);
return filterTruthy(sanitized).map((s) => removeQuotesFromString(s.replace(/\s/g, ""))); if (!findAllValidMathExpressions[CodingContractName.FindAllValidMathExpressions].validateAnswer(parsedAnswer)) {
return null;
}
return parsedAnswer;
}, },
validateAnswer: (ans): ans is string[] => validateAnswer: (ans): ans is string[] => Array.isArray(ans) && ans.every((s) => typeof s === "string"),
typeof ans === "object" && Array.isArray(ans) && ans.every((s) => typeof s === "string"),
}, },
}; };
@@ -1,5 +1,5 @@
import { CodingContractName } from "@enums"; import { CodingContractName } from "@enums";
import { CodingContractTypes, removeBracketsFromArrayString } from "../ContractTypes"; import { CodingContractTypes, parseArrayString } from "../ContractTypes";
import { exceptionAlert } from "../../utils/helpers/exceptionAlert"; import { exceptionAlert } from "../../utils/helpers/exceptionAlert";
import { getRandomIntInclusive } from "../../utils/helpers/getRandomIntInclusive"; import { getRandomIntInclusive } from "../../utils/helpers/getRandomIntInclusive";
@@ -67,10 +67,12 @@ export const generateIPAddresses: Pick<CodingContractTypes, CodingContractName.G
return ret.length === answer.length && ret.every((ip) => answer.includes(ip)); return ret.length === answer.length && ret.every((ip) => answer.includes(ip));
}, },
convertAnswer: (ans) => { convertAnswer: (ans) => {
const sanitized = removeBracketsFromArrayString(ans).replace(/\s/g, ""); const parsedAnswer = parseArrayString(ans);
return sanitized.split(",").map((ip) => ip.replace(/^(?<quote>['"])([\d.]*)\k<quote>$/g, "$2")); if (!generateIPAddresses[CodingContractName.GenerateIPAddresses].validateAnswer(parsedAnswer)) {
return null;
}
return parsedAnswer;
}, },
validateAnswer: (ans): ans is string[] => validateAnswer: (ans): ans is string[] => Array.isArray(ans) && ans.every((s) => typeof s === "string"),
typeof ans === "object" && Array.isArray(ans) && ans.every((s) => typeof s === "string"),
}, },
}; };
@@ -1,6 +1,6 @@
import { exceptionAlert } from "../../utils/helpers/exceptionAlert"; import { exceptionAlert } from "../../utils/helpers/exceptionAlert";
import { getRandomIntInclusive } from "../../utils/helpers/getRandomIntInclusive"; import { getRandomIntInclusive } from "../../utils/helpers/getRandomIntInclusive";
import { CodingContractTypes } from "../ContractTypes"; import { parseArrayString, CodingContractTypes } from "../ContractTypes";
import { CodingContractName } from "@enums"; import { CodingContractName } from "@enums";
export const largestRectangle: Pick<CodingContractTypes, CodingContractName.LargestRectangleInAMatrix> = { export const largestRectangle: Pick<CodingContractTypes, CodingContractName.LargestRectangleInAMatrix> = {
@@ -152,13 +152,7 @@ Answer: [[0,0],[3,1]]
return userArea === (solution[1][0] - solution[0][0] + 1) * (solution[1][1] - solution[0][1] + 1); return userArea === (solution[1][0] - solution[0][0] + 1) * (solution[1][1] - solution[0][1] + 1);
}, },
convertAnswer: (ans) => { convertAnswer: (ans) => {
let parsedAnswer: unknown; const parsedAnswer = parseArrayString(ans.replace(/\s/g, ""), true);
try {
parsedAnswer = JSON.parse(ans);
} catch (error) {
console.error("Invalid answer:", error);
return null;
}
if (!largestRectangle[CodingContractName.LargestRectangleInAMatrix].validateAnswer(parsedAnswer)) { if (!largestRectangle[CodingContractName.LargestRectangleInAMatrix].validateAnswer(parsedAnswer)) {
return null; return null;
} }
@@ -1,6 +1,6 @@
import { exceptionAlert } from "../../utils/helpers/exceptionAlert"; import { exceptionAlert } from "../../utils/helpers/exceptionAlert";
import { getRandomIntInclusive } from "../../utils/helpers/getRandomIntInclusive"; import { getRandomIntInclusive } from "../../utils/helpers/getRandomIntInclusive";
import { CodingContractTypes, convert2DArrayToString, removeBracketsFromArrayString } from "../ContractTypes"; import { CodingContractTypes, convert2DArrayToString, parseArrayString } from "../ContractTypes";
import { CodingContractName } from "@enums"; import { CodingContractName } from "@enums";
export const mergeOverlappingIntervals: Pick<CodingContractTypes, CodingContractName.MergeOverlappingIntervals> = { export const mergeOverlappingIntervals: Pick<CodingContractTypes, CodingContractName.MergeOverlappingIntervals> = {
@@ -70,20 +70,13 @@ export const mergeOverlappingIntervals: Pick<CodingContractTypes, CodingContract
return result.length === answer.length && result.every((a, i) => a[0] === answer[i][0] && a[1] === answer[i][1]); return result.length === answer.length && result.every((a, i) => a[0] === answer[i][0] && a[1] === answer[i][1]);
}, },
convertAnswer: (ans) => { convertAnswer: (ans) => {
const arrayRegex = /\[\d+,\d+\]/g; const parsedAnswer = parseArrayString(ans.replace(/\s/g, ""), true);
const matches = ans.replace(/\s/g, "").match(arrayRegex); if (!mergeOverlappingIntervals[CodingContractName.MergeOverlappingIntervals].validateAnswer(parsedAnswer)) {
if (matches === null) return null; return null;
const arr = matches.map((a) => }
removeBracketsFromArrayString(a) return parsedAnswer;
.split(",")
.map((n) => parseInt(n)),
);
// An inline function is needed here, so that TS knows this returns true if it matches the type
if (((a: number[][]): a is [number, number][] => a.every((n) => n.length === 2))(arr)) return arr;
return null;
}, },
validateAnswer: (ans): ans is [number, number][] => validateAnswer: (ans): ans is [number, number][] =>
typeof ans === "object" &&
Array.isArray(ans) && Array.isArray(ans) &&
ans.every((a) => Array.isArray(a) && a.length === 2 && a.every((n) => typeof n === "number")), ans.every((a) => Array.isArray(a) && a.length === 2 && a.every((n) => typeof n === "number")),
}, },
@@ -1,4 +1,4 @@
import { CodingContractTypes, removeBracketsFromArrayString } from "../ContractTypes"; import { CodingContractTypes, parseArrayString } from "../ContractTypes";
import { CodingContractName } from "@enums"; import { CodingContractName } from "@enums";
export const proper2ColoringOfAGraph: Pick<CodingContractTypes, CodingContractName.Proper2ColoringOfAGraph> = { export const proper2ColoringOfAGraph: Pick<CodingContractTypes, CodingContractName.Proper2ColoringOfAGraph> = {
@@ -121,14 +121,12 @@ export const proper2ColoringOfAGraph: Pick<CodingContractTypes, CodingContractNa
return data[1].every(([a, b]) => answer[a] !== answer[b]); return data[1].every(([a, b]) => answer[a] !== answer[b]);
}, },
convertAnswer: (ans) => { convertAnswer: (ans) => {
const sanitized = removeBracketsFromArrayString(ans).replace(/\s/g, ""); const parsedAnswer = parseArrayString(ans.replace(/\s/g, ""));
if (sanitized === "") return []; if (!proper2ColoringOfAGraph[CodingContractName.Proper2ColoringOfAGraph].validateAnswer(parsedAnswer)) {
const arr = sanitized.split(",").map((s) => parseInt(s, 10)); return null;
// An inline function is needed here, so that TS knows this returns true if it matches the type }
if (((a): a is (1 | 0)[] => !a.some((n) => n !== 1 && n !== 0))(arr)) return arr; return parsedAnswer;
return null;
}, },
validateAnswer: (ans): ans is (1 | 0)[] => validateAnswer: (ans): ans is (1 | 0)[] => Array.isArray(ans) && !ans.some((a) => a !== 1 && a !== 0),
typeof ans === "object" && Array.isArray(ans) && !ans.some((a) => a !== 1 && a !== 0),
}, },
}; };
@@ -1,6 +1,6 @@
import { exceptionAlert } from "../../utils/helpers/exceptionAlert"; import { exceptionAlert } from "../../utils/helpers/exceptionAlert";
import { getRandomIntInclusive } from "../../utils/helpers/getRandomIntInclusive"; import { getRandomIntInclusive } from "../../utils/helpers/getRandomIntInclusive";
import { CodingContractTypes, removeBracketsFromArrayString, removeQuotesFromString } from "../ContractTypes"; import { CodingContractTypes, parseArrayString } from "../ContractTypes";
import { CodingContractName } from "@enums"; import { CodingContractName } from "@enums";
export const sanitizeParenthesesInExpression: Pick< export const sanitizeParenthesesInExpression: Pick<
@@ -113,10 +113,16 @@ export const sanitizeParenthesesInExpression: Pick<
return res.every((sol) => answer.includes(sol)); return res.every((sol) => answer.includes(sol));
}, },
convertAnswer: (ans) => { convertAnswer: (ans) => {
const sanitized = removeBracketsFromArrayString(ans).split(","); const parsedAnswer = parseArrayString(ans);
return sanitized.map((s) => removeQuotesFromString(s.replace(/\s/g, ""))); if (
!sanitizeParenthesesInExpression[CodingContractName.SanitizeParenthesesInExpression].validateAnswer(
parsedAnswer,
)
) {
return null;
}
return parsedAnswer;
}, },
validateAnswer: (ans): ans is string[] => validateAnswer: (ans): ans is string[] => Array.isArray(ans) && ans.every((s) => typeof s === "string"),
typeof ans === "object" && Array.isArray(ans) && ans.every((s) => typeof s === "string"),
}, },
}; };
@@ -24,7 +24,7 @@ export const shortestPathInAGrid: Pick<CodingContractTypes, CodingContractName.S
" [[0,1],\n", " [[0,1],\n",
" [1,0]]\n", " [1,0]]\n",
"\n", "\n",
"Answer: ''", `Answer: ""`,
].join(" "); ].join(" ");
}, },
difficulty: 7, difficulty: 7,
@@ -1,5 +1,5 @@
import { CodingContractName } from "@enums"; import { CodingContractName } from "@enums";
import { removeBracketsFromArrayString, type CodingContractTypes } from "../ContractTypes"; import { parseArrayString, type CodingContractTypes } from "../ContractTypes";
import { exceptionAlert } from "../../utils/helpers/exceptionAlert"; import { exceptionAlert } from "../../utils/helpers/exceptionAlert";
import { getRandomIntInclusive } from "../../utils/helpers/getRandomIntInclusive"; import { getRandomIntInclusive } from "../../utils/helpers/getRandomIntInclusive";
@@ -127,10 +127,12 @@ export const spiralizeMatrix: Pick<CodingContractTypes, CodingContractName.Spira
return spiral.length === answer.length && spiral.every((n, i) => n === answer[i]); return spiral.length === answer.length && spiral.every((n, i) => n === answer[i]);
}, },
convertAnswer: (ans) => { convertAnswer: (ans) => {
const sanitized = removeBracketsFromArrayString(ans).replace(/\s/g, "").split(","); const parsedAnswer = parseArrayString(ans);
return sanitized.map((s) => parseInt(s)); if (!spiralizeMatrix[CodingContractName.SpiralizeMatrix].validateAnswer(parsedAnswer)) {
return null;
}
return parsedAnswer;
}, },
validateAnswer: (ans): ans is number[] => validateAnswer: (ans): ans is number[] => Array.isArray(ans) && ans.every((n) => typeof n === "number"),
typeof ans === "object" && Array.isArray(ans) && ans.every((n) => typeof n === "number"),
}, },
}; };
+95 -2
View File
@@ -7,7 +7,7 @@ export const CONSTANTS = {
VersionString: "3.0.0dev", VersionString: "3.0.0dev",
isDevBranch: true, isDevBranch: true,
isInTestEnvironment: globalThis.process?.env?.JEST_WORKER_ID !== undefined, isInTestEnvironment: globalThis.process?.env?.JEST_WORKER_ID !== undefined,
VersionNumber: 47, VersionNumber: 49,
/** Max level for any skill, assuming no multipliers. Determined by max numerical value in javascript for experience /** 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 * 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) // Also update Documentation/doc/en/changelog.md when appropriate (when doing a release)
LatestUpdate: ` LatestUpdate: `
## v3.0.0 development version: last updated 18 February 2026 ## v3.0.0 development version: last updated 13 April 2026
### BREAKING CHANGES ### BREAKING CHANGES
@@ -140,6 +140,10 @@ export const CONSTANTS = {
- Generate test contracts on executing host by default. Add support for optional parameter to specify the server (#2417) (@1337JiveTurkey) - Generate test contracts on executing host by default. Add support for optional parameter to specify the server (#2417) (@1337JiveTurkey)
- The "darkweb" server becomes a darknet server (Check new Dark Net feature in #2139) (@ficocelliguy) - The "darkweb" server becomes a darknet server (Check new Dark Net feature in #2139) (@ficocelliguy)
- Remove RAM cost of hacknet namespace and set RAM cost of each hacknet API (#2502) (@catloversg) - Remove RAM cost of hacknet namespace and set RAM cost of each hacknet API (#2502) (@catloversg)
- 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 ### MAJOR CHANGES
@@ -209,6 +213,22 @@ export const CONSTANTS = {
- Improve navigation system of in-game documentation viewer (#2499) (@catloversg) - Improve navigation system of in-game documentation viewer (#2499) (@catloversg)
- Use font family setting when rendering MUI Link component (#2511) (@catloversg) - Use font family setting when rendering MUI Link component (#2511) (@catloversg)
- Show hints of BitNode documentation and allow opening it in BitVerse (#2513) (@catloversg) - Show hints of BitNode documentation and allow opening it in BitVerse (#2513) (@catloversg)
- Show errors if using nano/vim with patterns that do not match any files (#2515) (@catloversg)
- Tweak CSS/Position of Darknet Docs link (#2517) (@d0sboots)
- Add indicator of RFA connection status to overview panel (#2497) (@catloversg)
- Fix issues with RFA auto-reconnecting feature (#2535) (@catloversg)
- Add inline script RAM usage text to each active script (#2546) (@vadien)
- Update toolbar of in-game editor (#2551) (@catloversg)
- Show "undefined" instead of -1 as pid in error popup when catching promise errors (#2555) (@catloversg)
- Add option to autosave scripts on focus change (#2565) (@catloversg)
- Prevent joining banned factions via UI (#2573) (@catloversg)
- 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 ### MISC
@@ -291,6 +311,31 @@ export const CONSTANTS = {
- More fixes and feedback in darknet (#2489) (@ficocelliguy) - More fixes and feedback in darknet (#2489) (@ficocelliguy)
- Fix missed cases in offline server handling (#2495) (@d0sboots) - Fix missed cases in offline server handling (#2495) (@d0sboots)
- Adjust darknet balance from player feedback (#2512) (@ficocelliguy) - Adjust darknet balance from player feedback (#2512) (@ficocelliguy)
- Add "Find Largest Rectangle in a Matrix" coding contract (#2519) (@Misha279-UA)
- Tweak Dnet based on player feedback (#2533, #2545, #2593) (@ficocelliguy)
- Allow parsing unknown options with data.flags in autocomplete (#2539) (@catloversg)
- Fix webstorm by using a mutationLock (#2542) (@d0sboots)
- Print error message when calling ns.ui.closeTail with nonexistent pid or pid of stopped scripts (#2557) (@catloversg)
- Add minimum width/height constraints to ns.ui.resizeTail (#2558) (@catloversg)
- Add API to minimize and expand tail windows (#2556) (@catloversg)
- Improve error messages for invalid sleeve numbers (#2567) (@catloversg)
- Improve help text of expr command (#2561) (@catloversg)
- Rework faction rumor (#2569) (@catloversg)
- Fix: hacknetNodeCost formula API throws when using documented optional parameter (#2577) (@catloversg)
- Rework intelligence override (#2575) (@catloversg)
- Electron: Allow opening dev tools via CLI arguments (#2589) (@catloversg)
- Support importing Steam Cloud save file manually (#2583) (@catloversg)
- Electron: Add UI menus and CLI flags to change log levels (#2596) (@catloversg)
- Import correct cloud file when multiple exist (#2599) (@catloversg)
- 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 ### DOCUMENTATION
@@ -336,6 +381,17 @@ export const CONSTANTS = {
- Add a note to CONTRIBUTING.md about the npm peer dep issue (#2474) (@d0sboots) - Add a note to CONTRIBUTING.md about the npm peer dep issue (#2474) (@d0sboots)
- Update darknet documentation (#2483) (@ficocelliguy) - Update darknet documentation (#2483) (@ficocelliguy)
- Fix invalid links in ns.sleep and ns.asleep (#2496) (@catloversg) - Fix invalid links in ns.sleep and ns.asleep (#2496) (@catloversg)
- Use relative links instead of absolute links (#2521) (@catloversg)
- Document quirky behavior of ns.flags when default value is nullish (#2528) (@catloversg)
- Clarify how share power affects reputation gain rate of non-hacking work (#2544) (@catloversg)
- Update guides (#2550) (@catloversg)
- Add missing newline after RAM cost (#2570) (@catloversg)
- 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 ### SPOILER CHANGES - UI
@@ -344,6 +400,11 @@ export const CONSTANTS = {
- Fix: Bladeburner console prints main body's HP instead of sleeve's HP (#2390) (@catloversg) - Fix: Bladeburner console prints main body's HP instead of sleeve's HP (#2390) (@catloversg)
- Reduce threshold of showing warning of low population (#2450) (@catloversg) - Reduce threshold of showing warning of low population (#2450) (@catloversg)
- Fix: Hacknet server UI shows NaN hash rate when 100% RAM is being used (#2500) (@catloversg) - Fix: Hacknet server UI shows NaN hash rate when 100% RAM is being used (#2500) (@catloversg)
- 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 ### SPOILER CHANGES - MISC
@@ -366,6 +427,14 @@ export const CONSTANTS = {
- Fix: Sleeves can earn exp and purchase augmentations when enabling disableSleeveExpAndAugmentation (#2467) (@catloversg) - Fix: Sleeves can earn exp and purchase augmentations when enabling disableSleeveExpAndAugmentation (#2467) (@catloversg)
- Add improved challenge achievement for BN15 (#2479) (@ficocelliguy) - Add improved challenge achievement for BN15 (#2479) (@ficocelliguy)
- Stop randomizing Bladeburner's action difficulty (#2491) (@catloversg) - Stop randomizing Bladeburner's action difficulty (#2491) (@catloversg)
- Adjusted Bladeburner's team bonus computation to make one member help (#2541) (@JoshuaCF)
- Rebalance charisma exp gain of Recruitment action (#2549) (@catloversg)
- 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 ### SPOILER CHANGES - DOCUMENTATION
@@ -382,6 +451,7 @@ export const CONSTANTS = {
- Update type of Player.factions, GangGenInfo.faction and CorpMaterialConstantData.name (#2347) (@catloversg) - Update type of Player.factions, GangGenInfo.faction and CorpMaterialConstantData.name (#2347) (@catloversg)
- Clarification of Singularity costs inside BN4 (#2403) (@gmcew) - Clarification of Singularity costs inside BN4 (#2403) (@gmcew)
- Update math notations in corporation docs (#2452) (@catloversg) - Update math notations in corporation docs (#2452) (@catloversg)
- Update BitNode recommendation short guide (#2523, #2529) (@catloversg)
### CODEBASE/REFACTOR/WORKFLOW/JEST/TOOL/DEPS ### CODEBASE/REFACTOR/WORKFLOW/JEST/TOOL/DEPS
@@ -449,5 +519,28 @@ export const CONSTANTS = {
- Refactor/adjust getPixelPosition in darknet UI code (#2501) (@d0sboots) - Refactor/adjust getPixelPosition in darknet UI code (#2501) (@d0sboots)
- Add tests for checking getAnswer and solver of coding contracts (#2503) (@catloversg) - Add tests for checking getAnswer and solver of coding contracts (#2503) (@catloversg)
- Refactor ImportSave component (#2505) (@catloversg) - Refactor ImportSave component (#2505) (@catloversg)
- Show coding contract names when their tests failed (#2520) (@catloversg)
- Workflow: Fix wrong instruction of generating docs (#2522) (@catloversg)
- Update comment of LoadingScreen of ComplexPage enum (#2527) (@catloversg)
- Make getPlayer 10x faster (#2548) (@d0sboots)
- Create monaco editor instance with null model (#2563) (@catloversg)
- Update dependencies (#2576) (@catloversg)
- Remove barrel imports in Bladeburner code (#2580) (@catloversg)
- Fix React warning in IPvGO scoring explanation popup (#2581) (@catloversg)
- Update Babel core, presets and module loader for webpack (#2585) (@catloversg)
- Add script to generate webpack bundle report (#2587) (@catloversg)
- Update Electron (#2591) (@catloversg)
- Mitigate issue of forcefullyCrashRenderer (#2597) (@catloversg)
- Split Settings.ts to reduce number of imports (#2600) (@catloversg)
- Remove duplicate random alphanumeric string functions (#2601) (@catloversg)
- Update comments to reflect changes in #2603 (#2606) (@catloversg)
- Replace ipExists() linear scan with O(1) Map.has() (#2621) (@lstutzman)
- 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; } as const;
+1 -1
View File
@@ -210,7 +210,7 @@ export const getTimingAttackConfig = (difficulty: number): ServerConfig => {
"I spent some time on it, but that's not the password", "I spent some time on it, but that's not the password",
]; ];
const alphanumeric = difficulty > 16 && Math.random() < 0.3; 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 { return {
modelId: ModelIds.TimingAttack, modelId: ModelIds.TimingAttack,
password: getPassword(length, alphanumeric), password: getPassword(length, alphanumeric),
+1 -1
View File
@@ -74,7 +74,7 @@ export const getCCTReward = (difficulty: number, server: DarknetServer): string
return getMoneyReward(difficulty); return getMoneyReward(difficulty);
} }
for (let i = 0; i < contractCount; i++) { for (let i = 0; i < contractCount; i++) {
generateContract({ server: server.hostname, rewardScaling: 1 / 5 }); generateContract({ server: server.hostname, rewardScaling: 1 / 2 });
} }
return `New coding contracts are now available on the network!`; return `New coding contracts are now available on the network!`;
}; };
+1 -9
View File
@@ -79,17 +79,9 @@ export const calculateAuthenticationTime = (
const underleveledFactor = applyUnderleveledFactor ? 1.5 + (chaRequired + 50) / (person.skills.charisma + 50) : 1; const underleveledFactor = applyUnderleveledFactor ? 1.5 + (chaRequired + 50) / (person.skills.charisma + 50) : 1;
const hasBootsFactor = Player.hasAugmentation(AugmentationName.TheBoots) ? 0.8 : 1; const hasBootsFactor = Player.hasAugmentation(AugmentationName.TheBoots) ? 0.8 : 1;
const hasSf15_2Factor = Player.activeSourceFileLvl(15) > 2 ? 0.8 : 1; const hasSf15_2Factor = Player.activeSourceFileLvl(15) > 2 ? 0.8 : 1;
const bonusTimeFactor = hasDarknetBonusTime() ? 0.75 : 1;
const time = const time =
baseTime * baseTime * skillFactor * backdoorFactor * underleveledFactor * hasBootsFactor * hasSf15_2Factor * threadsFactor;
skillFactor *
backdoorFactor *
underleveledFactor *
hasBootsFactor *
hasSf15_2Factor *
bonusTimeFactor *
threadsFactor;
// We need to call GetServer and check if it's a dnet server later because this function can be called by formulas // 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). // APIs (darknetServerData.hostname may be an invalid hostname).
+7 -6
View File
@@ -1,6 +1,6 @@
import { Player } from "@player"; import { Player } from "@player";
import { addClue } from "./effects"; import { addClue } from "./effects";
import { formatNumber } from "../../ui/formatNumber"; import { formatNumber, formatRam } from "../../ui/formatNumber";
import { logger } from "./offlineServerHandling"; import { logger } from "./offlineServerHandling";
import type { NetscriptContext } from "../../Netscript/APIWrapper"; import type { NetscriptContext } from "../../Netscript/APIWrapper";
import type { DarknetServer } from "../../Server/DarknetServer"; 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 { clampNumber } from "../../utils/helpers/clampNumber";
import { ResponseCodeEnum } from "../Enums"; import { ResponseCodeEnum } from "../Enums";
import { isLabyrinthServer } from "./labyrinth"; import { isLabyrinthServer } from "./labyrinth";
import { roundToTwo } from "../../utils/helpers/roundToTwo";
/* /*
* Handles the effects of removing some blocked RAM from a Darknet server. * 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 difficulty = server.difficulty + 1;
const ramBlockRemoved = getRamBlockRemoved(server, threads); const ramBlockRemoved = getRamBlockRemoved(server, threads);
server.blockedRam -= ramBlockRemoved; server.blockedRam = roundToTwo(server.blockedRam - ramBlockRemoved);
server.updateRamUsed(server.ramUsed - ramBlockRemoved); server.updateRamUsed(server.ramUsed - ramBlockRemoved);
if (server.blockedRam <= 0) { 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; const xpGained = Player.mults.charisma_exp * threads * 10 * 1.1 ** difficulty;
Player.gainCharismaExp(xpGained); Player.gainCharismaExp(xpGained);
const result = `Liberated ${formatNumber( const result = `Liberated ${formatRam(
ramBlockRemoved, ramBlockRemoved,
4, 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); logger(ctx)(result);
return { return {
success: true, success: true,
@@ -72,7 +73,7 @@ export const getRamBlockRemoved = (darknetServerData: DarknetServerData, threads
const charismaFactor = 1 + player.skills.charisma / 100; const charismaFactor = 1 + player.skills.charisma / 100;
const difficultyFactor = 2 * 0.92 ** (difficulty + 1); const difficultyFactor = 2 * 0.92 ** (difficulty + 1);
const baseAmount = 0.02; 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 [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)]);
}; };
+1 -1
View File
@@ -77,7 +77,7 @@ export function NetworkDisplayWrapper(): React.ReactElement {
useEffect(() => { useEffect(() => {
const clearSubscription = DarknetEvents.subscribe(() => updateDisplay()); const clearSubscription = DarknetEvents.subscribe(() => updateDisplay());
draggableBackground.current?.addEventListener("wheel", (e) => e.preventDefault()); draggableBackground.current?.addEventListener("wheel", (e) => e.preventDefault(), { passive: false });
scrollTo(DarknetState.netViewTopScroll, DarknetState.netViewLeftScroll); scrollTo(DarknetState.netViewTopScroll, DarknetState.netViewLeftScroll);
updateDisplay(); updateDisplay();
+4
View File
@@ -14,6 +14,7 @@ import { validBitNodes } from "../../BitNode/Constants";
import { DeleteServer, GetAllServers } from "../../Server/AllServers"; import { DeleteServer, GetAllServers } from "../../Server/AllServers";
import { HacknetServer } from "../../Hacknet/HacknetServer"; import { HacknetServer } from "../../Hacknet/HacknetServer";
import { AutoExpandAccordion } from "../../ui/AutoExpand/AutoExpandAccordion"; import { AutoExpandAccordion } from "../../ui/AutoExpand/AutoExpandAccordion";
import { getDarkscapeNavigator } from "../../DarkNet/effects/effects";
const useStyles = makeStyles()({ const useStyles = makeStyles()({
group: { group: {
@@ -46,6 +47,9 @@ export function SourceFilesDev({ parentRerender }: { parentRerender: () => void
Player.hacknetNodes = Player.hacknetNodes.filter((node) => typeof node === "string"); Player.hacknetNodes = Player.hacknetNodes.filter((node) => typeof node === "string");
} }
} }
if (sfN === 15 && sfLvl !== 0) {
getDarkscapeNavigator();
}
if (sfLvl === 0) { if (sfLvl === 0) {
Player.sourceFiles.delete(sfN); Player.sourceFiles.delete(sfN);
Player.bitNodeOptions.sourceFileOverrides.delete(sfN); Player.bitNodeOptions.sourceFileOverrides.delete(sfN);
@@ -196,7 +196,7 @@ achieved immortality - at least for those that could afford it.
This BitNode unlocks Sleeve and Grafting technology: This BitNode unlocks Sleeve and Grafting technology:
- Sleeve: Duplicate your consciousness into Synthoids, allowing you to perform different tasks asynchronously. You cannot buy Sleeves outside this BitNode. - Sleeve: Duplicate your consciousness into Synthoids, allowing you to perform different tasks asynchronously. You cannot buy Sleeves or upgrade them outside this BitNode.
- Grafting: Visit VitaLife in New Tokyo to get access to this technology. It allows you to graft augmentations, which is an alternative way of installing augmentations. - Grafting: Visit VitaLife in New Tokyo to get access to this technology. It allows you to graft augmentations, which is an alternative way of installing augmentations.
Destroying this BitNode will give you Source-File 10, or if you already have this Source-File, it will upgrade its level up to a maximum of 3. This Source-File unlocks Sleeve and Grafting API in other BitNodes. Each level of this Source-File also grants you a Sleeve. Destroying this BitNode will give you Source-File 10, or if you already have this Source-File, it will upgrade its level up to a maximum of 3. This Source-File unlocks Sleeve and Grafting API in other BitNodes. Each level of this Source-File also grants you a Sleeve.
+107 -10
View File
@@ -6,11 +6,29 @@ Coding Contracts are files with the `.cct` extension.
They can be accessed through the [Terminal](terminal.md) or through [Scripts](scripts.md) using the [Coding Contract API](../../../../../markdown/bitburner.codingcontract.md). They can be accessed through the [Terminal](terminal.md) or through [Scripts](scripts.md) using the [Coding Contract API](../../../../../markdown/bitburner.codingcontract.md).
Each contract has a limited number of attempts. Each contract has a limited number of attempts.
If you provide the wrong answer too many times and exceed the number of attempts, the contract will self destruct (delete itself). If you provide the wrong answer too many times and exceed the number of attempts, the contract will self-destruct (delete itself).
Coding Contracts are randomly generated and spawn over time. Initially, you'll only see a small range of the easier contracts, but as you progress further through the game more challenging ones will unlock. Coding Contracts are randomly generated and spawn over time. Initially, you'll only see a small range of the easier contracts, but as you progress further through the game more challenging ones will unlock.
They can appear on any [server](servers.md) (including your home computer), except for your purchased [servers](servers.md). They can appear on any [server](servers.md) (including your home computer), except for your purchased [servers](servers.md).
## Contract generation
### Online
Every 10 minutes, the game makes three independent attempts to generate a contract on normal servers, each with a base
25% chance. This probability decreases based on the total number of contracts across all servers. In most cases, you can
assume a ~25% success rate per attempt.
### Offline
When the game is launched after being offline, the offline time is used to calculate the number of generation attempts.
The same rules and probabilities from the online generation process apply.
### Dark Net
Opening cache files on [darknet](../programming/darknet.md) servers also has a chance to generate a contract, but with
lower rewards. Contracts generated this way grant 50% lower rewards than those generated randomly on normal servers.
## Running in Terminal ## Running in Terminal
To run a Coding Contract in the [Terminal](terminal.md), simply use the `run` command: To run a Coding Contract in the [Terminal](terminal.md), simply use the `run` command:
@@ -40,24 +58,92 @@ The [`getContractTypes`](../../../../../markdown/bitburner.codingcontract.getcon
## Submitting Solutions ## Submitting Solutions
### General rules
Different contract problem types will require different types of solutions. Different contract problem types will require different types of solutions.
Some may be numbers, others may be strings or arrays. Some may be numbers, others may be strings or arrays.
If a contract asks for a specific solution format, then use that. If a contract asks for a specific solution format, then use that.
Otherwise, follow these rules when submitting solutions: Otherwise, follow these rules when submitting solutions:
- String-type solutions should **not** have quotation marks surrounding the string (unless specifically asked for). - String-type solutions (e.g., Shortest Path in a Grid) should **not** have quotation marks surrounding the string (unless specifically asked for). For example, if your answer is `foo` (3 characters: f, o, o), just submit those 3 characters. Don't submit `"foo"` (5 characters).
Only quotation marks that are part of the actual string solution should be included. Only quotation marks that are part of the actual string solution should be included.
- With array-of-strings solutions (e.g., Generate IP Addresses), you need to use double quotes surrounding the string values. Don't use single quotes (`''`) or backticks (\`\`). For example, if your answer is an array containing `foo` (3 characters: f, o, o) and `bar` (3 characters: b, a, r), you should submit `["foo", "bar"]`. Don't submit `['foo', 'bar']`.
- Array-type solutions should be submitted with each element in the array separated by commas. - Array-type solutions should be submitted with each element in the array separated by commas.
Brackets are optional. - Numeric solutions should be submitted normally, as expected.
For example, both of the following are valid solution formats: - Read the description carefully. Some contracts (e.g., the "Square Root" contract) clearly specify the expected solution format.
- `1,2,3` - If the solution format is not a string, you should not convert the answer to a string. Read the next sections carefully if you do so.
- `[1,2,3]`
- If the solution is a multidimensional array, then all arrays that are not the outer-most array DO require the brackets. ### String conversion
For example, an array of arrays can be submitted as one of the following:
For convenience (e.g., submitting the answer via the UI) and backward compatibility, the game accepts a string answer even when
the solution format is not a string. In these cases, the game converts your string answer to the expected format. However,
this conversion has many pitfalls.
String conversion only matters when you submit the answer via the UI (your answer, typed in the text box, is always a string). When you call the `ns.codingcontract.attempt` API, you should never convert your non-string answer to a string unless specifically asked for.
First, with arrays, the outermost pair of brackets is optional. For example, both of the following are valid solution formats:
- `1,2,3`
- `[1,2,3]`
Note:
- If the solution is a multidimensional array, then all arrays that are not the outermost array DO require the brackets. For example, an array of arrays can be submitted as one of the following:
- `[1,2],[3,4]` - `[1,2],[3,4]`
- `[[1,2],[3,4]]` - `[[1,2],[3,4]]`
- The empty string is converted to an empty array.
- `"[]"` (the string that contains only 2 bracket characters; the double quotes are not part of that string) is converted to an empty array.
Numeric solutions should be submitted normally, as expected Second, in the UI:
- If your answer is an empty string, you must leave the text box empty. Do NOT use `""`, `''`, or \`\`.
- If the answer is a non-empty string, type it as is. For example, if your answer is the word `foo`, type `foo` (3 characters: f, o, o). Do NOT add any types of quotes.
- If the answer is an array that contains strings, use double quotes for strings. Do NOT use single quotes or backticks. For example, if your answer is an array containing the word `foo`, type `["foo"]` (7 characters: square bracket, double quote, f, o, o, double quote, square bracket). The brackets are optional, as stated above, but we recommend including them.
### Tips
If a contract does not expect a string, you should not submit a string. For contracts that do not expect a string
solution, your answer should never be a string, so if you submit a string, it means that you converted your non-string
answer to a string. This is usually the wrong thing to do.
Remember, string conversion is for UI convenience and backward compatibility. If you use NS APIs, do not perform any
string conversion unless specifically asked for.
For example, suppose a contract requires the answer to be an array containing strings, and you determine that those
strings are `foo` and `bar`. Your code should look like this:
```js
const firstString = "foo";
const secondString = "bar";
const answer = [firstString, secondString];
ns.codingcontract.attempt(answer, "filename.cct");
```
There is no conversion!
In the "General rules" section above, with array-of-strings solutions, we say `Don't use single quotes or backticks`.
However, this code works:
<!-- prettier-ignore -->
```js
const firstString = 'foo'; // Single quotes
const secondString = 'bar'; // Single quotes
const answer = [firstString, secondString];
ns.codingcontract.attempt(answer, "filename.cct");
```
Why is that?
In this code, you submit an array containing 2 strings. In JS, `"foo"` and `'foo'` are the same string. However, if you
submit your answer as a string, you need to convert your array to a string, and the string `["foo", "bar"]` is not the
same as the string `['foo', 'bar']`.
Internally, we use `JSON.parse` to convert the string answer, and `['foo', 'bar']` is not a valid string representation
of an array. In JSON, a string needs to be enclosed by double quotes. Using single quotes or backticks is not allowed.
This is another example of why you should not convert your answer to a string when not requested. If you submit your
array as it is, you do not need to care about the quote types.
## Rewards ## Rewards
@@ -68,7 +154,18 @@ There are currently four possible rewards for solving a Coding Contract:
- [Company](companies.md) [Reputation](reputation.md) for a specific [Company](companies.md) - [Company](companies.md) [Reputation](reputation.md) for a specific [Company](companies.md)
- Money - Money
The `amount` of the reward varies based on the difficulty of the problem posed by the Coding Contract. The reward type is randomly chosen at spawn time. If the chosen reward is invalid upon completion (e.g., requirements
are not met), it falls back to an alternative type:
- Specific faction reputation ⇒ Money
- All factions' reputation ⇒ Money
- Company reputation ⇒ Specific faction reputation or all factions' reputation (50% chance for each).
If the fallback reward is also invalid, the reward defaults to Money.
For example, if a contract is set to reward "All Factions' Reputation" but you have not joined any factions at the time
of submission, you will receive Money instead.
The amount of the reward varies based on the difficulty of the problem posed by the Coding Contract.
## Notes ## Notes
@@ -100,3 +100,9 @@ with a semicolon (;). For example:
$ run foo.js; tail foo.js $ run foo.js; tail foo.js
Chained commands do **not** wait for functions like `hack` or `wget` to finish executing, and so may not always work as expected. Chained commands do **not** wait for functions like `hack` or `wget` to finish executing, and so may not always work as expected.
## Quirks
When your scripts render a text box (e.g., `<input>`, `<textarea>`) with the `autoFocus` attribute in the terminal or
the tail log window, it may not focus automatically as expected. To be precise, the text box receives focus, but the
terminal may immediately reclaim it. This depends on the specific timing of the render.
@@ -367,12 +367,11 @@ Paste the following code into the [Script](../basic/scripts.md) editor:
/** @param {NS} ns */ /** @param {NS} ns */
export async function main(ns) { export async function main(ns) {
// How much RAM each cloud server will have. In this case, it'll // How much RAM each cloud server will have. In this case, it'll be 8GB.
// be 8GB.
const ram = 8; const ram = 8;
// Iterator we'll use for our loop // 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 // Continuously try to purchase cloud servers until we've reached the maximum
// amount of servers // 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 (ns.getServerMoneyAvailable("home") > ns.cloud.getRamLimit(ram)) {
// If we have enough money, then: // If we have enough money, then:
// 1. Purchase the server // 1. Purchase the server
// 2. Copy our hacking script onto the newly-purchased cloud 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 // 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 // 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.scp("early-hack-template.js", hostname);
ns.exec("early-hack-template.js", hostname, 3); ns.exec("early-hack-template.js", hostname, 3);
++i; ++i;
} }
//Make the script wait for a second before looping again. // Make the script wait for a second before looping again.
//Removing this line will cause an infinite loop and crash the game. // Removing this line will cause an infinite loop and crash the game.
await ns.sleep(1000); await ns.sleep(1000);
} }
} }
@@ -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: 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; const ram = 8;
var i = 0; let i = ns.cloud.getServerNames().length;
while (i < ns.cloud.getServerLimit()) { while (i < ns.cloud.getServerLimit()) {
if (ns.getServerMoneyAvailable("home") > ns.cloud.getRamLimit(ram)) { 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.scp("early-hack-template.js", hostname);
ns.exec("early-hack-template.js", hostname, 3); ns.exec("early-hack-template.js", hostname, 3);
++i; ++i;
+6 -2
View File
@@ -68,6 +68,7 @@ import file65 from "./doc/en/programming/remote_api.md?raw";
import file66 from "./doc/en/programming/typescript_react.md?raw"; import file66 from "./doc/en/programming/typescript_react.md?raw";
import nsDoc_bitburner__valueof_md from "../../markdown/bitburner._valueof.md?raw"; import nsDoc_bitburner__valueof_md from "../../markdown/bitburner._valueof.md?raw";
import nsDoc_bitburner_activefragment_chargedeffect_md from "../../markdown/bitburner.activefragment.chargedeffect.md?raw";
import nsDoc_bitburner_activefragment_highestcharge_md from "../../markdown/bitburner.activefragment.highestcharge.md?raw"; import nsDoc_bitburner_activefragment_highestcharge_md from "../../markdown/bitburner.activefragment.highestcharge.md?raw";
import nsDoc_bitburner_activefragment_md from "../../markdown/bitburner.activefragment.md?raw"; import nsDoc_bitburner_activefragment_md from "../../markdown/bitburner.activefragment.md?raw";
import nsDoc_bitburner_activefragment_numcharge_md from "../../markdown/bitburner.activefragment.numcharge.md?raw"; import nsDoc_bitburner_activefragment_numcharge_md from "../../markdown/bitburner.activefragment.numcharge.md?raw";
@@ -560,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_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_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_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_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_getbonustime_md from "../../markdown/bitburner.gang.getbonustime.md?raw";
import nsDoc_bitburner_gang_getchancetowinclash_md from "../../markdown/bitburner.gang.getchancetowinclash.md?raw"; import nsDoc_bitburner_gang_getchancetowinclash_md from "../../markdown/bitburner.gang.getchancetowinclash.md?raw";
@@ -571,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_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_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_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_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_gettasknames_md from "../../markdown/bitburner.gang.gettasknames.md?raw";
import nsDoc_bitburner_gang_gettaskstats_md from "../../markdown/bitburner.gang.gettaskstats.md?raw"; import nsDoc_bitburner_gang_gettaskstats_md from "../../markdown/bitburner.gang.gettaskstats.md?raw";
@@ -745,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_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_hacktime_md from "../../markdown/bitburner.hackingformulas.hacktime.md?raw";
import nsDoc_bitburner_hackingformulas_md from "../../markdown/bitburner.hackingformulas.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_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_chance_md from "../../markdown/bitburner.hackingmultipliers.chance.md?raw";
import nsDoc_bitburner_hackingmultipliers_growth_md from "../../markdown/bitburner.hackingmultipliers.growth.md?raw"; import nsDoc_bitburner_hackingmultipliers_growth_md from "../../markdown/bitburner.hackingmultipliers.growth.md?raw";
@@ -1663,6 +1665,7 @@ AllPages["en/programming/remote_api.md"] = file65;
AllPages["en/programming/typescript_react.md"] = file66; AllPages["en/programming/typescript_react.md"] = file66;
AllPages["nsDoc/bitburner._valueof.md"] = nsDoc_bitburner__valueof_md; AllPages["nsDoc/bitburner._valueof.md"] = nsDoc_bitburner__valueof_md;
AllPages["nsDoc/bitburner.activefragment.chargedeffect.md"] = nsDoc_bitburner_activefragment_chargedeffect_md;
AllPages["nsDoc/bitburner.activefragment.highestcharge.md"] = nsDoc_bitburner_activefragment_highestcharge_md; AllPages["nsDoc/bitburner.activefragment.highestcharge.md"] = nsDoc_bitburner_activefragment_highestcharge_md;
AllPages["nsDoc/bitburner.activefragment.md"] = nsDoc_bitburner_activefragment_md; AllPages["nsDoc/bitburner.activefragment.md"] = nsDoc_bitburner_activefragment_md;
AllPages["nsDoc/bitburner.activefragment.numcharge.md"] = nsDoc_bitburner_activefragment_numcharge_md; AllPages["nsDoc/bitburner.activefragment.numcharge.md"] = nsDoc_bitburner_activefragment_numcharge_md;
@@ -2155,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.ascendmember.md"] = nsDoc_bitburner_gang_ascendmember_md;
AllPages["nsDoc/bitburner.gang.canrecruitmember.md"] = nsDoc_bitburner_gang_canrecruitmember_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.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.getascensionresult.md"] = nsDoc_bitburner_gang_getascensionresult_md;
AllPages["nsDoc/bitburner.gang.getbonustime.md"] = nsDoc_bitburner_gang_getbonustime_md; AllPages["nsDoc/bitburner.gang.getbonustime.md"] = nsDoc_bitburner_gang_getbonustime_md;
AllPages["nsDoc/bitburner.gang.getchancetowinclash.md"] = nsDoc_bitburner_gang_getchancetowinclash_md; AllPages["nsDoc/bitburner.gang.getchancetowinclash.md"] = nsDoc_bitburner_gang_getchancetowinclash_md;
@@ -2166,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.getinstallresult.md"] = nsDoc_bitburner_gang_getinstallresult_md;
AllPages["nsDoc/bitburner.gang.getmemberinformation.md"] = nsDoc_bitburner_gang_getmemberinformation_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.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.getrecruitsavailable.md"] = nsDoc_bitburner_gang_getrecruitsavailable_md;
AllPages["nsDoc/bitburner.gang.gettasknames.md"] = nsDoc_bitburner_gang_gettasknames_md; AllPages["nsDoc/bitburner.gang.gettasknames.md"] = nsDoc_bitburner_gang_gettasknames_md;
AllPages["nsDoc/bitburner.gang.gettaskstats.md"] = nsDoc_bitburner_gang_gettaskstats_md; AllPages["nsDoc/bitburner.gang.gettaskstats.md"] = nsDoc_bitburner_gang_gettaskstats_md;
@@ -2340,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.hackpercent.md"] = nsDoc_bitburner_hackingformulas_hackpercent_md;
AllPages["nsDoc/bitburner.hackingformulas.hacktime.md"] = nsDoc_bitburner_hackingformulas_hacktime_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.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.hackingformulas.weakentime.md"] = nsDoc_bitburner_hackingformulas_weakentime_md;
AllPages["nsDoc/bitburner.hackingmultipliers.chance.md"] = nsDoc_bitburner_hackingmultipliers_chance_md; AllPages["nsDoc/bitburner.hackingmultipliers.chance.md"] = nsDoc_bitburner_hackingmultipliers_chance_md;
AllPages["nsDoc/bitburner.hackingmultipliers.growth.md"] = nsDoc_bitburner_hackingmultipliers_growth_md; AllPages["nsDoc/bitburner.hackingmultipliers.growth.md"] = nsDoc_bitburner_hackingmultipliers_growth_md;
+12 -8
View File
@@ -39,6 +39,7 @@ import { SpecialServers } from "../Server/data/SpecialServers";
import { CONSTANTS } from "../Constants"; import { CONSTANTS } from "../Constants";
import { BladeburnerConstants } from "../Bladeburner/data/Constants"; import { BladeburnerConstants } from "../Bladeburner/data/Constants";
import type { PlayerObject } from "../PersonObjects/Player/PlayerObject"; import type { PlayerObject } from "../PersonObjects/Player/PlayerObject";
import { CovenantCampaign } from "./ui/CovenantCampaign";
interface FactionInfoParams { interface FactionInfoParams {
infoText?: JSX.Element; infoText?: JSX.Element;
@@ -51,7 +52,7 @@ interface FactionInfoParams {
offerSecurityWork?: boolean; offerSecurityWork?: boolean;
special?: boolean; special?: boolean;
keepOnInstall?: boolean; keepOnInstall?: boolean;
assignment?: () => React.ReactElement; campaign?: () => React.ReactElement;
} }
/** Contains the "information" property for all the Factions, which is just a description of each faction */ /** Contains the "information" property for all the Factions, which is just a description of each faction */
@@ -65,10 +66,10 @@ export class FactionInfo {
/** The hint to show about how to get invited to this faction. */ /** The hint to show about how to get invited to this faction. */
rumorText: JSX.Element; rumorText: JSX.Element;
/** Conditions for being automatically inivited to this facton. */ /** Conditions for being automatically invited to this faction. */
inviteReqs: CompoundPlayerCondition; inviteReqs: CompoundPlayerCondition;
/** Conditions for automatically hearing a rumor about this facton. */ /** Conditions for automatically hearing a rumor about this faction. */
rumorReqs: CompoundPlayerCondition; rumorReqs: CompoundPlayerCondition;
/** A flag indicating if the faction supports field work to earn reputation. */ /** A flag indicating if the faction supports field work to earn reputation. */
@@ -87,7 +88,7 @@ export class FactionInfo {
special: boolean; special: boolean;
/** The data to display on the faction screen. */ /** The data to display on the faction screen. */
assignment?: () => React.ReactElement; campaign?: () => React.ReactElement;
constructor(params: FactionInfoParams) { constructor(params: FactionInfoParams) {
this.infoText = params.infoText ?? <></>; this.infoText = params.infoText ?? <></>;
@@ -101,7 +102,7 @@ export class FactionInfo {
this.keep = params.keepOnInstall ?? false; this.keep = params.keepOnInstall ?? false;
this.special = params.special ?? false; this.special = params.special ?? false;
this.assignment = params.assignment; this.campaign = params.campaign;
} }
offersWork(): boolean { offersWork(): boolean {
@@ -170,6 +171,9 @@ export const FactionInfos: Record<FactionName, FactionInfo> = {
], ],
offerHackingWork: true, offerHackingWork: true,
offerFieldWork: true, offerFieldWork: true,
campaign: () => {
return <CovenantCampaign />;
},
}), }),
// Megacorporations, each forms its own faction // Megacorporations, each forms its own faction
@@ -706,7 +710,7 @@ export const FactionInfos: Record<FactionName, FactionInfo> = {
offerFieldWork: false, offerFieldWork: false,
offerSecurityWork: false, offerSecurityWork: false,
special: true, special: true,
assignment: (): React.ReactElement => { campaign: (): React.ReactElement => {
return ( return (
<Option <Option
buttonText={"Open Bladeburner headquarters"} buttonText={"Open Bladeburner headquarters"}
@@ -768,7 +772,7 @@ export const FactionInfos: Record<FactionName, FactionInfo> = {
offerSecurityWork: false, offerSecurityWork: false,
special: true, special: true,
keepOnInstall: true, keepOnInstall: true,
assignment: (): React.ReactElement => { campaign: (): React.ReactElement => {
return ( return (
<Option <Option
buttonText={"Open Stanek's Gift"} buttonText={"Open Stanek's Gift"}
@@ -803,7 +807,7 @@ export const FactionInfos: Record<FactionName, FactionInfo> = {
offerSecurityWork: false, offerSecurityWork: false,
special: true, special: true,
keepOnInstall: true, keepOnInstall: true,
assignment: (): React.ReactElement => { campaign: (): React.ReactElement => {
return <Typography>{FactionName.ShadowsOfAnarchy} can only gain reputation by infiltrating.</Typography>; return <Typography>{FactionName.ShadowsOfAnarchy} can only gain reputation by infiltrating.</Typography>;
}, },
}), }),
+63
View File
@@ -0,0 +1,63 @@
import Typography from "@mui/material/Typography";
import { Player } from "@player";
import React, { useState } from "react";
import { CovenantPurchasesRoot } from "../../PersonObjects/Sleeve/ui/CovenantPurchasesRoot";
import { Modal } from "../../ui/React/Modal";
import { Option } from "./Option";
import { knowAboutBitverse } from "../../BitNode/BitNodeUtils";
function CovenantIncompleteCampaign() {
const [open, setOpen] = useState(false);
return (
<>
<Option
buttonText={"Research"}
infoText={"The Beginning of True Immortality"}
onClick={() => setOpen(true)}
></Option>
<Modal open={open} onClose={() => setOpen(false)}>
<Typography component="div">
You tried your best to help the research team, but this research isn't making any progress.
<br />
<br />
{knowAboutBitverse() ? (
"Maybe this research can only be completed in BitNode 10?"
) : (
<>
Research data is always randomly corrupted for unknown reasons, and a weird message is sent to you every
time it happens:
<br />
<br />
#@)($*&@__Y0U__^%$#@&*()__HAV3__(&@#*$%(@
<br />
()@#*$%(__N0T__@&$#)@*(__S33N__)(*@#&$)(
<br />
@&*($#@&__TH3__#@A&#@*)(@$#@)*
<br />
%$#@&()@__TRU1H__()*@#$&()@#$
</>
)}
</Typography>
</Modal>
</>
);
}
export function CovenantCampaign() {
const [open, setOpen] = useState(false);
if (Player.bitNodeN !== 10) {
return <CovenantIncompleteCampaign />;
}
return (
<>
<Option
buttonText={"Purchase & Upgrade Duplicate Sleeves"}
infoText={"Purchase Duplicate Sleeves and upgrades. These are permanent!"}
onClick={() => setOpen(true)}
></Option>
<CovenantPurchasesRoot open={open} onClose={() => setOpen(false)} />
</>
);
}
+35 -30
View File
@@ -3,7 +3,7 @@
* This is the component for displaying a single faction's UI, not the list of all * This is the component for displaying a single faction's UI, not the list of all
* accessible factions * accessible factions
*/ */
import React, { useState } from "react"; import React from "react";
import { DonateOption } from "./DonateOption"; import { DonateOption } from "./DonateOption";
import { Info } from "./Info"; import { Info } from "./Info";
@@ -16,12 +16,12 @@ import { Page } from "../../ui/Router";
import { Player } from "@player"; import { Player } from "@player";
import { Typography, Button } from "@mui/material"; import { Typography, Button } from "@mui/material";
import { CovenantPurchasesRoot } from "../../PersonObjects/Sleeve/ui/CovenantPurchasesRoot"; import { FactionWorkType } from "@enums";
import { FactionName, FactionWorkType } from "@enums";
import { GangButton } from "./GangButton"; import { GangButton } from "./GangButton";
import { FactionWork } from "../../Work/FactionWork"; import { FactionWork } from "../../Work/FactionWork";
import { useCycleRerender } from "../../ui/React/hooks"; import { useCycleRerender } from "../../ui/React/hooks";
import { favorNeededToDonate } from "../formulas/donation"; import { favorNeededToDonate } from "../formulas/donation";
import { knowAboutBitverse } from "../../BitNode/BitNodeUtils";
type FactionRootProps = { type FactionRootProps = {
faction: Faction; faction: Faction;
@@ -47,7 +47,6 @@ const augmentationsInfo =
"As your reputation with this faction rises, you will " + "As your reputation with this faction rises, you will " +
"unlock augmentations, which you can purchase to enhance " + "unlock augmentations, which you can purchase to enhance " +
"your abilities."; "your abilities.";
const sleevePurchasesInfo = "Purchase Duplicate Sleeves and upgrades. These are permanent!";
interface IMainProps { interface IMainProps {
faction: Faction; faction: Faction;
@@ -56,7 +55,6 @@ interface IMainProps {
} }
function MainPage({ faction, rerender, onAugmentations }: IMainProps): React.ReactElement { function MainPage({ faction, rerender, onAugmentations }: IMainProps): React.ReactElement {
const [sleevesOpen, setSleevesOpen] = useState(false);
const factionInfo = faction.getInfo(); const factionInfo = faction.getInfo();
function startWork(): void { function startWork(): void {
@@ -105,7 +103,6 @@ function MainPage({ faction, rerender, onAugmentations }: IMainProps): React.Rea
// should be shown // should be shown
const favorToDonate = favorNeededToDonate(); const favorToDonate = favorNeededToDonate();
const canDonate = faction.favor >= favorToDonate; const canDonate = faction.favor >= favorToDonate;
const canPurchaseSleeves = faction.name === FactionName.TheCovenant && Player.bitNodeN === 10;
return ( return (
<> <>
@@ -115,33 +112,41 @@ function MainPage({ faction, rerender, onAugmentations }: IMainProps): React.Rea
</Typography> </Typography>
<Info faction={faction} factionInfo={factionInfo} /> <Info faction={faction} factionInfo={factionInfo} />
<GangButton faction={faction} /> <GangButton faction={faction} />
{!isPlayersGang && factionInfo.offerHackingWork && ( {!isPlayersGang && (
<Option
buttonText={"Hacking Contracts"}
infoText={hackingContractsInfo}
onClick={() => startHackingContracts(faction)}
/>
)}
{!isPlayersGang && factionInfo.offerFieldWork && (
<Option buttonText={"Field Work"} infoText={fieldWorkInfo} onClick={() => startFieldWork(faction)} />
)}
{!isPlayersGang && factionInfo.offerSecurityWork && (
<Option buttonText={"Security Work"} infoText={securityWorkInfo} onClick={() => startSecurityWork(faction)} />
)}
{!isPlayersGang && factionInfo.offersWork() && (
<DonateOption faction={faction} rerender={rerender} favorToDonate={favorToDonate} disabled={!canDonate} />
)}
<Option buttonText={"Purchase Augmentations"} infoText={augmentationsInfo} onClick={onAugmentations} />
{canPurchaseSleeves && (
<> <>
<Option {factionInfo.offersWork() && (
buttonText={"Purchase & Upgrade Duplicate Sleeves"} <Typography>
infoText={sleevePurchasesInfo} Perform work/carry out assignments for your faction to help further its cause! By doing so, you will earn
onClick={() => setSleevesOpen(true)} reputation for your faction. You will also gain reputation passively over time, although at a very slow
/> rate.&nbsp;
<CovenantPurchasesRoot open={sleevesOpen} onClose={() => setSleevesOpen(false)} /> {knowAboutBitverse() && <>Note that the passive reputation gain is disabled in some BitNodes. </>}
Earning reputation will allow you to purchase augmentations through this faction, which are powerful
upgrades that enhance your abilities.
</Typography>
)}
{factionInfo.offerHackingWork && (
<Option
buttonText={"Hacking Contracts"}
infoText={hackingContractsInfo}
onClick={() => startHackingContracts(faction)}
/>
)}
{factionInfo.offerFieldWork && (
<Option buttonText={"Field Work"} infoText={fieldWorkInfo} onClick={() => startFieldWork(faction)} />
)}
{factionInfo.offerSecurityWork && (
<Option
buttonText={"Security Work"}
infoText={securityWorkInfo}
onClick={() => startSecurityWork(faction)}
/>
)}
{factionInfo.offersWork() && (
<DonateOption faction={faction} rerender={rerender} favorToDonate={favorToDonate} disabled={!canDonate} />
)}
</> </>
)} )}
<Option buttonText={"Purchase Augmentations"} infoText={augmentationsInfo} onClick={onAugmentations} />
</> </>
); );
} }
+20 -17
View File
@@ -9,33 +9,20 @@ import { FactionInfo } from "../FactionInfo";
import Typography from "@mui/material/Typography"; import Typography from "@mui/material/Typography";
import { useCycleRerender } from "../../ui/React/hooks"; import { useCycleRerender } from "../../ui/React/hooks";
import { knowAboutBitverse } from "../../BitNode/BitNodeUtils";
import { ReputationInfo } from "../../ui/React/ReputationInfo"; import { ReputationInfo } from "../../ui/React/ReputationInfo";
import { FavorInfo } from "../../ui/React/FavorInfo"; import { FavorInfo } from "../../ui/React/FavorInfo";
import Tooltip from "@mui/material/Tooltip";
import InfoIcon from "@mui/icons-material/Info";
import Grade from "@mui/icons-material/Grade";
interface IProps { interface IProps {
faction: Faction; faction: Faction;
factionInfo: FactionInfo; factionInfo: FactionInfo;
} }
function DefaultAssignment(): React.ReactElement {
return (
<Typography>
Perform work/carry out assignments for your faction to help further its cause! By doing so, you will earn
reputation for your faction. You will also gain reputation passively over time, although at a very slow
rate.&nbsp;
{knowAboutBitverse() && <>Note that the passive reputation gain is disabled in some BitNodes. </>}
Earning reputation will allow you to purchase augmentations through this faction, which are powerful upgrades that
enhance your abilities.
</Typography>
);
}
export function Info(props: IProps): React.ReactElement { export function Info(props: IProps): React.ReactElement {
useCycleRerender(); useCycleRerender();
const Assignment = props.factionInfo.assignment ?? DefaultAssignment;
return ( return (
<> <>
<Typography sx={{ whiteSpace: "pre-wrap" }}>{props.factionInfo.infoText}</Typography> <Typography sx={{ whiteSpace: "pre-wrap" }}>{props.factionInfo.infoText}</Typography>
@@ -50,7 +37,23 @@ export function Info(props: IProps): React.ReactElement {
<Typography>-------------------------</Typography> <Typography>-------------------------</Typography>
<FavorInfo favor={props.faction.favor} /> <FavorInfo favor={props.faction.favor} />
<Typography>-------------------------</Typography> <Typography>-------------------------</Typography>
<Assignment /> <Typography variant="h5" style={{ display: "flex", alignItems: "center" }}>
<Grade style={{ fontSize: "1.1em", marginRight: "10px" }} />
Special Campaign
<Tooltip
title={
<>
Some factions are developing special campaigns for researching breakthrough technology or executing
initiatives. Some campaigns may be complete, while others remain unfinished. Explore them now, and return
later if a campaign is not yet complete to see what unfolds.
</>
}
>
<InfoIcon sx={{ fontSize: "0.8em", marginLeft: "10px" }} />
</Tooltip>
</Typography>
{props.factionInfo.campaign ? props.factionInfo.campaign() : <Typography>None</Typography>}
<Typography>-------------------------</Typography>
</> </>
); );
} }
+25 -6
View File
@@ -9,6 +9,10 @@ import Tabs from "@mui/material/Tabs";
import Tab from "@mui/material/Tab"; import Tab from "@mui/material/Tab";
import { useCycleRerender } from "../../ui/React/hooks"; 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. */ /** React Component for all the gang stuff. */
export function GangRoot(): React.ReactElement { export function GangRoot(): React.ReactElement {
@@ -18,7 +22,7 @@ export function GangRoot(): React.ReactElement {
})(); })();
const [value, setValue] = React.useState(0); const [value, setValue] = React.useState(0);
function handleChange(event: React.SyntheticEvent, tab: number): void { function handleChange(__event: React.SyntheticEvent, tab: number): void {
setValue(tab); setValue(tab);
} }
@@ -26,11 +30,26 @@ export function GangRoot(): React.ReactElement {
return ( return (
<Context.Gang.Provider value={gang}> <Context.Gang.Provider value={gang}>
<Tabs variant="fullWidth" value={value} onChange={handleChange} sx={{ minWidth: "fit-content", maxWidth: "45%" }}> <div style={{ display: "flex" }}>
<Tab label="Management" /> <Tabs
<Tab label="Equipment" /> variant="fullWidth"
<Tab label="Territory" /> value={value}
</Tabs> 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 === 0 && <ManagementSubpage />}
{value === 1 && <EquipmentsSubpage />} {value === 1 && <EquipmentsSubpage />}
{value === 2 && <TerritorySubpage />} {value === 2 && <TerritorySubpage />}

Some files were not shown because too many files have changed in this diff Show More