diff --git a/src/PersonObjects/Sleeve/Sleeve.ts b/src/PersonObjects/Sleeve/Sleeve.ts index 4f6f5cf30..0088a7acc 100644 --- a/src/PersonObjects/Sleeve/Sleeve.ts +++ b/src/PersonObjects/Sleeve/Sleeve.ts @@ -37,8 +37,18 @@ 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 { SleeveBladeburnerGeneralWork } from "./Work/SleeveBladeburnerGeneralActionWork"; +import { SleeveInfiltrateWork } from "./Work/SleeveInfiltrateWork"; export class Sleeve extends Person { + currentWork: Work | null = null; /** * Stores the name of the class that the player is currently taking */ @@ -149,6 +159,13 @@ export class Sleeve extends Person { } } + shockBonus(): number { + return this.shock / 100; + } + syncBonus(): number { + return this.sync / 100; + } + /** * Commit crimes */ @@ -184,9 +201,8 @@ export class Sleeve extends Person { /** * Called to stop the current task */ - finishTask(p: IPlayer): ITaskTracker { - let retValue: ITaskTracker = createTaskTracker(); // Amount of exp to be gained by other sleeves - + finishTask(p: IPlayer): void { + this.currentWork = null; if (this.currentTask === SleeveTaskType.Crime) { // For crimes, all experience and money is gained at the end if (this.currentTaskTime >= this.currentTaskMaxTime) { @@ -194,7 +210,7 @@ export class Sleeve extends Person { if (!crime) { console.error(`Invalid data stored in sleeve.crimeType: ${this.crimeType}`); this.resetTaskStatus(p); - return retValue; + return; } if (Math.random() < crime.successRate(this)) { // Success @@ -205,22 +221,22 @@ export class Sleeve extends Person { const key = keysForIteration[i]; successGainRates[key] = this.gainRatesForTask[key] * 2; } - retValue = this.gainExperience(p, successGainRates); + this.gainExperience(p, successGainRates); this.gainMoney(p, this.gainRatesForTask); p.karma -= crime.karma * (this.sync / 100); } else { - retValue = this.gainExperience(p, this.gainRatesForTask); + this.gainExperience(p, this.gainRatesForTask); } // Do not reset task to IDLE this.currentTaskTime = 0; - return retValue; + return; } } else if (this.currentTask === SleeveTaskType.Bladeburner) { if (this.currentTaskMaxTime === 0) { this.currentTaskTime = 0; - return retValue; + return; } // For bladeburner, all experience and money is gained at the end const bb = p.bladeburner; @@ -228,14 +244,14 @@ export class Sleeve extends Person { const errorLogText = `bladeburner is null`; console.error(`Function: sleeves.finishTask; Message: '${errorLogText}'`); this.resetTaskStatus(p); - return retValue; + return; } if (this.currentTaskTime >= this.currentTaskMaxTime) { if (this.bbAction === "Infiltrate synthoids") { bb.infiltrateSynthoidCommunities(p); this.currentTaskTime = 0; - return retValue; + return; } let type: string; let name: string; @@ -252,19 +268,19 @@ export class Sleeve extends Person { const errorLogText = `Invalid action: type='${type}' name='${name}'`; console.error(`Function: sleeves.finishTask; Message: '${errorLogText}'`); this.resetTaskStatus(p); - return retValue; + return; } 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.gainExperience(p, bbRetValue); this.gainMoney(p, bbRetValue); // Do not reset task to IDLE this.currentTaskTime = 0; - return retValue; + return; } } } @@ -272,7 +288,7 @@ export class Sleeve extends Person { this.resetTaskStatus(p); - return retValue; + return; } /** @@ -431,25 +447,7 @@ export class Sleeve extends Person { * 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; - } - - 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) { + if (this.currentTask === SleeveTaskType.Company) { const companyName: string = this.currentTaskLocation; const company: Company | null = Companies[companyName]; if (company == null) { @@ -532,12 +530,16 @@ 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); + if (this.currentWork) { + this.currentWork.process(p, this, cyclesUsed); + this.storedCycles -= cyclesUsed; + return; + } + let time = cyclesUsed * CONSTANTS.MilliPerCycle; if (this.currentTaskMaxTime !== 0 && this.currentTaskTime + time > this.currentTaskMaxTime) { time = this.currentTaskMaxTime - this.currentTaskTime; @@ -556,36 +558,6 @@ export class Sleeve extends Person { 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); @@ -599,16 +571,6 @@ export class Sleeve extends Person { 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) { @@ -630,15 +592,10 @@ export class Sleeve extends Person { * Resets all parameters used to keep information about the current task */ resetTaskStatus(p: IPlayer): void { + this.currentWork = null; 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; @@ -654,24 +611,22 @@ export class Sleeve extends Person { } shockRecovery(p: IPlayer): boolean { - if (this.currentTask !== SleeveTaskType.Idle) { + if (this.currentTask !== SleeveTaskType.Idle || this.currentWork === null) { this.finishTask(p); } else { this.resetTaskStatus(p); } - - this.currentTask = SleeveTaskType.Recovery; + this.currentWork = new SleeveRecoveryWork(); return true; } synchronize(p: IPlayer): boolean { - if (this.currentTask !== SleeveTaskType.Idle) { + if (this.currentTask !== SleeveTaskType.Idle || this.currentWork !== null) { this.finishTask(p); } else { this.resetTaskStatus(p); } - - this.currentTask = SleeveTaskType.Synchro; + this.currentWork = new SleeveSynchroWork(); return true; } @@ -679,7 +634,7 @@ export class Sleeve extends Person { * Take a course at a university */ takeUniversityCourse(p: IPlayer, universityName: string, className: string): boolean { - if (this.currentTask !== SleeveTaskType.Idle) { + if (this.currentTask !== SleeveTaskType.Idle || this.currentWork) { this.finishTask(p); } else { this.resetTaskStatus(p); @@ -687,58 +642,54 @@ export class Sleeve extends Person { // 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.currentWork = new SleeveClassWork({ + classType: classType, + location: loc, + }); return true; } @@ -767,99 +718,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,7 +736,7 @@ export class Sleeve extends Person { return false; } - if (this.currentTask !== SleeveTaskType.Idle) { + if (this.currentTask !== SleeveTaskType.Idle || this.currentWork !== null) { this.finishTask(p); } else { this.resetTaskStatus(p); @@ -886,50 +744,10 @@ export class Sleeve extends Person { 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.currentWork = new SleeveCompanyWork({ companyName: companyName }); return true; } @@ -944,7 +762,7 @@ export class Sleeve extends Person { return false; } - if (this.currentTask !== SleeveTaskType.Idle) { + if (this.currentTask !== SleeveTaskType.Idle || this.currentWork === null) { this.finishTask(p); } else { this.resetTaskStatus(p); @@ -953,40 +771,25 @@ export class Sleeve extends Person { 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.currentWork = new SleeveFactionWork({ + factionWorkType: factionWorkType, + factionName: faction.name, + }); return true; } @@ -995,7 +798,7 @@ export class Sleeve extends Person { * Begin a gym workout task */ workoutAtGym(p: IPlayer, gymName: string, stat: string): boolean { - if (this.currentTask !== SleeveTaskType.Idle) { + if (this.currentTask !== SleeveTaskType.Idle || this.currentWork) { this.finishTask(p); } else { this.resetTaskStatus(p); @@ -1003,73 +806,60 @@ export class Sleeve extends Person { // 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.currentWork = new SleeveClassWork({ + classType: classType, + location: loc, + }); return true; } @@ -1078,7 +868,7 @@ export class Sleeve extends Person { * Begin a bladeburner task */ bladeburner(p: IPlayer, action: string, contract: string): boolean { - if (this.currentTask !== SleeveTaskType.Idle) { + if (this.currentTask !== SleeveTaskType.Idle || this.currentWork === null) { this.finishTask(p); } else { this.resetTaskStatus(p); @@ -1098,24 +888,26 @@ export class Sleeve extends Person { 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; + // time = this.getBladeburnerActionTime(p, "General", action); + // this.gainRatesForTask.hack = 20 * this.mults.hacking_exp; + // this.gainRatesForTask.cha = 20 * this.mults.charisma_exp; + this.currentWork = new SleeveBladeburnerGeneralWork("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), - )})`; + // 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), + // )})`; + this.currentWork = new SleeveBladeburnerGeneralWork("Recruitment"); break; case "Diplomacy": - time = this.getBladeburnerActionTime(p, "General", action); + // time = this.getBladeburnerActionTime(p, "General", action); + this.currentWork = new SleeveBladeburnerGeneralWork("Diplomacy"); break; case "Infiltrate synthoids": - time = 60000; - this.currentTaskLocation = "This will generate additional contracts and operations"; + this.currentWork = new SleeveInfiltrateWork(); break; case "Support main sleeve": p.bladeburner?.sleeveSupport(true); diff --git a/src/PersonObjects/Sleeve/Work/SleeveBladeburnerGeneralActionWork.ts b/src/PersonObjects/Sleeve/Work/SleeveBladeburnerGeneralActionWork.ts new file mode 100644 index 000000000..7297a3455 --- /dev/null +++ b/src/PersonObjects/Sleeve/Work/SleeveBladeburnerGeneralActionWork.ts @@ -0,0 +1,59 @@ +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"; + +export const isSleeveBladeburnerGeneralWork = (w: Work | null): w is SleeveBladeburnerGeneralWork => + w !== null && w.type === WorkType.BLADEBURNER_GENERAL; + +export class SleeveBladeburnerGeneralWork extends Work { + cyclesWorked = 0; + action: string; + + constructor(action?: string) { + super(WorkType.BLADEBURNER_GENERAL); + this.action = action ?? "Field analysis"; + } + + cyclesNeeded(player: IPlayer, sleeve: Sleeve): number { + const ret = player.bladeburner?.getActionTimeNetscriptFn(sleeve, "General", this.action); + if (!ret || typeof ret === "string") throw new Error(`Error querying ${this.action} 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("General", this.action); + if (!actionIdent) throw new Error(`Error getting ${this.action} action`); + player.bladeburner.completeAction(player, sleeve, actionIdent, false); + this.cyclesWorked -= this.cyclesNeeded(player, sleeve); + } + return 0; + } + + APICopy(): Record { + return { + type: this.type, + action: this.action, + }; + } + + /** + * Serialize the current object to a JSON save state. + */ + toJSON(): IReviverValue { + return Generic_toJSON("SleeveBladeburnerGeneralWork", this); + } + + /** + * Initiatizes a BladeburnerWork object from a JSON save state. + */ + static fromJSON(value: IReviverValue): SleeveBladeburnerGeneralWork { + return Generic_fromJSON(SleeveBladeburnerGeneralWork, value.data); + } +} + +Reviver.constructors.SleeveBladeburnerGeneralWork = SleeveBladeburnerGeneralWork; 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/SleeveFactionWork.ts b/src/PersonObjects/Sleeve/Work/SleeveFactionWork.ts new file mode 100644 index 000000000..a372c3aaa --- /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.currentWork = null; + 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..db5a08a93 --- /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.currentWork = null; + 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/SleeveSynchroWork.ts b/src/PersonObjects/Sleeve/Work/SleeveSynchroWork.ts new file mode 100644 index 000000000..b70665401 --- /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.currentWork = null; + 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..89bb26d14 --- /dev/null +++ b/src/PersonObjects/Sleeve/Work/Work.ts @@ -0,0 +1,28 @@ +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; +} + +export enum WorkType { + COMPANY = "COMPANY", + FACTION = "FACTION", + CRIME = "CRIME", + CLASS = "CLASS", + RECOVERY = "RECOVERY", + SYNCHRO = "SYNCHRO", + BLADEBURNER_GENERAL = "BLADEBURNER_GENERAL", + INFILTRATE = "INFILTRATE", + BLADEBURNER_SUPPORT = "SUPPORT", + BLADEBURNER_CONTRACTS = "CONTRACTS", +} diff --git a/src/PersonObjects/Sleeve/ui/SleeveElem.tsx b/src/PersonObjects/Sleeve/ui/SleeveElem.tsx index 64853d7f9..1f961f5a8 100644 --- a/src/PersonObjects/Sleeve/ui/SleeveElem.tsx +++ b/src/PersonObjects/Sleeve/ui/SleeveElem.tsx @@ -14,6 +14,13 @@ 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 { isSleeveBladeburnerGeneralWork } from "../Work/SleeveBladeburnerGeneralActionWork"; +import { isSleeveInfiltrateWork } from "../Work/SleeveInfiltrateWork"; interface IProps { sleeve: Sleeve; @@ -69,29 +76,6 @@ export function SleeveElem(props: IProps): React.ReactElement { 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; - } 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"); @@ -106,9 +90,6 @@ export function SleeveElem(props: IProps): React.ReactElement { 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 !== "------") { @@ -123,26 +104,75 @@ export function SleeveElem(props: IProps): React.ReactElement { ); 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]}`); } + 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; + } + 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 (isSleeveBladeburnerGeneralWork(props.sleeve.currentWork)) { + const w = props.sleeve.currentWork; + desc = ( + <> + This sleeve is currently attempting to perform {w.action}. ( + {((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)}%) + + ); + } + return ( <> diff --git a/src/PersonObjects/Sleeve/ui/StatsElement.tsx b/src/PersonObjects/Sleeve/ui/StatsElement.tsx index 5851319b0..8f5e6edac 100644 --- a/src/PersonObjects/Sleeve/ui/StatsElement.tsx +++ b/src/PersonObjects/Sleeve/ui/StatsElement.tsx @@ -13,6 +13,9 @@ import { use } from "../../../ui/Context"; import { Sleeve } from "../Sleeve"; import { SleeveTaskType } from "../SleeveTaskTypesEnum"; +import { isSleeveClassWork } from "../Work/SleeveClassWork"; +import { isSleeveFactionWork } from "../Work/SleeveFactionWork"; +import { isSleeveCompanyWork } from "../Work/SleeveCompanyWork"; interface IProps { sleeve: Sleeve; @@ -124,13 +127,52 @@ export function EarningsElement(props: IProps): React.ReactElement { data.push([`Reputation:`, ]); } } + if (isSleeveClassWork(props.sleeve.currentWork)) { + const rates = props.sleeve.currentWork.calculateRates(player, props.sleeve); + data = [ + [`Money:`, ], + [`Hacking Exp:`, `${numeralWrapper.formatExp(5 * rates.hackExp)} / sec`], + [`Strength Exp:`, `${numeralWrapper.formatExp(5 * rates.strExp)} / sec`], + [`Defense Exp:`, `${numeralWrapper.formatExp(5 * rates.defExp)} / sec`], + [`Dexterity Exp:`, `${numeralWrapper.formatExp(5 * rates.dexExp)} / sec`], + [`Agility Exp:`, `${numeralWrapper.formatExp(5 * rates.agiExp)} / sec`], + [`Charisma Exp:`, `${numeralWrapper.formatExp(5 * rates.chaExp)} / sec`], + ]; + } + if (isSleeveFactionWork(props.sleeve.currentWork)) { + const rates = props.sleeve.currentWork.getExpRates(props.sleeve); + const repGain = props.sleeve.currentWork.getReputationRate(props.sleeve); + data = [ + [`Hacking Exp:`, `${numeralWrapper.formatExp(5 * rates.hackExp)} / sec`], + [`Strength Exp:`, `${numeralWrapper.formatExp(5 * rates.strExp)} / sec`], + [`Defense Exp:`, `${numeralWrapper.formatExp(5 * rates.defExp)} / sec`], + [`Dexterity Exp:`, `${numeralWrapper.formatExp(5 * rates.dexExp)} / sec`], + [`Agility Exp:`, `${numeralWrapper.formatExp(5 * rates.agiExp)} / sec`], + [`Charisma Exp:`, `${numeralWrapper.formatExp(5 * rates.chaExp)} / sec`], + [`Reputation:`, ], + ]; + } + + if (isSleeveCompanyWork(props.sleeve.currentWork)) { + const rates = props.sleeve.currentWork.getGainRates(player, props.sleeve); + data = [ + [`Money:`, ], + [`Hacking Exp:`, `${numeralWrapper.formatExp(5 * rates.hackExp)} / sec`], + [`Strength Exp:`, `${numeralWrapper.formatExp(5 * rates.strExp)} / sec`], + [`Defense Exp:`, `${numeralWrapper.formatExp(5 * rates.defExp)} / sec`], + [`Dexterity Exp:`, `${numeralWrapper.formatExp(5 * rates.dexExp)} / sec`], + [`Agility Exp:`, `${numeralWrapper.formatExp(5 * rates.agiExp)} / sec`], + [`Charisma Exp:`, `${numeralWrapper.formatExp(5 * rates.chaExp)} / sec`], + [`Reputation:`, ], + ]; + } return ( - Earnings + Earnings {props.sleeve.storedCycles > 50 ? "(overclock)" : ""} {data.map(([a, b]) => ( diff --git a/src/PersonObjects/formulas/reputation.ts b/src/PersonObjects/formulas/reputation.ts index d9a500c70..e404bdd5e 100644 --- a/src/PersonObjects/formulas/reputation.ts +++ b/src/PersonObjects/formulas/reputation.ts @@ -1,8 +1,8 @@ -import { IPlayer } from "../IPlayer"; import { Faction } from "../../Faction/Faction"; import { CONSTANTS } from "../../Constants"; import { BitNodeMultipliers } from "../../BitNode/BitNodeMultipliers"; import { CalculateShareMult } from "../../NetworkShare/Share"; +import { IPerson } from "../IPerson"; function mult(f: Faction): number { let favorMult = 1 + f.favor / 100; @@ -12,7 +12,7 @@ function mult(f: Faction): number { return favorMult * BitNodeMultipliers.FactionWorkRepGain; } -export function getHackingWorkRepGain(p: IPlayer, f: Faction): number { +export function getHackingWorkRepGain(p: IPerson, f: Faction): number { return ( ((p.skills.hacking + p.skills.intelligence / 3) / CONSTANTS.MaxSkillLevel) * p.mults.faction_rep * @@ -22,7 +22,7 @@ export function getHackingWorkRepGain(p: IPlayer, f: Faction): number { ); } -export function getFactionSecurityWorkRepGain(p: IPlayer, f: Faction): number { +export function getFactionSecurityWorkRepGain(p: IPerson, f: Faction): number { const t = (0.9 * (p.skills.strength + @@ -35,7 +35,7 @@ export function getFactionSecurityWorkRepGain(p: IPlayer, f: Faction): number { return t * p.mults.faction_rep * mult(f) * p.getIntelligenceBonus(1); } -export function getFactionFieldWorkRepGain(p: IPlayer, f: Faction): number { +export function getFactionFieldWorkRepGain(p: IPerson, f: Faction): number { const t = (0.9 * (p.skills.strength + diff --git a/src/Work/ClassWork.tsx b/src/Work/ClassWork.tsx index d71c0bf1a..1a5d7dc63 100644 --- a/src/Work/ClassWork.tsx +++ b/src/Work/ClassWork.tsx @@ -147,13 +147,13 @@ export class ClassWork extends Work { } calculateRates(player: IPlayer): WorkStats { - return calculateClassEarningsRate(player, this); + return calculateClassEarningsRate(player, player, this.classType, this.location); } process(player: IPlayer, cycles: number): boolean { this.cyclesWorked += cycles; const rate = this.calculateRates(player); - const earnings = applyWorkStats(player, rate, cycles, "class"); + const earnings = applyWorkStats(player, player, rate, cycles, "class"); this.earnings = sumWorkStats(this.earnings, earnings); return false; } diff --git a/src/Work/CompanyWork.tsx b/src/Work/CompanyWork.tsx index ef495dd89..9b164e566 100644 --- a/src/Work/CompanyWork.tsx +++ b/src/Work/CompanyWork.tsx @@ -10,6 +10,8 @@ import { applyWorkStats, WorkStats } from "./WorkStats"; import { Company } from "../Company/Company"; import { dialogBoxCreate } from "../ui/React/DialogBox"; import { Reputation } from "../ui/React/Reputation"; +import { AugmentationNames } from "../Augmentation/data/AugmentationNames"; +import { CONSTANTS } from "../Constants"; interface CompanyWorkParams { companyName: string; @@ -32,14 +34,18 @@ export class CompanyWork extends Work { } getGainRates(player: IPlayer): WorkStats { - return calculateCompanyWorkStats(player, this.getCompany()); + let focusBonus = 1; + if (!player.hasAugmentation(AugmentationNames.NeuroreceptorManager)) { + focusBonus = player.focus ? 1 : CONSTANTS.BaseFocusBonus; + } + return calculateCompanyWorkStats(player, player, this.getCompany()); } process(player: IPlayer, cycles: number): boolean { this.cyclesWorked += cycles; const company = this.getCompany(); const gains = this.getGainRates(player); - applyWorkStats(player, gains, cycles, "work"); + applyWorkStats(player, player, gains, cycles, "work"); company.playerReputation += gains.reputation * cycles; influenceStockThroughCompanyWork(company, gains.reputation, cycles); return false; diff --git a/src/Work/FactionWork.tsx b/src/Work/FactionWork.tsx index 87b087aef..5c0f67b19 100644 --- a/src/Work/FactionWork.tsx +++ b/src/Work/FactionWork.tsx @@ -5,7 +5,7 @@ import { IPlayer } from "../PersonObjects/IPlayer"; import { FactionNames } from "../Faction/data/FactionNames"; import { Factions } from "../Faction/Factions"; import { Faction } from "../Faction/Faction"; -import { applyWorkStats, WorkStats } from "./WorkStats"; +import { applyWorkStats, scaleWorkStats, WorkStats } from "./WorkStats"; import { dialogBoxCreate } from "../ui/React/DialogBox"; import { Reputation } from "../ui/React/Reputation"; import { @@ -58,7 +58,12 @@ export class FactionWork extends Work { } getExpRates(player: IPlayer): WorkStats { - return calculateFactionExp(player, this.factionWorkType); + let focusBonus = 1; + if (!player.hasAugmentation(AugmentationNames.NeuroreceptorManager)) { + focusBonus = player.focus ? 1 : CONSTANTS.BaseFocusBonus; + } + const rate = calculateFactionExp(player, this.factionWorkType); + return scaleWorkStats(rate, focusBonus, false); } process(player: IPlayer, cycles: number): boolean { @@ -66,7 +71,7 @@ export class FactionWork extends Work { this.getFaction().playerReputation += this.getReputationRate(player) * cycles; const rate = this.getExpRates(player); - applyWorkStats(player, rate, cycles, "class"); + applyWorkStats(player, player, rate, cycles, "class"); return false; } diff --git a/src/Work/WorkStats.ts b/src/Work/WorkStats.ts index f0783617e..07472df41 100644 --- a/src/Work/WorkStats.ts +++ b/src/Work/WorkStats.ts @@ -1,3 +1,4 @@ +import { IPerson } from "src/PersonObjects/IPerson"; import { IPlayer } from "../PersonObjects/IPlayer"; export interface WorkStats { @@ -52,9 +53,10 @@ export const sumWorkStats = (w0: WorkStats, w1: WorkStats): WorkStats => { }; }; -export const scaleWorkStats = (w: WorkStats, n: number): WorkStats => { +export const scaleWorkStats = (w: WorkStats, n: number, scaleMoney = true): WorkStats => { + const m = scaleMoney ? n : 1; return { - money: w.money * n, + money: w.money * m, reputation: w.reputation * n, hackExp: w.hackExp * n, strExp: w.strExp * n, @@ -66,10 +68,34 @@ export const scaleWorkStats = (w: WorkStats, n: number): WorkStats => { }; }; -export const applyWorkStats = (player: IPlayer, workStats: WorkStats, cycles: number, source: string): WorkStats => { +export const applyWorkStats = ( + player: IPlayer, + target: IPerson, + workStats: WorkStats, + cycles: number, + source: string, +): WorkStats => { + const expStats = applyWorkStatsExp(target, workStats, cycles); const gains = { money: workStats.money * cycles, reputation: 0, + hackExp: expStats.hackExp, + strExp: expStats.strExp, + defExp: expStats.defExp, + dexExp: expStats.dexExp, + agiExp: expStats.agiExp, + chaExp: expStats.chaExp, + intExp: expStats.intExp, + }; + player.gainMoney(gains.money, source); + + return gains; +}; + +export const applyWorkStatsExp = (target: IPerson, workStats: WorkStats, cycles: number): WorkStats => { + const gains = { + money: 0, + reputation: 0, hackExp: workStats.hackExp * cycles, strExp: workStats.strExp * cycles, defExp: workStats.defExp * cycles, @@ -78,13 +104,12 @@ export const applyWorkStats = (player: IPlayer, workStats: WorkStats, cycles: nu chaExp: workStats.chaExp * cycles, intExp: workStats.intExp * cycles, }; - player.gainHackingExp(gains.hackExp); - player.gainStrengthExp(gains.strExp); - player.gainDefenseExp(gains.defExp); - player.gainDexterityExp(gains.dexExp); - player.gainAgilityExp(gains.agiExp); - player.gainCharismaExp(gains.chaExp); - player.gainIntelligenceExp(gains.intExp); - player.gainMoney(gains.money, source); + target.gainHackingExp(gains.hackExp); + target.gainStrengthExp(gains.strExp); + target.gainDefenseExp(gains.defExp); + target.gainDexterityExp(gains.dexExp); + target.gainAgilityExp(gains.agiExp); + target.gainCharismaExp(gains.chaExp); + target.gainIntelligenceExp(gains.intExp); return gains; }; diff --git a/src/Work/formulas/Class.ts b/src/Work/formulas/Class.ts index 37cd6db43..b5263498c 100644 --- a/src/Work/formulas/Class.ts +++ b/src/Work/formulas/Class.ts @@ -3,11 +3,13 @@ import { Location } from "../../Locations/Location"; import { BitNodeMultipliers } from "../../BitNode/BitNodeMultipliers"; import { CONSTANTS } from "../../Constants"; import { IPlayer } from "../../PersonObjects/IPlayer"; -import { Class, Classes, ClassWork } from "../ClassWork"; +import { Class, Classes, ClassType } from "../ClassWork"; import { WorkStats } from "../WorkStats"; import { Server } from "../../Server/Server"; import { GetServer } from "../../Server/AllServers"; import { serverMetadata } from "../../Server/data/servers"; +import { IPerson } from "../../PersonObjects/IPerson"; +import { LocationName } from "../../Locations/data/LocationNames"; const gameCPS = 1000 / CONSTANTS._idleSpeed; // 5 cycles per second @@ -18,13 +20,22 @@ export function calculateCost(classs: Class, location: Location): number { return classs.earnings.money * location.costMult * discount; } -export function calculateClassEarnings(player: IPlayer, work: ClassWork): WorkStats { +export function calculateClassEarnings( + player: IPlayer, + target: IPerson, + type: ClassType, + locationName: LocationName, +): WorkStats { //Find cost and exp gain per game cycle const hashManager = player.hashManager; - const classs = Classes[work.classType]; - const location = Locations[work.location]; + const classs = Classes[type]; + const location = Locations[locationName]; - const hashMult = work.isGym() ? hashManager.getTrainingMult() : hashManager.getStudyMult(); + const hashMult = [ClassType.GymAgility, ClassType.GymDefense, ClassType.GymStrength, ClassType.GymDexterity].includes( + type, + ) + ? hashManager.getTrainingMult() + : hashManager.getStudyMult(); const cost = calculateCost(classs, location) / gameCPS; const hackExp = ((classs.earnings.hackExp * location.expMult) / gameCPS) * hashMult; @@ -36,12 +47,12 @@ export function calculateClassEarnings(player: IPlayer, work: ClassWork): WorkSt return { money: cost, reputation: 0, - hackExp: hackExp * player.mults.hacking_exp * BitNodeMultipliers.ClassGymExpGain, - strExp: strExp * player.mults.strength_exp * BitNodeMultipliers.ClassGymExpGain, - defExp: defExp * player.mults.defense_exp * BitNodeMultipliers.ClassGymExpGain, - dexExp: dexExp * player.mults.dexterity_exp * BitNodeMultipliers.ClassGymExpGain, - agiExp: agiExp * player.mults.agility_exp * BitNodeMultipliers.ClassGymExpGain, - chaExp: chaExp * player.mults.charisma_exp * BitNodeMultipliers.ClassGymExpGain, + hackExp: hackExp * target.mults.hacking_exp * BitNodeMultipliers.ClassGymExpGain, + strExp: strExp * target.mults.strength_exp * BitNodeMultipliers.ClassGymExpGain, + defExp: defExp * target.mults.defense_exp * BitNodeMultipliers.ClassGymExpGain, + dexExp: dexExp * target.mults.dexterity_exp * BitNodeMultipliers.ClassGymExpGain, + agiExp: agiExp * target.mults.agility_exp * BitNodeMultipliers.ClassGymExpGain, + chaExp: chaExp * target.mults.charisma_exp * BitNodeMultipliers.ClassGymExpGain, intExp: 0, }; } diff --git a/src/Work/formulas/Company.ts b/src/Work/formulas/Company.ts index 489121290..bec83b884 100644 --- a/src/Work/formulas/Company.ts +++ b/src/Work/formulas/Company.ts @@ -5,16 +5,12 @@ import { WorkStats } from "../WorkStats"; import { BitNodeMultipliers } from "../../BitNode/BitNodeMultipliers"; import { CONSTANTS } from "../../Constants"; import { AugmentationNames } from "../../Augmentation/data/AugmentationNames"; +import { IPerson } from "src/PersonObjects/IPerson"; -export const calculateCompanyWorkStats = (player: IPlayer, company: Company): WorkStats => { +export const calculateCompanyWorkStats = (player: IPlayer, worker: IPerson, company: Company): WorkStats => { const companyPositionName = player.jobs[company.name]; const companyPosition = CompanyPositions[companyPositionName]; - let focusBonus = 1; - if (!player.hasAugmentation(AugmentationNames.NeuroreceptorManager)) { - focusBonus = player.focus ? 1 : CONSTANTS.BaseFocusBonus; - } - // If player has SF-11, calculate salary multiplier from favor let favorMult = 1 + company.favor / 100; if (isNaN(favorMult)) { @@ -27,60 +23,53 @@ export const calculateCompanyWorkStats = (player: IPlayer, company: Company): Wo } let jobPerformance = companyPosition.calculateJobPerformance( - player.skills.hacking, - player.skills.strength, - player.skills.defense, - player.skills.dexterity, - player.skills.agility, - player.skills.charisma, + worker.skills.hacking, + worker.skills.strength, + worker.skills.defense, + worker.skills.dexterity, + worker.skills.agility, + worker.skills.charisma, ); - jobPerformance += player.skills.intelligence / CONSTANTS.MaxSkillLevel; + jobPerformance += worker.skills.intelligence / CONSTANTS.MaxSkillLevel; return { money: - focusBonus * companyPosition.baseSalary * company.salaryMultiplier * - player.mults.work_money * + worker.mults.work_money * BitNodeMultipliers.CompanyWorkMoney * bn11Mult, - reputation: focusBonus * jobPerformance * player.mults.company_rep * favorMult, + reputation: jobPerformance * worker.mults.company_rep * favorMult, hackExp: - focusBonus * companyPosition.hackingExpGain * company.expMultiplier * - player.mults.hacking_exp * + worker.mults.hacking_exp * BitNodeMultipliers.CompanyWorkExpGain, strExp: - focusBonus * companyPosition.strengthExpGain * company.expMultiplier * - player.mults.strength_exp * + worker.mults.strength_exp * BitNodeMultipliers.CompanyWorkExpGain, defExp: - focusBonus * companyPosition.defenseExpGain * company.expMultiplier * - player.mults.defense_exp * + worker.mults.defense_exp * BitNodeMultipliers.CompanyWorkExpGain, dexExp: - focusBonus * companyPosition.dexterityExpGain * company.expMultiplier * - player.mults.dexterity_exp * + worker.mults.dexterity_exp * BitNodeMultipliers.CompanyWorkExpGain, agiExp: - focusBonus * companyPosition.agilityExpGain * company.expMultiplier * - player.mults.agility_exp * + worker.mults.agility_exp * BitNodeMultipliers.CompanyWorkExpGain, chaExp: - focusBonus * companyPosition.charismaExpGain * company.expMultiplier * - player.mults.charisma_exp * + worker.mults.charisma_exp * BitNodeMultipliers.CompanyWorkExpGain, intExp: 0, }; diff --git a/src/Work/formulas/Faction.ts b/src/Work/formulas/Faction.ts index 0e3cfb345..409dc0d52 100644 --- a/src/Work/formulas/Faction.ts +++ b/src/Work/formulas/Faction.ts @@ -1,7 +1,6 @@ -import { AugmentationNames } from "../../Augmentation/data/AugmentationNames"; +import { IPerson } from "../../PersonObjects/IPerson"; import { BitNodeMultipliers } from "../../BitNode/BitNodeMultipliers"; import { CONSTANTS } from "../../Constants"; -import { IPlayer } from "../../PersonObjects/IPlayer"; import { FactionWorkType } from "../data/FactionWorkType"; import { newWorkStats, WorkStats } from "../WorkStats"; @@ -26,27 +25,17 @@ export const FactionWorkStats: Record = { }), }; -export function calculateFactionExp(player: IPlayer, tpe: FactionWorkType): WorkStats { - let focusBonus = 1; - if (!player.hasAugmentation(AugmentationNames.NeuroreceptorManager)) { - focusBonus = player.focus ? 1 : CONSTANTS.BaseFocusBonus; - } +export function calculateFactionExp(person: IPerson, tpe: FactionWorkType): WorkStats { const baseStats = FactionWorkStats[tpe]; return { money: 0, reputation: 0, - hackExp: - (focusBonus * (baseStats.hackExp * player.mults.hacking_exp * BitNodeMultipliers.FactionWorkExpGain)) / gameCPS, - strExp: - (focusBonus * (baseStats.strExp * player.mults.strength_exp * BitNodeMultipliers.FactionWorkExpGain)) / gameCPS, - defExp: - (focusBonus * (baseStats.defExp * player.mults.defense_exp * BitNodeMultipliers.FactionWorkExpGain)) / gameCPS, - dexExp: - (focusBonus * (baseStats.dexExp * player.mults.dexterity_exp * BitNodeMultipliers.FactionWorkExpGain)) / gameCPS, - agiExp: - (focusBonus * (baseStats.agiExp * player.mults.agility_exp * BitNodeMultipliers.FactionWorkExpGain)) / gameCPS, - chaExp: - (focusBonus * (baseStats.chaExp * player.mults.charisma_exp * BitNodeMultipliers.FactionWorkExpGain)) / gameCPS, + hackExp: (baseStats.hackExp * person.mults.hacking_exp * BitNodeMultipliers.FactionWorkExpGain) / gameCPS, + strExp: (baseStats.strExp * person.mults.strength_exp * BitNodeMultipliers.FactionWorkExpGain) / gameCPS, + defExp: (baseStats.defExp * person.mults.defense_exp * BitNodeMultipliers.FactionWorkExpGain) / gameCPS, + dexExp: (baseStats.dexExp * person.mults.dexterity_exp * BitNodeMultipliers.FactionWorkExpGain) / gameCPS, + agiExp: (baseStats.agiExp * person.mults.agility_exp * BitNodeMultipliers.FactionWorkExpGain) / gameCPS, + chaExp: (baseStats.chaExp * person.mults.charisma_exp * BitNodeMultipliers.FactionWorkExpGain) / gameCPS, intExp: 0, }; }