bitburner-src/src/PersonObjects/Sleeve/Sleeve.ts

492 lines
15 KiB
TypeScript

/**
* Sleeves are bodies that contain the player's cloned consciousness.
* The player can use these bodies to perform different tasks synchronously.
*
* Each sleeve is its own individual, meaning it has its own stats/exp
*
* Sleeves are unlocked in BitNode-10.
*/
import { Player } from "@player";
import { Person } from "../Person";
import { Augmentation } from "../../Augmentation/Augmentation";
import { Crime } from "../../Crime/Crime";
import { Crimes } from "../../Crime/Crimes";
import { Companies } from "../../Company/Companies";
import { Company } from "../../Company/Company";
import { CompanyPosition } from "../../Company/CompanyPosition";
import { CompanyPositions } from "../../Company/CompanyPositions";
import { Contracts } from "../../Bladeburner/data/Contracts";
import { CONSTANTS } from "../../Constants";
import { Factions } from "../../Faction/Factions";
import { CityName } from "../../Locations/data/CityNames";
import { LocationName } from "../../Locations/data/LocationNames";
import { Generic_fromJSON, Generic_toJSON, IReviverValue, Reviver } from "../../utils/JSONReviver";
import { numeralWrapper } from "../../ui/numeralFormat";
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";
import * as sleeveMethods from "./SleeveMethods";
export class Sleeve extends Person {
currentWork: Work | null = null;
/** Clone retains 'memory' synchronization (and maybe exp?) upon prestige/installing Augs */
memory = 1;
/**
* Sleeve shock. Number between 0 and 100
* Trauma/shock that comes with being in a sleeve. Experience earned
* is multiplied by shock%. This gets applied before synchronization
*
* Reputation earned is also multiplied by shock%
*/
shock = 1;
/** Stored number of game "loop" cycles */
storedCycles = 0;
/**
* Synchronization. Number between 0 and 100
* When experience is earned by sleeve, both the player and the sleeve get
* sync% of the experience earned.
*/
sync = 1;
constructor() {
super();
this.shockRecovery();
}
applyAugmentation = sleeveMethods.applyAugmentation;
findPurchasableAugs = sleeveMethods.findPurchasableAugs;
shockBonus(): number {
return this.shock / 100;
}
syncBonus(): number {
return this.sync / 100;
}
startWork(w: Work): void {
if (this.currentWork) this.currentWork.finish();
this.currentWork = w;
}
stopWork(): void {
if (this.currentWork) this.currentWork.finish();
this.currentWork = null;
}
/** Commit crimes */
commitCrime(crimeKey: string): boolean {
const crime: Crime | null = Crimes[crimeKey] || Object.values(Crimes).find((crime) => crime.name === crimeKey);
if (!crime) {
return false;
}
this.startWork(new SleeveCrimeWork(crime.type));
return true;
}
/** Returns the cost of upgrading this sleeve's memory by a certain amount */
getMemoryUpgradeCost(n: number): number {
const amt = Math.round(n);
if (amt < 0) {
return 0;
}
if (this.memory + amt > 100) {
return this.getMemoryUpgradeCost(100 - this.memory);
}
const mult = 1.02;
const baseCost = 1e12;
let currCost = 0;
let currMemory = this.memory - 1;
for (let i = 0; i < n; ++i) {
currCost += Math.pow(mult, currMemory);
++currMemory;
}
return currCost * baseCost;
}
installAugmentation(aug: Augmentation): void {
this.exp.hacking = 0;
this.exp.strength = 0;
this.exp.defense = 0;
this.exp.dexterity = 0;
this.exp.agility = 0;
this.exp.charisma = 0;
this.applyAugmentation(aug);
this.augmentations.push({ name: aug.name, level: 1 });
this.updateSkillLevels();
}
/** Called on every sleeve for a Source File Prestige */
prestige(): void {
// Reset exp
this.exp.hacking = 0;
this.exp.strength = 0;
this.exp.defense = 0;
this.exp.dexterity = 0;
this.exp.agility = 0;
this.exp.charisma = 0;
this.updateSkillLevels();
this.hp.current = this.hp.max;
// Reset task-related stuff
this.stopWork();
this.shockRecovery();
// Reset augs and multipliers
this.augmentations = [];
this.resetMultipliers();
// Reset Location
this.city = CityName.Sector12;
// Reset sleeve-related stats
this.shock = 1;
this.storedCycles = 0;
this.sync = Math.max(this.memory, 1);
}
/**
* Process loop
* Returns an object containing the amount of experience that should be
* transferred to all other sleeves
*/
process(numCycles = 1): void {
// Only process once every second (5 cycles)
const CyclesPerSecond = 1000 / CONSTANTS.MilliPerCycle;
this.storedCycles += numCycles;
if (this.storedCycles < CyclesPerSecond) return;
let cyclesUsed = this.storedCycles;
cyclesUsed = Math.min(cyclesUsed, 15);
this.shock = Math.min(100, this.shock + 0.0001 * cyclesUsed);
if (!this.currentWork) return;
this.currentWork.process(this, cyclesUsed);
this.storedCycles -= cyclesUsed;
}
shockRecovery(): boolean {
this.startWork(new SleeveRecoveryWork());
return true;
}
synchronize(): boolean {
this.startWork(new SleeveSynchroWork());
return true;
}
/** Take a course at a university */
takeUniversityCourse(universityName: string, className: string): boolean {
// Set exp/money multipliers based on which university.
// Also check that the sleeve is in the right city
let loc: LocationName | undefined;
switch (universityName.toLowerCase()) {
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;
loc = LocationName.Sector12RothmanUniversity;
break;
}
case LocationName.VolhavenZBInstituteOfTechnology.toLowerCase(): {
if (this.city !== CityName.Volhaven) return false;
loc = LocationName.VolhavenZBInstituteOfTechnology;
break;
}
}
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":
classType = ClassType.DataStructures;
break;
case "networks":
classType = ClassType.Networks;
break;
case "algorithms":
classType = ClassType.Algorithms;
break;
case "management":
classType = ClassType.Management;
break;
case "leadership":
classType = ClassType.Leadership;
break;
}
if (!classType) return false;
this.startWork(
new SleeveClassWork({
classType: classType,
location: loc,
}),
);
return true;
}
/** Travel to another City. Costs money from player */
travel(newCity: CityName): boolean {
Player.loseMoney(CONSTANTS.TravelCost, "sleeves");
this.city = newCity;
return true;
}
tryBuyAugmentation(aug: Augmentation): boolean {
if (!Player.canAfford(aug.baseCost)) {
return false;
}
// Verify that this sleeve does not already have that augmentation.
if (this.hasAugmentation(aug.name)) return false;
Player.loseMoney(aug.baseCost, "sleeves");
this.installAugmentation(aug);
return true;
}
upgradeMemory(n: number): void {
this.memory = Math.min(100, Math.round(this.memory + n));
}
/**
* Start work for one of the player's companies
* Returns boolean indicating success
*/
workForCompany(companyName: string): boolean {
if (!Companies[companyName] || Player.jobs[companyName] == null) {
return false;
}
const company: Company | null = Companies[companyName];
const companyPosition: CompanyPosition | null = CompanyPositions[Player.jobs[companyName]];
if (company == null) return false;
if (companyPosition == null) return false;
this.startWork(new SleeveCompanyWork({ companyName: companyName }));
return true;
}
/**
* Start work for one of the player's factions
* Returns boolean indicating success
*/
workForFaction(factionName: string, workType: string): boolean {
const faction = Factions[factionName];
if (factionName === "" || !faction || !Player.factions.includes(factionName)) {
return false;
}
const factionInfo = faction.getInfo();
// Set type of work (hacking/field/security), and the experience gains
const sanitizedWorkType = workType.toLowerCase();
let factionWorkType: FactionWorkType;
if (sanitizedWorkType.includes("hack")) {
if (!factionInfo.offerHackingWork) return false;
factionWorkType = FactionWorkType.HACKING;
} else if (sanitizedWorkType.includes("field")) {
if (!factionInfo.offerFieldWork) return false;
factionWorkType = FactionWorkType.FIELD;
} else if (sanitizedWorkType.includes("security")) {
if (!factionInfo.offerSecurityWork) return false;
factionWorkType = FactionWorkType.SECURITY;
} else {
return false;
}
this.startWork(
new SleeveFactionWork({
factionWorkType: factionWorkType,
factionName: faction.name,
}),
);
return true;
}
/** Begin a gym workout task */
workoutAtGym(gymName: string, stat: string): boolean {
// Set exp/money multipliers based on which university.
// Also check that the sleeve is in the right city
let loc: LocationName | undefined;
switch (gymName.toLowerCase()) {
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;
loc = LocationName.AevumSnapFitnessGym;
break;
}
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;
loc = LocationName.Sector12PowerhouseGym;
break;
}
case LocationName.VolhavenMilleniumFitnessGym.toLowerCase(): {
if (this.city != CityName.Volhaven) return false;
loc = LocationName.VolhavenMilleniumFitnessGym;
break;
}
}
if (!loc) return false;
// Set experience/money gains based on class
const sanitizedStat: string = stat.toLowerCase();
// set stat to a default value.
let classType: ClassType | undefined;
if (sanitizedStat.includes("str")) {
classType = ClassType.GymStrength;
}
if (sanitizedStat.includes("def")) {
classType = ClassType.GymDefense;
}
if (sanitizedStat.includes("dex")) {
classType = ClassType.GymDexterity;
}
if (sanitizedStat.includes("agi")) {
classType = ClassType.GymAgility;
}
// if stat is still equals its default value, then validation has failed.
if (!classType) return false;
this.startWork(
new SleeveClassWork({
classType: classType,
location: loc,
}),
);
return true;
}
/** Begin a bladeburner task */
bladeburner(action: string, contract: string): boolean {
if (!Player.bladeburner) return false;
switch (action) {
case "Field analysis":
this.startWork(new SleeveBladeburnerWork({ type: "General", name: "Field Analysis" }));
return true;
case "Recruitment":
this.startWork(new SleeveBladeburnerWork({ type: "General", name: "Recruitment" }));
return true;
case "Diplomacy":
this.startWork(new SleeveBladeburnerWork({ type: "General", name: "Diplomacy" }));
return true;
case "Hyperbolic Regeneration Chamber":
this.startWork(new SleeveBladeburnerWork({ type: "General", name: "Hyperbolic Regeneration Chamber" }));
return true;
case "Infiltrate synthoids":
this.startWork(new SleeveInfiltrateWork());
return true;
case "Support main sleeve":
this.startWork(new SleeveSupportWork());
return true;
case "Take on contracts":
if (!Contracts[contract]) return false;
this.startWork(new SleeveBladeburnerWork({ type: "Contracts", name: contract }));
return true;
}
return false;
}
recruitmentSuccessChance(): number {
return Math.max(0, Math.min(1, Player.bladeburner?.getRecruitmentSuccessChance(this) ?? 0));
}
contractSuccessChance(type: string, name: string): string {
const bb = Player.bladeburner;
if (bb === null) {
const errorLogText = `bladeburner is null`;
console.error(`Function: sleeves.contractSuccessChance; Message: '${errorLogText}'`);
return "0%";
}
const chances = bb.getActionEstimatedSuccessChanceNetscriptFn(this, type, name);
if (typeof chances === "string") {
console.error(`Function: sleeves.contractSuccessChance; Message: '${chances}'`);
return "0%";
}
if (chances[0] >= 1) {
return "100%";
} else {
return `${numeralWrapper.formatPercentage(chances[0])} - ${numeralWrapper.formatPercentage(chances[1])}`;
}
}
takeDamage(amt: number): boolean {
if (typeof amt !== "number") {
console.warn(`Player.takeDamage() called without a numeric argument: ${amt}`);
return false;
}
this.hp.current -= amt;
if (this.hp.current <= 0) {
this.shock = Math.max(0, this.shock - 0.5);
this.hp.current = this.hp.max;
return true;
} else {
return false;
}
}
whoAmI(): string {
return "Sleeve";
}
/** Serialize the current object to a JSON save state. */
toJSON(): IReviverValue {
return Generic_toJSON("Sleeve", this);
}
/** Initializes a Sleeve object from a JSON save state. */
static fromJSON(value: IReviverValue): Sleeve {
return Generic_fromJSON(Sleeve, value.data);
}
}
Reviver.constructors.Sleeve = Sleeve;