BLADEBURNER: Store BlackOp team count in save data (#2675)

This commit is contained in:
catloversg
2026-04-20 02:20:08 +07:00
committed by GitHub
parent a7409a01cc
commit 2aa5092d85
12 changed files with 963 additions and 783 deletions
+25 -4
View File
@@ -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;
+22 -9
View File
@@ -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";
@@ -92,7 +92,7 @@ export class Bladeburner implements OperationTeam {
}
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(BlackOperations)]) {
for (const action of [...Object.values(this.operations), ...Object.values(this.blackOperations)]) {
action.teamCount = Math.min(action.teamCount, this._teamSize);
}
}
@@ -120,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,
@@ -143,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
@@ -1431,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];
}
@@ -1450,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];
}
}
@@ -1461,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 {
@@ -1473,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);
/**
@@ -1496,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.
File diff suppressed because it is too large Load Diff
+3 -1
View File
@@ -113,7 +113,9 @@ export function createContracts(): Record<BladeburnerContractName, Contract> {
export function loadContractsData(data: unknown, contracts: Record<BladeburnerContractName, Contract>) {
// 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];
+3 -1
View File
@@ -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];
+4 -4
View File
@@ -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();
+7 -6
View File
@@ -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);
+2 -2
View File
@@ -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()) {