diff --git a/src/Bladeburner/Bladeburner.tsx b/src/Bladeburner/Bladeburner.tsx index 4ef0074bf..1dca43922 100644 --- a/src/Bladeburner/Bladeburner.tsx +++ b/src/Bladeburner/Bladeburner.tsx @@ -1264,7 +1264,7 @@ export class Bladeburner implements IBladeburner { } } - completeAction(player: IPlayer, person: IPerson, actionIdent: IActionIdentifier): ITaskTracker { + completeAction(player: IPlayer, person: IPerson, actionIdent: IActionIdentifier, isPlayer = true): ITaskTracker { let retValue = createTaskTracker(); switch (actionIdent.type) { case ActionTypes["Contract"]: @@ -1281,10 +1281,12 @@ export class Bladeburner implements IBladeburner { difficulty / BladeburnerConstants.DiffMultLinearFactor; const rewardMultiplier = Math.pow(action.rewardFac, action.level - 1); - // Stamina loss is based on difficulty - this.stamina -= BladeburnerConstants.BaseStaminaLoss * difficultyMultiplier; - if (this.stamina < 0) { - this.stamina = 0; + if (isPlayer) { + // Stamina loss is based on difficulty + this.stamina -= BladeburnerConstants.BaseStaminaLoss * difficultyMultiplier; + if (this.stamina < 0) { + this.stamina = 0; + } } // Process Contract/Operation success/failure diff --git a/src/Bladeburner/IBladeburner.ts b/src/Bladeburner/IBladeburner.ts index 65ecff71c..71849aac6 100644 --- a/src/Bladeburner/IBladeburner.ts +++ b/src/Bladeburner/IBladeburner.ts @@ -100,7 +100,7 @@ export interface IBladeburner { completeOperation(success: boolean, player: IPlayer): void; getActionObject(actionId: IActionIdentifier): IAction | null; completeContract(success: boolean, actionIdent: IActionIdentifier): void; - completeAction(player: IPlayer, person: IPerson, actionIdent: IActionIdentifier): ITaskTracker; + completeAction(player: IPlayer, person: IPerson, actionIdent: IActionIdentifier, isPlayer?: boolean): ITaskTracker; infiltrateSynthoidCommunities(): void; changeRank(player: IPlayer, change: number): void; processAction(router: IRouter, player: IPlayer, seconds: number): void; diff --git a/src/Netscript/RamCostGenerator.ts b/src/Netscript/RamCostGenerator.ts index 9d9eef719..6cba2d99d 100644 --- a/src/Netscript/RamCostGenerator.ts +++ b/src/Netscript/RamCostGenerator.ts @@ -282,6 +282,7 @@ const sleeve: IMap = { getSleeveAugmentations: RamCostConstants.ScriptSleeveBaseRamCost, getSleevePurchasableAugs: RamCostConstants.ScriptSleeveBaseRamCost, purchaseSleeveAug: RamCostConstants.ScriptSleeveBaseRamCost, + setToBladeburnerAction: RamCostConstants.ScriptSleeveBaseRamCost, }; // Stanek API diff --git a/src/NetscriptFunctions/Sleeve.ts b/src/NetscriptFunctions/Sleeve.ts index ed58bbfdc..fa5575b0a 100644 --- a/src/NetscriptFunctions/Sleeve.ts +++ b/src/NetscriptFunctions/Sleeve.ts @@ -310,5 +310,36 @@ export function NetscriptSleeve(player: IPlayer, workerScript: WorkerScript, hel return player.sleeves[sleeveNumber].tryBuyAugmentation(player, aug); }, + setToBladeburnerAction: function (_sleeveNumber: unknown, _action: unknown, _contract?: unknown): boolean { + updateRam("setToBladeburnerAction"); + const sleeveNumber = helper.number("setToBladeburnerAction", "sleeveNumber", _sleeveNumber); + const action = helper.string("setToBladeburnerAction", "action", _action); + let contract: string; + if (typeof _contract === "undefined") { + contract = "------"; + } else { + contract = helper.string("setToBladeburnerAction", "contract", _contract); + } + checkSleeveAPIAccess("setToBladeburnerAction"); + checkSleeveNumber("setToBladeburnerAction", sleeveNumber); + + // Cannot Take on Contracts if another sleeve is performing that action + if (action === "Take on contracts") { + for (let i = 0; i < player.sleeves.length; ++i) { + if (i === sleeveNumber) { + continue; + } + const other = player.sleeves[i]; + if (other.currentTask === SleeveTaskType.Bladeburner && other.bbAction === action) { + throw helper.makeRuntimeErrorMsg( + "sleeve.setToBladeburnerAction", + `Sleeve ${sleeveNumber} cannot take of contracts because Sleeve ${i} is already performing that action.`, + ); + } + } + } + + return player.sleeves[sleeveNumber].bladeburner(player, action, contract); + }, }; } diff --git a/src/PersonObjects/Sleeve/Sleeve.ts b/src/PersonObjects/Sleeve/Sleeve.ts index e3088fe0f..52285a8af 100644 --- a/src/PersonObjects/Sleeve/Sleeve.ts +++ b/src/PersonObjects/Sleeve/Sleeve.ts @@ -35,6 +35,8 @@ import { LocationName } from "../../Locations/data/LocationNames"; import { Generic_fromJSON, Generic_toJSON, Reviver } from "../../utils/JSONReviver"; import { BladeburnerConstants } from "../../Bladeburner/data/Constants"; +import { numeralWrapper } from "../../ui/numeralFormat"; +import { capitalizeFirstLetter, capitalizeEachWord } from "../../utils/StringHelperFunctions"; export class Sleeve extends Person { /** @@ -221,6 +223,10 @@ export class Sleeve extends Person { return retValue; } } else if (this.currentTask === SleeveTaskType.Bladeburner) { + if (this.currentTaskMaxTime === 0) { + this.currentTaskTime = 0; + return retValue; + } // For bladeburner, all experience and money is gained at the end const bb = p.bladeburner; if (bb === null) { @@ -236,10 +242,9 @@ export class Sleeve extends Person { this.currentTaskTime = 0; return retValue; } - let type: string; let name: string; - if (this.bbAction === "Take on Contracts") { + if (this.bbAction === "Take on contracts") { type = "Contracts"; name = this.bbContract; } else { @@ -257,7 +262,7 @@ export class Sleeve extends Person { const action = bb.getActionObject(actionIdent); if ((action?.count ?? 0) > 0) { - const bbRetValue = bb.completeAction(p, this, actionIdent); + const bbRetValue = bb.completeAction(p, this, actionIdent, false); if (bbRetValue) { retValue = this.gainExperience(p, bbRetValue); this.gainMoney(p, bbRetValue); @@ -647,7 +652,8 @@ export class Sleeve extends Person { if (this.currentTask == SleeveTaskType.Class) { const retVal = createTaskTracker(); retVal.int = CONSTANTS.IntelligenceClassBaseExpGain * Math.round(this.currentTaskTime / 1000); - this.gainExperience(p, retVal); //Wont be shared with other sleeves + const r = this.gainExperience(p, retVal); + p.sleeves.filter((s) => s != this).forEach((s) => s.gainExperience(p, r, 1, true)); } this.earningsForTask = createTaskTracker(); this.gainRatesForTask = createTaskTracker(); @@ -660,7 +666,7 @@ export class Sleeve extends Person { this.gymStatType = ""; this.className = ""; this.bbAction = ""; - this.bbContract = ""; + this.bbContract = "------"; } shockRecovery(p: IPlayer): boolean { @@ -1091,8 +1097,10 @@ export class Sleeve extends Person { this.currentTaskLocation = ""; let time = 0; + + this.bbContract = "------"; switch (action) { - case "Field Analysis": + case "Field analysis": time = this.getBladeburnerActionTime(p, "General", action); this.gainRatesForTask.hack = 20 * this.hacking_exp_mult; this.gainRatesForTask.cha = 20 * this.charisma_exp_mult; @@ -1101,7 +1109,9 @@ export class Sleeve extends Person { time = this.getBladeburnerActionTime(p, "General", action); this.gainRatesForTask.cha = 2 * BladeburnerConstants.BaseStatGain * (p.bladeburner?.getRecruitmentTime(this) ?? 0) * 1000; - this.currentTaskLocation = (p.bladeburner?.getRecruitmentSuccessChance(this) ?? 0).toString() + "%"; + this.currentTaskLocation = `(Success Rate: ${numeralWrapper.formatPercentage( + this.recrutmentSuccessChance(p), + )})`; break; case "Diplomacy": time = this.getBladeburnerActionTime(p, "General", action); @@ -1114,20 +1124,24 @@ export class Sleeve extends Person { p.bladeburner?.sleeveSupport(true); time = 0; break; - case "Take on Contracts": + case "Take on contracts": time = this.getBladeburnerActionTime(p, "Contracts", contract); this.contractGainRates(p, "Contracts", contract); this.currentTaskLocation = this.contractSuccessChance(p, "Contracts", contract); + this.bbContract = capitalizeEachWord(contract.toLowerCase()); break; } - this.bbAction = action; - this.bbContract = contract; + this.bbAction = capitalizeFirstLetter(action.toLowerCase()); this.currentTaskMaxTime = time; this.currentTask = SleeveTaskType.Bladeburner; return true; } + recrutmentSuccessChance(p: IPlayer): number { + return Math.max(0, Math.min(1, p.bladeburner?.getRecruitmentSuccessChance(this) ?? 0)); + } + contractSuccessChance(p: IPlayer, type: string, name: string): string { const bb = p.bladeburner; if (bb === null) { @@ -1143,7 +1157,7 @@ export class Sleeve extends Person { if (chances[0] >= 1) { return "100%"; } else { - return `${chances[0] * 100}% - ${chances[1] * 100}%`; + return `${numeralWrapper.formatPercentage(chances[0])} - ${numeralWrapper.formatPercentage(chances[1])}`; } } diff --git a/src/PersonObjects/Sleeve/ui/SleeveElem.tsx b/src/PersonObjects/Sleeve/ui/SleeveElem.tsx index 024d14830..21fb8ed92 100644 --- a/src/PersonObjects/Sleeve/ui/SleeveElem.tsx +++ b/src/PersonObjects/Sleeve/ui/SleeveElem.tsx @@ -122,8 +122,7 @@ export function SleeveElem(props: IProps): React.ReactElement { } desc = ( <> - This sleeve is currently attempting to {props.sleeve.bbAction} - {message} + This sleeve is currently attempting to {props.sleeve.bbAction}. {message} ); break; diff --git a/src/PersonObjects/Sleeve/ui/TaskSelector.tsx b/src/PersonObjects/Sleeve/ui/TaskSelector.tsx index 2218bdef8..fbd40e280 100644 --- a/src/PersonObjects/Sleeve/ui/TaskSelector.tsx +++ b/src/PersonObjects/Sleeve/ui/TaskSelector.tsx @@ -23,12 +23,12 @@ const universitySelectorOptions: string[] = [ const gymSelectorOptions: string[] = ["Train Strength", "Train Defense", "Train Dexterity", "Train Agility"]; const bladeburnerSelectorOptions: string[] = [ - "Field Analysis", + "Field analysis", "Recruitment", "Diplomacy", "Infiltrate synthoids", "Support main sleeve", - "Take on Contracts", + "Take on contracts", ]; interface IProps { @@ -101,7 +101,7 @@ function possibleContracts(player: IPlayer, sleeve: Sleeve): string[] { if (sleeve === otherSleeve) { continue; } - if (otherSleeve.currentTask === SleeveTaskType.Bladeburner && otherSleeve.bbAction == "Take on Contracts") { + if (otherSleeve.currentTask === SleeveTaskType.Bladeburner && otherSleeve.bbAction == "Take on contracts") { contracts = contracts.filter((x) => x != otherSleeve.bbContract); } } @@ -202,7 +202,7 @@ const tasks: { return { first: bladeburnerSelectorOptions, second: (s1: string) => { - if (s1 === "Take on Contracts") { + if (s1 === "Take on contracts") { return possibleContracts(player, sleeve); } else { return ["------"]; diff --git a/src/ScriptEditor/NetscriptDefinitions.d.ts b/src/ScriptEditor/NetscriptDefinitions.d.ts index e98b03557..cc649032a 100644 --- a/src/ScriptEditor/NetscriptDefinitions.d.ts +++ b/src/ScriptEditor/NetscriptDefinitions.d.ts @@ -3777,6 +3777,20 @@ export interface Sleeve { * @returns True if the aug was purchased and installed on the sleeve, false otherwise. */ purchaseSleeveAug(sleeveNumber: number, augName: string): boolean; + + /** + * Set a sleeve to perform bladeburner actions. + * @remarks + * RAM cost: 4 GB + * + * Return a boolean indicating whether or not the sleeve started working out. + * + * @param sleeveNumber - Index of the sleeve to workout at the gym. + * @param action - Name of the action to be performed. + * @param contract - Name of the contract if applicable. + * @returns True if the sleeve started working out, false otherwise. + */ + setToBladeburnerAction(sleeveNumber: number, action: string, contract?: string): boolean; } /** diff --git a/src/utils/StringHelperFunctions.ts b/src/utils/StringHelperFunctions.ts index 58c685871..60bc09d6e 100644 --- a/src/utils/StringHelperFunctions.ts +++ b/src/utils/StringHelperFunctions.ts @@ -117,6 +117,17 @@ function cyrb53(str: string, seed = 0): string { return (4294967296 * (2097151 & h2) + (h1 >>> 0)).toString(16); } +function capitalizeFirstLetter(s: string): string { + return s.charAt(0).toUpperCase() + s.slice(1); +} + +function capitalizeEachWord(s: string): string { + return s + .split(" ") + .map((word) => capitalizeFirstLetter(word)) + .join(" "); +} + export { convertTimeMsToTimeElapsedString, longestCommonStart, @@ -124,4 +135,6 @@ export { formatNumber, generateRandomString, cyrb53, + capitalizeFirstLetter, + capitalizeEachWord, };