diff --git a/src/Bladeburner/Bladeburner.ts b/src/Bladeburner/Bladeburner.ts index 27a706194..4f2db9f24 100644 --- a/src/Bladeburner/Bladeburner.ts +++ b/src/Bladeburner/Bladeburner.ts @@ -56,6 +56,7 @@ import { throwIfReachable } from "../utils/helpers/throwIfReachable"; import { loadActionIdentifier } from "./utils/loadActionIdentifier"; import { pluralize } from "../utils/I18nUtils"; import { calculateActionRankGain, calculateActionReputationGain } from "./Formulas"; +import { processWorkStats } from "../Work/Formulas"; export const BladeburnerPromise: PromisePair = { promise: null, resolve: null }; @@ -1204,7 +1205,8 @@ export class Bladeburner implements OperationTeam { const __a: never = action; } } - return retValue; + + return processWorkStats(person, retValue); } infiltrateSynthoidCommunities(): void { diff --git a/src/NetscriptFunctions/Sleeve.ts b/src/NetscriptFunctions/Sleeve.ts index 1649bffd0..fd14f7e7c 100644 --- a/src/NetscriptFunctions/Sleeve.ts +++ b/src/NetscriptFunctions/Sleeve.ts @@ -245,16 +245,16 @@ export function NetscriptSleeve(): InternalAPI { checkSleeveAPIAccess(ctx); checkSleeveNumber(ctx, sleeveNumber); - if (Player.sleeves[sleeveNumber].shock > 0) { - throw helpers.errorMessage(ctx, `Sleeve shock too high: Sleeve ${sleeveNumber}`); - } - const aug = Augmentations[augName]; if (!aug) { throw helpers.errorMessage(ctx, `Invalid aug: ${augName}`); } - return Player.sleeves[sleeveNumber].tryBuyAugmentation(aug); + const result = Player.sleeves[sleeveNumber].purchaseAugmentation(aug); + if (!result.success) { + helpers.log(ctx, () => result.message); + } + return result.success; }, getSleeveAugmentationPrice: (ctx) => (_augName) => { checkSleeveAPIAccess(ctx); diff --git a/src/PersonObjects/Sleeve/Sleeve.ts b/src/PersonObjects/Sleeve/Sleeve.ts index 447c4b089..b81279ec6 100644 --- a/src/PersonObjects/Sleeve/Sleeve.ts +++ b/src/PersonObjects/Sleeve/Sleeve.ts @@ -7,7 +7,7 @@ * Sleeves are unlocked in BitNode-10. */ -import type { SleevePerson } from "@nsdefs"; +import type { Result, SleevePerson } from "@nsdefs"; import type { Augmentation } from "../../Augmentation/Augmentation"; import type { SleeveWork } from "./Work/Work"; @@ -50,6 +50,7 @@ import { getFactionAugmentationsFiltered } from "../../Faction/FactionHelpers"; import { Augmentations } from "../../Augmentation/Augmentations"; import { getAugCost } from "../../Augmentation/AugmentationHelpers"; import type { MoneySource } from "../../utils/MoneySourceTracker"; +import { formatMoney, formatSleeveShock } from "../../ui/formatNumber"; export class Sleeve extends Person implements SleevePerson { currentWork: SleeveWork | null = null; @@ -338,20 +339,62 @@ export class Sleeve extends Person implements SleevePerson { return true; } - tryBuyAugmentation(aug: Augmentation): boolean { + /** + * This function is only used in UI code for checking whether the "Manage Augmentations" button can be enabled. If you + * want to check if the player can purchase a specific augmentation, you need to call canPurchaseAugmentation. + */ + checkPreconditionsOfPurchasingAugmentations(): Result { + if (Player.bitNodeOptions.disableSleeveExpAndAugmentation) { + return { + success: false, + message: `The "Disable Sleeves' experience and augmentation" option was enabled. You cannot purchase augmentations for your sleeves.`, + }; + } + + if (this.shock > 0) { + return { + success: false, + message: `You must reduce the sleeve shock to 0. The current shock is ${formatSleeveShock(this.shock)}.`, + }; + } + + return { success: true }; + } + + canPurchaseAugmentation(aug: Augmentation): Result { + const checkingPreconditions = this.checkPreconditionsOfPurchasingAugmentations(); + if (!checkingPreconditions.success) { + return checkingPreconditions; + } + if (!Player.canAfford(aug.baseCost)) { - return false; + return { + success: false, + message: `You must have at least ${formatMoney(aug.baseCost)}.`, + }; } // Verify that this sleeve does not already have that augmentation. - if (this.hasAugmentation(aug.name)) return false; + if (this.hasAugmentation(aug.name)) { + return { success: false, message: `This sleeve already has "${aug.name}" augmentation.` }; + } // Verify that the augmentation is available for purchase. - if (!this.findPurchasableAugs().includes(aug)) return false; + if (!this.findPurchasableAugs().includes(aug)) { + return { success: false, message: `"${aug.name}" is not in the list of purchasable augmentations.` }; + } + return { success: true }; + } + + purchaseAugmentation(aug: Augmentation): Result { + const validationResult = this.canPurchaseAugmentation(aug); + if (!validationResult.success) { + return validationResult; + } Player.loseMoney(aug.baseCost, "sleeves"); this.installAugmentation(aug); - return true; + return { success: true }; } upgradeMemory(n: number): void { diff --git a/src/PersonObjects/Sleeve/ui/SleeveAugmentationsModal.tsx b/src/PersonObjects/Sleeve/ui/SleeveAugmentationsModal.tsx index e5ac735ed..2850a27da 100644 --- a/src/PersonObjects/Sleeve/ui/SleeveAugmentationsModal.tsx +++ b/src/PersonObjects/Sleeve/ui/SleeveAugmentationsModal.tsx @@ -1,10 +1,10 @@ import { Container, Typography, Paper } from "@mui/material"; import React from "react"; import { PurchasableAugmentations } from "../../../Augmentation/ui/PurchasableAugmentations"; -import { Player } from "@player"; import { Modal } from "../../../ui/React/Modal"; import { Sleeve } from "../Sleeve"; import { useRerender } from "../../../ui/React/hooks"; +import { dialogBoxCreate } from "../../../ui/React/DialogBox"; interface IProps { open: boolean; @@ -43,10 +43,14 @@ export function SleeveAugmentationsModal(props: IProps): React.ReactElement { augNames={availableAugs.map((aug) => aug.name)} ownedAugNames={ownedAugNames} canPurchase={(aug) => { - return Player.money >= aug.baseCost; + return props.sleeve.canPurchaseAugmentation(aug).success; }} purchaseAugmentation={(aug) => { - props.sleeve.tryBuyAugmentation(aug); + const result = props.sleeve.purchaseAugmentation(aug); + if (!result.success) { + dialogBoxCreate(result.message); + return; + } rerender(); }} rerender={rerender} diff --git a/src/PersonObjects/Sleeve/ui/SleeveElem.tsx b/src/PersonObjects/Sleeve/ui/SleeveElem.tsx index bf63e87cb..1fab9d1af 100644 --- a/src/PersonObjects/Sleeve/ui/SleeveElem.tsx +++ b/src/PersonObjects/Sleeve/ui/SleeveElem.tsx @@ -212,6 +212,7 @@ export function SleeveElem(props: SleeveElemProps): React.ReactElement { } } const desc = getWorkDescription(props.sleeve, progress); + const checkingPreconditionsResult = props.sleeve.checkPreconditionsOfPurchasingAugmentations(); return ( <> @@ -231,12 +232,14 @@ export function SleeveElem(props: SleeveElemProps): React.ReactElement { 0 ? Unlocked when sleeve has fully recovered : ""} + title={ + !checkingPreconditionsResult.success && {checkingPreconditionsResult.message} + } > diff --git a/src/Work/Formulas.ts b/src/Work/Formulas.ts index ea25d19e0..5ecf79de0 100644 --- a/src/Work/Formulas.ts +++ b/src/Work/Formulas.ts @@ -21,7 +21,7 @@ import { CompanyPosition } from "../Company/CompanyPosition"; import { isMember } from "../utils/EnumHelper"; import { getMultiplierFromCharisma } from "../DarkNet/effects/effects"; -function processWorkStats(person: IPerson, workStats: WorkStats): WorkStats { +export function processWorkStats(person: IPerson, workStats: WorkStats): WorkStats { // "person" can be a normal object that the player passes to NS APIs, so we cannot use `person instanceof Sleeve`. if (Player.bitNodeOptions.disableSleeveExpAndAugmentation && "shock" in person) { workStats.hackExp = 0; diff --git a/test/jest/Netscript/Sleeve.test.ts b/test/jest/Netscript/Sleeve.test.ts index 819e7364f..2e2af3e21 100644 --- a/test/jest/Netscript/Sleeve.test.ts +++ b/test/jest/Netscript/Sleeve.test.ts @@ -1,4 +1,4 @@ -import { FactionName } from "@enums"; +import { AugmentationName, FactionName } from "@enums"; import { Player } from "@player"; import { joinFaction } from "../../../src/Faction/FactionHelpers"; import { Factions } from "../../../src/Faction/Factions"; @@ -7,7 +7,7 @@ import { MaxSleevesFromCovenant, recalculateNumberOfOwnedSleeves, } from "../../../src/PersonObjects/Sleeve/SleeveCovenantPurchases"; -import { getNS, initGameEnvironment, setupBasicTestingEnvironment } from "../Utilities"; +import { getNS, getWorkerScriptAndNS, initGameEnvironment, setupBasicTestingEnvironment } from "../Utilities"; import { prestigeSourceFile } from "../../../src/Prestige"; beforeAll(() => { @@ -100,6 +100,15 @@ describe("Success", () => { expect(sleeve.memory).toStrictEqual(100); expect(Player.money).toStrictEqual(0); }); + test("purchaseSleeveAug", () => { + const ns = getNS(); + const augName = AugmentationName.BitWire; + Player.sleeves[0].shock = 0; + // CyberSec offers BitWire. + joinFaction(Factions.CyberSec); + Factions.CyberSec.playerReputation = 1e10; + expect(ns.sleeve.purchaseSleeveAug(0, augName)).toStrictEqual(true); + }); }); describe("Failure", () => { @@ -169,4 +178,39 @@ describe("Failure", () => { expect(result.success).toStrictEqual(false); expect(result.message).toContain("You must have at least"); }); + + test("purchaseSleeveAug", () => { + // Set BN10 and recalculate to get the first sleeve. + Player.bitNodeN = 10; + recalculateNumberOfOwnedSleeves(); + const { ws, ns } = getWorkerScriptAndNS(); + const augName = AugmentationName.BitWire; + + // disableSleeveExpAndAugmentation = true + Player.bitNodeOptions.disableSleeveExpAndAugmentation = true; + expect(ns.sleeve.purchaseSleeveAug(0, augName)).toStrictEqual(false); + expect(ws.scriptRef.logs[0]).toMatch(`The "Disable Sleeves' experience and augmentation" option was enabled`); + + // shock > 0 + Player.bitNodeOptions.disableSleeveExpAndAugmentation = false; + expect(ns.sleeve.purchaseSleeveAug(0, augName)).toStrictEqual(false); + expect(ws.scriptRef.logs[1]).toMatch("You must reduce the sleeve shock to 0"); + + // Not enough money + Player.sleeves[0].shock = 0; + Player.money = 0; + expect(ns.sleeve.purchaseSleeveAug(0, augName)).toStrictEqual(false); + expect(ws.scriptRef.logs[2]).toMatch("You must have at least"); + + // Already have the augmentation + Player.money = 1e15; + Player.sleeves[0].augmentations.push({ name: augName, level: 1 }); + expect(ns.sleeve.purchaseSleeveAug(0, augName)).toStrictEqual(false); + expect(ws.scriptRef.logs[3]).toMatch(`This sleeve already has "${augName}" augmentation`); + + // Non-purchasable augmentation + Player.sleeves[0].augmentations = []; + expect(ns.sleeve.purchaseSleeveAug(0, augName)).toStrictEqual(false); + expect(ws.scriptRef.logs[4]).toMatch(`"${augName}" is not in the list of purchasable augmentations`); + }); });