mirror of
https://github.com/bitburner-official/bitburner-src.git
synced 2026-04-21 00:32:51 +02:00
Compare commits
26 Commits
54287e5f7f
...
dev
| Author | SHA1 | Date | |
|---|---|---|---|
| 2aa5092d85 | |||
| a7409a01cc | |||
| a99109d9c7 | |||
| 95af138c39 | |||
| f5bbc26495 | |||
| f8ec7f4294 | |||
| c06c6590c9 | |||
| 45bce6e45e | |||
| c21d1f44b2 | |||
| 956e00f789 | |||
| c5536d252b | |||
| a99ca64455 | |||
| cb14655325 | |||
| 9ab3e0bcb4 | |||
| cc9144c01b | |||
| fb3fa00b3d | |||
| 8cbd6ff9e1 | |||
| 00a1bc2f6e | |||
| be6fcd206f | |||
| a6a112198e | |||
| 732aadb2d6 | |||
| 85c9ac0181 | |||
| e232f37550 | |||
| 6074721c59 | |||
| 09e46d757b | |||
| 5cb0d559df |
@@ -2,7 +2,17 @@ name: Build artifacts
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
git-sha:
|
||||
description: "Commit SHA-1 to checkout"
|
||||
required: false
|
||||
default: ""
|
||||
workflow_call:
|
||||
inputs:
|
||||
git-sha:
|
||||
type: string
|
||||
required: false
|
||||
default: ""
|
||||
|
||||
env:
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
@@ -13,6 +23,8 @@ jobs:
|
||||
runs-on: windows-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
ref: ${{ github.event.inputs.git-sha || inputs.git-sha || github.sha }}
|
||||
- name: Use Node.js 24
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
@@ -46,6 +58,8 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
ref: ${{ github.event.inputs.git-sha || inputs.git-sha || github.sha }}
|
||||
- name: Use Node.js 24
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
@@ -77,6 +91,8 @@ jobs:
|
||||
runs-on: macos-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
ref: ${{ github.event.inputs.git-sha || inputs.git-sha || github.sha }}
|
||||
- name: Use Node.js 24
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
|
||||
@@ -12,7 +12,7 @@ Default value:
|
||||
|
||||
- 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:**
|
||||
|
||||
|
||||
+3
-3
@@ -1,15 +1,15 @@
|
||||
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
|
||||
|
||||
[Home](./index.md) > [bitburner](./bitburner.md) > [Gang](./bitburner.gang.md) > [getOtherGangInformation](./bitburner.gang.getotherganginformation.md)
|
||||
[Home](./index.md) > [bitburner](./bitburner.md) > [Gang](./bitburner.gang.md) > [getAllGangInformation](./bitburner.gang.getallganginformation.md)
|
||||
|
||||
## Gang.getOtherGangInformation() method
|
||||
## Gang.getAllGangInformation() method
|
||||
|
||||
Get information about all gangs.
|
||||
|
||||
**Signature:**
|
||||
|
||||
```typescript
|
||||
getOtherGangInformation(): Record<string, GangOtherInfoObject>;
|
||||
getAllGangInformation(): Record<string, GangOtherInfoObject>;
|
||||
```
|
||||
**Returns:**
|
||||
|
||||
+11
-11
@@ -61,6 +61,17 @@ Check if you can recruit a new gang member.
|
||||
Create a gang.
|
||||
|
||||
|
||||
</td></tr>
|
||||
<tr><td>
|
||||
|
||||
[getAllGangInformation()](./bitburner.gang.getallganginformation.md)
|
||||
|
||||
|
||||
</td><td>
|
||||
|
||||
Get information about all gangs.
|
||||
|
||||
|
||||
</td></tr>
|
||||
<tr><td>
|
||||
|
||||
@@ -182,17 +193,6 @@ Get information about a specific gang member.
|
||||
List all gang members.
|
||||
|
||||
|
||||
</td></tr>
|
||||
<tr><td>
|
||||
|
||||
[getOtherGangInformation()](./bitburner.gang.getotherganginformation.md)
|
||||
|
||||
|
||||
</td><td>
|
||||
|
||||
Get information about all gangs.
|
||||
|
||||
|
||||
</td></tr>
|
||||
<tr><td>
|
||||
|
||||
|
||||
@@ -126,6 +126,17 @@ Calculate hack percent for one thread. (Ex: 0.25 would steal 25% of the server's
|
||||
Calculate hack time.
|
||||
|
||||
|
||||
</td></tr>
|
||||
<tr><td>
|
||||
|
||||
[weakenEffect(threads, cores)](./bitburner.hackingformulas.weakeneffect.md)
|
||||
|
||||
|
||||
</td><td>
|
||||
|
||||
Calculate the security decrease from a weaken operation. Unlike other hacking formulas, weaken effect depends only on thread count and core count, not on server or player properties. The core bonus formula is .
|
||||
|
||||
|
||||
</td></tr>
|
||||
<tr><td>
|
||||
|
||||
|
||||
@@ -0,0 +1,72 @@
|
||||
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
|
||||
|
||||
[Home](./index.md) > [bitburner](./bitburner.md) > [HackingFormulas](./bitburner.hackingformulas.md) > [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.
|
||||
|
||||
@@ -123,7 +123,7 @@ Default value:
|
||||
|
||||
- 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>
|
||||
|
||||
@@ -454,7 +454,7 @@
|
||||
"CHALLENGE_BN9": {
|
||||
"ID": "CHALLENGE_BN9",
|
||||
"Name": "BN9: Challenge",
|
||||
"Description": "Destroy BN9 without using hacknet servers."
|
||||
"Description": "Destroy BN9 without using hacknet servers or hacknet nodes."
|
||||
},
|
||||
"CHALLENGE_BN10": {
|
||||
"ID": "CHALLENGE_BN10",
|
||||
|
||||
@@ -4,19 +4,10 @@ import { AchievementList } from "./AchievementList";
|
||||
import { achievements } from "./Achievements";
|
||||
import { Box, Typography } from "@mui/material";
|
||||
import { Player } from "@player";
|
||||
import { makeStyles } from "tss-react/mui";
|
||||
|
||||
const useStyles = makeStyles()({
|
||||
root: {
|
||||
width: 50,
|
||||
userSelect: "none",
|
||||
},
|
||||
});
|
||||
|
||||
export function AchievementsRoot(): JSX.Element {
|
||||
const { classes } = useStyles();
|
||||
return (
|
||||
<div className={classes.root} style={{ width: "100%" }}>
|
||||
<div style={{ width: "100%" }}>
|
||||
<Typography variant="h4">Achievements</Typography>
|
||||
<Box mx={2}>
|
||||
<Typography>
|
||||
|
||||
@@ -93,3 +93,12 @@ export function finishBitNode() {
|
||||
}
|
||||
wd.backdoorInstalled = true;
|
||||
}
|
||||
|
||||
/**
|
||||
* BitNode level is not something that is stored, but rather calculated from the current BN and SF level. The concept
|
||||
* appeared because saying "Enter BN1.2" is shorter than saying "Enter BN1 with SF1.1". This is how we display it in the
|
||||
* BitVerse UI and other places. This function is used to consistently calculate this "level".
|
||||
*/
|
||||
export function getBitNodeLevel(bn = Player.bitNodeN, sfLevel = Player.activeSourceFileLvl(bn)): number {
|
||||
return Math.min(sfLevel + 1, bn === 12 ? Number.MAX_VALUE : 3);
|
||||
}
|
||||
|
||||
@@ -271,13 +271,13 @@ function IntelligenceOverride({
|
||||
disabled={!enabled}
|
||||
value={intelligenceOverride !== undefined ? intelligenceOverride : ""}
|
||||
onChange={(event) => {
|
||||
// Empty string will be automatically changed to "0".
|
||||
// Empty string will be automatically changed to "1".
|
||||
if (event.target.value === "") {
|
||||
callbacks.setIntelligenceOverride(0);
|
||||
callbacks.setIntelligenceOverride(1);
|
||||
return;
|
||||
}
|
||||
const value = Number.parseInt(event.target.value);
|
||||
if (!Number.isInteger(value) || value < 0) {
|
||||
if (!Number.isInteger(value) || value < 1) {
|
||||
return;
|
||||
}
|
||||
callbacks.setIntelligenceOverride(value);
|
||||
|
||||
@@ -20,7 +20,7 @@ import { StatsRow } from "../../ui/React/StatsRow";
|
||||
import { defaultMultipliers, getBitNodeMultipliers } from "../BitNode";
|
||||
import { BitNodeMultipliers } from "../BitNodeMultipliers";
|
||||
import { PartialRecord, getRecordEntries } from "../../Types/Record";
|
||||
import { canAccessBitNodeFeature } from "../BitNodeUtils";
|
||||
import { canAccessBitNodeFeature, getBitNodeLevel } from "../BitNodeUtils";
|
||||
|
||||
interface IProps {
|
||||
n: number;
|
||||
@@ -56,8 +56,7 @@ export const BitNodeMultipliersDisplay = ({ n, level, hideMultsIfCannotAccessFea
|
||||
// If not, then we have to assume that we want the next level up from the
|
||||
// current node's source file, so we get the min of that, the SF's max level,
|
||||
// or if it's BN12, ∞
|
||||
const maxSfLevel = n === 12 ? Number.MAX_VALUE : 3;
|
||||
const mults = getBitNodeMultipliers(n, level ?? Math.min(Player.activeSourceFileLvl(n) + 1, maxSfLevel));
|
||||
const mults = getBitNodeMultipliers(n, level ?? getBitNodeLevel(n));
|
||||
|
||||
return (
|
||||
<Box sx={{ columnCount: 2, columnGap: 1, mb: n === 1 ? 0 : -2 }}>
|
||||
|
||||
@@ -10,6 +10,7 @@ import Button from "@mui/material/Button";
|
||||
import { BitNodeMultiplierDescription } from "./BitnodeMultipliersDescription";
|
||||
import { BitNodeAdvancedOptions } from "./BitNodeAdvancedOptions";
|
||||
import { JSONMap } from "../../Types/Jsonable";
|
||||
import { getBitNodeLevel } from "../BitNodeUtils";
|
||||
|
||||
interface IProps {
|
||||
open: boolean;
|
||||
@@ -37,7 +38,7 @@ export function PortalModal(props: IProps): React.ReactElement {
|
||||
const bitNode = BitNodes[bitNodeKey];
|
||||
if (bitNode == null) throw new Error(`Could not find BitNode object for number: ${props.n}`);
|
||||
const maxSourceFileLevel = props.n === 12 ? "∞" : "3";
|
||||
const newLevel = Math.min(props.level + 1, props.n === 12 ? Number.MAX_VALUE : 3);
|
||||
const newLevel = getBitNodeLevel(props.n, props.level);
|
||||
|
||||
let currentSourceFiles = new Map(Player.sourceFiles);
|
||||
if (!props.flume) {
|
||||
|
||||
@@ -6,6 +6,8 @@ import { ActionClass, ActionParams } from "./Action";
|
||||
import { operationSkillSuccessBonus, operationTeamSuccessBonus } from "./Operation";
|
||||
import { getEnumHelper } from "../../utils/EnumHelper";
|
||||
import type { TeamActionWithCasualties } from "./TeamCasualties";
|
||||
import { constructorsForReviver, Generic_fromJSON, type IReviverValue } from "../../utils/JSONReviver";
|
||||
import { clampInteger } from "../../utils/helpers/clampNumber";
|
||||
|
||||
interface BlackOpParams {
|
||||
name: BladeburnerBlackOpName;
|
||||
@@ -32,11 +34,11 @@ export class BlackOperation extends ActionClass implements TeamActionWithCasualt
|
||||
return getEnumHelper("BladeburnerBlackOpName").isMember(name);
|
||||
}
|
||||
|
||||
constructor(params: ActionParams & BlackOpParams) {
|
||||
constructor(params: (ActionParams & BlackOpParams) | null = null) {
|
||||
super(params);
|
||||
this.name = params.name;
|
||||
this.reqdRank = params.reqdRank;
|
||||
this.n = params.n;
|
||||
this.name = params?.name ?? BladeburnerBlackOpName.OperationTyphoon;
|
||||
this.reqdRank = params?.reqdRank ?? 0;
|
||||
this.n = params?.n ?? 0;
|
||||
}
|
||||
|
||||
getAvailability(bladeburner: Bladeburner): Availability {
|
||||
@@ -65,4 +67,23 @@ export class BlackOperation extends ActionClass implements TeamActionWithCasualt
|
||||
getTeamSuccessBonus = operationTeamSuccessBonus;
|
||||
|
||||
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;
|
||||
|
||||
@@ -44,12 +44,18 @@ export function resolveTeamCasualties(action: TeamActionWithCasualties, team: Op
|
||||
*/
|
||||
const losses =
|
||||
minCasualties <= maxCasualties ? team.getTeamCasualtiesRoll(minCasualties, maxCasualties) : minCasualties;
|
||||
team.teamSize -= losses;
|
||||
if (team.teamSize < team.sleeveSize) {
|
||||
team.killRandomSupportingSleeves(team.sleeveSize - team.teamSize);
|
||||
// Calculate the new teamSize in a temporary variable and call the setter team.teamSize ONCE.
|
||||
// Note that it's important to call the setter only once; otherwise, the team count of each operation won't be reset
|
||||
// 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.
|
||||
team.teamSize = team.sleeveSize;
|
||||
newTeamSize = team.sleeveSize;
|
||||
}
|
||||
team.teamSize = newTeamSize;
|
||||
team.teamLost += losses;
|
||||
|
||||
return losses;
|
||||
|
||||
@@ -47,7 +47,7 @@ import { createContracts, loadContractsData } from "./data/Contracts";
|
||||
import { createOperations, loadOperationsData } from "./data/Operations";
|
||||
import { clampInteger, clampNumber } from "../utils/helpers/clampNumber";
|
||||
import { parseCommand } from "../Terminal/Parser";
|
||||
import { BlackOperations } from "./data/BlackOperations";
|
||||
import { createBlackOperations, loadBlackOperationsData } from "./data/BlackOperations";
|
||||
import { GeneralActions } from "./data/GeneralActions";
|
||||
import { PlayerObject } from "../PersonObjects/Player/PlayerObject";
|
||||
import { Sleeve } from "../PersonObjects/Sleeve/Sleeve";
|
||||
@@ -72,7 +72,31 @@ export class Bladeburner implements OperationTeam {
|
||||
skillPoints = 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() {
|
||||
return Player.sleevesSupportingBladeburner().length;
|
||||
}
|
||||
@@ -96,9 +120,13 @@ export class Bladeburner implements OperationTeam {
|
||||
staminaBonus = 0;
|
||||
maxStamina = 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>;
|
||||
operations: Record<BladeburnerOperationName, Operation>;
|
||||
blackOperations: Record<BladeburnerBlackOpName, BlackOperation>;
|
||||
// Array for quick lookup by BlackOp number
|
||||
blackOperationArray: BlackOperation[];
|
||||
numBlackOpsComplete = 0;
|
||||
logging = {
|
||||
general: true,
|
||||
@@ -119,6 +147,11 @@ export class Bladeburner implements OperationTeam {
|
||||
constructor() {
|
||||
this.contracts = createContracts();
|
||||
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
|
||||
@@ -1407,7 +1440,7 @@ export class Bladeburner implements OperationTeam {
|
||||
case BladeburnerActionType.Operation:
|
||||
return this.operations[actionId.name];
|
||||
case BladeburnerActionType.BlackOp:
|
||||
return BlackOperations[actionId.name];
|
||||
return this.blackOperations[actionId.name];
|
||||
case BladeburnerActionType.General:
|
||||
return GeneralActions[actionId.name];
|
||||
}
|
||||
@@ -1426,7 +1459,7 @@ export class Bladeburner implements OperationTeam {
|
||||
case BladeburnerActionType.Operation:
|
||||
return this.operations[name as BladeburnerOperationName];
|
||||
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;
|
||||
}
|
||||
|
||||
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
|
||||
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. */
|
||||
toJSON(): IReviverValue {
|
||||
@@ -1449,9 +1484,10 @@ export class Bladeburner implements OperationTeam {
|
||||
/** Initializes a Bladeburner object from a JSON save state. */
|
||||
static fromJSON(value: IReviverValue): Bladeburner {
|
||||
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 operationsData = value.data.operations;
|
||||
const blackOperationsData = value.data.blackOperations;
|
||||
const bladeburner = Generic_fromJSON(Bladeburner, value.data, Bladeburner.keysToLoad);
|
||||
|
||||
/**
|
||||
@@ -1472,10 +1508,11 @@ export class Bladeburner implements OperationTeam {
|
||||
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.
|
||||
loadContractsData(contractsData, bladeburner.contracts);
|
||||
loadOperationsData(operationsData, bladeburner.operations);
|
||||
loadBlackOperationsData(blackOperationsData, bladeburner.blackOperations);
|
||||
// Regenerate skill multiplier data, which is not included in savedata
|
||||
bladeburner.updateSkillMultipliers();
|
||||
// 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.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;
|
||||
}
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -113,7 +113,9 @@ export function createContracts(): Record<BladeburnerContractName, Contract> {
|
||||
export function loadContractsData(data: unknown, contracts: Record<BladeburnerContractName, Contract>) {
|
||||
// loading data as "unknown" and typechecking it down is probably not necessary
|
||||
// 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);
|
||||
for (const contractName of Object.values(BladeburnerContractName)) {
|
||||
const loadedContract = data[contractName];
|
||||
|
||||
@@ -230,7 +230,9 @@ export function createOperations(): Record<BladeburnerOperationName, Operation>
|
||||
export function loadOperationsData(data: unknown, operations: Record<BladeburnerOperationName, Operation>) {
|
||||
// loading data as "unknown" and typechecking it down is probably not necessary
|
||||
// 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);
|
||||
for (const operationName of Object.values(BladeburnerOperationName)) {
|
||||
const loadedOperation = data[operationName];
|
||||
|
||||
@@ -7,7 +7,7 @@ import { BlackOpElem } from "./BlackOpElem";
|
||||
import { Router } from "../../ui/GameRoot";
|
||||
import { Page } from "../../ui/Router";
|
||||
import { CorruptibleText } from "../../ui/React/CorruptibleText";
|
||||
import { blackOpsArray } from "../data/BlackOperations";
|
||||
import { numberOfBlackOperations } from "../data/BlackOperations";
|
||||
import { finishBitNode } from "../../BitNode/BitNodeUtils";
|
||||
import { Player } from "@player";
|
||||
|
||||
@@ -16,7 +16,7 @@ interface BlackOpPageProps {
|
||||
}
|
||||
|
||||
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 (
|
||||
<>
|
||||
@@ -36,11 +36,11 @@ export function BlackOpPage({ bladeburner }: BlackOpPageProps): React.ReactEleme
|
||||
Unaffected by Charisma.
|
||||
</Typography>
|
||||
|
||||
{bladeburner.numBlackOpsComplete >= blackOpsArray.length && (
|
||||
{bladeburner.numBlackOpsComplete >= numberOfBlackOperations && (
|
||||
<Button
|
||||
sx={{ my: 1, p: 1 }}
|
||||
onClick={() => {
|
||||
if (!Player.bladeburner || Player.bladeburner.numBlackOpsComplete < blackOpsArray.length) {
|
||||
if (!Player.bladeburner || Player.bladeburner.numBlackOpsComplete < numberOfBlackOperations) {
|
||||
return;
|
||||
}
|
||||
finishBitNode();
|
||||
|
||||
@@ -3,7 +3,7 @@ import type { Bladeburner } from "../Bladeburner";
|
||||
import React, { useMemo } from "react";
|
||||
import { CopyableText } from "../../ui/React/CopyableText";
|
||||
import { formatBigNumber } from "../../ui/formatNumber";
|
||||
import { Box, IconButton, Paper, Typography } from "@mui/material";
|
||||
import { Box, IconButton, Paper, Tooltip, Typography } from "@mui/material";
|
||||
import AddIcon from "@mui/icons-material/Add";
|
||||
import CloseIcon from "@mui/icons-material/Close";
|
||||
import { Skill } from "../Skill";
|
||||
@@ -18,10 +18,8 @@ export function SkillElem({ skill, bladeburner, onUpgrade }: SkillElemProps): Re
|
||||
const skillName = skill.name;
|
||||
const skillLevel = bladeburner.getSkillLevel(skillName);
|
||||
const pointCost = useMemo(() => skill.calculateCost(skillLevel), [skill, skillLevel]);
|
||||
// No need to support "+1" button when the skill level reaches Number.MAX_SAFE_INTEGER.
|
||||
const isSupported = skillLevel < Number.MAX_SAFE_INTEGER;
|
||||
// Use skill.canUpgrade() instead of reimplementing all conditional checks.
|
||||
const canLevel = isSupported && skill.canUpgrade(bladeburner, 1).available;
|
||||
const check = skill.canUpgrade(bladeburner, 1);
|
||||
/**
|
||||
* maxLvl is only useful when we check if we should show "MAX LEVEL". For the check of the icon button, we don't need
|
||||
* it. This condition is checked in skill.canUpgrade().
|
||||
@@ -37,10 +35,14 @@ export function SkillElem({ skill, bladeburner, onUpgrade }: SkillElemProps): Re
|
||||
<Paper sx={{ my: 1, p: 1 }}>
|
||||
<Box display="flex" flexDirection="row" alignItems="center">
|
||||
<CopyableText variant="h6" color="primary" value={skillName} />
|
||||
{!canLevel ? (
|
||||
<IconButton disabled>
|
||||
<CloseIcon />
|
||||
</IconButton>
|
||||
{!check.available ? (
|
||||
<Tooltip title={check.error}>
|
||||
<span>
|
||||
<IconButton disabled>
|
||||
<CloseIcon />
|
||||
</IconButton>
|
||||
</span>
|
||||
</Tooltip>
|
||||
) : (
|
||||
<IconButton onClick={onClick}>
|
||||
<AddIcon />
|
||||
@@ -51,7 +53,7 @@ export function SkillElem({ skill, bladeburner, onUpgrade }: SkillElemProps): Re
|
||||
{maxLvl ? (
|
||||
<Typography>MAX LEVEL</Typography>
|
||||
) : (
|
||||
<Typography>Skill Points required: {isSupported ? formatBigNumber(pointCost) : "N/A"}</Typography>
|
||||
<Typography>Skill Points required: {formatBigNumber(pointCost)}</Typography>
|
||||
)}
|
||||
<Typography>{skill.desc}</Typography>
|
||||
</Paper>
|
||||
|
||||
@@ -15,25 +15,26 @@ interface TeamSizeModalProps {
|
||||
}
|
||||
|
||||
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 {
|
||||
// Prevent reloading page when submitting form
|
||||
event.preventDefault();
|
||||
if (teamSize === undefined) return;
|
||||
const num = Math.round(teamSize);
|
||||
if (isNaN(num) || num < 0) {
|
||||
dialogBoxCreate("Invalid value entered for number of Team Members (must be numeric and non-negative)");
|
||||
} else {
|
||||
action.teamCount = num;
|
||||
if (!Number.isInteger(teamSize) || teamSize < 0) {
|
||||
dialogBoxCreate("Invalid value entered for number of Team Members (must be a non-negative integer)");
|
||||
return;
|
||||
}
|
||||
action.teamCount = teamSize;
|
||||
onClose();
|
||||
}
|
||||
|
||||
function onTeamSize(event: React.ChangeEvent<HTMLInputElement>): void {
|
||||
const x = parseFloat(event.target.value);
|
||||
if (x > bladeburner.teamSize) setTeamSize(bladeburner.teamSize);
|
||||
else setTeamSize(x);
|
||||
const newTeamSize = Number(event.target.value);
|
||||
if (newTeamSize > bladeburner.teamSize) {
|
||||
setTeamSize(bladeburner.teamSize);
|
||||
} else {
|
||||
setTeamSize(newTeamSize);
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
|
||||
+25
-2
@@ -7,7 +7,7 @@ export const CONSTANTS = {
|
||||
VersionString: "3.0.0dev",
|
||||
isDevBranch: true,
|
||||
isInTestEnvironment: globalThis.process?.env?.JEST_WORKER_ID !== undefined,
|
||||
VersionNumber: 48,
|
||||
VersionNumber: 49,
|
||||
|
||||
/** Max level for any skill, assuming no multipliers. Determined by max numerical value in javascript for experience
|
||||
* and the skill level formula in Player.js. Note that all this means it that when experience hits MAX_INT, then
|
||||
@@ -111,7 +111,7 @@ export const CONSTANTS = {
|
||||
|
||||
// Also update Documentation/doc/en/changelog.md when appropriate (when doing a release)
|
||||
LatestUpdate: `
|
||||
## v3.0.0 development version: last updated 18 February 2026
|
||||
## v3.0.0 development version: last updated 13 April 2026
|
||||
|
||||
### BREAKING CHANGES
|
||||
|
||||
@@ -143,6 +143,7 @@ export const CONSTANTS = {
|
||||
- Cancel sleeve's current task when calling ns.sleeve.travel() (#2559) (@catloversg)
|
||||
- Make ns.cloud.purchaseServer() and ns.cloud.deleteServer() use hostname as provided (#2560) (@catloversg)
|
||||
- Make implicit string conversion consistent across all coding contracts (#2608) (@catloversg)
|
||||
- Rename ns.gang.getOtherGangInformation to getAllGangInformation (#2635) (@lstutzman)
|
||||
|
||||
### MAJOR CHANGES
|
||||
|
||||
@@ -224,6 +225,10 @@ export const CONSTANTS = {
|
||||
- Fix: Import save comparison popup shows wrong BN level (#2595) (@catloversg)
|
||||
- Fix: Cannot type in text boxes rendered by players' scripts when terminal tab is shown (#2615, #2622) (@lstutzman, @catloversg)
|
||||
- Navigate to gym/university instead of city when stopping focusing on gym/class work (#2613) (@lstutzman)
|
||||
- Ensure prompts shown by ns.prompt do not lose focus in the terminal tab (#2631) (@catloversg)
|
||||
- Remove unnecessary max-width of tab list in in-game editor (#2643) (@catloversg)
|
||||
- Add hooks to sidebar for players to attach custom content (#2651) (@catloversg)
|
||||
- Use exponential notation when formatting very small HP or thread values (#2656) (@catloversg)
|
||||
|
||||
### MISC
|
||||
|
||||
@@ -325,6 +330,12 @@ export const CONSTANTS = {
|
||||
- Dnet: Remove packet capture (#2594) (@ficocelliguy)
|
||||
- Generate more frequent and lower-reward coding contracts (#2603) (@ficocelliguy)
|
||||
- Electron: Fix issues in edge cases of using --export-save (#2590) (@catloversg)
|
||||
- Fix recursive alias detection causing infinite recursion (#2610) (@lstutzman)
|
||||
- Add "hidden" mkdir command (#2646) (@catloversg)
|
||||
- Dnet: Remove bonus time effect on authentication and heartbleed speed; fix ram rounding (#2627) (@ficocelliguy)
|
||||
- Fix tab completion for multi-word quoted autocomplete options (#2612) (@lstutzman)
|
||||
- Add weakenEffect to formulas.hacking namespace (#2626) (@lstutzman)
|
||||
- Update description of "cat" in "help" command (#2654) (@catloversg)
|
||||
|
||||
### DOCUMENTATION
|
||||
|
||||
@@ -378,6 +389,9 @@ export const CONSTANTS = {
|
||||
- Update mention of outdated getStockForecast API (#2578) (@catloversg)
|
||||
- Fix newline issues in IPvGO docs and add missing RAM cost (#2602) (@catloversg)
|
||||
- Document coding contract's generation and rewards (#2624) (@catloversg)
|
||||
- Clarify scp and exec darknet permissions in API docs (#2634) (@lstutzman)
|
||||
- Update RAM cost of hacknet APIs and remove unnecessary RAM cost docs (#2639) (@catloversg)
|
||||
- Update tutorial script for buying cloud servers (#2653) (@catloversg)
|
||||
|
||||
### SPOILER CHANGES - UI
|
||||
|
||||
@@ -389,6 +403,8 @@ export const CONSTANTS = {
|
||||
- Prevent ending BNs through reuse of Bladeburner UI event handler (#2574) (@catloversg)
|
||||
- Always show Black Operations list (#2592) (@catloversg)
|
||||
- Show hints of Sleeves mechanic in pre-endgame (#2605) (@catloversg)
|
||||
- Consistently calculate BitNode "level" (#2645) (@catloversg)
|
||||
- Add tooltips explaining why Bladeburner skill upgrades are disabled (#2648) (@catloversg)
|
||||
|
||||
### SPOILER CHANGES - MISC
|
||||
|
||||
@@ -416,6 +432,9 @@ export const CONSTANTS = {
|
||||
- Add APIs to get rank gain and rank loss of an action (#2572) (@catloversg)
|
||||
- Reduce RAM cost of inGang and inBladeburner APIs (#2582) (@catloversg)
|
||||
- Fix skillMaxUpgradeCount returning 1 at extreme skill levels (#2611) (@lstutzman)
|
||||
- API: Expose charged effects of Stanek's Gift active fragments (#2638) (@catloversg)
|
||||
- Apply SF override to charisma calculations (#2642) (@catloversg)
|
||||
- Update description of "BN9: Challenge" achievement (#2647) (@catloversg)
|
||||
|
||||
### SPOILER CHANGES - DOCUMENTATION
|
||||
|
||||
@@ -519,5 +538,9 @@ export const CONSTANTS = {
|
||||
- Refactor and fix issues in db.ts (#2623) (@catloversg)
|
||||
- Add dependency array to TerminalInput keydown useEffect (#2620) (@lstutzman)
|
||||
- Add dependency array to GameRoot useEffect (#2617) (@lstutzman)
|
||||
- Dev menu: Initialize dark net data when setting SF15 level (#2632) (@catloversg)
|
||||
- Use type-only imports in ArrayHelpers.ts (#2630) (@catloversg)
|
||||
- Remove redundant "$" from JS/TS regex in webpack config (#2649) (@catloversg)
|
||||
- Allow specifying commit hash id when building artifacts (#2652) (@catloversg)
|
||||
`,
|
||||
} as const;
|
||||
|
||||
@@ -210,7 +210,7 @@ export const getTimingAttackConfig = (difficulty: number): ServerConfig => {
|
||||
"I spent some time on it, but that's not the password",
|
||||
];
|
||||
const alphanumeric = difficulty > 16 && Math.random() < 0.3;
|
||||
const length = (alphanumeric ? 0 : 3) + difficulty / 4;
|
||||
const length = Math.min((alphanumeric ? 0 : 3) + difficulty / 4, 8);
|
||||
return {
|
||||
modelId: ModelIds.TimingAttack,
|
||||
password: getPassword(length, alphanumeric),
|
||||
|
||||
@@ -79,17 +79,9 @@ export const calculateAuthenticationTime = (
|
||||
const underleveledFactor = applyUnderleveledFactor ? 1.5 + (chaRequired + 50) / (person.skills.charisma + 50) : 1;
|
||||
const hasBootsFactor = Player.hasAugmentation(AugmentationName.TheBoots) ? 0.8 : 1;
|
||||
const hasSf15_2Factor = Player.activeSourceFileLvl(15) > 2 ? 0.8 : 1;
|
||||
const bonusTimeFactor = hasDarknetBonusTime() ? 0.75 : 1;
|
||||
|
||||
const time =
|
||||
baseTime *
|
||||
skillFactor *
|
||||
backdoorFactor *
|
||||
underleveledFactor *
|
||||
hasBootsFactor *
|
||||
hasSf15_2Factor *
|
||||
bonusTimeFactor *
|
||||
threadsFactor;
|
||||
baseTime * skillFactor * backdoorFactor * underleveledFactor * hasBootsFactor * hasSf15_2Factor * threadsFactor;
|
||||
|
||||
// We need to call GetServer and check if it's a dnet server later because this function can be called by formulas
|
||||
// APIs (darknetServerData.hostname may be an invalid hostname).
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { Player } from "@player";
|
||||
import { addClue } from "./effects";
|
||||
import { formatNumber } from "../../ui/formatNumber";
|
||||
import { formatNumber, formatRam } from "../../ui/formatNumber";
|
||||
import { logger } from "./offlineServerHandling";
|
||||
import type { NetscriptContext } from "../../Netscript/APIWrapper";
|
||||
import type { DarknetServer } from "../../Server/DarknetServer";
|
||||
@@ -12,6 +12,7 @@ import type { DarknetServerData, Person as IPerson } from "@nsdefs";
|
||||
import { clampNumber } from "../../utils/helpers/clampNumber";
|
||||
import { ResponseCodeEnum } from "../Enums";
|
||||
import { isLabyrinthServer } from "./labyrinth";
|
||||
import { roundToTwo } from "../../utils/helpers/roundToTwo";
|
||||
|
||||
/*
|
||||
* Handles the effects of removing some blocked RAM from a Darknet server.
|
||||
@@ -21,7 +22,7 @@ export const handleRamBlockRemoved = (ctx: NetscriptContext, server: DarknetServ
|
||||
const difficulty = server.difficulty + 1;
|
||||
|
||||
const ramBlockRemoved = getRamBlockRemoved(server, threads);
|
||||
server.blockedRam -= ramBlockRemoved;
|
||||
server.blockedRam = roundToTwo(server.blockedRam - ramBlockRemoved);
|
||||
server.updateRamUsed(server.ramUsed - ramBlockRemoved);
|
||||
|
||||
if (server.blockedRam <= 0) {
|
||||
@@ -30,10 +31,10 @@ export const handleRamBlockRemoved = (ctx: NetscriptContext, server: DarknetServ
|
||||
const xpGained = Player.mults.charisma_exp * threads * 10 * 1.1 ** difficulty;
|
||||
Player.gainCharismaExp(xpGained);
|
||||
|
||||
const result = `Liberated ${formatNumber(
|
||||
const result = `Liberated ${formatRam(
|
||||
ramBlockRemoved,
|
||||
4,
|
||||
)}gb of RAM from the server owner's processes. (Gained ${formatNumber(xpGained, 1)} cha xp.)`;
|
||||
)} of RAM from the server owner's processes. (Gained ${formatNumber(xpGained, 1)} cha xp.)`;
|
||||
logger(ctx)(result);
|
||||
return {
|
||||
success: true,
|
||||
@@ -72,7 +73,7 @@ export const getRamBlockRemoved = (darknetServerData: DarknetServerData, threads
|
||||
const charismaFactor = 1 + player.skills.charisma / 100;
|
||||
const difficultyFactor = 2 * 0.92 ** (difficulty + 1);
|
||||
const baseAmount = 0.02;
|
||||
return clampNumber(baseAmount * difficultyFactor * threads * charismaFactor, 0, remainingRamBlock);
|
||||
return roundToTwo(clampNumber(baseAmount * difficultyFactor * threads * charismaFactor, 0, remainingRamBlock));
|
||||
};
|
||||
|
||||
/*
|
||||
@@ -100,5 +101,5 @@ export const getRamBlock = (maxRam: number): number => {
|
||||
return [16, 32, maxRam - 8][Math.floor(Math.random() * 3)];
|
||||
}
|
||||
|
||||
return [maxRam, maxRam - 8, maxRam - 64, maxRam / 2][Math.floor(Math.random() * 4)];
|
||||
return roundToTwo([maxRam, maxRam - 8, maxRam - 64, maxRam / 2][Math.floor(Math.random() * 4)]);
|
||||
};
|
||||
|
||||
@@ -77,7 +77,7 @@ export function NetworkDisplayWrapper(): React.ReactElement {
|
||||
|
||||
useEffect(() => {
|
||||
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);
|
||||
updateDisplay();
|
||||
|
||||
|
||||
@@ -367,12 +367,11 @@ Paste the following code into the [Script](../basic/scripts.md) editor:
|
||||
|
||||
/** @param {NS} ns */
|
||||
export async function main(ns) {
|
||||
// How much RAM each cloud server will have. In this case, it'll
|
||||
// be 8GB.
|
||||
// How much RAM each cloud server will have. In this case, it'll be 8GB.
|
||||
const ram = 8;
|
||||
|
||||
// Iterator we'll use for our loop
|
||||
let i = 0;
|
||||
let i = ns.cloud.getServerNames().length;
|
||||
|
||||
// Continuously try to purchase cloud servers until we've reached the maximum
|
||||
// amount of servers
|
||||
@@ -381,16 +380,16 @@ Paste the following code into the [Script](../basic/scripts.md) editor:
|
||||
if (ns.getServerMoneyAvailable("home") > ns.cloud.getRamLimit(ram)) {
|
||||
// If we have enough money, then:
|
||||
// 1. Purchase the server
|
||||
// 2. Copy our hacking script onto the newly-purchased cloud server
|
||||
// 3. Run our hacking script on the newly-purchased cloud server with 3 threads
|
||||
// 2. Copy our hacking script onto the newly purchased cloud server
|
||||
// 3. Run our hacking script on the newly purchased cloud server with 3 threads
|
||||
// 4. Increment our iterator to indicate that we've bought a new server
|
||||
let hostname = ns.cloud.purchaseServer("cloud-server-" + i, ram);
|
||||
const hostname = ns.cloud.purchaseServer("cloud-server-" + i, ram);
|
||||
ns.scp("early-hack-template.js", hostname);
|
||||
ns.exec("early-hack-template.js", hostname, 3);
|
||||
++i;
|
||||
}
|
||||
//Make the script wait for a second before looping again.
|
||||
//Removing this line will cause an infinite loop and crash the game.
|
||||
// Make the script wait for a second before looping again.
|
||||
// Removing this line will cause an infinite loop and crash the game.
|
||||
await ns.sleep(1000);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -33,12 +33,12 @@ Adding a sleep like in the first example, or changing the code so that the `awai
|
||||
|
||||
Common infinite loop when translating the server purchasing script in starting guide to scripts is to have a while loop, where the condition's change is conditional:
|
||||
|
||||
var ram = 8;
|
||||
var i = 0;
|
||||
const ram = 8;
|
||||
let i = ns.cloud.getServerNames().length;
|
||||
|
||||
while (i < ns.cloud.getServerLimit()) {
|
||||
if (ns.getServerMoneyAvailable("home") > ns.cloud.getRamLimit(ram)) {
|
||||
var hostname = ns.cloud.purchaseServer("cloud-server-" + i, ram);
|
||||
const hostname = ns.cloud.purchaseServer("cloud-server-" + i, ram);
|
||||
ns.scp("early-hack-template.js", hostname);
|
||||
ns.exec("early-hack-template.js", hostname, 3);
|
||||
++i;
|
||||
|
||||
@@ -561,6 +561,7 @@ import nsDoc_bitburner_gameinfo_versionnumber_md from "../../markdown/bitburner.
|
||||
import nsDoc_bitburner_gang_ascendmember_md from "../../markdown/bitburner.gang.ascendmember.md?raw";
|
||||
import nsDoc_bitburner_gang_canrecruitmember_md from "../../markdown/bitburner.gang.canrecruitmember.md?raw";
|
||||
import nsDoc_bitburner_gang_creategang_md from "../../markdown/bitburner.gang.creategang.md?raw";
|
||||
import nsDoc_bitburner_gang_getallganginformation_md from "../../markdown/bitburner.gang.getallganginformation.md?raw";
|
||||
import nsDoc_bitburner_gang_getascensionresult_md from "../../markdown/bitburner.gang.getascensionresult.md?raw";
|
||||
import nsDoc_bitburner_gang_getbonustime_md from "../../markdown/bitburner.gang.getbonustime.md?raw";
|
||||
import nsDoc_bitburner_gang_getchancetowinclash_md from "../../markdown/bitburner.gang.getchancetowinclash.md?raw";
|
||||
@@ -572,7 +573,6 @@ import nsDoc_bitburner_gang_getganginformation_md from "../../markdown/bitburner
|
||||
import nsDoc_bitburner_gang_getinstallresult_md from "../../markdown/bitburner.gang.getinstallresult.md?raw";
|
||||
import nsDoc_bitburner_gang_getmemberinformation_md from "../../markdown/bitburner.gang.getmemberinformation.md?raw";
|
||||
import nsDoc_bitburner_gang_getmembernames_md from "../../markdown/bitburner.gang.getmembernames.md?raw";
|
||||
import nsDoc_bitburner_gang_getotherganginformation_md from "../../markdown/bitburner.gang.getotherganginformation.md?raw";
|
||||
import nsDoc_bitburner_gang_getrecruitsavailable_md from "../../markdown/bitburner.gang.getrecruitsavailable.md?raw";
|
||||
import nsDoc_bitburner_gang_gettasknames_md from "../../markdown/bitburner.gang.gettasknames.md?raw";
|
||||
import nsDoc_bitburner_gang_gettaskstats_md from "../../markdown/bitburner.gang.gettaskstats.md?raw";
|
||||
@@ -746,6 +746,7 @@ import nsDoc_bitburner_hackingformulas_hackexp_md from "../../markdown/bitburner
|
||||
import nsDoc_bitburner_hackingformulas_hackpercent_md from "../../markdown/bitburner.hackingformulas.hackpercent.md?raw";
|
||||
import nsDoc_bitburner_hackingformulas_hacktime_md from "../../markdown/bitburner.hackingformulas.hacktime.md?raw";
|
||||
import nsDoc_bitburner_hackingformulas_md from "../../markdown/bitburner.hackingformulas.md?raw";
|
||||
import nsDoc_bitburner_hackingformulas_weakeneffect_md from "../../markdown/bitburner.hackingformulas.weakeneffect.md?raw";
|
||||
import nsDoc_bitburner_hackingformulas_weakentime_md from "../../markdown/bitburner.hackingformulas.weakentime.md?raw";
|
||||
import nsDoc_bitburner_hackingmultipliers_chance_md from "../../markdown/bitburner.hackingmultipliers.chance.md?raw";
|
||||
import nsDoc_bitburner_hackingmultipliers_growth_md from "../../markdown/bitburner.hackingmultipliers.growth.md?raw";
|
||||
@@ -2157,6 +2158,7 @@ AllPages["nsDoc/bitburner.gameinfo.versionnumber.md"] = nsDoc_bitburner_gameinfo
|
||||
AllPages["nsDoc/bitburner.gang.ascendmember.md"] = nsDoc_bitburner_gang_ascendmember_md;
|
||||
AllPages["nsDoc/bitburner.gang.canrecruitmember.md"] = nsDoc_bitburner_gang_canrecruitmember_md;
|
||||
AllPages["nsDoc/bitburner.gang.creategang.md"] = nsDoc_bitburner_gang_creategang_md;
|
||||
AllPages["nsDoc/bitburner.gang.getallganginformation.md"] = nsDoc_bitburner_gang_getallganginformation_md;
|
||||
AllPages["nsDoc/bitburner.gang.getascensionresult.md"] = nsDoc_bitburner_gang_getascensionresult_md;
|
||||
AllPages["nsDoc/bitburner.gang.getbonustime.md"] = nsDoc_bitburner_gang_getbonustime_md;
|
||||
AllPages["nsDoc/bitburner.gang.getchancetowinclash.md"] = nsDoc_bitburner_gang_getchancetowinclash_md;
|
||||
@@ -2168,7 +2170,6 @@ AllPages["nsDoc/bitburner.gang.getganginformation.md"] = nsDoc_bitburner_gang_ge
|
||||
AllPages["nsDoc/bitburner.gang.getinstallresult.md"] = nsDoc_bitburner_gang_getinstallresult_md;
|
||||
AllPages["nsDoc/bitburner.gang.getmemberinformation.md"] = nsDoc_bitburner_gang_getmemberinformation_md;
|
||||
AllPages["nsDoc/bitburner.gang.getmembernames.md"] = nsDoc_bitburner_gang_getmembernames_md;
|
||||
AllPages["nsDoc/bitburner.gang.getotherganginformation.md"] = nsDoc_bitburner_gang_getotherganginformation_md;
|
||||
AllPages["nsDoc/bitburner.gang.getrecruitsavailable.md"] = nsDoc_bitburner_gang_getrecruitsavailable_md;
|
||||
AllPages["nsDoc/bitburner.gang.gettasknames.md"] = nsDoc_bitburner_gang_gettasknames_md;
|
||||
AllPages["nsDoc/bitburner.gang.gettaskstats.md"] = nsDoc_bitburner_gang_gettaskstats_md;
|
||||
@@ -2342,6 +2343,7 @@ AllPages["nsDoc/bitburner.hackingformulas.hackexp.md"] = nsDoc_bitburner_hacking
|
||||
AllPages["nsDoc/bitburner.hackingformulas.hackpercent.md"] = nsDoc_bitburner_hackingformulas_hackpercent_md;
|
||||
AllPages["nsDoc/bitburner.hackingformulas.hacktime.md"] = nsDoc_bitburner_hackingformulas_hacktime_md;
|
||||
AllPages["nsDoc/bitburner.hackingformulas.md"] = nsDoc_bitburner_hackingformulas_md;
|
||||
AllPages["nsDoc/bitburner.hackingformulas.weakeneffect.md"] = nsDoc_bitburner_hackingformulas_weakeneffect_md;
|
||||
AllPages["nsDoc/bitburner.hackingformulas.weakentime.md"] = nsDoc_bitburner_hackingformulas_weakentime_md;
|
||||
AllPages["nsDoc/bitburner.hackingmultipliers.chance.md"] = nsDoc_bitburner_hackingmultipliers_chance_md;
|
||||
AllPages["nsDoc/bitburner.hackingmultipliers.growth.md"] = nsDoc_bitburner_hackingmultipliers_growth_md;
|
||||
|
||||
@@ -9,6 +9,10 @@ import Tabs from "@mui/material/Tabs";
|
||||
import Tab from "@mui/material/Tab";
|
||||
|
||||
import { useCycleRerender } from "../../ui/React/hooks";
|
||||
import Button from "@mui/material/Button";
|
||||
import { Router } from "../../ui/GameRoot";
|
||||
import { Page } from "../../ui/Router";
|
||||
import { Factions } from "../../Faction/Factions";
|
||||
|
||||
/** React Component for all the gang stuff. */
|
||||
export function GangRoot(): React.ReactElement {
|
||||
@@ -18,7 +22,7 @@ export function GangRoot(): React.ReactElement {
|
||||
})();
|
||||
const [value, setValue] = React.useState(0);
|
||||
|
||||
function handleChange(event: React.SyntheticEvent, tab: number): void {
|
||||
function handleChange(__event: React.SyntheticEvent, tab: number): void {
|
||||
setValue(tab);
|
||||
}
|
||||
|
||||
@@ -26,11 +30,26 @@ export function GangRoot(): React.ReactElement {
|
||||
|
||||
return (
|
||||
<Context.Gang.Provider value={gang}>
|
||||
<Tabs variant="fullWidth" value={value} onChange={handleChange} sx={{ minWidth: "fit-content", maxWidth: "45%" }}>
|
||||
<Tab label="Management" />
|
||||
<Tab label="Equipment" />
|
||||
<Tab label="Territory" />
|
||||
</Tabs>
|
||||
<div style={{ display: "flex" }}>
|
||||
<Tabs
|
||||
variant="fullWidth"
|
||||
value={value}
|
||||
onChange={handleChange}
|
||||
sx={{ minWidth: "fit-content", maxWidth: "45%" }}
|
||||
>
|
||||
<Tab label="Management" />
|
||||
<Tab label="Equipment" />
|
||||
<Tab label="Territory" />
|
||||
</Tabs>
|
||||
<Button
|
||||
style={{ marginLeft: "20px" }}
|
||||
onClick={() => {
|
||||
Router.toPage(Page.Faction, { faction: Factions[gang.facName] });
|
||||
}}
|
||||
>
|
||||
Faction
|
||||
</Button>
|
||||
</div>
|
||||
{value === 0 && <ManagementSubpage />}
|
||||
{value === 1 && <EquipmentsSubpage />}
|
||||
{value === 2 && <TerritorySubpage />}
|
||||
|
||||
+24
-24
@@ -4,37 +4,37 @@ let pidCounter = 1;
|
||||
|
||||
/** Find and return the next available PID for a script */
|
||||
export function generateNextPid(): number {
|
||||
let tempCounter = pidCounter;
|
||||
let pidCandidate = pidCounter;
|
||||
|
||||
// Cap the number of search iterations at some arbitrary value to avoid
|
||||
// infinite loops. We'll assume that players wont have 1mil+ running scripts
|
||||
let found = false;
|
||||
for (let i = 0; i < 1e6; ) {
|
||||
if (!workerScripts.has(tempCounter + i)) {
|
||||
found = true;
|
||||
tempCounter = tempCounter + i;
|
||||
break;
|
||||
// infinite loops. We'll assume that players won't have a million running scripts.
|
||||
for (let attemptCounter = 0; attemptCounter < 1e6; ++attemptCounter, ++pidCandidate) {
|
||||
// ensure the candidate PID is a safe integer
|
||||
if (pidCandidate >= Number.MAX_SAFE_INTEGER) {
|
||||
pidCandidate = 1;
|
||||
}
|
||||
|
||||
if (i === Number.MAX_SAFE_INTEGER - 1) {
|
||||
i = 1;
|
||||
} else {
|
||||
++i;
|
||||
// ensure the PID is not in use
|
||||
if (workerScripts.has(pidCandidate)) {
|
||||
continue;
|
||||
}
|
||||
// found a PID that's not in use
|
||||
pidCounter = pidCandidate + 1;
|
||||
return pidCandidate;
|
||||
}
|
||||
|
||||
if (found) {
|
||||
pidCounter = tempCounter + 1;
|
||||
if (pidCounter >= Number.MAX_SAFE_INTEGER) {
|
||||
pidCounter = 1;
|
||||
}
|
||||
|
||||
return tempCounter;
|
||||
} else {
|
||||
return -1;
|
||||
}
|
||||
// ran out of attempts without finding an unused PID :-(
|
||||
return -1;
|
||||
}
|
||||
|
||||
/**
|
||||
* Reset the PID counter to 1.
|
||||
*
|
||||
* Note that the list of recently finished scripts has to be
|
||||
* cleared (`recentScripts.splice(0)`) when resetting the PID counter.
|
||||
* Otherwise scripts which re-use a PID that is still in the
|
||||
* list of recent scripts do not show up there when they finish
|
||||
* (if the previous script with that PID is still in the list at that point),
|
||||
* because the function `AddRecentScript` de-duplicates scripts by PID.
|
||||
*/
|
||||
export function resetPidCounter(): void {
|
||||
pidCounter = 1;
|
||||
}
|
||||
|
||||
@@ -275,7 +275,7 @@ const gang = {
|
||||
getMemberNames: RamCostConstants.GangApiBase / 4,
|
||||
renameMember: 0,
|
||||
getGangInformation: RamCostConstants.GangApiBase / 2,
|
||||
getOtherGangInformation: RamCostConstants.GangApiBase / 2,
|
||||
getAllGangInformation: RamCostConstants.GangApiBase / 2,
|
||||
getMemberInformation: RamCostConstants.GangApiBase / 2,
|
||||
canRecruitMember: RamCostConstants.GangApiBase / 4,
|
||||
getRecruitsAvailable: RamCostConstants.GangApiBase / 4,
|
||||
@@ -692,6 +692,7 @@ export const RamCosts: RamCostTree<NSFull> = {
|
||||
hackTime: 0,
|
||||
growTime: 0,
|
||||
weakenTime: 0,
|
||||
weakenEffect: 0,
|
||||
},
|
||||
hacknetNodes: {
|
||||
moneyGainRate: 0,
|
||||
|
||||
@@ -16,7 +16,7 @@ import { helpers } from "../Netscript/NetscriptHelpers";
|
||||
import { getEnumHelper } from "../utils/EnumHelper";
|
||||
import { Skills } from "../Bladeburner/data/Skills";
|
||||
import { assertStringWithNSContext } from "../Netscript/TypeAssertion";
|
||||
import { BlackOperations, blackOpsArray } from "../Bladeburner/data/BlackOperations";
|
||||
import { numberOfBlackOperations } from "../Bladeburner/data/BlackOperations";
|
||||
import { checkSleeveAPIAccess, checkSleeveNumber } from "../NetscriptFunctions/Sleeve";
|
||||
import { canAccessBitNodeFeature } from "../BitNode/BitNodeUtils";
|
||||
import {
|
||||
@@ -81,20 +81,21 @@ export function NetscriptBladeburner(): InternalAPI<INetscriptBladeburner> {
|
||||
return Object.values(BladeburnerOperationName);
|
||||
},
|
||||
getBlackOpNames: (ctx) => () => {
|
||||
getBladeburner(ctx);
|
||||
const bladeburner = getBladeburner(ctx);
|
||||
// Ensures they are sent in the correct order
|
||||
return blackOpsArray.map((blackOp) => blackOp.name);
|
||||
return bladeburner.blackOperationArray.map((blackOp) => blackOp.name);
|
||||
},
|
||||
getNextBlackOp: (ctx) => () => {
|
||||
const bladeburner = getBladeburner(ctx);
|
||||
if (bladeburner.numBlackOpsComplete >= blackOpsArray.length) return null;
|
||||
const blackOp = blackOpsArray[bladeburner.numBlackOpsComplete];
|
||||
if (bladeburner.numBlackOpsComplete >= numberOfBlackOperations) return null;
|
||||
const blackOp = bladeburner.blackOperationArray[bladeburner.numBlackOpsComplete];
|
||||
return { name: blackOp.name, rank: blackOp.reqdRank };
|
||||
},
|
||||
getBlackOpRank: (ctx) => (_blackOpName) => {
|
||||
checkBladeburnerAccess(ctx);
|
||||
const blackOpName = getEnumHelper("BladeburnerBlackOpName").nsGetMember(ctx, _blackOpName);
|
||||
return BlackOperations[blackOpName].reqdRank;
|
||||
const bladeburner = getBladeburner(ctx);
|
||||
return bladeburner.blackOperations[blackOpName].reqdRank;
|
||||
},
|
||||
getGeneralActionNames: (ctx) => () => {
|
||||
getBladeburner(ctx);
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { Player } from "@player";
|
||||
import { calculateServerGrowth, calculateGrowMoney } from "../Server/formulas/grow";
|
||||
import { numCycleForGrowthCorrected } from "../Server/ServerHelpers";
|
||||
import { getWeakenEffect, numCycleForGrowthCorrected } from "../Server/ServerHelpers";
|
||||
import {
|
||||
calculateMoneyGainRate,
|
||||
calculateLevelUpgradeCost,
|
||||
@@ -235,6 +235,14 @@ export function NetscriptFormulas(): InternalAPI<IFormulas> {
|
||||
checkFormulasAccess(ctx);
|
||||
return calculateWeakenTime(server, person) * 1000;
|
||||
},
|
||||
weakenEffect:
|
||||
(ctx) =>
|
||||
(_threads, _cores = 1) => {
|
||||
const threads = helpers.number(ctx, "threads", _threads);
|
||||
const cores = helpers.number(ctx, "cores", _cores);
|
||||
checkFormulasAccess(ctx);
|
||||
return getWeakenEffect(threads, cores);
|
||||
},
|
||||
},
|
||||
hacknetNodes: {
|
||||
moneyGainRate:
|
||||
|
||||
@@ -2,7 +2,7 @@ import type { Gang as IGang, EquipmentStats, GangOtherInfoObject } from "@nsdefs
|
||||
import type { Gang } from "../Gang/Gang";
|
||||
import type { GangMember } from "../Gang/GangMember";
|
||||
import type { GangMemberTask } from "../Gang/GangMemberTask";
|
||||
import type { InternalAPI, NetscriptContext } from "../Netscript/APIWrapper";
|
||||
import { type InternalAPI, type NetscriptContext, setRemovedFunctions } from "../Netscript/APIWrapper";
|
||||
|
||||
import { GangPromise, RecruitmentResult } from "../Gang/Gang";
|
||||
import { Player } from "@player";
|
||||
@@ -37,7 +37,7 @@ export function NetscriptGang(): InternalAPI<IGang> {
|
||||
return task;
|
||||
};
|
||||
|
||||
return {
|
||||
const gangFunctions: InternalAPI<IGang> = {
|
||||
createGang: (ctx) => (_faction) => {
|
||||
const faction = getEnumHelper("FactionName").nsGetMember(ctx, _faction);
|
||||
if (Player.gang) {
|
||||
@@ -117,7 +117,7 @@ export function NetscriptGang(): InternalAPI<IGang> {
|
||||
equipmentCostMult: 1 / gang.getDiscount(),
|
||||
};
|
||||
},
|
||||
getOtherGangInformation: (ctx) => () => {
|
||||
getAllGangInformation: (ctx) => () => {
|
||||
getGang(ctx);
|
||||
const cpy: Record<string, GangOtherInfoObject> = {};
|
||||
for (const gang of Object.keys(AllGangs)) {
|
||||
@@ -362,4 +362,10 @@ export function NetscriptGang(): InternalAPI<IGang> {
|
||||
return GangPromise.promise;
|
||||
},
|
||||
};
|
||||
|
||||
// Removed functions
|
||||
setRemovedFunctions(gangFunctions, {
|
||||
getOtherGangInformation: { version: "3.0.0", replacement: "gang.getAllGangInformation" },
|
||||
});
|
||||
return gangFunctions;
|
||||
}
|
||||
|
||||
@@ -45,7 +45,7 @@ import { ScriptFilePath, resolveScriptFilePath } from "../Paths/ScriptFilePath";
|
||||
import { getRecordEntries } from "../Types/Record";
|
||||
import { JobTracks } from "../Company/data/JobTracks";
|
||||
import { ServerConstants } from "../Server/data/Constants";
|
||||
import { blackOpsArray } from "../Bladeburner/data/BlackOperations";
|
||||
import { numberOfBlackOperations } from "../Bladeburner/data/BlackOperations";
|
||||
import { calculateEffectiveRequiredReputation } from "../Company/utils";
|
||||
import { addRepToFavor } from "../Faction/formulas/favor";
|
||||
import { validBitNodes } from "../BitNode/Constants";
|
||||
@@ -1176,7 +1176,7 @@ export function NetscriptSingularity(): InternalAPI<ISingularity> {
|
||||
if (!Player.bladeburner) {
|
||||
return false;
|
||||
}
|
||||
return Player.bladeburner.numBlackOpsComplete >= blackOpsArray.length;
|
||||
return Player.bladeburner.numBlackOpsComplete >= numberOfBlackOperations;
|
||||
};
|
||||
|
||||
if (!hackingRequirements() && !bladeburnerRequirements()) {
|
||||
|
||||
@@ -147,6 +147,16 @@ export abstract class Person implements IPerson {
|
||||
}
|
||||
|
||||
overrideIntelligence(): void {
|
||||
// Reset intelligence data if the player has not unlocked Intelligence.
|
||||
// Note that this check cannot reset intelligence data in some edge cases (e.g., bitflume from non-BN5 to BN5). This
|
||||
// is an accepted limitation.
|
||||
// For more information, please check https://github.com/bitburner-official/bitburner-src/pull/2666
|
||||
if (Player.sourceFileLvl(5) === 0 && Player.bitNodeN !== 5) {
|
||||
this.skills.intelligence = 0;
|
||||
this.exp.intelligence = 0;
|
||||
this.persistentIntelligenceData.exp = 0;
|
||||
return;
|
||||
}
|
||||
const persistentIntelligenceSkill = this.calculateSkill(this.persistentIntelligenceData.exp, 1);
|
||||
// Reset exp and skill to the persistent values if there is no limit (intelligenceOverride) or the limit is greater
|
||||
// than or equal to the persistent skill.
|
||||
@@ -172,7 +182,7 @@ export abstract class Person implements IPerson {
|
||||
* Don't change sourceFileLvl to activeSourceFileLvl. When the player has int level, the ability to gain more int is
|
||||
* a permanent benefit.
|
||||
*/
|
||||
if (Player.sourceFileLvl(5) > 0 || this.skills.intelligence > 0 || Player.bitNodeN === 5) {
|
||||
if (Player.sourceFileLvl(5) > 0 || Player.bitNodeN === 5) {
|
||||
this.exp.intelligence += exp;
|
||||
this.skills.intelligence = Math.floor(this.calculateSkill(this.exp.intelligence, 1));
|
||||
this.persistentIntelligenceData.exp += exp;
|
||||
|
||||
@@ -135,12 +135,14 @@ export function prestigeAugmentation(this: PlayerObject): void {
|
||||
this.hp.current = this.hp.max;
|
||||
|
||||
this.finishWork(true, true);
|
||||
// We need to call overrideIntelligence here instead of prestigeSourceFile to reset intelligence data when installing
|
||||
// augmentations.
|
||||
this.overrideIntelligence();
|
||||
}
|
||||
|
||||
export function prestigeSourceFile(this: PlayerObject): void {
|
||||
this.entropy = 0;
|
||||
this.prestigeAugmentation();
|
||||
this.overrideIntelligence();
|
||||
this.karma = 0;
|
||||
// Duplicate sleeves are reset to level 1 every Bit Node (but the number of sleeves you have persists)
|
||||
this.sleeves.forEach((sleeve) => sleeve.prestige());
|
||||
|
||||
@@ -5,6 +5,11 @@ import { clampNumber } from "../../utils/helpers/clampNumber";
|
||||
* stat level. Stat-agnostic (same formula for every stat)
|
||||
*/
|
||||
export function calculateSkill(exp: number, mult = 1): number {
|
||||
// Mult can be 0 in BN12 when the player has a very high SF12 level. In this case, the skill level will never change
|
||||
// from its initial value (1 for most stats, except intelligence).
|
||||
if (mult === 0) {
|
||||
return 1;
|
||||
}
|
||||
const value = Math.floor(mult * (32 * Math.log(exp + 534.6) - 200));
|
||||
return clampNumber(value, 1);
|
||||
}
|
||||
@@ -12,7 +17,7 @@ export function calculateSkill(exp: number, mult = 1): number {
|
||||
export function calculateExp(skill: number, mult = 1): number {
|
||||
const floorSkill = Math.floor(skill);
|
||||
let value = Math.exp((skill / mult + 200) / 32) - 534.6;
|
||||
if (skill === floorSkill && Number.isFinite(skill)) {
|
||||
if (skill === floorSkill && Number.isFinite(skill) && Number.isFinite(value)) {
|
||||
// Check for floating point rounding issues that would cause the inverse
|
||||
// operation to return the wrong result.
|
||||
let calcSkill = calculateSkill(value, mult);
|
||||
|
||||
@@ -190,6 +190,8 @@ export function prestigeAugmentation(): void {
|
||||
}
|
||||
}
|
||||
|
||||
// clear recent scripts
|
||||
recentScripts.splice(0);
|
||||
resetPidCounter();
|
||||
ProgramsSeen.clear();
|
||||
InvitationsSeen.clear();
|
||||
|
||||
@@ -35,9 +35,6 @@ function giveSourceFile(bitNodeNumber: number): void {
|
||||
}
|
||||
} else {
|
||||
Player.sourceFiles.set(bitNodeNumber, 1);
|
||||
if (bitNodeNumber === 5 && Player.skills.intelligence === 0) {
|
||||
Player.skills.intelligence = 1;
|
||||
}
|
||||
dialogBoxCreate(
|
||||
<>
|
||||
You received a Source-File for destroying a BitNode!
|
||||
@@ -63,13 +60,6 @@ export function enterBitNode(
|
||||
|
||||
if (!isFlume) {
|
||||
giveSourceFile(destroyedBitNode);
|
||||
} else if (Player.sourceFileLvl(5) === 0 && newBitNode !== 5) {
|
||||
Player.skills.intelligence = 0;
|
||||
Player.exp.intelligence = 0;
|
||||
Player.persistentIntelligenceData.exp = 0;
|
||||
}
|
||||
if (newBitNode === 5 && Player.skills.intelligence === 0) {
|
||||
Player.skills.intelligence = 1;
|
||||
}
|
||||
// Set new Bit Node
|
||||
Player.bitNodeN = newBitNode;
|
||||
|
||||
+6
-2
@@ -36,6 +36,7 @@ import { loadInfiltrations } from "./Infiltration/SaveLoadInfiltration";
|
||||
import { InfiltrationState } from "./Infiltration/formulas/game";
|
||||
import { hasDarknetAccess } from "./DarkNet/utils/darknetAuthUtils";
|
||||
import { loadSettings } from "./Settings/SettingsUtils";
|
||||
import { getBitNodeLevel } from "./BitNode/BitNodeUtils";
|
||||
|
||||
/* SaveObject.js
|
||||
* Defines the object used to save/load games
|
||||
@@ -270,7 +271,7 @@ class BitburnerSaveObject implements BitburnerSaveObjectType {
|
||||
* - Base64 format: save file uses .json extension. Save data is the base64-encoded json save string.
|
||||
*/
|
||||
const extension = canUseBinaryFormat() ? "json.gz" : "json";
|
||||
return `bitburnerSave_${epochTime}_BN${bn}x${Player.sourceFileLvl(bn) + 1}.${extension}`;
|
||||
return `bitburnerSave_${epochTime}_BN${bn}x${getBitNodeLevel()}.${extension}`;
|
||||
}
|
||||
|
||||
async exportGame(): Promise<void> {
|
||||
@@ -430,7 +431,10 @@ class BitburnerSaveObject implements BitburnerSaveObjectType {
|
||||
achievements: importedPlayer.achievements?.length ?? 0,
|
||||
|
||||
bitNode: importedPlayer.bitNodeN,
|
||||
bitNodeLevel: importedPlayer.sourceFileLvl(importedPlayer.bitNodeN) + 1,
|
||||
bitNodeLevel: getBitNodeLevel(
|
||||
importedPlayer.bitNodeN,
|
||||
importedPlayer.activeSourceFileLvl(importedPlayer.bitNodeN),
|
||||
),
|
||||
sourceFiles: [...importedPlayer.sourceFiles].reduce<number>((total, [__bn, lvl]) => (total += lvl), 0),
|
||||
exploits: importedPlayer.exploits.length,
|
||||
|
||||
|
||||
+12
-2
@@ -1852,7 +1852,7 @@ export type Task = StudyTask | CompanyWorkTask | CreateProgramWorkTask | CrimeTa
|
||||
*
|
||||
* - 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.
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
@@ -4879,7 +4879,7 @@ export interface Gang {
|
||||
*
|
||||
* @returns Object containing territory and power information about all gangs, including the player's gang, if any.
|
||||
*/
|
||||
getOtherGangInformation(): Record<string, GangOtherInfoObject>;
|
||||
getAllGangInformation(): Record<string, GangOtherInfoObject>;
|
||||
|
||||
/**
|
||||
* Get information about a specific gang member.
|
||||
@@ -6299,6 +6299,16 @@ interface HackingFormulas {
|
||||
* @returns The calculated weaken time, in milliseconds.
|
||||
*/
|
||||
weakenTime(server: Server, player: Person): number;
|
||||
/**
|
||||
* Calculate the security decrease from a weaken operation.
|
||||
* Unlike other hacking formulas, weaken effect depends only on thread count and
|
||||
* core count, not on server or player properties. The core bonus formula is
|
||||
* {@code 1 + (cores - 1) / 16}.
|
||||
* @param threads - Number of threads running weaken.
|
||||
* @param cores - Number of cores on the host server. Default 1.
|
||||
* @returns The security decrease amount.
|
||||
*/
|
||||
weakenEffect(threads: number, cores?: number): number;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -26,6 +26,7 @@ import { Settings } from "../Settings/Settings";
|
||||
import type { ScriptKey } from "../utils/helpers/scriptKey";
|
||||
import { assertObject } from "../utils/TypeAssertion";
|
||||
import { clampNumber } from "../utils/helpers/clampNumber";
|
||||
import { roundToTwo } from "../utils/helpers/roundToTwo";
|
||||
|
||||
export interface BaseServerConstructorParams {
|
||||
adminRights?: boolean;
|
||||
@@ -233,7 +234,7 @@ export abstract class BaseServer implements IServer {
|
||||
}
|
||||
|
||||
updateRamUsed(ram: number): void {
|
||||
this.ramUsed = clampNumber(ram, 0, this.maxRam);
|
||||
this.ramUsed = roundToTwo(clampNumber(ram, 0, this.maxRam));
|
||||
}
|
||||
|
||||
pushProgram(program: ProgramFilePath | CompletedProgramName): void {
|
||||
|
||||
@@ -359,6 +359,7 @@ export function SidebarRoot(props: { page: Page }): React.ReactElement {
|
||||
canStaneksGift && { key_: Page.StaneksGift, icon: DeveloperBoardIcon },
|
||||
]}
|
||||
/>
|
||||
<Typography id="sidebar-extra-hook-0"></Typography>
|
||||
<Divider />
|
||||
<SidebarAccordion
|
||||
key_="Character"
|
||||
@@ -386,6 +387,7 @@ export function SidebarRoot(props: { page: Page }): React.ReactElement {
|
||||
canOpenGrafting && { key_: Page.Grafting, icon: BiotechIcon },
|
||||
]}
|
||||
/>
|
||||
<Typography id="sidebar-extra-hook-1"></Typography>
|
||||
<Divider />
|
||||
<SidebarAccordion
|
||||
key_="World"
|
||||
@@ -411,6 +413,7 @@ export function SidebarRoot(props: { page: Page }): React.ReactElement {
|
||||
canDarkNet && { key_: Page.DarkNet, icon: ShareIcon },
|
||||
]}
|
||||
/>
|
||||
<Typography id="sidebar-extra-hook-2"></Typography>
|
||||
<Divider />
|
||||
<SidebarAccordion
|
||||
key_="Help"
|
||||
@@ -428,6 +431,7 @@ export function SidebarRoot(props: { page: Page }): React.ReactElement {
|
||||
process.env.NODE_ENV === "development" && { key_: Page.DevMenu, icon: DeveloperBoardIcon },
|
||||
]}
|
||||
/>
|
||||
<Typography id="sidebar-extra-hook-3"></Typography>
|
||||
</List>
|
||||
</Drawer>
|
||||
);
|
||||
|
||||
@@ -5,7 +5,7 @@ export const TerminalHelpText: string[] = [
|
||||
" analyze Get information about the current machine ",
|
||||
" backdoor Install a backdoor on the current machine ",
|
||||
" buy [-l/-a/program] Purchase a program through the Dark Web",
|
||||
" cat [file] Display a .msg, .lit, or text file",
|
||||
" cat [file] Display the contents of a file",
|
||||
" cd [dir] Change to a new directory",
|
||||
" changelog Display changelog",
|
||||
" check [script] [args...] Print a script's logs to Terminal",
|
||||
|
||||
@@ -78,6 +78,7 @@ import { commitHash } from "../utils/helpers/commitHash";
|
||||
import { apr1 } from "./commands/apr1";
|
||||
import { changelog } from "./commands/changelog";
|
||||
import { clear } from "./commands/clear";
|
||||
import { mkdir } from "./commands/mkdir";
|
||||
import { currentNodeMults } from "../BitNode/BitNodeMultipliers";
|
||||
import { Engine } from "../engine";
|
||||
import { Directory, resolveDirectory, root } from "../Paths/Directory";
|
||||
@@ -134,8 +135,12 @@ export const TerminalCommands: Record<string, (args: (string | number | boolean)
|
||||
vim: vim,
|
||||
weaken: weaken,
|
||||
wget: wget,
|
||||
mkdir: mkdir,
|
||||
};
|
||||
|
||||
// "mkdir" is a "hidden" command; i.e., it is not shown in help text or autocomplete.
|
||||
export const supportedCommands = Object.keys(TerminalCommands).filter((command) => command !== "mkdir");
|
||||
|
||||
export class Terminal {
|
||||
// Flags to determine whether the player is currently running a hack or an analyze
|
||||
action: TTimer | null = null;
|
||||
@@ -877,8 +882,7 @@ export class Terminal {
|
||||
}
|
||||
|
||||
function findSimilarCommands(command: string): string[] {
|
||||
const commands = Object.keys(TerminalCommands);
|
||||
const offByOneLetter = commands.filter((c) => {
|
||||
const offByOneLetter = supportedCommands.filter((c) => {
|
||||
if (c.length !== command.length) return false;
|
||||
let diff = 0;
|
||||
for (let i = 0; i < c.length; i++) {
|
||||
@@ -886,6 +890,6 @@ function findSimilarCommands(command: string): string[] {
|
||||
}
|
||||
return diff === 1;
|
||||
});
|
||||
const subset = commands.filter((c) => c.includes(command)).sort((a, b) => a.length - b.length);
|
||||
const subset = supportedCommands.filter((c) => c.includes(command)).sort((a, b) => a.length - b.length);
|
||||
return Array.from(new Set([...offByOneLetter, ...subset])).slice(0, 3);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
import { Terminal } from "../../Terminal";
|
||||
|
||||
export function mkdir(): void {
|
||||
Terminal.error(
|
||||
"Directories do not exist in the Bitburner filesystem. They are simply part of the file path.\n" +
|
||||
`For example, with "/foo/bar.txt", there is no actual "/foo" directory.`,
|
||||
);
|
||||
}
|
||||
@@ -11,20 +11,27 @@ import libarg from "arg";
|
||||
import { getAllDirectories, resolveDirectory, root } from "../Paths/Directory";
|
||||
import { isLegacyScript, resolveScriptFilePath } from "../Paths/ScriptFilePath";
|
||||
import { enums } from "../NetscriptFunctions";
|
||||
import { TerminalCommands } from "./Terminal";
|
||||
import { supportedCommands } from "./Terminal";
|
||||
import { Terminal } from "../Terminal";
|
||||
import { parseUnknownError } from "../utils/ErrorHelper";
|
||||
import { DarknetServer } from "../Server/DarknetServer";
|
||||
import { CompletedProgramName } from "@enums";
|
||||
|
||||
/** Extract the text being autocompleted, handling unclosed double quotes as a single token */
|
||||
export function extractCurrentText(terminalText: string): string {
|
||||
const quoteCount = (terminalText.match(/"/g) || []).length;
|
||||
if (quoteCount % 2 === 1) return terminalText.substring(terminalText.lastIndexOf('"'));
|
||||
return /[^ ]*$/.exec(terminalText)?.[0] ?? "";
|
||||
}
|
||||
|
||||
/** Suggest all completion possibilities for the last argument in the last command being typed
|
||||
* @param terminalText The current full text entered in the terminal
|
||||
* @param baseDir The current working directory.
|
||||
* @returns Array of possible string replacements for the current text being autocompleted.
|
||||
*/
|
||||
export async function getTabCompletionPossibilities(terminalText: string, baseDir = root): Promise<string[]> {
|
||||
// Get the current command text
|
||||
const currentText = /[^ ]*$/.exec(terminalText)?.[0] ?? "";
|
||||
// Get the current command text, treating unclosed quotes as a single token
|
||||
const currentText = extractCurrentText(terminalText);
|
||||
// Remove the current text from the commands string
|
||||
const valueWithoutCurrent = terminalText.substring(0, terminalText.length - currentText.length);
|
||||
// Parse the commands string, this handles alias replacement as well.
|
||||
@@ -84,7 +91,7 @@ export async function getTabCompletionPossibilities(terminalText: string, baseDi
|
||||
|
||||
const addAliases = () => addGeneric({ iterable: Aliases.keys() });
|
||||
const addGlobalAliases = () => addGeneric({ iterable: GlobalAliases.keys() });
|
||||
const addCommands = () => addGeneric({ iterable: Object.keys(TerminalCommands) });
|
||||
const addCommands = () => addGeneric({ iterable: supportedCommands });
|
||||
const addDarkwebItems = () => addGeneric({ iterable: Object.values(DarkWebItems).map((item) => item.program) });
|
||||
const addServerNames = () =>
|
||||
addGeneric({
|
||||
|
||||
@@ -6,7 +6,7 @@ import { Paper, Popper, TextField, Typography } from "@mui/material";
|
||||
import { KEY } from "../../utils/KeyboardEventKey";
|
||||
import { Terminal } from "../../Terminal";
|
||||
import { Player } from "@player";
|
||||
import { getTabCompletionPossibilities } from "../getTabCompletionPossibilities";
|
||||
import { extractCurrentText, getTabCompletionPossibilities } from "../getTabCompletionPossibilities";
|
||||
import { Settings } from "../../Settings/Settings";
|
||||
import { longestCommonStart } from "../../utils/StringHelperFunctions";
|
||||
import { exceptionAlert } from "../../utils/helpers/exceptionAlert";
|
||||
@@ -266,13 +266,16 @@ export function TerminalInput(): React.ReactElement {
|
||||
if (possibilities.length === 0) return;
|
||||
|
||||
setSearchResults([]);
|
||||
// Use quote-aware replacement: if mid-quote, replace from the opening quote
|
||||
const currentText = extractCurrentText(value);
|
||||
const replacePattern = currentText.startsWith('"') ? /"[^"]*$/ : /[^ ]*$/;
|
||||
if (possibilities.length === 1) {
|
||||
saveValue(value.replace(/[^ ]*$/, possibilities[0]) + " ");
|
||||
saveValue(value.replace(replacePattern, possibilities[0]) + " ");
|
||||
return;
|
||||
}
|
||||
// More than one possibility, check to see if there is a longer common string than currentText.
|
||||
const longestMatch = longestCommonStart(possibilities);
|
||||
saveValue(value.replace(/[^ ]*$/, longestMatch));
|
||||
saveValue(value.replace(replacePattern, longestMatch));
|
||||
setPossibilities(possibilities);
|
||||
}
|
||||
|
||||
|
||||
+2
-2
@@ -157,7 +157,7 @@ const Engine = {
|
||||
messages: 150,
|
||||
mechanicProcess: 5, // Process Bladeburner
|
||||
contractGeneration: 3000, // Generate Coding Contracts
|
||||
achievementsCounter: 60, // Check if we have new achievements
|
||||
achievementsCounter: 5, // Check if we have new achievements
|
||||
},
|
||||
|
||||
decrementAllCounters: function (numCycles = 1) {
|
||||
@@ -215,7 +215,7 @@ const Engine = {
|
||||
|
||||
if (Engine.Counters.achievementsCounter <= 0) {
|
||||
calculateAchievements();
|
||||
Engine.Counters.achievementsCounter = 300;
|
||||
Engine.Counters.achievementsCounter = 5;
|
||||
}
|
||||
|
||||
// This **MUST** remain the last block in the function!
|
||||
|
||||
@@ -17,7 +17,7 @@ import { StatsRow } from "./React/StatsRow";
|
||||
import { StatsTable } from "./React/StatsTable";
|
||||
import { useCycleRerender } from "./React/hooks";
|
||||
import { getMaxRep } from "../Go/effects/effect";
|
||||
import { canAccessBitNodeFeature, knowAboutBitverse } from "../BitNode/BitNodeUtils";
|
||||
import { canAccessBitNodeFeature, getBitNodeLevel, knowAboutBitverse } from "../BitNode/BitNodeUtils";
|
||||
|
||||
interface EmployersModalProps {
|
||||
open: boolean;
|
||||
@@ -103,11 +103,10 @@ function MultiplierTable(props: MultTableProps): React.ReactElement {
|
||||
function CurrentBitNode(): React.ReactElement {
|
||||
if (knowAboutBitverse()) {
|
||||
const index = "BitNode" + Player.bitNodeN;
|
||||
const lvl = Math.min(Player.sourceFileLvl(Player.bitNodeN) + 1, Player.bitNodeN === 12 ? Number.MAX_VALUE : 3);
|
||||
return (
|
||||
<Paper sx={{ mb: 1, p: 1 }}>
|
||||
<Typography variant="h5">
|
||||
BitNode {Player.bitNodeN}: {BitNodes[index].name} (Level {lvl})
|
||||
BitNode {Player.bitNodeN}: {BitNodes[index].name} (Level {getBitNodeLevel()})
|
||||
</Typography>
|
||||
<Typography component="div" sx={{ whiteSpace: "pre-wrap", overflowWrap: "break-word" }}>
|
||||
{BitNodes[index].info}
|
||||
|
||||
+12
-2
@@ -193,8 +193,18 @@ export const formatInt = (n: number) => formatNumber(n, 3, 1000, true);
|
||||
export const formatSleeveMemory = formatInt;
|
||||
export const formatShares = formatInt;
|
||||
|
||||
/** Display an integer up to 999,999 before collapsing to suffixed form with 3 fractional digits */
|
||||
export const formatHp = (n: number) => formatNumber(n, 3, 1e6, true);
|
||||
/**
|
||||
* Format a number using basicFormatter for values below 1e6, and a suffixed form with up to 3 fractional digits for
|
||||
* values at or above 1e6. This uses formatNumber, so check that function for nuanced details.
|
||||
*
|
||||
* Values in the range (0, 0.001) are displayed in exponential notation.
|
||||
*/
|
||||
export const formatHp = (n: number) => {
|
||||
if (n > 0 && n < 0.001) {
|
||||
return formatExponential(n);
|
||||
}
|
||||
return formatNumber(n, 3, 1e6, true);
|
||||
};
|
||||
export const formatThreads = formatHp;
|
||||
|
||||
/** Display an integer up to 999,999,999 before collapsing to suffixed form with 3 fractional digits */
|
||||
|
||||
@@ -624,5 +624,20 @@ export const breakingChanges300: VersionBreakingChange = {
|
||||
`- Read the "General rules", "String conversion", and "Tips" sections on the "Coding Contracts" page carefully.`,
|
||||
showWarning: false,
|
||||
},
|
||||
{
|
||||
brokenAPIs: [
|
||||
{
|
||||
name: "ns.gang.getOtherGangInformation",
|
||||
migration: {
|
||||
searchValue: "getOtherGangInformation",
|
||||
replaceValue: "getAllGangInformation",
|
||||
},
|
||||
},
|
||||
],
|
||||
info:
|
||||
"ns.gang.getOtherGangInformation() was renamed to ns.gang.getAllGangInformation().\n" +
|
||||
"The function was renamed because it returns information about all gangs, including the player's own gang.",
|
||||
showWarning: false,
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
@@ -637,4 +637,11 @@ Error: ${e}`,
|
||||
if (ver < 48) {
|
||||
showAPIBreaks("3.0.0", breakingChanges300);
|
||||
}
|
||||
if (ver < 49 && Player.sourceFileLvl(5) === 0 && Player.bitNodeN !== 5) {
|
||||
for (const person of [Player, ...Player.sleeves]) {
|
||||
person.persistentIntelligenceData.exp = 0;
|
||||
person.exp.intelligence = 0;
|
||||
person.skills.intelligence = 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,6 +7,7 @@ import { Operation } from "../../../src/Bladeburner/Actions/Operation";
|
||||
import {
|
||||
AugmentationName,
|
||||
BladeburnerActionType,
|
||||
BladeburnerBlackOpName,
|
||||
BladeburnerContractName,
|
||||
BladeburnerGeneralActionName,
|
||||
BladeburnerOperationName,
|
||||
@@ -17,15 +18,15 @@ import { FormatsNeedToChange } from "../../../src/ui/formatNumber";
|
||||
import { CrimeWork } from "../../../src/Work/CrimeWork";
|
||||
import type { Action, ActionIdentifier } from "../../../src/Bladeburner/Types";
|
||||
import type { Skills } from "@nsdefs";
|
||||
import { BlackOperations } from "../../../src/Bladeburner/data/BlackOperations";
|
||||
import { applyAugmentation } from "../../../src/Augmentation/AugmentationHelpers";
|
||||
import { PlayerOwnedAugmentation } from "../../../src/Augmentation/PlayerOwnedAugmentation";
|
||||
import { BlackOperation } from "../../../src/Bladeburner/Actions/BlackOperation";
|
||||
|
||||
describe("Bladeburner Actions", () => {
|
||||
const SampleContract = Contract.createId(BladeburnerContractName.Tracking);
|
||||
const SampleGeneralAction = GeneralAction.createId(BladeburnerGeneralActionName.Diplomacy);
|
||||
const SampleOperation = Operation.createId(BladeburnerOperationName.Assassination);
|
||||
const SampleBlackOp = BlackOperations["Operation Centurion"].id;
|
||||
const SampleBlackOp = BlackOperation.createId(BladeburnerBlackOpName.OperationCenturion);
|
||||
|
||||
const ENOUGH_TIME_TO_FINISH_ACTION = 1e5;
|
||||
const BASE_STAT_EXP = 1e6;
|
||||
@@ -37,7 +38,8 @@ describe("Bladeburner Actions", () => {
|
||||
|
||||
const contracts = Object.values(new Bladeburner().contracts);
|
||||
const operations = Object.values(new Bladeburner().operations);
|
||||
const nonGeneralActions = [contracts, operations, Object.values(BlackOperations)].flat();
|
||||
const blackOperations = Object.values(new Bladeburner().blackOperations);
|
||||
const nonGeneralActions = [contracts, operations, blackOperations].flat();
|
||||
|
||||
describe("Without Simulacrum", () => {
|
||||
it("Starting an action cancels player's work immediately", () => {
|
||||
@@ -139,16 +141,13 @@ describe("Bladeburner Actions", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe.each([SampleContract, SampleOperation, BlackOperations["Operation Archangel"].id])(
|
||||
"non-general actions increase rank",
|
||||
(id) => {
|
||||
it(`${id.type}`, () => {
|
||||
before = bb.rank;
|
||||
complete(id, forceSuccess);
|
||||
expect(bb.rank).toBeGreaterThan(before);
|
||||
});
|
||||
},
|
||||
);
|
||||
describe.each([SampleContract, SampleOperation, SampleBlackOp])("non-general actions increase rank", (id) => {
|
||||
it(`${id.type}`, () => {
|
||||
before = bb.rank;
|
||||
complete(id, forceSuccess);
|
||||
expect(bb.rank).toBeGreaterThan(before);
|
||||
});
|
||||
});
|
||||
|
||||
describe("non-general actions increase rank", () => {
|
||||
let beforeMinor, minorGain, beforeMajor, majorGain;
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import { Player, setPlayer } from "@player";
|
||||
import { FormatsNeedToChange } from "../../../src/ui/formatNumber";
|
||||
import type { ActionIdFor } from "../../../src/Bladeburner/Types";
|
||||
import type { Bladeburner } from "../../../src/Bladeburner/Bladeburner";
|
||||
import { BlackOperation } from "../../../src/Bladeburner/Actions/BlackOperation";
|
||||
@@ -9,6 +8,9 @@ import { SleeveSupportWork } from "../../../src/PersonObjects/Sleeve/Work/Sleeve
|
||||
import { BladeburnerBlackOpName, BladeburnerContractName, BladeburnerOperationName } from "@enums";
|
||||
import { PlayerObject } from "../../../src/PersonObjects/Player/PlayerObject";
|
||||
import { recalculateNumberOfOwnedSleeves } from "../../../src/PersonObjects/Sleeve/SleeveCovenantPurchases";
|
||||
import { initGameEnvironment } from "../Utilities";
|
||||
|
||||
initGameEnvironment();
|
||||
|
||||
/**
|
||||
* You may want to use hook to help with debugging
|
||||
@@ -27,11 +29,6 @@ describe("Bladeburner Team", () => {
|
||||
let inst: Bladeburner;
|
||||
let action: BlackOperation | Operation;
|
||||
|
||||
beforeAll(() => {
|
||||
/* Initialise Formatters. Dependency of Bladeburner */
|
||||
FormatsNeedToChange.emit();
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
setPlayer(new PlayerObject());
|
||||
Player.init();
|
||||
@@ -133,6 +130,51 @@ describe("Bladeburner Team", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("Check teamSize and teamCount", () => {
|
||||
test("Failed action", () => {
|
||||
teamSize(10);
|
||||
startAction(OP);
|
||||
forceMaxCasualties();
|
||||
for (const action of [...Object.values(inst.operations), ...Object.values(inst.blackOperations)]) {
|
||||
action.teamCount = 10;
|
||||
expect(action.teamCount).toStrictEqual(10);
|
||||
}
|
||||
actionFails();
|
||||
expect(inst.teamSize).toStrictEqual(0);
|
||||
for (const action of [...Object.values(inst.operations), ...Object.values(inst.blackOperations)]) {
|
||||
expect(action.teamCount).toStrictEqual(0);
|
||||
}
|
||||
});
|
||||
test("Sleeves", () => {
|
||||
teamSize(1);
|
||||
supportingSleeves(8);
|
||||
expect(inst.teamSize).toStrictEqual(9);
|
||||
startAction(OP);
|
||||
forceMaxCasualties();
|
||||
for (const action of [...Object.values(inst.operations), ...Object.values(inst.blackOperations)]) {
|
||||
action.teamCount = 9;
|
||||
expect(action.teamCount).toStrictEqual(9);
|
||||
}
|
||||
actionFails();
|
||||
// The teamCount of all operations/black operations should be 8, not 0.
|
||||
assertSleevesHaveBeenShocked();
|
||||
expect(inst.teamSize).toStrictEqual(8);
|
||||
for (const action of [...Object.values(inst.operations), ...Object.values(inst.blackOperations)]) {
|
||||
expect(action.teamCount).toStrictEqual(8);
|
||||
}
|
||||
Player.sleeves[0].stopWork();
|
||||
expect(inst.teamSize).toStrictEqual(7);
|
||||
for (const action of [...Object.values(inst.operations), ...Object.values(inst.blackOperations)]) {
|
||||
expect(action.teamCount).toStrictEqual(7);
|
||||
}
|
||||
Player.sleeves[0].startWork(new SleeveSupportWork());
|
||||
expect(inst.teamSize).toStrictEqual(8);
|
||||
for (const action of [...Object.values(inst.operations), ...Object.values(inst.blackOperations)]) {
|
||||
expect(action.teamCount).toStrictEqual(7);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
function teamSize(n: number) {
|
||||
inst.teamSize = n;
|
||||
}
|
||||
|
||||
@@ -105,4 +105,31 @@ describe("v3", () => {
|
||||
`bitburnerSave_backup_2.8.1_${Math.round(lastUpdate / 1000)}.json.gz`,
|
||||
);
|
||||
});
|
||||
|
||||
describe("Intelligence migration bug", () => {
|
||||
test("No change in exp and skill level", async () => {
|
||||
const saveData = new Uint8Array(fs.readFileSync("test/jest/Migration/save-files/v2.8.1_SF1.1_SF10.3.gz"));
|
||||
const mockedDownload = await loadGameFromSaveData(saveData);
|
||||
|
||||
for (const person of [Player, ...Player.sleeves]) {
|
||||
expect(person.persistentIntelligenceData.exp).toStrictEqual(0);
|
||||
expect(person.exp.intelligence).toStrictEqual(0);
|
||||
expect(person.skills.intelligence).toStrictEqual(0);
|
||||
}
|
||||
|
||||
expect(mockedDownload).toHaveBeenCalledWith(saveData, "bitburnerSave_backup_2.8.1_1776173824.json.gz");
|
||||
});
|
||||
test("Reset wrong exp and skill level", async () => {
|
||||
const saveData = new Uint8Array(fs.readFileSync("test/jest/Migration/save-files/v3.0.0_int_migration_bug.gz"));
|
||||
const mockedDownload = await loadGameFromSaveData(saveData);
|
||||
|
||||
for (const person of [Player, ...Player.sleeves]) {
|
||||
expect(person.persistentIntelligenceData.exp).toStrictEqual(0);
|
||||
expect(person.exp.intelligence).toStrictEqual(0);
|
||||
expect(person.skills.intelligence).toStrictEqual(0);
|
||||
}
|
||||
|
||||
expect(mockedDownload).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
Binary file not shown.
Binary file not shown.
@@ -34,6 +34,7 @@ import {
|
||||
import { getMostRecentAuthLog } from "../../../src/DarkNet/models/packetSniffing";
|
||||
import type { Result } from "@nsdefs";
|
||||
import { assertNonNullish } from "../../../src/utils/TypeAssertion";
|
||||
import { roundToTwo } from "../../../src/utils/helpers/roundToTwo";
|
||||
|
||||
const hostnameOfNonExistentServer = "fake-server";
|
||||
const errorMessageForNonExistentServer = `Invalid host: '${hostnameOfNonExistentServer}'`;
|
||||
@@ -53,7 +54,9 @@ beforeEach(() => {
|
||||
getDarkscapeNavigator();
|
||||
Player.getHomeComputer().programs.push(CompletedProgramName.formulas);
|
||||
Player.mults.charisma = 1e10;
|
||||
Player.mults.hacking = 1e10;
|
||||
Player.gainCharismaExp(1e100);
|
||||
Player.gainHackingExp(1e100);
|
||||
getNsOnServerNearLabyrinth();
|
||||
});
|
||||
|
||||
@@ -1223,9 +1226,13 @@ describe("Use IP instead of hostname", () => {
|
||||
server.ramUsed = server.blockedRam = 1;
|
||||
|
||||
const ns = getNS(server.hostname);
|
||||
const initialBlockedRam = server.blockedRam;
|
||||
const result3 = await ns.dnet.memoryReallocation(ns.getIP());
|
||||
const updatedBlockedRam = getDarknetServerOrThrow(server.hostname).blockedRam;
|
||||
expect(result3.success).toStrictEqual(true);
|
||||
expect(result3.code).toStrictEqual(ResponseCodeEnum.Success);
|
||||
expect(updatedBlockedRam).toBeLessThan(initialBlockedRam);
|
||||
expect(updatedBlockedRam).toEqual(roundToTwo(updatedBlockedRam));
|
||||
});
|
||||
test("getBlockedRam", () => {
|
||||
const ns = getNsOnNonDarkwebDarknetServer();
|
||||
|
||||
@@ -0,0 +1,46 @@
|
||||
import { FactionName } from "@enums";
|
||||
import { Player } from "@player";
|
||||
import { Gang } from "../../../src/Gang/Gang";
|
||||
import { AllGangs } from "../../../src/Gang/AllGangs";
|
||||
import { getNS, initGameEnvironment, setupBasicTestingEnvironment } from "../Utilities";
|
||||
|
||||
beforeAll(() => {
|
||||
initGameEnvironment();
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
setupBasicTestingEnvironment();
|
||||
// Give the player a gang so gang API is accessible
|
||||
Player.gang = new Gang(FactionName.SlumSnakes, false);
|
||||
});
|
||||
|
||||
describe("ns.gang.getAllGangInformation", () => {
|
||||
it("should return territory and power info for all gangs including the player's", () => {
|
||||
const ns = getNS();
|
||||
const info = ns.gang.getAllGangInformation();
|
||||
const gangNames = Object.keys(info);
|
||||
|
||||
// Should include all 7 gangs
|
||||
expect(gangNames).toHaveLength(Object.keys(AllGangs).length);
|
||||
|
||||
// Should include the player's own gang
|
||||
expect(info[FactionName.SlumSnakes]).toBeDefined();
|
||||
|
||||
// Each entry should have power and territory
|
||||
for (const name of gangNames) {
|
||||
expect(info[name]).toHaveProperty("power");
|
||||
expect(info[name]).toHaveProperty("territory");
|
||||
expect(typeof info[name].power).toBe("number");
|
||||
expect(typeof info[name].territory).toBe("number");
|
||||
}
|
||||
});
|
||||
|
||||
it("should return copies, not references to the original AllGangs data", () => {
|
||||
const ns = getNS();
|
||||
const info = ns.gang.getAllGangInformation();
|
||||
|
||||
// Mutating the returned data should not affect AllGangs
|
||||
info[FactionName.SlumSnakes].power = 999999;
|
||||
expect(AllGangs[FactionName.SlumSnakes].power).not.toBe(999999);
|
||||
});
|
||||
});
|
||||
@@ -1,5 +1,4 @@
|
||||
import { installAugmentations } from "../../../src/Augmentation/AugmentationHelpers";
|
||||
import { blackOpsArray } from "../../../src/Bladeburner/data/BlackOperations";
|
||||
import { AugmentationName, CompanyName, CompletedProgramName, FactionName, JobField, JobName } from "@enums";
|
||||
import { Player } from "@player";
|
||||
import { prestigeSourceFile } from "../../../src/Prestige";
|
||||
@@ -14,6 +13,7 @@ import { Companies } from "../../../src/Company/Companies";
|
||||
import { CompanyPositions } from "../../../src/Company/CompanyPositions";
|
||||
import { getTorRouter } from "../../../src/Server/ServerHelpers";
|
||||
import * as exceptionAlertModule from "../../../src/utils/helpers/exceptionAlert";
|
||||
import { numberOfBlackOperations } from "../../../src/Bladeburner/data/BlackOperations";
|
||||
|
||||
const nextBN = 4;
|
||||
|
||||
@@ -55,8 +55,12 @@ function testIntelligenceOverride(
|
||||
setUpBeforePrestige = () => {},
|
||||
): void {
|
||||
Player.sourceFiles.set(5, 1);
|
||||
// The intelligence skill level starts at 0.
|
||||
expect(Player.skills.intelligence).toStrictEqual(0);
|
||||
prestigeSourceFile(true);
|
||||
// Start without exp.
|
||||
expect(Player.exp.intelligence).toStrictEqual(0);
|
||||
// When having SF5 and the skill level is 0, it's set to 1.
|
||||
expect(Player.skills.intelligence).toStrictEqual(1);
|
||||
expect(Player.persistentIntelligenceData.exp).toStrictEqual(0);
|
||||
// Gain 1e6 exp (skill = 242).
|
||||
@@ -159,6 +163,30 @@ function testIntelligenceOverride(
|
||||
expect(Player.persistentIntelligenceData.exp).toStrictEqual(1e6 + intelligenceExpGainOnPrestige * 2);
|
||||
}
|
||||
|
||||
/** Sets intelligence exp while bypassing the requirements (SF5 or being in BN5). */
|
||||
function manuallySetIntelligenceExp(exp: number): void {
|
||||
Player.exp.intelligence = exp;
|
||||
Player.skills.intelligence = Math.floor(Player.calculateSkill(Player.exp.intelligence, 1));
|
||||
Player.persistentIntelligenceData.exp = exp;
|
||||
}
|
||||
|
||||
function expectIntelligenceExp(exp: number): void {
|
||||
expect(Player.exp.intelligence).toStrictEqual(exp);
|
||||
expect(Player.skills.intelligence).toStrictEqual(Math.floor(Player.calculateSkill(exp, 1)));
|
||||
expect(Player.persistentIntelligenceData.exp).toStrictEqual(exp);
|
||||
}
|
||||
|
||||
/**
|
||||
* This function is not equivalent to expectIntelligenceExp(0). The intelligence skill level starts at 0, not 1 like
|
||||
* other stats. This function specifically verifies the initial state of intelligence data (before entering BN5).
|
||||
*/
|
||||
function expectInitialIntelligenceData(): void {
|
||||
expect(Player.exp.intelligence).toStrictEqual(0);
|
||||
// The intelligence skill level starts at 0.
|
||||
expect(Player.skills.intelligence).toStrictEqual(0);
|
||||
expect(Player.persistentIntelligenceData.exp).toStrictEqual(0);
|
||||
}
|
||||
|
||||
function setUpBeforeDestroyingWD(): void {
|
||||
Player.queueAugmentation(AugmentationName.TheRedPill);
|
||||
installAugmentations();
|
||||
@@ -166,7 +194,7 @@ function setUpBeforeDestroyingWD(): void {
|
||||
const wdServer = GetServerOrThrow(SpecialServers.WorldDaemon);
|
||||
wdServer.hasAdminRights = true;
|
||||
Player.startBladeburner();
|
||||
setNumBlackOpsComplete(blackOpsArray.length);
|
||||
setNumBlackOpsComplete(numberOfBlackOperations);
|
||||
}
|
||||
|
||||
describe("b1tflum3", () => {
|
||||
@@ -642,3 +670,177 @@ describe("purchaseProgram", () => {
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("Intelligence", () => {
|
||||
beforeEach(() => {
|
||||
setupBasicTestingEnvironment();
|
||||
expect(Player.bitNodeN).toStrictEqual(1);
|
||||
expect(Player.sourceFileLvl(5)).toStrictEqual(0);
|
||||
expectInitialIntelligenceData();
|
||||
});
|
||||
test("Get SF5", () => {
|
||||
// This is the most common scenario. Some checks in this test will be repeated in other tests.
|
||||
const ns = getNS();
|
||||
ns.singularity.b1tflum3(5);
|
||||
expectIntelligenceExp(0);
|
||||
|
||||
Player.gainIntelligenceExp(1000);
|
||||
expectIntelligenceExp(1000);
|
||||
|
||||
setUpBeforeDestroyingWD();
|
||||
ns.singularity.destroyW0r1dD43m0n(5);
|
||||
expectIntelligenceExp(1300);
|
||||
expect(Player.sourceFileLvl(5)).toStrictEqual(1);
|
||||
|
||||
setUpBeforeDestroyingWD();
|
||||
ns.singularity.destroyW0r1dD43m0n(5);
|
||||
expectIntelligenceExp(1600);
|
||||
expect(Player.sourceFileLvl(5)).toStrictEqual(2);
|
||||
|
||||
setUpBeforeDestroyingWD();
|
||||
ns.singularity.destroyW0r1dD43m0n(1);
|
||||
expectIntelligenceExp(1900);
|
||||
expect(Player.sourceFileLvl(5)).toStrictEqual(3);
|
||||
});
|
||||
test("Can gain intelligence exp with SF5", () => {
|
||||
Player.sourceFiles.set(5, 1);
|
||||
const ns = getNS();
|
||||
ns.singularity.b1tflum3(1);
|
||||
expectIntelligenceExp(0);
|
||||
|
||||
Player.gainIntelligenceExp(1000);
|
||||
expectIntelligenceExp(1000);
|
||||
|
||||
ns.singularity.b1tflum3(1);
|
||||
expectIntelligenceExp(1000);
|
||||
|
||||
setUpBeforeDestroyingWD();
|
||||
ns.singularity.destroyW0r1dD43m0n(1);
|
||||
expectIntelligenceExp(1300);
|
||||
});
|
||||
test("Can gain intelligence exp in BN5", () => {
|
||||
const ns = getNS();
|
||||
ns.singularity.b1tflum3(5);
|
||||
expectIntelligenceExp(0);
|
||||
|
||||
Player.gainIntelligenceExp(1000);
|
||||
expectIntelligenceExp(1000);
|
||||
});
|
||||
describe("Reset intelligence data", () => {
|
||||
test("Install augmentations", () => {
|
||||
manuallySetIntelligenceExp(50);
|
||||
expectIntelligenceExp(50);
|
||||
Player.queueAugmentation(AugmentationName.Targeting1);
|
||||
expect(installAugmentations()).toStrictEqual(true);
|
||||
expectInitialIntelligenceData();
|
||||
});
|
||||
test("Bitflume", () => {
|
||||
const ns = getNS();
|
||||
|
||||
// Bitflume from non-BN5 to non-BN5.
|
||||
expect(Player.bitNodeN).toStrictEqual(1);
|
||||
manuallySetIntelligenceExp(50);
|
||||
expectIntelligenceExp(50);
|
||||
ns.singularity.b1tflum3(1);
|
||||
// Reset intelligence data
|
||||
expectInitialIntelligenceData();
|
||||
|
||||
// We intentionally skip this scenario.
|
||||
// For more information, please check https://github.com/bitburner-official/bitburner-src/pull/2666
|
||||
// // Bitflume from non-BN5 to BN5.
|
||||
// expect(Player.bitNodeN).toStrictEqual(1);
|
||||
// manuallySetIntelligenceExp(50);
|
||||
// expectIntelligenceExp(50);
|
||||
// ns.singularity.b1tflum3(5);
|
||||
// // Reset intelligence data and skill = 1
|
||||
// expectIntelligenceExp(0);
|
||||
|
||||
// Bitflume from non-BN5 to BN5.
|
||||
ns.singularity.b1tflum3(5);
|
||||
// Check if skill is set to 1.
|
||||
expectIntelligenceExp(0);
|
||||
|
||||
// Bitflume from BN5 to BN5.
|
||||
expect(Player.bitNodeN).toStrictEqual(5);
|
||||
Player.gainIntelligenceExp(50);
|
||||
expectIntelligenceExp(50);
|
||||
// Bitflume to BN5 again.
|
||||
ns.singularity.b1tflum3(5);
|
||||
// Not lose exp when bitfluming from BN5 to BN5.
|
||||
expectIntelligenceExp(50);
|
||||
|
||||
// Bitflume from BN5 to non-BN5.
|
||||
expect(Player.bitNodeN).toStrictEqual(5);
|
||||
Player.gainIntelligenceExp(50);
|
||||
// 50 exp from the previous scenario + 50 exp from this scenario.
|
||||
expectIntelligenceExp(100);
|
||||
ns.singularity.b1tflum3(1);
|
||||
// Reset intelligence data
|
||||
expectInitialIntelligenceData();
|
||||
});
|
||||
test("Destroy WD", () => {
|
||||
const ns = getNS();
|
||||
|
||||
// Destroy WD of non-BN5 and jump to non-BN5.
|
||||
expect(Player.bitNodeN).toStrictEqual(1);
|
||||
manuallySetIntelligenceExp(50);
|
||||
expectIntelligenceExp(50);
|
||||
setUpBeforeDestroyingWD();
|
||||
ns.singularity.destroyW0r1dD43m0n(1);
|
||||
// Reset intelligence data
|
||||
expectInitialIntelligenceData();
|
||||
|
||||
// We intentionally skip this scenario.
|
||||
// For more information, please check https://github.com/bitburner-official/bitburner-src/pull/2666
|
||||
// // Destroy WD of non-BN5 and jump to BN5.
|
||||
// expect(Player.bitNodeN).toStrictEqual(1);
|
||||
// manuallySetIntelligenceExp(50);
|
||||
// expectIntelligenceExp(50);
|
||||
// setUpBeforeDestroyingWD();
|
||||
// ns.singularity.destroyW0r1dD43m0n(5);
|
||||
// // Reset intelligence data and skill = 1
|
||||
// expectIntelligenceExp(0);
|
||||
|
||||
// Destroy WD of BN5 and jump to BN5.
|
||||
ns.singularity.b1tflum3(5);
|
||||
// Check the initial state that we want to test: in BN5 and do not have SF5.
|
||||
expect(Player.bitNodeN).toStrictEqual(5);
|
||||
expect(Player.sourceFileLvl(5)).toStrictEqual(0);
|
||||
// Check if skill is set to 1.
|
||||
expectIntelligenceExp(0);
|
||||
Player.gainIntelligenceExp(50);
|
||||
expectIntelligenceExp(50);
|
||||
setUpBeforeDestroyingWD();
|
||||
ns.singularity.destroyW0r1dD43m0n(5);
|
||||
// 50 exp from Player.gainIntelligenceExp() + 300 exp reward of destroying WD.
|
||||
expectIntelligenceExp(350);
|
||||
|
||||
// Destroy WD of BN5 and jump to non-BN5.
|
||||
Player.gainIntelligenceExp(50);
|
||||
// 350 exp from the previous scenario + 50 exp from Player.gainIntelligenceExp().
|
||||
expectIntelligenceExp(400);
|
||||
setUpBeforeDestroyingWD();
|
||||
ns.singularity.destroyW0r1dD43m0n(1);
|
||||
expectIntelligenceExp(700);
|
||||
});
|
||||
});
|
||||
test("Cannot gain intelligence exp without SF5 or being in BN5", () => {
|
||||
const ns = getNS();
|
||||
Player.gainIntelligenceExp(1000);
|
||||
expectInitialIntelligenceData();
|
||||
|
||||
ns.singularity.b1tflum3(1);
|
||||
expectInitialIntelligenceData();
|
||||
|
||||
setUpBeforeDestroyingWD();
|
||||
ns.singularity.destroyW0r1dD43m0n(1);
|
||||
expectInitialIntelligenceData();
|
||||
});
|
||||
test("Cannot gain intelligence exp even with intelligence skill > 0", () => {
|
||||
manuallySetIntelligenceExp(50);
|
||||
expectIntelligenceExp(50);
|
||||
|
||||
Player.gainIntelligenceExp(1000);
|
||||
expectIntelligenceExp(50);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
import { getWeakenEffect } from "../../../src/Server/ServerHelpers";
|
||||
|
||||
describe("getWeakenEffect (formulas.hacking.weakenEffect)", () => {
|
||||
it("returns 0.05 per thread with single core", () => {
|
||||
expect(getWeakenEffect(1, 1)).toBe(0.05);
|
||||
expect(getWeakenEffect(100, 1)).toBe(5.0);
|
||||
});
|
||||
|
||||
it("applies core bonus correctly", () => {
|
||||
// Core bonus: 1 + (cores - 1) / 16
|
||||
// 8 cores: 1 + 7/16 = 1.4375
|
||||
expect(getWeakenEffect(1, 8)).toBeCloseTo(0.071875);
|
||||
expect(getWeakenEffect(100, 8)).toBeCloseTo(7.1875);
|
||||
});
|
||||
|
||||
it("returns 0 for 0 threads", () => {
|
||||
expect(getWeakenEffect(0, 1)).toBe(0);
|
||||
});
|
||||
|
||||
it("handles single core (no bonus)", () => {
|
||||
// Core bonus with 1 core: 1 + 0/16 = 1.0
|
||||
expect(getWeakenEffect(50, 1)).toBe(2.5);
|
||||
});
|
||||
|
||||
it("handles max cores (8)", () => {
|
||||
// 8 cores: 1 + 7/16 = 1.4375
|
||||
// 10 threads * 0.05 * 1.4375 = 0.71875
|
||||
expect(getWeakenEffect(10, 8)).toBeCloseTo(0.71875);
|
||||
});
|
||||
});
|
||||
@@ -1,7 +1,7 @@
|
||||
/* eslint-disable no-await-in-loop */
|
||||
|
||||
import { Player } from "../../../src/Player";
|
||||
import { getTabCompletionPossibilities } from "../../../src/Terminal/getTabCompletionPossibilities";
|
||||
import { getTabCompletionPossibilities, extractCurrentText } from "../../../src/Terminal/getTabCompletionPossibilities";
|
||||
import { Server } from "../../../src/Server/Server";
|
||||
import { AddToAllServers, prestigeAllServers } from "../../../src/Server/AllServers";
|
||||
import { LocationName } from "../../../src/Enums";
|
||||
@@ -187,6 +187,27 @@ describe("getTabCompletionPossibilities", function () {
|
||||
});
|
||||
});
|
||||
|
||||
describe("extractCurrentText", () => {
|
||||
it("returns last word for unquoted input", () => {
|
||||
expect(extractCurrentText("run myscript.js foo")).toBe("foo");
|
||||
});
|
||||
it("returns empty string for input ending with space", () => {
|
||||
expect(extractCurrentText("run myscript.js ")).toBe("");
|
||||
});
|
||||
it("returns text from opening quote for unclosed double quote", () => {
|
||||
expect(extractCurrentText('run myscript.js "nonunique se')).toBe('"nonunique se');
|
||||
});
|
||||
it("returns last word when all quotes are closed", () => {
|
||||
expect(extractCurrentText('run myscript.js "arg1" foo')).toBe("foo");
|
||||
});
|
||||
it("handles empty input", () => {
|
||||
expect(extractCurrentText("")).toBe("");
|
||||
});
|
||||
it("returns text from opening quote with one word inside", () => {
|
||||
expect(extractCurrentText('run "partial')).toBe('"partial');
|
||||
});
|
||||
});
|
||||
|
||||
function asDirectory(dir: string): Directory {
|
||||
if (!isAbsolutePath(dir) || !isDirectoryPath(dir)) throw new Error(`Directory ${dir} failed typechecking`);
|
||||
return dir;
|
||||
|
||||
@@ -75,6 +75,7 @@ exports[`Check Save File Continuity PlayerSave continuity 1`] = `
|
||||
"bladeburner": {
|
||||
"ctor": "Bladeburner",
|
||||
"data": {
|
||||
"_teamSize": 0,
|
||||
"action": null,
|
||||
"actionTimeCurrent": 0,
|
||||
"actionTimeOverflow": 0,
|
||||
@@ -84,6 +85,134 @@ exports[`Check Save File Continuity PlayerSave continuity 1`] = `
|
||||
"automateEnabled": false,
|
||||
"automateThreshHigh": 0,
|
||||
"automateThreshLow": 0,
|
||||
"blackOperations": {
|
||||
"Operation Annihilus": {
|
||||
"ctor": "BlackOperation",
|
||||
"data": {
|
||||
"teamCount": 0,
|
||||
},
|
||||
},
|
||||
"Operation Archangel": {
|
||||
"ctor": "BlackOperation",
|
||||
"data": {
|
||||
"teamCount": 0,
|
||||
},
|
||||
},
|
||||
"Operation Ares": {
|
||||
"ctor": "BlackOperation",
|
||||
"data": {
|
||||
"teamCount": 0,
|
||||
},
|
||||
},
|
||||
"Operation Centurion": {
|
||||
"ctor": "BlackOperation",
|
||||
"data": {
|
||||
"teamCount": 0,
|
||||
},
|
||||
},
|
||||
"Operation Daedalus": {
|
||||
"ctor": "BlackOperation",
|
||||
"data": {
|
||||
"teamCount": 0,
|
||||
},
|
||||
},
|
||||
"Operation Deckard": {
|
||||
"ctor": "BlackOperation",
|
||||
"data": {
|
||||
"teamCount": 0,
|
||||
},
|
||||
},
|
||||
"Operation Hyron": {
|
||||
"ctor": "BlackOperation",
|
||||
"data": {
|
||||
"teamCount": 0,
|
||||
},
|
||||
},
|
||||
"Operation Ion Storm": {
|
||||
"ctor": "BlackOperation",
|
||||
"data": {
|
||||
"teamCount": 0,
|
||||
},
|
||||
},
|
||||
"Operation Juggernaut": {
|
||||
"ctor": "BlackOperation",
|
||||
"data": {
|
||||
"teamCount": 0,
|
||||
},
|
||||
},
|
||||
"Operation K": {
|
||||
"ctor": "BlackOperation",
|
||||
"data": {
|
||||
"teamCount": 0,
|
||||
},
|
||||
},
|
||||
"Operation Morpheus": {
|
||||
"ctor": "BlackOperation",
|
||||
"data": {
|
||||
"teamCount": 0,
|
||||
},
|
||||
},
|
||||
"Operation Red Dragon": {
|
||||
"ctor": "BlackOperation",
|
||||
"data": {
|
||||
"teamCount": 0,
|
||||
},
|
||||
},
|
||||
"Operation Shoulder of Orion": {
|
||||
"ctor": "BlackOperation",
|
||||
"data": {
|
||||
"teamCount": 0,
|
||||
},
|
||||
},
|
||||
"Operation Titan": {
|
||||
"ctor": "BlackOperation",
|
||||
"data": {
|
||||
"teamCount": 0,
|
||||
},
|
||||
},
|
||||
"Operation Typhoon": {
|
||||
"ctor": "BlackOperation",
|
||||
"data": {
|
||||
"teamCount": 0,
|
||||
},
|
||||
},
|
||||
"Operation Tyrell": {
|
||||
"ctor": "BlackOperation",
|
||||
"data": {
|
||||
"teamCount": 0,
|
||||
},
|
||||
},
|
||||
"Operation Ultron": {
|
||||
"ctor": "BlackOperation",
|
||||
"data": {
|
||||
"teamCount": 0,
|
||||
},
|
||||
},
|
||||
"Operation Vindictus": {
|
||||
"ctor": "BlackOperation",
|
||||
"data": {
|
||||
"teamCount": 0,
|
||||
},
|
||||
},
|
||||
"Operation Wallace": {
|
||||
"ctor": "BlackOperation",
|
||||
"data": {
|
||||
"teamCount": 0,
|
||||
},
|
||||
},
|
||||
"Operation X": {
|
||||
"ctor": "BlackOperation",
|
||||
"data": {
|
||||
"teamCount": 0,
|
||||
},
|
||||
},
|
||||
"Operation Zero": {
|
||||
"ctor": "BlackOperation",
|
||||
"data": {
|
||||
"teamCount": 0,
|
||||
},
|
||||
},
|
||||
},
|
||||
"cities": {
|
||||
"Aevum": {
|
||||
"ctor": "City",
|
||||
@@ -281,7 +410,6 @@ exports[`Check Save File Continuity PlayerSave continuity 1`] = `
|
||||
"staminaBonus": 0,
|
||||
"storedCycles": 0,
|
||||
"teamLost": 0,
|
||||
"teamSize": 0,
|
||||
"totalSkillPoints": 666,
|
||||
},
|
||||
},
|
||||
|
||||
+1
-1
@@ -134,7 +134,7 @@ module.exports = (env, argv) => {
|
||||
module: {
|
||||
rules: [
|
||||
{
|
||||
test: /\.(js$|jsx|ts|tsx)$/,
|
||||
test: /\.(js|jsx|ts|tsx)$/,
|
||||
exclude: /node_modules/,
|
||||
resourceQuery: { not: /raw/ },
|
||||
use: {
|
||||
|
||||
Reference in New Issue
Block a user