diff --git a/doc/source/v2.0.0_migration.rst b/doc/source/v2.0.0_migration.rst index e8bba2d80..97777cfe4 100644 --- a/doc/source/v2.0.0_migration.rst +++ b/doc/source/v2.0.0_migration.rst @@ -95,10 +95,10 @@ Singularity This means calls like 'ns.connect' need to be changed to 'ns.singularity.connect' -stock.buy and stock.sell ------------------------- +stock.buy, stock.sell, stock.short +---------------------------------- - These 2 functions were renamed to stock.buyStock and stock.sellStock because 'buy' and 'sell' + These functions were renamed to stock.buyStock, stock.sellStock, and stock.buyShort because 'buy', 'sell', and 'short' are very common tokens that would trick the ram calculation. corporation.bribe diff --git a/src/Bladeburner/Bladeburner.tsx b/src/Bladeburner/Bladeburner.tsx index 73b6c21b1..c57e47beb 100644 --- a/src/Bladeburner/Bladeburner.tsx +++ b/src/Bladeburner/Bladeburner.tsx @@ -36,6 +36,8 @@ import { joinFaction } from "../Faction/FactionHelpers"; import { WorkerScript } from "../Netscript/WorkerScript"; import { FactionNames } from "../Faction/data/FactionNames"; import { KEY } from "../utils/helpers/keyCodes"; +import { isSleeveInfiltrateWork } from "../PersonObjects/Sleeve/Work/SleeveInfiltrateWork"; +import { isSleeveSupportWork } from "../PersonObjects/Sleeve/Work/SleeveSupportWork"; interface BlackOpsAttempt { error?: string; @@ -1124,7 +1126,7 @@ export class Bladeburner implements IBladeburner { const losses = getRandomInt(0, max); this.teamSize -= losses; if (this.teamSize < this.sleeveSize) { - const sup = player.sleeves.filter((x) => x.bbAction == "Support main sleeve"); + const sup = player.sleeves.filter((x) => isSleeveSupportWork(x.currentWork)); for (let i = 0; i > this.teamSize - this.sleeveSize; i--) { const r = Math.floor(Math.random() * sup.length); sup[r].takeDamage(sup[r].hp.max); @@ -1438,7 +1440,7 @@ export class Bladeburner implements IBladeburner { const losses = getRandomInt(1, teamLossMax); this.teamSize -= losses; if (this.teamSize < this.sleeveSize) { - const sup = player.sleeves.filter((x) => x.bbAction == "Support main sleeve"); + const sup = player.sleeves.filter((x) => isSleeveSupportWork(x.currentWork)); for (let i = 0; i > this.teamSize - this.sleeveSize; i--) { const r = Math.floor(Math.random() * sup.length); sup[r].takeDamage(sup[r].hp.max); @@ -1452,7 +1454,7 @@ export class Bladeburner implements IBladeburner { } } } catch (e: unknown) { - exceptionAlert(e); + exceptionAlert(String(e)); } break; } @@ -1602,7 +1604,7 @@ export class Bladeburner implements IBladeburner { } infiltrateSynthoidCommunities(p: IPlayer): void { - const infilSleeves = p.sleeves.filter((s) => s.bbAction === "Infiltrate synthoids").length; + const infilSleeves = p.sleeves.filter((s) => isSleeveInfiltrateWork(s.currentWork)).length; const amt = Math.pow(infilSleeves, -0.5) / 2; for (const contract of Object.keys(this.contracts)) { this.contracts[contract].count += amt; diff --git a/src/Bladeburner/data/GeneralActions.tsx b/src/Bladeburner/data/GeneralActions.tsx index 3cc393612..1725b21cf 100644 --- a/src/Bladeburner/data/GeneralActions.tsx +++ b/src/Bladeburner/data/GeneralActions.tsx @@ -1,11 +1,13 @@ import React from "react"; +import { newWorkStats, WorkStats } from "../../Work/WorkStats"; -interface IContract { +interface IGeneral { desc: JSX.Element; + exp: WorkStats; } export const GeneralActions: { - [key: string]: IContract | undefined; + [key: string]: IGeneral | undefined; } = { Training: { desc: ( @@ -14,6 +16,12 @@ export const GeneralActions: { all combat stats and also increases your max stamina. ), + exp: newWorkStats({ + strExp: 30, + defExp: 30, + dexExp: 30, + agiExp: 30, + }), }, "Field Analysis": { @@ -27,6 +35,10 @@ export const GeneralActions: { Does NOT require stamina. ), + exp: newWorkStats({ + hackExp: 20, + chaExp: 20, + }), }, Recruitment: { @@ -38,6 +50,9 @@ export const GeneralActions: { Does NOT require stamina. ), + exp: newWorkStats({ + chaExp: 120, + }), }, Diplomacy: { @@ -50,6 +65,9 @@ export const GeneralActions: { Does NOT require stamina. ), + exp: newWorkStats({ + chaExp: 120, + }), }, "Hyperbolic Regeneration Chamber": { @@ -61,6 +79,7 @@ export const GeneralActions: {
), + exp: newWorkStats(), }, "Incite Violence": { desc: ( @@ -69,5 +88,12 @@ export const GeneralActions: { additional contracts and operations, at the cost of increased Chaos. ), + exp: newWorkStats({ + strExp: 10, + defExp: 10, + dexExp: 10, + agiExp: 10, + chaExp: 10, + }), }, }; diff --git a/src/Crime/Crimes.ts b/src/Crime/Crimes.ts index 66d100d62..407eb85ad 100644 --- a/src/Crime/Crimes.ts +++ b/src/Crime/Crimes.ts @@ -6,7 +6,7 @@ import { IMap } from "../types"; import { CrimeType } from "../utils/WorkType"; export const Crimes: IMap = { - Shoplift: new Crime("Shoplift", CrimeType.Shoplift, 2e3, 15e3, 1 / 20, 0.1, { + Shoplift: new Crime("Shoplift", CrimeType.SHOPLIFT, 2e3, 15e3, 1 / 20, 0.1, { dexterity_success_weight: 1, agility_success_weight: 1, @@ -14,7 +14,7 @@ export const Crimes: IMap = { agility_exp: 2, }), - RobStore: new Crime("Rob Store", CrimeType.RobStore, 60e3, 400e3, 1 / 5, 0.5, { + RobStore: new Crime("Rob Store", CrimeType.ROB_STORE, 60e3, 400e3, 1 / 5, 0.5, { hacking_exp: 30, dexterity_exp: 45, agility_exp: 45, @@ -26,7 +26,7 @@ export const Crimes: IMap = { intelligence_exp: 7.5 * CONSTANTS.IntelligenceCrimeBaseExpGain, }), - Mug: new Crime("Mug", CrimeType.Mug, 4e3, 36e3, 1 / 5, 0.25, { + Mug: new Crime("Mug", CrimeType.MUG, 4e3, 36e3, 1 / 5, 0.25, { strength_exp: 3, defense_exp: 3, dexterity_exp: 3, @@ -38,7 +38,7 @@ export const Crimes: IMap = { agility_success_weight: 0.5, }), - Larceny: new Crime("Larceny", CrimeType.Larceny, 90e3, 800e3, 1 / 3, 1.5, { + Larceny: new Crime("Larceny", CrimeType.LARCENY, 90e3, 800e3, 1 / 3, 1.5, { hacking_exp: 45, dexterity_exp: 60, agility_exp: 60, @@ -50,7 +50,7 @@ export const Crimes: IMap = { intelligence_exp: 15 * CONSTANTS.IntelligenceCrimeBaseExpGain, }), - DealDrugs: new Crime("Deal Drugs", CrimeType.Drugs, 10e3, 120e3, 1, 0.5, { + DealDrugs: new Crime("Deal Drugs", CrimeType.DRUGS, 10e3, 120e3, 1, 0.5, { dexterity_exp: 5, agility_exp: 5, charisma_exp: 10, @@ -60,7 +60,7 @@ export const Crimes: IMap = { agility_success_weight: 1, }), - BondForgery: new Crime("Bond Forgery", CrimeType.BondForgery, 300e3, 4.5e6, 1 / 2, 0.1, { + BondForgery: new Crime("Bond Forgery", CrimeType.BOND_FORGERY, 300e3, 4.5e6, 1 / 2, 0.1, { hacking_exp: 100, dexterity_exp: 150, charisma_exp: 15, @@ -71,7 +71,7 @@ export const Crimes: IMap = { intelligence_exp: 60 * CONSTANTS.IntelligenceCrimeBaseExpGain, }), - TraffickArms: new Crime("Traffick Arms", CrimeType.TraffickArms, 40e3, 600e3, 2, 1, { + TraffickArms: new Crime("Traffick Arms", CrimeType.TRAFFIC_ARMS, 40e3, 600e3, 2, 1, { strength_exp: 20, defense_exp: 20, dexterity_exp: 20, @@ -85,7 +85,7 @@ export const Crimes: IMap = { agility_success_weight: 1, }), - Homicide: new Crime("Homicide", CrimeType.Homicide, 3e3, 45e3, 1, 3, { + Homicide: new Crime("Homicide", CrimeType.HOMICIDE, 3e3, 45e3, 1, 3, { strength_exp: 2, defense_exp: 2, dexterity_exp: 2, @@ -99,7 +99,7 @@ export const Crimes: IMap = { kills: 1, }), - GrandTheftAuto: new Crime("Grand Theft Auto", CrimeType.GrandTheftAuto, 80e3, 1.6e6, 8, 5, { + GrandTheftAuto: new Crime("Grand Theft Auto", CrimeType.GRAND_THEFT_AUTO, 80e3, 1.6e6, 8, 5, { strength_exp: 20, defense_exp: 20, dexterity_exp: 20, @@ -115,7 +115,7 @@ export const Crimes: IMap = { intelligence_exp: 16 * CONSTANTS.IntelligenceCrimeBaseExpGain, }), - Kidnap: new Crime("Kidnap", CrimeType.Kidnap, 120e3, 3.6e6, 5, 6, { + Kidnap: new Crime("Kidnap", CrimeType.KIDNAP, 120e3, 3.6e6, 5, 6, { strength_exp: 80, defense_exp: 80, dexterity_exp: 80, @@ -130,7 +130,7 @@ export const Crimes: IMap = { intelligence_exp: 26 * CONSTANTS.IntelligenceCrimeBaseExpGain, }), - Assassination: new Crime("Assassination", CrimeType.Assassination, 300e3, 12e6, 8, 10, { + Assassination: new Crime("Assassination", CrimeType.ASSASSINATION, 300e3, 12e6, 8, 10, { strength_exp: 300, defense_exp: 300, dexterity_exp: 300, @@ -145,7 +145,7 @@ export const Crimes: IMap = { kills: 1, }), - Heist: new Crime("Heist", CrimeType.Heist, 600e3, 120e6, 18, 15, { + Heist: new Crime("Heist", CrimeType.HEIST, 600e3, 120e6, 18, 15, { hacking_exp: 450, strength_exp: 450, defense_exp: 450, diff --git a/src/Netscript/RamCostGenerator.ts b/src/Netscript/RamCostGenerator.ts index aac166b3e..80a854886 100644 --- a/src/Netscript/RamCostGenerator.ts +++ b/src/Netscript/RamCostGenerator.ts @@ -136,7 +136,7 @@ const stock = { getSaleGain: RamCostConstants.ScriptGetStockRamCost, buyStock: RamCostConstants.ScriptBuySellStockRamCost, sellStock: RamCostConstants.ScriptBuySellStockRamCost, - short: RamCostConstants.ScriptBuySellStockRamCost, + buyShort: RamCostConstants.ScriptBuySellStockRamCost, sellShort: RamCostConstants.ScriptBuySellStockRamCost, placeOrder: RamCostConstants.ScriptBuySellStockRamCost, cancelOrder: RamCostConstants.ScriptBuySellStockRamCost, diff --git a/src/NetscriptFunctions/Sleeve.ts b/src/NetscriptFunctions/Sleeve.ts index a6b88e580..eff3d882b 100644 --- a/src/NetscriptFunctions/Sleeve.ts +++ b/src/NetscriptFunctions/Sleeve.ts @@ -1,5 +1,4 @@ import { IPlayer } from "../PersonObjects/IPlayer"; -import { SleeveTaskType } from "../PersonObjects/Sleeve/SleeveTaskTypesEnum"; import { findSleevePurchasableAugs } from "../PersonObjects/Sleeve/SleeveHelpers"; import { StaticAugmentations } from "../Augmentation/StaticAugmentations"; import { CityName } from "../Locations/data/CityNames"; @@ -15,7 +14,9 @@ import { } from "../ScriptEditor/NetscriptDefinitions"; import { checkEnum } from "../utils/helpers/checkEnum"; import { InternalAPI, NetscriptContext } from "../Netscript/APIWrapper"; -import { FactionWorkType } from "../Work/data/FactionWorkType"; +import { isSleeveBladeburnerWork } from "../PersonObjects/Sleeve/Work/SleeveBladeburnerWork"; +import { isSleeveFactionWork } from "../PersonObjects/Sleeve/Work/SleeveFactionWork"; +import { isSleeveCompanyWork } from "../PersonObjects/Sleeve/Work/SleeveCompanyWork"; export function NetscriptSleeve(player: IPlayer): InternalAPI { const checkSleeveAPIAccess = function (ctx: NetscriptContext): void { @@ -120,7 +121,7 @@ export function NetscriptSleeve(player: IPlayer): InternalAPI { continue; } const other = player.sleeves[i]; - if (other.currentTask === SleeveTaskType.Company && other.currentTaskLocation === companyName) { + if (isSleeveCompanyWork(other.currentWork) && other.currentWork.companyName === companyName) { throw ctx.makeRuntimeErrorMsg( `Sleeve ${sleeveNumber} cannot work for company ${companyName} because Sleeve ${i} is already working for them.`, ); @@ -144,7 +145,7 @@ export function NetscriptSleeve(player: IPlayer): InternalAPI { continue; } const other = player.sleeves[i]; - if (other.currentTask === SleeveTaskType.Faction && other.currentTaskLocation === factionName) { + if (isSleeveFactionWork(other.currentWork) && other.currentWork.factionName === factionName) { throw ctx.makeRuntimeErrorMsg( `Sleeve ${sleeveNumber} cannot work for faction ${factionName} because Sleeve ${i} is already working for them.`, ); @@ -180,20 +181,14 @@ export function NetscriptSleeve(player: IPlayer): InternalAPI { }, getTask: (ctx: NetscriptContext) => - (_sleeveNumber: unknown): SleeveTask => { + (_sleeveNumber: unknown): SleeveTask | null => { const sleeveNumber = ctx.helper.number("sleeveNumber", _sleeveNumber); checkSleeveAPIAccess(ctx); checkSleeveNumber(ctx, sleeveNumber); const sl = player.sleeves[sleeveNumber]; - return { - task: SleeveTaskType[sl.currentTask], - crime: sl.crimeType, - location: sl.currentTaskLocation, - gymStatType: sl.gymStatType, - factionWorkType: FactionWorkType[sl.factionWorkType], - className: sl.className, - }; + if (sl.currentWork === null) return null; + return sl.currentWork.APICopy(); }, getInformation: (ctx: NetscriptContext) => @@ -229,36 +224,6 @@ export function NetscriptSleeve(player: IPlayer): InternalAPI { strengthExp: sl.mults.strength_exp, workMoney: sl.mults.work_money, }, - - timeWorked: sl.currentTaskTime, - earningsForSleeves: { - workHackExpGain: sl.earningsForSleeves.hack, - workStrExpGain: sl.earningsForSleeves.str, - workDefExpGain: sl.earningsForSleeves.def, - workDexExpGain: sl.earningsForSleeves.dex, - workAgiExpGain: sl.earningsForSleeves.agi, - workChaExpGain: sl.earningsForSleeves.cha, - workMoneyGain: sl.earningsForSleeves.money, - }, - earningsForPlayer: { - workHackExpGain: sl.earningsForPlayer.hack, - workStrExpGain: sl.earningsForPlayer.str, - workDefExpGain: sl.earningsForPlayer.def, - workDexExpGain: sl.earningsForPlayer.dex, - workAgiExpGain: sl.earningsForPlayer.agi, - workChaExpGain: sl.earningsForPlayer.cha, - workMoneyGain: sl.earningsForPlayer.money, - }, - earningsForTask: { - workHackExpGain: sl.earningsForTask.hack, - workStrExpGain: sl.earningsForTask.str, - workDefExpGain: sl.earningsForTask.def, - workDexExpGain: sl.earningsForTask.dex, - workAgiExpGain: sl.earningsForTask.agi, - workChaExpGain: sl.earningsForTask.cha, - workMoneyGain: sl.earningsForTask.money, - }, - workRepGain: sl.getRepGain(player), }; }, getSleeveAugmentations: @@ -349,11 +314,7 @@ export function NetscriptSleeve(player: IPlayer): InternalAPI { continue; } const other = player.sleeves[i]; - if ( - other.currentTask === SleeveTaskType.Bladeburner && - other.bbAction === action && - other.bbContract === contract - ) { + if (isSleeveBladeburnerWork(other.currentWork) && other.currentWork.actionName === contract) { throw ctx.helper.makeRuntimeErrorMsg( `Sleeve ${sleeveNumber} cannot take on contracts because Sleeve ${i} is already performing that action.`, ); diff --git a/src/NetscriptFunctions/StockMarket.ts b/src/NetscriptFunctions/StockMarket.ts index 26ab02e37..041450038 100644 --- a/src/NetscriptFunctions/StockMarket.ts +++ b/src/NetscriptFunctions/StockMarket.ts @@ -165,7 +165,7 @@ export function NetscriptStockMarket(player: IPlayer, workerScript: WorkerScript return res ? stock.getBidPrice() : 0; }, - short: + buyShort: (ctx: NetscriptContext) => (_symbol: unknown, _shares: unknown): number => { const symbol = ctx.helper.string("symbol", _symbol); diff --git a/src/PersonObjects/Sleeve/Sleeve.ts b/src/PersonObjects/Sleeve/Sleeve.ts index 4f6f5cf30..5f98e4fab 100644 --- a/src/PersonObjects/Sleeve/Sleeve.ts +++ b/src/PersonObjects/Sleeve/Sleeve.ts @@ -6,7 +6,6 @@ * * Sleeves are unlocked in BitNode-10. */ -import { SleeveTaskType } from "./SleeveTaskTypesEnum"; import { IPlayer } from "../IPlayer"; import { Person } from "../Person"; @@ -14,8 +13,6 @@ import { ITaskTracker, createTaskTracker } from "../ITaskTracker"; import { Augmentation } from "../../Augmentation/Augmentation"; -import { BitNodeMultipliers } from "../../BitNode/BitNodeMultipliers"; - import { Crime } from "../../Crime/Crime"; import { Crimes } from "../../Crime/Crimes"; @@ -33,88 +30,22 @@ import { CityName } from "../../Locations/data/CityNames"; import { LocationName } from "../../Locations/data/LocationNames"; import { Generic_fromJSON, Generic_toJSON, IReviverValue, Reviver } from "../../utils/JSONReviver"; -import { BladeburnerConstants } from "../../Bladeburner/data/Constants"; import { numeralWrapper } from "../../ui/numeralFormat"; -import { capitalizeFirstLetter, capitalizeEachWord } from "../../utils/StringHelperFunctions"; import { FactionWorkType } from "../../Work/data/FactionWorkType"; +import { Work } from "./Work/Work"; +import { SleeveClassWork } from "./Work/SleeveClassWork"; +import { ClassType } from "../../Work/ClassWork"; +import { SleeveSynchroWork } from "./Work/SleeveSynchroWork"; +import { SleeveRecoveryWork } from "./Work/SleeveRecoveryWork"; +import { SleeveFactionWork } from "./Work/SleeveFactionWork"; +import { SleeveCompanyWork } from "./Work/SleeveCompanyWork"; +import { SleeveInfiltrateWork } from "./Work/SleeveInfiltrateWork"; +import { SleeveSupportWork } from "./Work/SleeveSupportWork"; +import { SleeveBladeburnerWork } from "./Work/SleeveBladeburnerWork"; +import { SleeveCrimeWork } from "./Work/SleeveCrimeWork"; export class Sleeve extends Person { - /** - * Stores the name of the class that the player is currently taking - */ - className = ""; - - /** - * Stores the type of crime the sleeve is currently attempting - * Must match the name of a Crime object - */ - crimeType = ""; - - /** - * Enum value for current task - */ - currentTask: SleeveTaskType = SleeveTaskType.Idle; - - /** - * Contains details about the sleeve's current task. The info stored - * in this depends on the task type - * - * Faction/Company Work: Name of Faction/Company - * Crime: Money earned if successful - * Class/Gym: Name of university/gym - * Bladeburner: success chance - */ - currentTaskLocation = ""; - - /** - * Maximum amount of time (in milliseconds) that can be spent on current task. - */ - currentTaskMaxTime = 0; - - /** - * Milliseconds spent on current task - */ - currentTaskTime = 0; - - /** - * Keeps track of experience earned for other sleeves - */ - earningsForSleeves: ITaskTracker = createTaskTracker(); - - /** - * Keeps track of experience + money earned for player - */ - earningsForPlayer: ITaskTracker = createTaskTracker(); - - /** - * Keeps track of experienced earned in the current task/action - */ - earningsForTask: ITaskTracker = createTaskTracker(); - - /** - * Keeps track of what type of work sleeve is doing for faction, if applicable - */ - factionWorkType: FactionWorkType = FactionWorkType.HACKING; - - /** - * Records experience gain rate for the current task - */ - gainRatesForTask: ITaskTracker = createTaskTracker(); - - /** - * String that stores what stat the sleeve is training at the gym - */ - gymStatType = ""; - - /** - * String that stores what stat the sleeve is training at the gym - */ - bbAction = ""; - - /** - * String that stores what stat the sleeve is training at the gym - */ - bbContract = ""; + currentWork: Work | null = null; /** * Clone retains 'memory' synchronization (and maybe exp?) upon prestige/installing Augs @@ -149,6 +80,23 @@ export class Sleeve extends Person { } } + shockBonus(): number { + return this.shock / 100; + } + syncBonus(): number { + return this.sync / 100; + } + + startWork(player: IPlayer, w: Work): void { + if (this.currentWork) this.currentWork.finish(player); + this.currentWork = w; + } + + stopWork(player: IPlayer): void { + if (this.currentWork) this.currentWork.finish(player); + this.currentWork = null; + } + /** * Commit crimes */ @@ -158,121 +106,19 @@ export class Sleeve extends Person { return false; } - if (this.currentTask !== SleeveTaskType.Idle) { - this.finishTask(p); - } else { - this.resetTaskStatus(p); - } - - this.gainRatesForTask.hack = crime.hacking_exp * this.mults.hacking_exp * BitNodeMultipliers.CrimeExpGain; - this.gainRatesForTask.str = crime.strength_exp * this.mults.strength_exp * BitNodeMultipliers.CrimeExpGain; - this.gainRatesForTask.def = crime.defense_exp * this.mults.defense_exp * BitNodeMultipliers.CrimeExpGain; - this.gainRatesForTask.dex = crime.dexterity_exp * this.mults.dexterity_exp * BitNodeMultipliers.CrimeExpGain; - this.gainRatesForTask.agi = crime.agility_exp * this.mults.agility_exp * BitNodeMultipliers.CrimeExpGain; - this.gainRatesForTask.cha = crime.charisma_exp * this.mults.charisma_exp * BitNodeMultipliers.CrimeExpGain; - this.gainRatesForTask.int = crime.intelligence_exp; - this.gainRatesForTask.money = crime.money * this.mults.crime_money * BitNodeMultipliers.CrimeMoney; - - this.currentTaskLocation = String(this.gainRatesForTask.money); - - this.crimeType = crime.name; - this.currentTaskMaxTime = crime.time; - this.currentTask = SleeveTaskType.Crime; + this.startWork(p, new SleeveCrimeWork(crime.type)); return true; } /** * Called to stop the current task */ - finishTask(p: IPlayer): ITaskTracker { - let retValue: ITaskTracker = createTaskTracker(); // Amount of exp to be gained by other sleeves - - if (this.currentTask === SleeveTaskType.Crime) { - // For crimes, all experience and money is gained at the end - if (this.currentTaskTime >= this.currentTaskMaxTime) { - const crime: Crime | undefined = Object.values(Crimes).find((crime) => crime.name === this.crimeType); - if (!crime) { - console.error(`Invalid data stored in sleeve.crimeType: ${this.crimeType}`); - this.resetTaskStatus(p); - return retValue; - } - if (Math.random() < crime.successRate(this)) { - // Success - const successGainRates: ITaskTracker = createTaskTracker(); - - const keysForIteration: (keyof ITaskTracker)[] = Object.keys(successGainRates) as (keyof ITaskTracker)[]; - for (let i = 0; i < keysForIteration.length; ++i) { - const key = keysForIteration[i]; - successGainRates[key] = this.gainRatesForTask[key] * 2; - } - retValue = this.gainExperience(p, successGainRates); - this.gainMoney(p, this.gainRatesForTask); - - p.karma -= crime.karma * (this.sync / 100); - } else { - retValue = this.gainExperience(p, this.gainRatesForTask); - } - - // Do not reset task to IDLE - this.currentTaskTime = 0; - 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) { - const errorLogText = `bladeburner is null`; - console.error(`Function: sleeves.finishTask; Message: '${errorLogText}'`); - this.resetTaskStatus(p); - return retValue; - } - - if (this.currentTaskTime >= this.currentTaskMaxTime) { - if (this.bbAction === "Infiltrate synthoids") { - bb.infiltrateSynthoidCommunities(p); - this.currentTaskTime = 0; - return retValue; - } - let type: string; - let name: string; - if (this.bbAction === "Take on contracts") { - type = "Contracts"; - name = this.bbContract; - } else { - type = "General"; - name = this.bbAction; - } - - const actionIdent = bb.getActionIdFromTypeAndName(type, name); - if (actionIdent === null) { - const errorLogText = `Invalid action: type='${type}' name='${name}'`; - console.error(`Function: sleeves.finishTask; Message: '${errorLogText}'`); - this.resetTaskStatus(p); - return retValue; - } - - const action = bb.getActionObject(actionIdent); - if ((action?.count ?? 0) > 0) { - const bbRetValue = bb.completeAction(p, this, actionIdent, false); - if (bbRetValue) { - retValue = this.gainExperience(p, bbRetValue); - this.gainMoney(p, bbRetValue); - - // Do not reset task to IDLE - this.currentTaskTime = 0; - return retValue; - } - } - } - } + finishTask(p: IPlayer): void { + this.stopWork(p); this.resetTaskStatus(p); - return retValue; + return; } /** @@ -326,43 +172,31 @@ export class Sleeve extends Person { if (pHackExp > 0) { this.gainHackingExp(pHackExp); p.gainHackingExp(pHackExp); - this.earningsForPlayer.hack += pHackExp; - this.earningsForTask.hack += pHackExp; } if (pStrExp > 0) { this.gainStrengthExp(pStrExp); p.gainStrengthExp(pStrExp); - this.earningsForPlayer.str += pStrExp; - this.earningsForTask.str += pStrExp; } if (pDefExp > 0) { this.gainDefenseExp(pDefExp); p.gainDefenseExp(pDefExp); - this.earningsForPlayer.def += pDefExp; - this.earningsForTask.def += pDefExp; } if (pDexExp > 0) { this.gainDexterityExp(pDexExp); p.gainDexterityExp(pDexExp); - this.earningsForPlayer.dex += pDexExp; - this.earningsForTask.dex += pDexExp; } if (pAgiExp > 0) { this.gainAgilityExp(pAgiExp); p.gainAgilityExp(pAgiExp); - this.earningsForPlayer.agi += pAgiExp; - this.earningsForTask.agi += pAgiExp; } if (pChaExp > 0) { this.gainCharismaExp(pChaExp); p.gainCharismaExp(pChaExp); - this.earningsForPlayer.cha += pChaExp; - this.earningsForTask.cha += pChaExp; } if (pIntExp > 0) { @@ -370,14 +204,6 @@ export class Sleeve extends Person { p.gainIntelligenceExp(pIntExp); } - // Record earnings for other sleeves - this.earningsForSleeves.hack += pHackExp * (this.sync / 100); - this.earningsForSleeves.str += pStrExp * (this.sync / 100); - this.earningsForSleeves.def += pDefExp * (this.sync / 100); - this.earningsForSleeves.dex += pDexExp * (this.sync / 100); - this.earningsForSleeves.agi += pAgiExp * (this.sync / 100); - this.earningsForSleeves.cha += pChaExp * (this.sync / 100); - // Return the experience to be gained by other sleeves return { hack: pHackExp * (this.sync / 100), @@ -394,12 +220,12 @@ export class Sleeve extends Person { /** * Earn money for player */ - gainMoney(p: IPlayer, task: ITaskTracker, numCycles = 1): void { - const gain: number = task.money * numCycles; - this.earningsForTask.money += gain; - this.earningsForPlayer.money += gain; - p.gainMoney(gain, "sleeves"); - } + // gainMoney(p: IPlayer, task: ITaskTracker, numCycles = 1): void { + // const gain: number = task.money * numCycles; + // this.earningsForTask.money += gain; + // this.earningsForPlayer.money += gain; + // p.gainMoney(gain, "sleeves"); + // } /** * Returns the cost of upgrading this sleeve's memory by a certain amount @@ -430,54 +256,36 @@ export class Sleeve extends Person { * Gets reputation gain for the current task * Only applicable when working for company or faction */ - getRepGain(p: IPlayer): number { - if (this.currentTask === SleeveTaskType.Faction) { - let favorMult = 1; - const fac: Faction | null = Factions[this.currentTaskLocation]; - if (fac != null) { - favorMult = 1 + fac.favor / 100; - } + // getRepGain(p: IPlayer): number { + // if (this.currentTask === SleeveTaskType.Company) { + // const companyName: string = this.currentTaskLocation; + // const company: Company | null = Companies[companyName]; + // if (company == null) { + // console.error(`Invalid company found when trying to calculate rep gain: ${companyName}`); + // return 0; + // } - switch (this.factionWorkType) { - case FactionWorkType.HACKING: - return this.getFactionHackingWorkRepGain() * (this.shock / 100) * favorMult; - case FactionWorkType.FIELD: - return this.getFactionFieldWorkRepGain() * (this.shock / 100) * favorMult; - case FactionWorkType.SECURITY: - return this.getFactionSecurityWorkRepGain() * (this.shock / 100) * favorMult; - default: - console.warn(`Invalid Sleeve.factionWorkType property in Sleeve.getRepGain(): ${this.factionWorkType}`); - return 0; - } - } else if (this.currentTask === SleeveTaskType.Company) { - const companyName: string = this.currentTaskLocation; - const company: Company | null = Companies[companyName]; - if (company == null) { - console.error(`Invalid company found when trying to calculate rep gain: ${companyName}`); - return 0; - } + // const companyPosition: CompanyPosition | null = CompanyPositions[p.jobs[companyName]]; + // if (companyPosition == null) { + // console.error(`Invalid company position name found when trying to calculate rep gain: ${p.jobs[companyName]}`); + // return 0; + // } - const companyPosition: CompanyPosition | null = CompanyPositions[p.jobs[companyName]]; - if (companyPosition == null) { - console.error(`Invalid company position name found when trying to calculate rep gain: ${p.jobs[companyName]}`); - return 0; - } + // const jobPerformance: number = companyPosition.calculateJobPerformance( + // this.skills.hacking, + // this.skills.strength, + // this.skills.defense, + // this.skills.dexterity, + // this.skills.agility, + // this.skills.charisma, + // ); + // const favorMult = 1 + company.favor / 100; - const jobPerformance: number = companyPosition.calculateJobPerformance( - this.skills.hacking, - this.skills.strength, - this.skills.defense, - this.skills.dexterity, - this.skills.agility, - this.skills.charisma, - ); - const favorMult = 1 + company.favor / 100; - - return jobPerformance * this.mults.company_rep * favorMult; - } else { - return 0; - } - } + // return jobPerformance * this.mults.company_rep * favorMult; + // } else { + // return 0; + // } + // } installAugmentation(aug: Augmentation): void { this.exp.hacking = 0; @@ -505,8 +313,6 @@ export class Sleeve extends Person { // Reset task-related stuff this.resetTaskStatus(p); - this.earningsForSleeves = createTaskTracker(); - this.earningsForPlayer = createTaskTracker(); this.shockRecovery(p); // Reset augs and multipliers @@ -532,93 +338,19 @@ export class Sleeve extends Person { // Only process once every second (5 cycles) const CyclesPerSecond = 1000 / CONSTANTS.MilliPerCycle; this.storedCycles += numCycles; - if (this.storedCycles < CyclesPerSecond) { - return; - } + if (this.storedCycles < CyclesPerSecond) return; let cyclesUsed = this.storedCycles; cyclesUsed = Math.min(cyclesUsed, 15); - let time = cyclesUsed * CONSTANTS.MilliPerCycle; - if (this.currentTaskMaxTime !== 0 && this.currentTaskTime + time > this.currentTaskMaxTime) { - time = this.currentTaskMaxTime - this.currentTaskTime; - cyclesUsed = Math.floor(time / CONSTANTS.MilliPerCycle); - - if (time < 0 || cyclesUsed < 0) { - console.warn(`Sleeve.process() calculated negative cycle usage`); - time = 0; - cyclesUsed = 0; - } + if (this.currentWork) { + this.currentWork.process(p, this, cyclesUsed); + this.storedCycles -= cyclesUsed; + return; } - this.currentTaskTime += time; - // Shock gradually goes towards 100 this.shock = Math.min(100, this.shock + 0.0001 * cyclesUsed); - switch (this.currentTask) { - case SleeveTaskType.Idle: - break; - case SleeveTaskType.Class: - case SleeveTaskType.Gym: - this.updateTaskGainRates(p); - this.gainExperience(p, this.gainRatesForTask, cyclesUsed); - this.gainMoney(p, this.gainRatesForTask, cyclesUsed); - break; - case SleeveTaskType.Faction: { - this.gainExperience(p, this.gainRatesForTask, cyclesUsed); - this.gainMoney(p, this.gainRatesForTask, cyclesUsed); - - // Gain faction reputation - const fac: Faction = Factions[this.currentTaskLocation]; - if (!(fac instanceof Faction)) { - console.error(`Invalid faction for Sleeve task: ${this.currentTaskLocation}`); - break; - } - - // If the player has a gang with the faction the sleeve is working - // for, we need to reset the sleeve's task - if (p.gang) { - if (fac.name === p.gang.facName) { - this.resetTaskStatus(p); - } - } - - fac.playerReputation += this.getRepGain(p) * cyclesUsed; - break; - } - case SleeveTaskType.Company: { - this.gainExperience(p, this.gainRatesForTask, cyclesUsed); - this.gainMoney(p, this.gainRatesForTask, cyclesUsed); - - const company: Company = Companies[this.currentTaskLocation]; - if (!(company instanceof Company)) { - console.error(`Invalid company for Sleeve task: ${this.currentTaskLocation}`); - break; - } - - company.playerReputation += this.getRepGain(p) * cyclesUsed; - break; - } - case SleeveTaskType.Recovery: - this.shock = Math.min(100, this.shock + 0.0002 * cyclesUsed); - if (this.shock >= 100) this.resetTaskStatus(p); - break; - case SleeveTaskType.Synchro: - this.sync = Math.min(100, this.sync + p.getIntelligenceBonus(0.5) * 0.0002 * cyclesUsed); - if (this.sync >= 100) this.resetTaskStatus(p); - break; - default: - break; - } - - if (this.currentTaskMaxTime !== 0 && this.currentTaskTime >= this.currentTaskMaxTime) { - if (this.currentTask === SleeveTaskType.Crime || this.currentTask === SleeveTaskType.Bladeburner) { - this.finishTask(p); - } else { - this.finishTask(p); - } - } - this.updateStatLevels(); this.storedCycles -= cyclesUsed; @@ -630,48 +362,16 @@ export class Sleeve extends Person { * Resets all parameters used to keep information about the current task */ resetTaskStatus(p: IPlayer): void { - if (this.bbAction == "Support main sleeve") { - p.bladeburner?.sleeveSupport(false); - } - if (this.currentTask == SleeveTaskType.Class) { - const retVal = createTaskTracker(); - retVal.int = CONSTANTS.IntelligenceClassBaseExpGain * Math.round(this.currentTaskTime / 1000); - 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(); - this.currentTask = SleeveTaskType.Idle; - this.currentTaskTime = 0; - this.currentTaskMaxTime = 0; - this.factionWorkType = FactionWorkType.HACKING; - this.crimeType = ""; - this.currentTaskLocation = ""; - this.gymStatType = ""; - this.className = ""; - this.bbAction = ""; - this.bbContract = "------"; + this.stopWork(p); } shockRecovery(p: IPlayer): boolean { - if (this.currentTask !== SleeveTaskType.Idle) { - this.finishTask(p); - } else { - this.resetTaskStatus(p); - } - - this.currentTask = SleeveTaskType.Recovery; + this.startWork(p, new SleeveRecoveryWork()); return true; } synchronize(p: IPlayer): boolean { - if (this.currentTask !== SleeveTaskType.Idle) { - this.finishTask(p); - } else { - this.resetTaskStatus(p); - } - - this.currentTask = SleeveTaskType.Synchro; + this.startWork(p, new SleeveSynchroWork()); return true; } @@ -679,66 +379,59 @@ export class Sleeve extends Person { * Take a course at a university */ takeUniversityCourse(p: IPlayer, universityName: string, className: string): boolean { - if (this.currentTask !== SleeveTaskType.Idle) { - this.finishTask(p); - } else { - this.resetTaskStatus(p); - } - // Set exp/money multipliers based on which university. // Also check that the sleeve is in the right city - let costMult = 1; + let loc: LocationName | undefined; switch (universityName.toLowerCase()) { - case LocationName.AevumSummitUniversity.toLowerCase(): - if (this.city !== CityName.Aevum) { - return false; - } - this.currentTaskLocation = LocationName.AevumSummitUniversity; - costMult = 4; + case LocationName.AevumSummitUniversity.toLowerCase(): { + if (this.city !== CityName.Aevum) return false; + loc = LocationName.AevumSummitUniversity; break; - case LocationName.Sector12RothmanUniversity.toLowerCase(): - if (this.city !== CityName.Sector12) { - return false; - } - this.currentTaskLocation = LocationName.Sector12RothmanUniversity; - costMult = 3; + } + case LocationName.Sector12RothmanUniversity.toLowerCase(): { + if (this.city !== CityName.Sector12) return false; + loc = LocationName.Sector12RothmanUniversity; break; - case LocationName.VolhavenZBInstituteOfTechnology.toLowerCase(): - if (this.city !== CityName.Volhaven) { - return false; - } - this.currentTaskLocation = LocationName.VolhavenZBInstituteOfTechnology; - costMult = 5; + } + case LocationName.VolhavenZBInstituteOfTechnology.toLowerCase(): { + if (this.city !== CityName.Volhaven) return false; + loc = LocationName.VolhavenZBInstituteOfTechnology; break; - default: - return false; + } } + if (!loc) return false; // Set experience/money gains based on class + let classType: ClassType | undefined; switch (className.toLowerCase()) { case "study computer science": + classType = ClassType.StudyComputerScience; break; case "data structures": - this.gainRatesForTask.money = -1 * (CONSTANTS.ClassDataStructuresBaseCost * costMult); + classType = ClassType.DataStructures; break; case "networks": - this.gainRatesForTask.money = -1 * (CONSTANTS.ClassNetworksBaseCost * costMult); + classType = ClassType.Networks; break; case "algorithms": - this.gainRatesForTask.money = -1 * (CONSTANTS.ClassAlgorithmsBaseCost * costMult); + classType = ClassType.Algorithms; break; case "management": - this.gainRatesForTask.money = -1 * (CONSTANTS.ClassManagementBaseCost * costMult); + classType = ClassType.Management; break; case "leadership": - this.gainRatesForTask.money = -1 * (CONSTANTS.ClassLeadershipBaseCost * costMult); + classType = ClassType.Leadership; break; - default: - return false; } + if (!classType) return false; - this.className = className; - this.currentTask = SleeveTaskType.Class; + this.startWork( + p, + new SleeveClassWork({ + classType: classType, + location: loc, + }), + ); return true; } @@ -767,99 +460,6 @@ export class Sleeve extends Person { return true; } - updateTaskGainRates(p: IPlayer): void { - if (this.currentTask === SleeveTaskType.Class) { - let expMult = 1; - switch (this.currentTaskLocation.toLowerCase()) { - case LocationName.AevumSummitUniversity.toLowerCase(): - expMult = 3; - break; - case LocationName.Sector12RothmanUniversity.toLowerCase(): - expMult = 2; - break; - case LocationName.VolhavenZBInstituteOfTechnology.toLowerCase(): - expMult = 4; - break; - default: - return; - } - - const totalExpMult = expMult * p.hashManager.getStudyMult(); - switch (this.className.toLowerCase()) { - case "study computer science": - this.gainRatesForTask.hack = - CONSTANTS.ClassStudyComputerScienceBaseExp * totalExpMult * this.mults.hacking_exp; - break; - case "data structures": - this.gainRatesForTask.hack = CONSTANTS.ClassDataStructuresBaseExp * totalExpMult * this.mults.hacking_exp; - break; - case "networks": - this.gainRatesForTask.hack = CONSTANTS.ClassNetworksBaseExp * totalExpMult * this.mults.hacking_exp; - break; - case "algorithms": - this.gainRatesForTask.hack = CONSTANTS.ClassAlgorithmsBaseExp * totalExpMult * this.mults.hacking_exp; - break; - case "management": - this.gainRatesForTask.cha = CONSTANTS.ClassManagementBaseExp * totalExpMult * this.mults.charisma_exp; - break; - case "leadership": - this.gainRatesForTask.cha = CONSTANTS.ClassLeadershipBaseExp * totalExpMult * this.mults.charisma_exp; - break; - default: - break; - } - - return; - } - - if (this.currentTask === SleeveTaskType.Gym) { - // Get gym exp multiplier - let expMult = 1; - switch (this.currentTaskLocation.toLowerCase()) { - case LocationName.AevumCrushFitnessGym.toLowerCase(): - expMult = 2; - break; - case LocationName.AevumSnapFitnessGym.toLowerCase(): - expMult = 5; - break; - case LocationName.Sector12IronGym.toLowerCase(): - expMult = 1; - break; - case LocationName.Sector12PowerhouseGym.toLowerCase(): - expMult = 10; - break; - case LocationName.VolhavenMilleniumFitnessGym.toLowerCase(): - expMult = 4; - break; - default: - return; - } - - // Set stat gain rate - const baseGymExp = 1; - const totalExpMultiplier = p.hashManager.getTrainingMult() * expMult; - switch (this.gymStatType) { - case "none": // Note : due to the way Sleeve.workOutAtGym() is currently designed, this should never happend. - break; - case "str": - this.gainRatesForTask.str = baseGymExp * totalExpMultiplier * this.mults.strength_exp; - break; - case "def": - this.gainRatesForTask.def = baseGymExp * totalExpMultiplier * this.mults.defense_exp; - break; - case "dex": - this.gainRatesForTask.dex = baseGymExp * totalExpMultiplier * this.mults.dexterity_exp; - break; - case "agi": - this.gainRatesForTask.agi = baseGymExp * totalExpMultiplier * this.mults.agility_exp; - break; - } - return; - } - - console.warn(`Sleeve.updateTaskGainRates() called for unexpected task type ${this.currentTask}`); - } - upgradeMemory(n: number): void { if (n < 0) { console.warn(`Sleeve.upgradeMemory() called with negative value: ${n}`); @@ -878,58 +478,12 @@ export class Sleeve extends Person { return false; } - if (this.currentTask !== SleeveTaskType.Idle) { - this.finishTask(p); - } else { - this.resetTaskStatus(p); - } - const company: Company | null = Companies[companyName]; const companyPosition: CompanyPosition | null = CompanyPositions[p.jobs[companyName]]; - if (company == null) { - return false; - } - if (companyPosition == null) { - return false; - } - this.gainRatesForTask.money = - companyPosition.baseSalary * - company.salaryMultiplier * - this.mults.work_money * - BitNodeMultipliers.CompanyWorkMoney; - this.gainRatesForTask.hack = - companyPosition.hackingExpGain * - company.expMultiplier * - this.mults.hacking_exp * - BitNodeMultipliers.CompanyWorkExpGain; - this.gainRatesForTask.str = - companyPosition.strengthExpGain * - company.expMultiplier * - this.mults.strength_exp * - BitNodeMultipliers.CompanyWorkExpGain; - this.gainRatesForTask.def = - companyPosition.defenseExpGain * - company.expMultiplier * - this.mults.defense_exp * - BitNodeMultipliers.CompanyWorkExpGain; - this.gainRatesForTask.dex = - companyPosition.dexterityExpGain * - company.expMultiplier * - this.mults.dexterity_exp * - BitNodeMultipliers.CompanyWorkExpGain; - this.gainRatesForTask.agi = - companyPosition.agilityExpGain * - company.expMultiplier * - this.mults.agility_exp * - BitNodeMultipliers.CompanyWorkExpGain; - this.gainRatesForTask.cha = - companyPosition.charismaExpGain * - company.expMultiplier * - this.mults.charisma_exp * - BitNodeMultipliers.CompanyWorkExpGain; + if (company == null) return false; + if (companyPosition == null) return false; - this.currentTaskLocation = companyName; - this.currentTask = SleeveTaskType.Company; + this.startWork(p, new SleeveCompanyWork({ companyName: companyName })); return true; } @@ -944,49 +498,31 @@ export class Sleeve extends Person { return false; } - if (this.currentTask !== SleeveTaskType.Idle) { - this.finishTask(p); - } else { - this.resetTaskStatus(p); - } - const factionInfo = faction.getInfo(); // Set type of work (hacking/field/security), and the experience gains - const sanitizedWorkType: string = workType.toLowerCase(); + const sanitizedWorkType = workType.toLowerCase(); + let factionWorkType: FactionWorkType; if (sanitizedWorkType.includes("hack")) { - if (!factionInfo.offerHackingWork) { - return false; - } - this.factionWorkType = FactionWorkType.HACKING; - this.gainRatesForTask.hack = 0.15 * this.mults.hacking_exp * BitNodeMultipliers.FactionWorkExpGain; + if (!factionInfo.offerHackingWork) return false; + factionWorkType = FactionWorkType.HACKING; } else if (sanitizedWorkType.includes("field")) { - if (!factionInfo.offerFieldWork) { - return false; - } - this.factionWorkType = FactionWorkType.FIELD; - this.gainRatesForTask.hack = 0.1 * this.mults.hacking_exp * BitNodeMultipliers.FactionWorkExpGain; - this.gainRatesForTask.str = 0.1 * this.mults.strength_exp * BitNodeMultipliers.FactionWorkExpGain; - this.gainRatesForTask.def = 0.1 * this.mults.defense_exp * BitNodeMultipliers.FactionWorkExpGain; - this.gainRatesForTask.dex = 0.1 * this.mults.dexterity_exp * BitNodeMultipliers.FactionWorkExpGain; - this.gainRatesForTask.agi = 0.1 * this.mults.agility_exp * BitNodeMultipliers.FactionWorkExpGain; - this.gainRatesForTask.cha = 0.1 * this.mults.charisma_exp * BitNodeMultipliers.FactionWorkExpGain; + if (!factionInfo.offerFieldWork) return false; + factionWorkType = FactionWorkType.FIELD; } else if (sanitizedWorkType.includes("security")) { - if (!factionInfo.offerSecurityWork) { - return false; - } - this.factionWorkType = FactionWorkType.SECURITY; - this.gainRatesForTask.hack = 0.1 * this.mults.hacking_exp * BitNodeMultipliers.FactionWorkExpGain; - this.gainRatesForTask.str = 0.15 * this.mults.strength_exp * BitNodeMultipliers.FactionWorkExpGain; - this.gainRatesForTask.def = 0.15 * this.mults.defense_exp * BitNodeMultipliers.FactionWorkExpGain; - this.gainRatesForTask.dex = 0.15 * this.mults.dexterity_exp * BitNodeMultipliers.FactionWorkExpGain; - this.gainRatesForTask.agi = 0.15 * this.mults.agility_exp * BitNodeMultipliers.FactionWorkExpGain; + if (!factionInfo.offerSecurityWork) return false; + factionWorkType = FactionWorkType.SECURITY; } else { return false; } - this.currentTaskLocation = factionName; - this.currentTask = SleeveTaskType.Faction; + this.startWork( + p, + new SleeveFactionWork({ + factionWorkType: factionWorkType, + factionName: faction.name, + }), + ); return true; } @@ -995,81 +531,65 @@ export class Sleeve extends Person { * Begin a gym workout task */ workoutAtGym(p: IPlayer, gymName: string, stat: string): boolean { - if (this.currentTask !== SleeveTaskType.Idle) { - this.finishTask(p); - } else { - this.resetTaskStatus(p); - } - // Set exp/money multipliers based on which university. // Also check that the sleeve is in the right city - let costMult = 1; + let loc: LocationName | undefined; switch (gymName.toLowerCase()) { - case LocationName.AevumCrushFitnessGym.toLowerCase(): - if (this.city != CityName.Aevum) { - return false; - } - this.currentTaskLocation = LocationName.AevumCrushFitnessGym; - costMult = 3; + case LocationName.AevumCrushFitnessGym.toLowerCase(): { + if (this.city != CityName.Aevum) return false; + loc = LocationName.AevumCrushFitnessGym; break; - case LocationName.AevumSnapFitnessGym.toLowerCase(): - if (this.city != CityName.Aevum) { - return false; - } - this.currentTaskLocation = LocationName.AevumSnapFitnessGym; - costMult = 10; + } + case LocationName.AevumSnapFitnessGym.toLowerCase(): { + if (this.city != CityName.Aevum) return false; + loc = LocationName.AevumSnapFitnessGym; break; - case LocationName.Sector12IronGym.toLowerCase(): - if (this.city != CityName.Sector12) { - return false; - } - this.currentTaskLocation = LocationName.Sector12IronGym; - costMult = 1; + } + case LocationName.Sector12IronGym.toLowerCase(): { + if (this.city != CityName.Sector12) return false; + loc = LocationName.Sector12IronGym; break; - case LocationName.Sector12PowerhouseGym.toLowerCase(): - if (this.city != CityName.Sector12) { - return false; - } - this.currentTaskLocation = LocationName.Sector12PowerhouseGym; - costMult = 20; + } + case LocationName.Sector12PowerhouseGym.toLowerCase(): { + if (this.city != CityName.Sector12) return false; + loc = LocationName.Sector12PowerhouseGym; break; - case LocationName.VolhavenMilleniumFitnessGym.toLowerCase(): - if (this.city != CityName.Volhaven) { - return false; - } - this.currentTaskLocation = LocationName.VolhavenMilleniumFitnessGym; - costMult = 7; + } + case LocationName.VolhavenMilleniumFitnessGym.toLowerCase(): { + if (this.city != CityName.Volhaven) return false; + loc = LocationName.VolhavenMilleniumFitnessGym; break; - default: - return false; + } } + if (!loc) return false; // Set experience/money gains based on class const sanitizedStat: string = stat.toLowerCase(); // set stat to a default value. - stat = "none"; + let classType: ClassType | undefined; if (sanitizedStat.includes("str")) { - stat = "str"; + classType = ClassType.GymStrength; } if (sanitizedStat.includes("def")) { - stat = "def"; + classType = ClassType.GymDefense; } if (sanitizedStat.includes("dex")) { - stat = "dex"; + classType = ClassType.GymDexterity; } if (sanitizedStat.includes("agi")) { - stat = "agi"; + classType = ClassType.GymAgility; } // if stat is still equals its default value, then validation has failed. - if (stat === "none") { - return false; - } + if (!classType) return false; - // Set cost - this.gainRatesForTask.money = -1 * (CONSTANTS.ClassGymBaseCost * costMult); - this.gymStatType = stat; - this.currentTask = SleeveTaskType.Gym; + this.startWork( + p, + new SleeveClassWork({ + classType: classType, + location: loc, + }), + ); return true; } @@ -1078,60 +598,27 @@ export class Sleeve extends Person { * Begin a bladeburner task */ bladeburner(p: IPlayer, action: string, contract: string): boolean { - if (this.currentTask !== SleeveTaskType.Idle) { - this.finishTask(p); - } else { - this.resetTaskStatus(p); - } - - this.gainRatesForTask.hack = 0; - this.gainRatesForTask.str = 0; - this.gainRatesForTask.def = 0; - this.gainRatesForTask.dex = 0; - this.gainRatesForTask.agi = 0; - this.gainRatesForTask.cha = 0; - this.gainRatesForTask.money = 0; - this.currentTaskLocation = ""; - - let time = 0; - - this.bbContract = "------"; switch (action) { case "Field analysis": - time = this.getBladeburnerActionTime(p, "General", action); - this.gainRatesForTask.hack = 20 * this.mults.hacking_exp; - this.gainRatesForTask.cha = 20 * this.mults.charisma_exp; - break; + this.startWork(p, new SleeveBladeburnerWork({ type: "General", name: "Field Analysis" })); + return true; case "Recruitment": - time = this.getBladeburnerActionTime(p, "General", action); - this.gainRatesForTask.cha = - 2 * BladeburnerConstants.BaseStatGain * (p.bladeburner?.getRecruitmentTime(this) ?? 0) * 1000; - this.currentTaskLocation = `(Success Rate: ${numeralWrapper.formatPercentage( - this.recruitmentSuccessChance(p), - )})`; - break; + this.startWork(p, new SleeveBladeburnerWork({ type: "General", name: "Recruitment" })); + return true; case "Diplomacy": - time = this.getBladeburnerActionTime(p, "General", action); - break; + this.startWork(p, new SleeveBladeburnerWork({ type: "General", name: "Diplomacy" })); + return true; case "Infiltrate synthoids": - time = 60000; - this.currentTaskLocation = "This will generate additional contracts and operations"; - break; + this.startWork(p, new SleeveInfiltrateWork()); + return true; case "Support main sleeve": - p.bladeburner?.sleeveSupport(true); - time = 0; - break; + this.startWork(p, new SleeveSupportWork(p)); + return true; 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.startWork(p, new SleeveBladeburnerWork({ type: "Contracts", name: contract })); + return true; } - this.bbAction = capitalizeFirstLetter(action.toLowerCase()); - this.currentTaskMaxTime = time; - this.currentTask = SleeveTaskType.Bladeburner; return true; } @@ -1158,38 +645,38 @@ export class Sleeve extends Person { } } - contractGainRates(p: IPlayer, type: string, name: string): void { - const bb = p.bladeburner; - if (bb === null) { - const errorLogText = `bladeburner is null`; - console.error(`Function: sleeves.contractGainRates; Message: '${errorLogText}'`); - return; - } - const actionIdent = bb.getActionIdFromTypeAndName(type, name); - if (actionIdent === null) { - const errorLogText = `Invalid action: type='${type}' name='${name}'`; - console.error(`Function: sleeves.contractGainRates; Message: '${errorLogText}'`); - this.resetTaskStatus(p); - return; - } - const action = bb.getActionObject(actionIdent); - if (action === null) { - const errorLogText = `Invalid action: type='${type}' name='${name}'`; - console.error(`Function: sleeves.contractGainRates; Message: '${errorLogText}'`); - this.resetTaskStatus(p); - return; - } - const retValue = bb.getActionStats(action, true); - this.gainRatesForTask.hack = retValue.hack; - this.gainRatesForTask.str = retValue.str; - this.gainRatesForTask.def = retValue.def; - this.gainRatesForTask.dex = retValue.dex; - this.gainRatesForTask.agi = retValue.agi; - this.gainRatesForTask.cha = retValue.cha; - const rewardMultiplier = Math.pow(action.rewardFac, action.level - 1); - this.gainRatesForTask.money = - BladeburnerConstants.ContractBaseMoneyGain * rewardMultiplier * bb.skillMultipliers.money; - } + // contractGainRates(p: IPlayer, type: string, name: string): void { + // const bb = p.bladeburner; + // if (bb === null) { + // const errorLogText = `bladeburner is null`; + // console.error(`Function: sleeves.contractGainRates; Message: '${errorLogText}'`); + // return; + // } + // const actionIdent = bb.getActionIdFromTypeAndName(type, name); + // if (actionIdent === null) { + // const errorLogText = `Invalid action: type='${type}' name='${name}'`; + // console.error(`Function: sleeves.contractGainRates; Message: '${errorLogText}'`); + // this.resetTaskStatus(p); + // return; + // } + // const action = bb.getActionObject(actionIdent); + // if (action === null) { + // const errorLogText = `Invalid action: type='${type}' name='${name}'`; + // console.error(`Function: sleeves.contractGainRates; Message: '${errorLogText}'`); + // this.resetTaskStatus(p); + // return; + // } + // const retValue = bb.getActionStats(action, true); + // this.gainRatesForTask.hack = retValue.hack; + // this.gainRatesForTask.str = retValue.str; + // this.gainRatesForTask.def = retValue.def; + // this.gainRatesForTask.dex = retValue.dex; + // this.gainRatesForTask.agi = retValue.agi; + // this.gainRatesForTask.cha = retValue.cha; + // const rewardMultiplier = Math.pow(action.rewardFac, action.level - 1); + // this.gainRatesForTask.money = + // BladeburnerConstants.ContractBaseMoneyGain * rewardMultiplier * bb.skillMultipliers.money; + // } getBladeburnerActionTime(p: IPlayer, type: string, name: string): number { //Maybe find workerscript and use original diff --git a/src/PersonObjects/Sleeve/Work/SleeveBladeburnerWork.ts b/src/PersonObjects/Sleeve/Work/SleeveBladeburnerWork.ts new file mode 100644 index 000000000..16a6f98cd --- /dev/null +++ b/src/PersonObjects/Sleeve/Work/SleeveBladeburnerWork.ts @@ -0,0 +1,74 @@ +import { IPlayer } from "../../IPlayer"; +import { Generic_fromJSON, Generic_toJSON, IReviverValue, Reviver } from "../../../utils/JSONReviver"; +import { Sleeve } from "../Sleeve"; +import { Work, WorkType } from "./Work"; +import { CONSTANTS } from "../../../Constants"; +import { GeneralActions } from "../../../Bladeburner/data/GeneralActions"; +import { applyWorkStatsExp, WorkStats } from "../../../Work/WorkStats"; + +interface SleeveBladeburnerWorkParams { + type: string; + name: string; +} + +export const isSleeveBladeburnerWork = (w: Work | null): w is SleeveBladeburnerWork => + w !== null && w.type === WorkType.BLADEBURNER; + +export class SleeveBladeburnerWork extends Work { + cyclesWorked = 0; + actionType: string; + actionName: string; + + constructor(params?: SleeveBladeburnerWorkParams) { + super(WorkType.BLADEBURNER); + this.actionType = params?.type ?? "General"; + this.actionName = params?.name ?? "Field analysis"; + } + + cyclesNeeded(player: IPlayer, sleeve: Sleeve): number { + const ret = player.bladeburner?.getActionTimeNetscriptFn(sleeve, this.actionType, this.actionName); + if (!ret || typeof ret === "string") throw new Error(`Error querying ${this.actionName} time`); + return ret / CONSTANTS._idleSpeed; + } + + process(player: IPlayer, sleeve: Sleeve, cycles: number): number { + if (!player.bladeburner) throw new Error("sleeve doing blade work without being a member"); + this.cyclesWorked += cycles; + while (this.cyclesWorked > this.cyclesNeeded(player, sleeve)) { + const actionIdent = player.bladeburner.getActionIdFromTypeAndName(this.actionType, this.actionName); + if (!actionIdent) throw new Error(`Error getting ${this.actionName} action`); + player.bladeburner.completeAction(player, sleeve, actionIdent, false); + let exp: WorkStats | undefined; + if (this.actionType === "General") { + exp = GeneralActions[this.actionName]?.exp; + if (!exp) throw new Error(`Somehow there was no exp for action ${this.actionType} ${this.actionName}`); + applyWorkStatsExp(sleeve, exp, 1); + } + this.cyclesWorked -= this.cyclesNeeded(player, sleeve); + } + return 0; + } + + APICopy(): Record { + return { + actionType: this.actionType, + actionName: this.actionName, + }; + } + + /** + * Serialize the current object to a JSON save state. + */ + toJSON(): IReviverValue { + return Generic_toJSON("SleeveBladeburnerWork", this); + } + + /** + * Initiatizes a BladeburnerWork object from a JSON save state. + */ + static fromJSON(value: IReviverValue): SleeveBladeburnerWork { + return Generic_fromJSON(SleeveBladeburnerWork, value.data); + } +} + +Reviver.constructors.SleeveBladeburnerWork = SleeveBladeburnerWork; diff --git a/src/PersonObjects/Sleeve/Work/SleeveClassWork.ts b/src/PersonObjects/Sleeve/Work/SleeveClassWork.ts new file mode 100644 index 000000000..6c6be3b8c --- /dev/null +++ b/src/PersonObjects/Sleeve/Work/SleeveClassWork.ts @@ -0,0 +1,71 @@ +import { IPlayer } from "../../IPlayer"; +import { Generic_fromJSON, Generic_toJSON, IReviverValue, Reviver } from "../../../utils/JSONReviver"; +import { Work, WorkType } from "./Work"; +import { ClassType } from "../../../Work/ClassWork"; +import { LocationName } from "../../../Locations/data/LocationNames"; +import { calculateClassEarnings } from "../../../Work/formulas/Class"; +import { Sleeve } from "../Sleeve"; +import { applyWorkStats, applyWorkStatsExp, scaleWorkStats, WorkStats } from "../../../Work/WorkStats"; + +export const isSleeveClassWork = (w: Work | null): w is SleeveClassWork => w !== null && w.type === WorkType.CLASS; + +interface ClassWorkParams { + classType: ClassType; + location: LocationName; +} + +export class SleeveClassWork extends Work { + classType: ClassType; + location: LocationName; + + constructor(params?: ClassWorkParams) { + super(WorkType.CLASS); + this.classType = params?.classType ?? ClassType.StudyComputerScience; + this.location = params?.location ?? LocationName.Sector12RothmanUniversity; + } + + calculateRates(player: IPlayer, sleeve: Sleeve): WorkStats { + return scaleWorkStats( + calculateClassEarnings(player, sleeve, this.classType, this.location), + sleeve.shockBonus(), + false, + ); + } + + isGym(): boolean { + return [ClassType.GymAgility, ClassType.GymDefense, ClassType.GymDexterity, ClassType.GymStrength].includes( + this.classType, + ); + } + + process(player: IPlayer, sleeve: Sleeve, cycles: number): number { + let rate = this.calculateRates(player, sleeve); + applyWorkStatsExp(sleeve, rate, cycles); + rate = scaleWorkStats(rate, sleeve.syncBonus(), false); + applyWorkStats(player, player, rate, cycles, "sleeves"); + player.sleeves.filter((s) => s != sleeve).forEach((s) => applyWorkStatsExp(s, rate, cycles)); + return 0; + } + APICopy(): Record { + return { + type: this.type, + classType: this.classType, + location: this.location, + }; + } + /** + * Serialize the current object to a JSON save state. + */ + toJSON(): IReviverValue { + return Generic_toJSON("SleeveClassWork", this); + } + + /** + * Initiatizes a ClassWork object from a JSON save state. + */ + static fromJSON(value: IReviverValue): SleeveClassWork { + return Generic_fromJSON(SleeveClassWork, value.data); + } +} + +Reviver.constructors.SleeveClassWork = SleeveClassWork; diff --git a/src/PersonObjects/Sleeve/Work/SleeveCompanyWork.ts b/src/PersonObjects/Sleeve/Work/SleeveCompanyWork.ts new file mode 100644 index 000000000..9aeb1ca41 --- /dev/null +++ b/src/PersonObjects/Sleeve/Work/SleeveCompanyWork.ts @@ -0,0 +1,70 @@ +import { IPlayer } from "../../IPlayer"; +import { Generic_fromJSON, Generic_toJSON, IReviverValue, Reviver } from "../../../utils/JSONReviver"; +import { Sleeve } from "../Sleeve"; +import { Work, WorkType } from "./Work"; +import { LocationName } from "../../../Locations/data/LocationNames"; +import { Companies } from "../../../Company/Companies"; +import { Company } from "../../../Company/Company"; +import { calculateCompanyWorkStats } from "../../../Work/formulas/Company"; +import { applyWorkStats, applyWorkStatsExp, WorkStats } from "../../../Work/WorkStats"; +import { influenceStockThroughCompanyWork } from "../../../StockMarket/PlayerInfluencing"; + +interface SleeveCompanyWorkParams { + companyName: string; +} + +export const isSleeveCompanyWork = (w: Work | null): w is SleeveCompanyWork => + w !== null && w.type === WorkType.COMPANY; + +export class SleeveCompanyWork extends Work { + companyName: string; + + constructor(params?: SleeveCompanyWorkParams) { + super(WorkType.COMPANY); + this.companyName = params?.companyName ?? LocationName.NewTokyoNoodleBar; + } + + getCompany(): Company { + const c = Companies[this.companyName]; + if (!c) throw new Error(`Company not found: '${this.companyName}'`); + return c; + } + + getGainRates(player: IPlayer, sleeve: Sleeve): WorkStats { + return calculateCompanyWorkStats(player, sleeve, this.getCompany()); + } + + process(player: IPlayer, sleeve: Sleeve, cycles: number): number { + const company = this.getCompany(); + const gains = this.getGainRates(player, sleeve); + applyWorkStatsExp(sleeve, gains, cycles); + applyWorkStats(player, player, gains, cycles, "sleeves"); + player.sleeves.filter((s) => s != sleeve).forEach((s) => applyWorkStatsExp(s, gains, cycles)); + company.playerReputation += gains.reputation * cycles; + influenceStockThroughCompanyWork(company, gains.reputation, cycles); + return 0; + } + + APICopy(): Record { + return { + type: this.type, + companyName: this.companyName, + }; + } + + /** + * Serialize the current object to a JSON save state. + */ + toJSON(): IReviverValue { + return Generic_toJSON("SleeveCompanyWork", this); + } + + /** + * Initiatizes a CompanyWork object from a JSON save state. + */ + static fromJSON(value: IReviverValue): SleeveCompanyWork { + return Generic_fromJSON(SleeveCompanyWork, value.data); + } +} + +Reviver.constructors.SleeveCompanyWork = SleeveCompanyWork; diff --git a/src/PersonObjects/Sleeve/Work/SleeveCrimeWork.ts b/src/PersonObjects/Sleeve/Work/SleeveCrimeWork.ts new file mode 100644 index 000000000..f89a6f5fe --- /dev/null +++ b/src/PersonObjects/Sleeve/Work/SleeveCrimeWork.ts @@ -0,0 +1,84 @@ +import { IPlayer } from "../../IPlayer"; +import { Generic_fromJSON, Generic_toJSON, IReviverValue, Reviver } from "../../../utils/JSONReviver"; +import { Sleeve } from "../Sleeve"; +import { Work, WorkType } from "./Work"; +import { CrimeType } from "../../../utils/WorkType"; +import { Crimes } from "../../../Crime/Crimes"; +import { Crime } from "../../../Crime/Crime"; +import { applyWorkStats, newWorkStats, scaleWorkStats, WorkStats } from "../../../Work/WorkStats"; +import { CONSTANTS } from "../../../Constants"; + +export const isSleeveCrimeWork = (w: Work | null): w is SleeveCrimeWork => w !== null && w.type === WorkType.CRIME; + +export class SleeveCrimeWork extends Work { + crimeType: CrimeType; + cyclesWorked = 0; + constructor(crimeType?: CrimeType) { + super(WorkType.CRIME); + this.crimeType = crimeType ?? CrimeType.SHOPLIFT; + } + + getCrime(): Crime { + const crime = Object.values(Crimes).find((crime) => crime.type === this.crimeType); + if (!crime) throw new Error("crime should not be undefined"); + return crime; + } + + getExp(): WorkStats { + const crime = this.getCrime(); + return newWorkStats({ + money: crime.money, + hackExp: crime.hacking_exp, + strExp: crime.strength_exp, + defExp: crime.defense_exp, + dexExp: crime.dexterity_exp, + agiExp: crime.agility_exp, + chaExp: crime.charisma_exp, + intExp: crime.intelligence_exp, + }); + } + + cyclesNeeded(): number { + return this.getCrime().time / CONSTANTS._idleSpeed; + } + + process(player: IPlayer, sleeve: Sleeve, cycles: number): number { + this.cyclesWorked += cycles; + + const crime = this.getCrime(); + const gains = this.getExp(); + if (this.cyclesWorked >= this.cyclesNeeded()) { + if (Math.random() < crime.successRate(sleeve)) { + applyWorkStats(player, sleeve, gains, 1, "sleeves"); + + player.karma -= crime.karma * sleeve.syncBonus(); + } else { + applyWorkStats(player, sleeve, scaleWorkStats(gains, 0.25), 1, "sleeves"); + } + this.cyclesWorked -= this.cyclesNeeded(); + } + return 0; + } + + APICopy(): Record { + return { + type: this.type, + }; + } + + /** + * Serialize the current object to a JSON save state. + */ + toJSON(): IReviverValue { + return Generic_toJSON("SleeveCrimeWork", this); + } + + /** + * Initiatizes a RecoveryWork object from a JSON save state. + */ + static fromJSON(value: IReviverValue): SleeveCrimeWork { + return Generic_fromJSON(SleeveCrimeWork, value.data); + } +} + +Reviver.constructors.SleeveCrimeWork = SleeveCrimeWork; diff --git a/src/PersonObjects/Sleeve/Work/SleeveFactionWork.ts b/src/PersonObjects/Sleeve/Work/SleeveFactionWork.ts new file mode 100644 index 000000000..ec732cdb5 --- /dev/null +++ b/src/PersonObjects/Sleeve/Work/SleeveFactionWork.ts @@ -0,0 +1,96 @@ +import { IPlayer } from "../../IPlayer"; +import { Generic_fromJSON, Generic_toJSON, IReviverValue, Reviver } from "../../../utils/JSONReviver"; +import { Sleeve } from "../Sleeve"; +import { Work, WorkType } from "./Work"; +import { FactionWorkType } from "../../../Work/data/FactionWorkType"; +import { FactionNames } from "../../../Faction/data/FactionNames"; +import { Factions } from "../../../Faction/Factions"; +import { calculateFactionExp } from "../../../Work/formulas/Faction"; +import { applyWorkStatsExp, scaleWorkStats, WorkStats } from "../../../Work/WorkStats"; +import { Faction } from "../../../Faction/Faction"; +import { + getFactionFieldWorkRepGain, + getFactionSecurityWorkRepGain, + getHackingWorkRepGain, +} from "../../../PersonObjects/formulas/reputation"; + +interface SleeveFactionWorkParams { + factionWorkType: FactionWorkType; + factionName: string; +} + +export const isSleeveFactionWork = (w: Work | null): w is SleeveFactionWork => + w !== null && w.type === WorkType.FACTION; + +export class SleeveFactionWork extends Work { + factionWorkType: FactionWorkType; + factionName: string; + + constructor(params?: SleeveFactionWorkParams) { + super(WorkType.FACTION); + this.factionWorkType = params?.factionWorkType ?? FactionWorkType.HACKING; + this.factionName = params?.factionName ?? FactionNames.Sector12; + } + + getExpRates(sleeve: Sleeve): WorkStats { + return scaleWorkStats(calculateFactionExp(sleeve, this.factionWorkType), sleeve.shockBonus()); + } + + getReputationRate(sleeve: Sleeve): number { + const faction = this.getFaction(); + const repFormulas = { + [FactionWorkType.HACKING]: getHackingWorkRepGain, + [FactionWorkType.FIELD]: getFactionFieldWorkRepGain, + [FactionWorkType.SECURITY]: getFactionSecurityWorkRepGain, + }; + return repFormulas[this.factionWorkType](sleeve, faction) * sleeve.shockBonus(); + } + + getFaction(): Faction { + const f = Factions[this.factionName]; + if (!f) throw new Error(`Faction work started with invalid / unknown faction: '${this.factionName}'`); + return f; + } + + process(player: IPlayer, sleeve: Sleeve, cycles: number): number { + if (player.gang) { + if (this.factionName === player.gang.facName) { + sleeve.stopWork(player); + return 0; + } + } + + let exp = this.getExpRates(sleeve); + applyWorkStatsExp(sleeve, exp, cycles); + exp = scaleWorkStats(exp, sleeve.syncBonus()); + applyWorkStatsExp(player, exp, cycles); + player.sleeves.filter((s) => s != sleeve).forEach((s) => applyWorkStatsExp(s, exp, cycles)); + const rep = this.getReputationRate(sleeve); + this.getFaction().playerReputation += rep; + return 0; + } + + APICopy(): Record { + return { + type: this.type, + factionWorkType: this.factionWorkType, + factionName: this.factionName, + }; + } + + /** + * Serialize the current object to a JSON save state. + */ + toJSON(): IReviverValue { + return Generic_toJSON("SleeveFactionWork", this); + } + + /** + * Initiatizes a FactionWork object from a JSON save state. + */ + static fromJSON(value: IReviverValue): SleeveFactionWork { + return Generic_fromJSON(SleeveFactionWork, value.data); + } +} + +Reviver.constructors.SleeveFactionWork = SleeveFactionWork; diff --git a/src/PersonObjects/Sleeve/Work/SleeveInfiltrateWork.ts b/src/PersonObjects/Sleeve/Work/SleeveInfiltrateWork.ts new file mode 100644 index 000000000..4d196165b --- /dev/null +++ b/src/PersonObjects/Sleeve/Work/SleeveInfiltrateWork.ts @@ -0,0 +1,54 @@ +import { IPlayer } from "../../IPlayer"; +import { Generic_fromJSON, Generic_toJSON, IReviverValue, Reviver } from "../../../utils/JSONReviver"; +import { Sleeve } from "../Sleeve"; +import { Work, WorkType } from "./Work"; +import { CONSTANTS } from "../../../Constants"; + +const infiltrateCycles = 600000 / CONSTANTS._idleSpeed; + +export const isSleeveInfiltrateWork = (w: Work | null): w is SleeveInfiltrateWork => + w !== null && w.type === WorkType.INFILTRATE; + +export class SleeveInfiltrateWork extends Work { + cyclesWorked = 0; + + constructor() { + super(WorkType.INFILTRATE); + } + + cyclesNeeded(): number { + return infiltrateCycles; + } + + process(player: IPlayer, sleeve: Sleeve, cycles: number): number { + if (!player.bladeburner) throw new Error("sleeve doing blade work without being a member"); + this.cyclesWorked += cycles; + if (this.cyclesWorked > this.cyclesNeeded()) { + this.cyclesWorked -= this.cyclesNeeded(); + player.bladeburner.infiltrateSynthoidCommunities(player); + } + return 0; + } + + APICopy(): Record { + return { + type: this.type, + }; + } + + /** + * Serialize the current object to a JSON save state. + */ + toJSON(): IReviverValue { + return Generic_toJSON("SleeveInfiltrateWork", this); + } + + /** + * Initiatizes a BladeburnerWork object from a JSON save state. + */ + static fromJSON(value: IReviverValue): SleeveInfiltrateWork { + return Generic_fromJSON(SleeveInfiltrateWork, value.data); + } +} + +Reviver.constructors.SleeveInfiltrateWork = SleeveInfiltrateWork; diff --git a/src/PersonObjects/Sleeve/Work/SleeveRecoveryWork.ts b/src/PersonObjects/Sleeve/Work/SleeveRecoveryWork.ts new file mode 100644 index 000000000..5f0de0a5d --- /dev/null +++ b/src/PersonObjects/Sleeve/Work/SleeveRecoveryWork.ts @@ -0,0 +1,41 @@ +import { IPlayer } from "../../IPlayer"; +import { Generic_fromJSON, Generic_toJSON, IReviverValue, Reviver } from "../../../utils/JSONReviver"; +import { Sleeve } from "../Sleeve"; +import { Work, WorkType } from "./Work"; + +export const isSleeveRecoveryWork = (w: Work | null): w is SleeveRecoveryWork => + w !== null && w.type === WorkType.RECOVERY; + +export class SleeveRecoveryWork extends Work { + constructor() { + super(WorkType.RECOVERY); + } + + process(player: IPlayer, sleeve: Sleeve, cycles: number): number { + sleeve.shock = Math.min(100, sleeve.shock + 0.0002 * cycles); + if (sleeve.shock >= 100) sleeve.stopWork(player); + return 0; + } + + APICopy(): Record { + return { + type: this.type, + }; + } + + /** + * Serialize the current object to a JSON save state. + */ + toJSON(): IReviverValue { + return Generic_toJSON("SleeveRecoveryWork", this); + } + + /** + * Initiatizes a RecoveryWork object from a JSON save state. + */ + static fromJSON(value: IReviverValue): SleeveRecoveryWork { + return Generic_fromJSON(SleeveRecoveryWork, value.data); + } +} + +Reviver.constructors.SleeveRecoveryWork = SleeveRecoveryWork; diff --git a/src/PersonObjects/Sleeve/Work/SleeveSupportWork.ts b/src/PersonObjects/Sleeve/Work/SleeveSupportWork.ts new file mode 100644 index 000000000..5dfa02b7c --- /dev/null +++ b/src/PersonObjects/Sleeve/Work/SleeveSupportWork.ts @@ -0,0 +1,43 @@ +import { IPlayer } from "../../../PersonObjects/IPlayer"; +import { Generic_fromJSON, Generic_toJSON, IReviverValue, Reviver } from "../../../utils/JSONReviver"; +import { Work, WorkType } from "./Work"; + +export const isSleeveSupportWork = (w: Work | null): w is SleeveSupportWork => + w !== null && w.type === WorkType.SUPPORT; + +export class SleeveSupportWork extends Work { + constructor(player?: IPlayer) { + super(WorkType.SUPPORT); + if (player) player.bladeburner?.sleeveSupport(true); + } + + process(): number { + return 0; + } + + finish(player: IPlayer): void { + player.bladeburner?.sleeveSupport(false); + } + + APICopy(): Record { + return { + type: this.type, + }; + } + + /** + * Serialize the current object to a JSON save state. + */ + toJSON(): IReviverValue { + return Generic_toJSON("SleeveSupportWork", this); + } + + /** + * Initiatizes a BladeburnerWork object from a JSON save state. + */ + static fromJSON(value: IReviverValue): SleeveSupportWork { + return Generic_fromJSON(SleeveSupportWork, value.data); + } +} + +Reviver.constructors.SleeveSupportWork = SleeveSupportWork; diff --git a/src/PersonObjects/Sleeve/Work/SleeveSynchroWork.ts b/src/PersonObjects/Sleeve/Work/SleeveSynchroWork.ts new file mode 100644 index 000000000..09b5584d3 --- /dev/null +++ b/src/PersonObjects/Sleeve/Work/SleeveSynchroWork.ts @@ -0,0 +1,41 @@ +import { IPlayer } from "../../IPlayer"; +import { Generic_fromJSON, Generic_toJSON, IReviverValue, Reviver } from "../../../utils/JSONReviver"; +import { Sleeve } from "../Sleeve"; +import { Work, WorkType } from "./Work"; + +export const isSleeveSynchroWork = (w: Work | null): w is SleeveSynchroWork => + w !== null && w.type === WorkType.SYNCHRO; + +export class SleeveSynchroWork extends Work { + constructor() { + super(WorkType.SYNCHRO); + } + + process(player: IPlayer, sleeve: Sleeve, cycles: number): number { + sleeve.sync = Math.min(100, sleeve.sync + player.getIntelligenceBonus(0.5) * 0.0002 * cycles); + if (sleeve.sync >= 100) sleeve.stopWork(player); + return 0; + } + + APICopy(): Record { + return { + type: this.type, + }; + } + + /** + * Serialize the current object to a JSON save state. + */ + toJSON(): IReviverValue { + return Generic_toJSON("SleeveSynchroWork", this); + } + + /** + * Initiatizes a SynchroWork object from a JSON save state. + */ + static fromJSON(value: IReviverValue): SleeveSynchroWork { + return Generic_fromJSON(SleeveSynchroWork, value.data); + } +} + +Reviver.constructors.SleeveSynchroWork = SleeveSynchroWork; diff --git a/src/PersonObjects/Sleeve/Work/Work.ts b/src/PersonObjects/Sleeve/Work/Work.ts new file mode 100644 index 000000000..37703df1a --- /dev/null +++ b/src/PersonObjects/Sleeve/Work/Work.ts @@ -0,0 +1,30 @@ +import { IPlayer } from "../../IPlayer"; +import { IReviverValue } from "../../../utils/JSONReviver"; +import { Sleeve } from "../Sleeve"; + +export abstract class Work { + type: WorkType; + + constructor(type: WorkType) { + this.type = type; + } + + abstract process(player: IPlayer, sleeve: Sleeve, cycles: number): number; + abstract APICopy(): Record; + abstract toJSON(): IReviverValue; + finish(_player: IPlayer): void { + /* left for children to implement */ + } +} + +export enum WorkType { + COMPANY = "COMPANY", + FACTION = "FACTION", + CRIME = "CRIME", + CLASS = "CLASS", + RECOVERY = "RECOVERY", + SYNCHRO = "SYNCHRO", + BLADEBURNER = "BLADEBURNER", + INFILTRATE = "INFILTRATE", + SUPPORT = "SUPPORT", +} diff --git a/src/PersonObjects/Sleeve/ui/MoreEarningsModal.tsx b/src/PersonObjects/Sleeve/ui/MoreEarningsModal.tsx deleted file mode 100644 index 7b0c76654..000000000 --- a/src/PersonObjects/Sleeve/ui/MoreEarningsModal.tsx +++ /dev/null @@ -1,58 +0,0 @@ -import { Sleeve } from "../Sleeve"; -import { numeralWrapper } from "../../../ui/numeralFormat"; -import { Money } from "../../../ui/React/Money"; -import * as React from "react"; -import { StatsTable } from "../../../ui/React/StatsTable"; -import { Modal } from "../../../ui/React/Modal"; - -interface IProps { - open: boolean; - onClose: () => void; - sleeve: Sleeve; -} - -export function MoreEarningsModal(props: IProps): React.ReactElement { - return ( - - ], - ["Hacking Exp ", numeralWrapper.formatExp(props.sleeve.earningsForTask.hack)], - ["Strength Exp ", numeralWrapper.formatExp(props.sleeve.earningsForTask.str)], - ["Defense Exp ", numeralWrapper.formatExp(props.sleeve.earningsForTask.def)], - ["Dexterity Exp ", numeralWrapper.formatExp(props.sleeve.earningsForTask.dex)], - ["Agility Exp ", numeralWrapper.formatExp(props.sleeve.earningsForTask.agi)], - ["Charisma Exp ", numeralWrapper.formatExp(props.sleeve.earningsForTask.cha)], - ]} - title="Earnings for Current Task:" - /> -
- ], - ["Hacking Exp: ", numeralWrapper.formatExp(props.sleeve.earningsForPlayer.hack)], - ["Strength Exp: ", numeralWrapper.formatExp(props.sleeve.earningsForPlayer.str)], - ["Defense Exp: ", numeralWrapper.formatExp(props.sleeve.earningsForPlayer.def)], - ["Dexterity Exp: ", numeralWrapper.formatExp(props.sleeve.earningsForPlayer.dex)], - ["Agility Exp: ", numeralWrapper.formatExp(props.sleeve.earningsForPlayer.agi)], - ["Charisma Exp: ", numeralWrapper.formatExp(props.sleeve.earningsForPlayer.cha)], - ]} - title="Total Earnings for Host Consciousness:" - /> -
- ], - ["Hacking Exp: ", numeralWrapper.formatExp(props.sleeve.earningsForSleeves.hack)], - ["Strength Exp: ", numeralWrapper.formatExp(props.sleeve.earningsForSleeves.str)], - ["Defense Exp: ", numeralWrapper.formatExp(props.sleeve.earningsForSleeves.def)], - ["Dexterity Exp: ", numeralWrapper.formatExp(props.sleeve.earningsForSleeves.dex)], - ["Agility Exp: ", numeralWrapper.formatExp(props.sleeve.earningsForSleeves.agi)], - ["Charisma Exp: ", numeralWrapper.formatExp(props.sleeve.earningsForSleeves.cha)], - ]} - title="Total Earnings for Other Sleeves:" - /> -
-
- ); -} diff --git a/src/PersonObjects/Sleeve/ui/SleeveElem.tsx b/src/PersonObjects/Sleeve/ui/SleeveElem.tsx index 64853d7f9..8edf74759 100644 --- a/src/PersonObjects/Sleeve/ui/SleeveElem.tsx +++ b/src/PersonObjects/Sleeve/ui/SleeveElem.tsx @@ -2,18 +2,24 @@ import { Box, Button, Paper, Tooltip, Typography } from "@mui/material"; import React, { useState } from "react"; import { FactionWorkType } from "../../../Work/data/FactionWorkType"; import { CONSTANTS } from "../../../Constants"; -import { Crimes } from "../../../Crime/Crimes"; import { use } from "../../../ui/Context"; import { numeralWrapper } from "../../../ui/numeralFormat"; import { ProgressBar } from "../../../ui/React/Progress"; import { Sleeve } from "../Sleeve"; -import { SleeveTaskType } from "../SleeveTaskTypesEnum"; -import { MoreEarningsModal } from "./MoreEarningsModal"; import { MoreStatsModal } from "./MoreStatsModal"; import { SleeveAugmentationsModal } from "./SleeveAugmentationsModal"; import { EarningsElement, StatsElement } from "./StatsElement"; import { TaskSelector } from "./TaskSelector"; import { TravelModal } from "./TravelModal"; +import { isSleeveClassWork } from "../Work/SleeveClassWork"; +import { isSleeveSynchroWork } from "../Work/SleeveSynchroWork"; +import { isSleeveRecoveryWork } from "../Work/SleeveRecoveryWork"; +import { isSleeveFactionWork } from "../Work/SleeveFactionWork"; +import { isSleeveCompanyWork } from "../Work/SleeveCompanyWork"; +import { isSleeveInfiltrateWork } from "../Work/SleeveInfiltrateWork"; +import { isSleeveSupportWork } from "../Work/SleeveSupportWork"; +import { isSleeveBladeburnerWork } from "../Work/SleeveBladeburnerWork"; +import { isSleeveCrimeWork } from "../Work/SleeveCrimeWork"; interface IProps { sleeve: Sleeve; @@ -23,7 +29,6 @@ interface IProps { export function SleeveElem(props: IProps): React.ReactElement { const player = use.Player(); const [statsOpen, setStatsOpen] = useState(false); - const [earningsOpen, setEarningsOpen] = useState(false); const [travelOpen, setTravelOpen] = useState(false); const [augmentationsOpen, setAugmentationsOpen] = useState(false); @@ -64,83 +69,85 @@ export function SleeveElem(props: IProps): React.ReactElement { props.rerender(); } - let desc = <>; - switch (props.sleeve.currentTask) { - case SleeveTaskType.Idle: - desc = <>This sleeve is currently idle; - break; - case SleeveTaskType.Company: - desc = <>This sleeve is currently working your job at {props.sleeve.currentTaskLocation}.; - break; - case SleeveTaskType.Faction: { - let doing = "nothing"; - switch (props.sleeve.factionWorkType) { - case FactionWorkType.FIELD: - doing = "Field work"; - break; - case FactionWorkType.HACKING: - doing = "Hacking contracts"; - break; - case FactionWorkType.SECURITY: - doing = "Security work"; - break; - } - desc = ( - <> - This sleeve is currently doing {doing} for {props.sleeve.currentTaskLocation}. - - ); - break; + let desc = <>This sleeve is currently idle; + + if (isSleeveCrimeWork(props.sleeve.currentWork)) { + const w = props.sleeve.currentWork; + const crime = w.getCrime(); + desc = ( + <> + This sleeve is currently attempting to {crime.type} (Success Rate:{" "} + {numeralWrapper.formatPercentage(crime.successRate(props.sleeve))}). + + ); + } + + if (isSleeveClassWork(props.sleeve.currentWork)) { + if (props.sleeve.currentWork.isGym()) + desc = <>This sleeve is currently working out at {props.sleeve.currentWork.location}.; + else desc = <>This sleeve is currently studying at {props.sleeve.currentWork.location}.; + } + if (isSleeveSynchroWork(props.sleeve.currentWork)) { + desc = ( + <> + This sleeve is currently set to synchronize with the original consciousness. This causes the Sleeve's + synchronization to increase. + + ); + } + if (isSleeveRecoveryWork(props.sleeve.currentWork)) { + desc = ( + <> + This sleeve is currently set to focus on shock recovery. This causes the Sleeve's shock to decrease at a faster + rate. + + ); + } + if (isSleeveFactionWork(props.sleeve.currentWork)) { + let doing = "nothing"; + switch (props.sleeve.currentWork.factionWorkType) { + case FactionWorkType.FIELD: + doing = "Field work"; + break; + case FactionWorkType.HACKING: + doing = "Hacking contracts"; + break; + case FactionWorkType.SECURITY: + doing = "Security work"; + break; } - case SleeveTaskType.Crime: { - const crime = Object.values(Crimes).find((crime) => crime.name === props.sleeve.crimeType); - if (!crime) throw new Error("crime should not be undefined"); - desc = ( - <> - This sleeve is currently attempting to {crime.type} (Success Rate:{" "} - {numeralWrapper.formatPercentage(crime.successRate(props.sleeve))}). - - ); - break; - } - case SleeveTaskType.Class: - desc = <>This sleeve is currently studying/taking a course at {props.sleeve.currentTaskLocation}.; - break; - case SleeveTaskType.Gym: - desc = <>This sleeve is currently working out at {props.sleeve.currentTaskLocation}.; - break; - case SleeveTaskType.Bladeburner: { - let message = ""; - if (props.sleeve.bbContract !== "------") { - message = ` - ${props.sleeve.bbContract} (Success Rate: ${props.sleeve.currentTaskLocation})`; - } else if (props.sleeve.currentTaskLocation !== "") { - message = props.sleeve.currentTaskLocation; - } - desc = ( - <> - This sleeve is currently attempting to {props.sleeve.bbAction}. {message} - - ); - break; - } - case SleeveTaskType.Recovery: - desc = ( - <> - This sleeve is currently set to focus on shock recovery. This causes the Sleeve's shock to decrease at a - faster rate. - - ); - break; - case SleeveTaskType.Synchro: - desc = ( - <> - This sleeve is currently set to synchronize with the original consciousness. This causes the Sleeve's - synchronization to increase. - - ); - break; - default: - console.error(`Invalid/Unrecognized taskValue in updateSleeveTaskDescription(): ${abc[0]}`); + desc = ( + <> + This sleeve is currently doing {doing} for {props.sleeve.currentWork.factionName}. + + ); + } + if (isSleeveCompanyWork(props.sleeve.currentWork)) { + desc = <>This sleeve is currently working your job at {props.sleeve.currentWork.companyName}.; + } + + if (isSleeveBladeburnerWork(props.sleeve.currentWork)) { + const w = props.sleeve.currentWork; + desc = ( + <> + This sleeve is currently attempting to perform {w.actionName}. ( + {((100 * w.cyclesWorked) / w.cyclesNeeded(player, props.sleeve)).toFixed(2)}%) + + ); + } + + if (isSleeveInfiltrateWork(props.sleeve.currentWork)) { + const w = props.sleeve.currentWork; + desc = ( + <> + This sleeve is currently attempting to infiltrate synthoids communities. ( + {((100 * w.cyclesWorked) / w.cyclesNeeded()).toFixed(2)}%) + + ); + } + + if (isSleeveSupportWork(props.sleeve.currentWork)) { + desc = <>This sleeve is currently supporting you in your bladeburner activities.; } return ( @@ -150,7 +157,6 @@ export function SleeveElem(props: IProps): React.ReactElement { - Insufficient funds : ""}>