diff --git a/src/Bladeburner/Actions/BlackOperation.ts b/src/Bladeburner/Actions/BlackOperation.ts index 7810fca5e..2780f3831 100644 --- a/src/Bladeburner/Actions/BlackOperation.ts +++ b/src/Bladeburner/Actions/BlackOperation.ts @@ -1,9 +1,10 @@ import type { Bladeburner } from "../Bladeburner"; -import type { Availability, ActionIdentifier } from "../Types"; +import type { ActionIdFor, Availability } from "../Types"; import { BladeburnerActionType, BladeburnerBlackOpName } from "@enums"; import { ActionClass, ActionParams } from "./Action"; import { operationSkillSuccessBonus, operationTeamSuccessBonus } from "./Operation"; +import { getEnumHelper } from "../../utils/EnumHelper"; interface BlackOpParams { name: BladeburnerBlackOpName; @@ -12,13 +13,22 @@ interface BlackOpParams { } export class BlackOperation extends ActionClass { - type: BladeburnerActionType.BlackOp = BladeburnerActionType.BlackOp; - name: BladeburnerBlackOpName; + readonly type: BladeburnerActionType.BlackOp = BladeburnerActionType.BlackOp; + readonly name: BladeburnerBlackOpName; n: number; reqdRank: number; teamCount = 0; - get id(): ActionIdentifier { - return { type: this.type, name: this.name }; + + get id() { + return BlackOperation.createId(this.name); + } + + static createId(name: BladeburnerBlackOpName): ActionIdFor { + return { type: BladeburnerActionType.BlackOp, name }; + } + + static IsAcceptedName(name: unknown): name is BladeburnerBlackOpName { + return getEnumHelper("BladeburnerBlackOpName").isMember(name); } constructor(params: ActionParams & BlackOpParams) { diff --git a/src/Bladeburner/Actions/Contract.ts b/src/Bladeburner/Actions/Contract.ts index 5949d5dea..34b0d444d 100644 --- a/src/Bladeburner/Actions/Contract.ts +++ b/src/Bladeburner/Actions/Contract.ts @@ -1,20 +1,30 @@ import type { Bladeburner } from "../Bladeburner"; -import type { ActionIdentifier } from "../Types"; +import type { ActionIdFor } from "../Types"; import { Generic_fromJSON, IReviverValue, constructorsForReviver } from "../../utils/JSONReviver"; import { BladeburnerActionType, BladeburnerContractName, BladeburnerMultName } from "../Enums"; import { LevelableActionClass, LevelableActionParams } from "./LevelableAction"; +import { getEnumHelper } from "../../utils/EnumHelper"; export class Contract extends LevelableActionClass { - type: BladeburnerActionType.Contract = BladeburnerActionType.Contract; - name: BladeburnerContractName = BladeburnerContractName.Tracking; - get id(): ActionIdentifier { - return { type: this.type, name: this.name }; + readonly type: BladeburnerActionType.Contract = BladeburnerActionType.Contract; + readonly name: BladeburnerContractName; + + get id() { + return Contract.createId(this.name); + } + + static IsAcceptedName(name: unknown): name is BladeburnerContractName { + return getEnumHelper("BladeburnerContractName").isMember(name); + } + + static createId(name: BladeburnerContractName): ActionIdFor { + return { type: BladeburnerActionType.Contract, name }; } constructor(params: (LevelableActionParams & { name: BladeburnerContractName }) | null = null) { super(params); - if (params) this.name = params.name; + this.name = params?.name ?? BladeburnerContractName.Tracking; } getActionTypeSkillSuccessBonus(inst: Bladeburner): number { diff --git a/src/Bladeburner/Actions/GeneralAction.ts b/src/Bladeburner/Actions/GeneralAction.ts index 10a8d686a..d1414c701 100644 --- a/src/Bladeburner/Actions/GeneralAction.ts +++ b/src/Bladeburner/Actions/GeneralAction.ts @@ -1,10 +1,11 @@ import type { Person } from "../../PersonObjects/Person"; import type { Bladeburner } from "../Bladeburner"; -import type { ActionIdentifier } from "../Types"; +import type { ActionIdFor } from "../Types"; import { BladeburnerActionType, BladeburnerGeneralActionName } from "@enums"; import { ActionClass, ActionParams } from "./Action"; import { clampNumber } from "../../utils/helpers/clampNumber"; +import { getEnumHelper } from "../../utils/EnumHelper"; type GeneralActionParams = ActionParams & { name: BladeburnerGeneralActionName; @@ -13,10 +14,19 @@ type GeneralActionParams = ActionParams & { }; export class GeneralAction extends ActionClass { - type: BladeburnerActionType.General = BladeburnerActionType.General; - name: BladeburnerGeneralActionName; - get id(): ActionIdentifier { - return { type: this.type, name: this.name }; + readonly type: BladeburnerActionType.General = BladeburnerActionType.General; + readonly name: BladeburnerGeneralActionName; + + get id() { + return GeneralAction.createId(this.name); + } + + static IsAcceptedName(name: unknown): name is BladeburnerGeneralActionName { + return getEnumHelper("BladeburnerGeneralActionName").isMember(name); + } + + static createId(name: BladeburnerGeneralActionName): ActionIdFor { + return { type: BladeburnerActionType.General, name }; } constructor(params: GeneralActionParams) { diff --git a/src/Bladeburner/Actions/Operation.ts b/src/Bladeburner/Actions/Operation.ts index 93dafce7e..b619d0643 100644 --- a/src/Bladeburner/Actions/Operation.ts +++ b/src/Bladeburner/Actions/Operation.ts @@ -1,7 +1,7 @@ import type { Person } from "../../PersonObjects/Person"; import type { BlackOperation } from "./BlackOperation"; import type { Bladeburner } from "../Bladeburner"; -import type { Availability, ActionIdentifier, SuccessChanceParams } from "../Types"; +import type { ActionIdFor, Availability, SuccessChanceParams } from "../Types"; import { BladeburnerActionType, BladeburnerMultName, BladeburnerOperationName } from "@enums"; import { BladeburnerConstants } from "../data/Constants"; @@ -9,6 +9,7 @@ import { ActionClass } from "./Action"; import { Generic_fromJSON, IReviverValue, constructorsForReviver } from "../../utils/JSONReviver"; import { LevelableActionClass, LevelableActionParams } from "./LevelableAction"; import { clampInteger } from "../../utils/helpers/clampNumber"; +import { getEnumHelper } from "../../utils/EnumHelper"; export interface OperationParams extends LevelableActionParams { name: BladeburnerOperationName; @@ -16,18 +17,26 @@ export interface OperationParams extends LevelableActionParams { } export class Operation extends LevelableActionClass { - type: BladeburnerActionType.Operation = BladeburnerActionType.Operation; - name = BladeburnerOperationName.Investigation; + readonly type: BladeburnerActionType.Operation = BladeburnerActionType.Operation; + readonly name: BladeburnerOperationName; teamCount = 0; - get id(): ActionIdentifier { - return { type: this.type, name: this.name }; + + get id() { + return Operation.createId(this.name); + } + + static IsAcceptedName(name: unknown): name is BladeburnerOperationName { + return getEnumHelper("BladeburnerOperationName").isMember(name); + } + + static createId(name: BladeburnerOperationName): ActionIdFor { + return { type: BladeburnerActionType.Operation, name }; } constructor(params: OperationParams | null = null) { super(params); - if (!params) return; - this.name = params.name; - if (params.getAvailability) this.getAvailability = params.getAvailability; + this.name = params?.name ?? BladeburnerOperationName.Investigation; + if (params && params.getAvailability) this.getAvailability = params.getAvailability; } // These functions are shared between operations and blackops, so they are defined outside of Operation @@ -45,6 +54,7 @@ export class Operation extends LevelableActionClass { return 1; } + getSuccessChance(inst: Bladeburner, person: Person, params: SuccessChanceParams) { if (this.name === BladeburnerOperationName.Raid && inst.getCurrentCity().comms <= 0) { return 0; diff --git a/src/Bladeburner/Bladeburner.ts b/src/Bladeburner/Bladeburner.ts index a7d36f538..fe3cb41e5 100644 --- a/src/Bladeburner/Bladeburner.ts +++ b/src/Bladeburner/Bladeburner.ts @@ -1,6 +1,6 @@ import type { PromisePair } from "../Types/Promises"; import type { BlackOperation, Contract, GeneralAction, Operation } from "./Actions"; -import type { ActionIdentifier, Action, Attempt } from "./Types"; +import type { Action, ActionIdFor, ActionIdentifier, Attempt } from "./Types"; import type { Person } from "../PersonObjects/Person"; import type { Skills as PersonSkills } from "../PersonObjects/Skills"; @@ -49,6 +49,7 @@ import { BlackOperations } from "./data/BlackOperations"; import { GeneralActions } from "./data/GeneralActions"; import { PlayerObject } from "../PersonObjects/Player/PlayerObject"; import { Sleeve } from "../PersonObjects/Sleeve/Sleeve"; +import { autoCompleteTypeShorthand } from "./utils/terminalShorthands"; export const BladeburnerPromise: PromisePair = { promise: null, resolve: null }; @@ -433,61 +434,53 @@ export class Bladeburner { highLow = true; } - let actionId: ActionIdentifier; - switch (type) { - case "stamina": - // For stamina, the "name" variable is actually the stamina threshold - if (isNaN(parseFloat(name))) { - this.postToConsole("Invalid value specified for stamina threshold (must be numeric): " + name); + if (type === "stamina") { + // For stamina, the "name" variable is actually the stamina threshold + if (isNaN(parseFloat(name))) { + this.postToConsole("Invalid value specified for stamina threshold (must be numeric): " + name); + } else { + if (highLow) { + this.automateThreshHigh = Number(name); } else { - if (highLow) { - this.automateThreshHigh = Number(name); - } else { - this.automateThreshLow = Number(name); - } - this.log("Automate (" + (highLow ? "HIGH" : "LOW") + ") stamina threshold set to " + name); + this.automateThreshLow = Number(name); } - return; - case "general": - case "gen": { - if (!getEnumHelper("BladeburnerGeneralActionName").isMember(name)) { + this.log("Automate (" + (highLow ? "HIGH" : "LOW") + ") stamina threshold set to " + name); + } + return; + } + + const actionId = autoCompleteTypeShorthand(type, name); + + if (actionId === null) { + switch (type) { + case "general": + case "gen": { this.postToConsole("Invalid General Action name specified: " + name); return; } - actionId = { type: BladeburnerActionType.General, name }; - break; - } - case "contract": - case "contracts": { - if (!getEnumHelper("BladeburnerContractName").isMember(name)) { + case "contract": + case "contracts": { this.postToConsole("Invalid Contract name specified: " + name); return; } - actionId = { type: BladeburnerActionType.Contract, name }; - break; - } - case "ops": - case "op": - case "operations": - case "operation": - if (!getEnumHelper("BladeburnerOperationName").isMember(name)) { + case "ops": + case "op": + case "operations": + case "operation": this.postToConsole("Invalid Operation name specified: " + name); return; - } - actionId = { type: BladeburnerActionType.Operation, name }; - break; - default: - this.postToConsole("Invalid use of automate command."); - return; + default: + this.postToConsole("Invalid use of automate command."); + return; + } } + if (highLow) { this.automateActionHigh = actionId; } else { this.automateActionLow = actionId; } this.log("Automate (" + (highLow ? "HIGH" : "LOW") + ") action set to " + name); - - return; } } @@ -1406,10 +1399,10 @@ export class Bladeburner { } /** Return the action based on an ActionIdentifier, discriminating types when possible */ - getActionObject(actionId: ActionIdentifier & { type: BladeburnerActionType.BlackOp }): BlackOperation; - getActionObject(actionId: ActionIdentifier & { type: BladeburnerActionType.Operation }): Operation; - getActionObject(actionId: ActionIdentifier & { type: BladeburnerActionType.Contract }): Contract; - getActionObject(actionId: ActionIdentifier & { type: BladeburnerActionType.General }): GeneralAction; + getActionObject(actionId: ActionIdFor): BlackOperation; + getActionObject(actionId: ActionIdFor): Operation; + getActionObject(actionId: ActionIdFor): Contract; + getActionObject(actionId: ActionIdFor): GeneralAction; getActionObject(actionId: ActionIdentifier): Action; getActionObject(actionId: ActionIdentifier): Action { switch (actionId.type) { @@ -1427,36 +1420,8 @@ export class Bladeburner { /** Fuzzy matching for action identifiers. Should be removed in 3.0 */ getActionFromTypeAndName(type: string, name: string): Action | null { if (!type || !name) return null; - const convertedType = type.toLowerCase().trim(); - switch (convertedType) { - case "contract": - case "contracts": - case "contr": - if (!getEnumHelper("BladeburnerContractName").isMember(name)) return null; - return this.contracts[name]; - case "operation": - case "operations": - case "op": - case "ops": - if (!getEnumHelper("BladeburnerOperationName").isMember(name)) return null; - return this.operations[name]; - case "blackoperation": - case "black operation": - case "black operations": - case "black op": - case "black ops": - case "blackop": - case "blackops": - if (!getEnumHelper("BladeburnerBlackOpName").isMember(name)) return null; - return BlackOperations[name]; - case "general": - case "general action": - case "gen": { - if (!getEnumHelper("BladeburnerGeneralActionName").isMember(name)) return null; - return GeneralActions[name]; - } - } - return null; + const id = autoCompleteTypeShorthand(type, name); + return id ? this.getActionObject(id) : null; } static keysToSave = getKeyList(Bladeburner, { removedKeys: ["skillMultipliers"] }); diff --git a/src/Bladeburner/Types.ts b/src/Bladeburner/Types.ts index 02a1204f8..00517f0d2 100644 --- a/src/Bladeburner/Types.ts +++ b/src/Bladeburner/Types.ts @@ -1,11 +1,4 @@ import type { BlackOperation, Contract, GeneralAction, Operation } from "./Actions"; -import type { - BladeburnerActionType, - BladeburnerBlackOpName, - BladeburnerContractName, - BladeburnerOperationName, - BladeburnerGeneralActionName, -} from "@enums"; export interface SuccessChanceParams { /** Whether the success chance should be based on estimated statistics */ @@ -21,11 +14,12 @@ type AttemptFailure = { success?: undefined; message: string }; export type Attempt = AttemptSuccess | AttemptFailure; export type Action = Contract | Operation | BlackOperation | GeneralAction; +export type ActionIdFor = Pick; export type ActionIdentifier = - | { type: BladeburnerActionType.BlackOp; name: BladeburnerBlackOpName } - | { type: BladeburnerActionType.Contract; name: BladeburnerContractName } - | { type: BladeburnerActionType.Operation; name: BladeburnerOperationName } - | { type: BladeburnerActionType.General; name: BladeburnerGeneralActionName }; + | ActionIdFor + | ActionIdFor + | ActionIdFor + | ActionIdFor; export type LevelableAction = Contract | Operation; diff --git a/src/Bladeburner/utils/terminalShorthands.ts b/src/Bladeburner/utils/terminalShorthands.ts new file mode 100644 index 000000000..f0a27314f --- /dev/null +++ b/src/Bladeburner/utils/terminalShorthands.ts @@ -0,0 +1,39 @@ +import { ActionIdentifier } from "../Types"; +import { BladeburnerActionType } from "@enums"; +import { BlackOperation, Contract, GeneralAction, Operation } from "../Actions"; + +const resolveActionIdentifierFromName = (name: unknown): ActionIdentifier | null => { + if (Contract.IsAcceptedName(name)) return Contract.createId(name); + if (BlackOperation.IsAcceptedName(name)) return BlackOperation.createId(name); + if (GeneralAction.IsAcceptedName(name)) return GeneralAction.createId(name); + if (Operation.IsAcceptedName(name)) return Operation.createId(name); + + return null; +}; + +/** Resolve identifier by auto completing from a fuzzy type match, e.g. "blackops" */ +export function autoCompleteTypeShorthand(typeShorthand: string, name: string): ActionIdentifier | null { + let id = resolveActionIdentifierFromName(name); + + if (id && !TerminalShorthands[id.type].includes(typeShorthand.toLowerCase().trim())) { + id = null; + } + + return id; +} + +/** These shorthands match those documented in the BB Terminal Help */ +export const TerminalShorthands = { + [BladeburnerActionType.Contract]: ["contract", "contracts", "contr"], + [BladeburnerActionType.Operation]: ["operation", "operations", "op", "ops"], + [BladeburnerActionType.BlackOp]: [ + "blackoperation", + "black operation", + "black operations", + "black op", + "black ops", + "blackop", + "blackops", + ], + [BladeburnerActionType.General]: ["general", "general action", "gen"], +} as const; diff --git a/src/PersonObjects/Sleeve/Work/SleeveBladeburnerWork.ts b/src/PersonObjects/Sleeve/Work/SleeveBladeburnerWork.ts index 3bfaa4992..df86ba240 100644 --- a/src/PersonObjects/Sleeve/Work/SleeveBladeburnerWork.ts +++ b/src/PersonObjects/Sleeve/Work/SleeveBladeburnerWork.ts @@ -70,6 +70,7 @@ export class SleeveBladeburnerWork extends SleeveWorkClass { this.finish(); } } + get nextCompletion(): Promise { if (!this.nextCompletionPair.promise) this.nextCompletionPair.promise = new Promise((r) => (this.nextCompletionPair.resolve = r)); diff --git a/src/utils/JSONReviver.ts b/src/utils/JSONReviver.ts index 6c9b66de3..f9849d4da 100644 --- a/src/utils/JSONReviver.ts +++ b/src/utils/JSONReviver.ts @@ -12,6 +12,7 @@ export interface IReviverValue { ctor: string; data: T; } + function isReviverValue(value: unknown): value is IReviverValue { return ( typeof value === "object" && value !== null && "ctor" in value && typeof value.ctor === "string" && "data" in value diff --git a/test/jest/Netscript/Bladeburner/TerminalMatching.test.ts b/test/jest/Netscript/Bladeburner/TerminalMatching.test.ts new file mode 100644 index 000000000..830df43fa --- /dev/null +++ b/test/jest/Netscript/Bladeburner/TerminalMatching.test.ts @@ -0,0 +1,38 @@ +import { autoCompleteTypeShorthand, TerminalShorthands } from "../../../../src/Bladeburner/utils/terminalShorthands"; +import { + BladeburnerActionType, + BladeburnerBlackOpName, + BladeburnerContractName, + BladeburnerGeneralActionName, + BladeburnerOperationName, +} from "@enums"; + +const ShorthandCases = (type: keyof typeof TerminalShorthands) => TerminalShorthands[type].map(Array); + +describe("Bladeburner Actions", () => { + const EXAMPLES = [ + [BladeburnerActionType.General, BladeburnerGeneralActionName.Diplomacy], + [BladeburnerActionType.BlackOp, BladeburnerBlackOpName.OperationTyphoon], + [BladeburnerActionType.Contract, BladeburnerContractName.BountyHunter], + [BladeburnerActionType.Operation, BladeburnerOperationName.Assassination], + ] as const; + + describe("May be described with shorthands", () => { + describe.each(EXAMPLES)("Type: %s", (type, name) => { + it.each(ShorthandCases(type))("%s", (shorthand) => { + const action = autoCompleteTypeShorthand(shorthand, name); + expect(action).toMatchObject({ type, name }); + }); + }); + }); + + it("Does not match for existing action where type differs", () => { + const action = autoCompleteTypeShorthand(BladeburnerActionType.Contract, BladeburnerOperationName.Assassination); + expect(action).toBeNull(); + }); + + it("Does not match for undocumented shorthands", () => { + const action = autoCompleteTypeShorthand("blackoperations", BladeburnerOperationName.Assassination); + expect(action).toBeNull(); + }); +});