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

516 lines
15 KiB
TypeScript
Raw Normal View History

/**
2019-01-10 09:20:04 +01:00
* 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 { IPlayer } from "../IPlayer";
import { Person } from "../Person";
2019-02-26 09:51:48 +01:00
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 { CONSTANTS } from "../../Constants";
import { Faction } from "../../Faction/Faction";
import { Factions } from "../../Faction/Factions";
import { CityName } from "../../Locations/data/CityNames";
import { LocationName } from "../../Locations/data/LocationNames";
2022-07-15 01:00:10 +02:00
import { Generic_fromJSON, Generic_toJSON, IReviverValue, Reviver } from "../../utils/JSONReviver";
import { numeralWrapper } from "../../ui/numeralFormat";
import { FactionWorkType } from "../../Work/data/FactionWorkType";
2022-07-28 02:37:32 +02:00
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";
2022-07-28 08:46:34 +02:00
import { SleeveSupportWork } from "./Work/SleeveSupportWork";
import { SleeveBladeburnerWork } from "./Work/SleeveBladeburnerWork";
import { SleeveCrimeWork } from "./Work/SleeveCrimeWork";
export class Sleeve extends Person {
2022-07-28 02:37:32 +02:00
currentWork: Work | null = null;
2021-09-05 01:09:30 +02:00
/**
* 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 multipled 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
2022-07-28 20:35:55 +02:00
* sync% of the experience earned.
2021-09-05 01:09:30 +02:00
*/
sync = 1;
constructor(p: IPlayer | null = null) {
super();
if (p != null) {
this.shockRecovery(p);
}
}
2022-07-28 02:37:32 +02:00
shockBonus(): number {
return this.shock / 100;
}
2022-07-28 20:35:55 +02:00
2022-07-28 02:37:32 +02:00
syncBonus(): number {
return this.sync / 100;
}
2022-07-28 08:46:34 +02:00
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;
}
2021-09-05 01:09:30 +02:00
/**
* Commit crimes
*/
commitCrime(p: IPlayer, crimeKey: string): boolean {
2021-10-18 20:59:18 +02:00
const crime: Crime | null = Crimes[crimeKey] || Object.values(Crimes).find((crime) => crime.name === crimeKey);
if (!crime) {
2021-09-05 01:09:30 +02:00
return false;
}
2022-07-28 08:46:34 +02:00
this.startWork(p, new SleeveCrimeWork(crime.type));
2021-09-05 01:09:30 +02:00
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;
}
2021-09-05 01:09:30 +02:00
if (this.memory + amt > 100) {
return this.getMemoryUpgradeCost(100 - this.memory);
}
2021-09-05 01:09:30 +02:00
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;
}
2021-09-05 01:09:30 +02:00
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;
2021-09-05 01:09:30 +02:00
this.applyAugmentation(aug);
this.augmentations.push({ name: aug.name, level: 1 });
this.updateStatLevels();
}
/**
2022-02-03 14:50:08 +01:00
* Called on every sleeve for a Source File Prestige
2021-09-05 01:09:30 +02:00
*/
prestige(p: IPlayer): 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;
2022-07-29 23:05:56 +02:00
this.updateStatLevels();
2021-09-05 01:09:30 +02:00
// Reset task-related stuff
2022-07-28 20:35:55 +02:00
this.stopWork(p);
2021-09-05 01:09:30 +02:00
this.shockRecovery(p);
// Reset augs and multipliers
this.augmentations = [];
this.resetMultipliers();
2022-02-03 14:50:08 +01:00
// Reset Location
this.city = CityName.Sector12;
2021-09-05 01:09:30 +02:00
// 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
*/
2022-07-27 06:50:21 +02:00
process(p: IPlayer, numCycles = 1): void {
2021-09-05 01:09:30 +02:00
// Only process once every second (5 cycles)
const CyclesPerSecond = 1000 / CONSTANTS.MilliPerCycle;
this.storedCycles += numCycles;
2022-07-28 02:37:32 +02:00
if (this.storedCycles < CyclesPerSecond) return;
2021-09-05 01:09:30 +02:00
let cyclesUsed = this.storedCycles;
cyclesUsed = Math.min(cyclesUsed, 15);
this.shock = Math.min(100, this.shock + 0.0001 * cyclesUsed);
2022-07-28 20:35:55 +02:00
if (!this.currentWork) return;
this.currentWork.process(p, this, cyclesUsed);
2021-09-05 01:09:30 +02:00
this.storedCycles -= cyclesUsed;
}
shockRecovery(p: IPlayer): boolean {
2022-07-28 08:46:34 +02:00
this.startWork(p, new SleeveRecoveryWork());
2021-09-05 01:09:30 +02:00
return true;
}
2021-09-05 01:09:30 +02:00
synchronize(p: IPlayer): boolean {
2022-07-28 08:46:34 +02:00
this.startWork(p, new SleeveSynchroWork());
2021-09-05 01:09:30 +02:00
return true;
}
/**
* Take a course at a university
*/
2021-09-09 05:47:34 +02:00
takeUniversityCourse(p: IPlayer, universityName: string, className: string): boolean {
2021-09-05 01:09:30 +02:00
// Set exp/money multipliers based on which university.
// Also check that the sleeve is in the right city
2022-07-28 02:37:32 +02:00
let loc: LocationName | undefined;
2021-09-05 01:09:30 +02:00
switch (universityName.toLowerCase()) {
2022-07-28 02:37:32 +02:00
case LocationName.AevumSummitUniversity.toLowerCase(): {
if (this.city !== CityName.Aevum) return false;
loc = LocationName.AevumSummitUniversity;
2021-09-05 01:09:30 +02:00
break;
2022-07-28 02:37:32 +02:00
}
case LocationName.Sector12RothmanUniversity.toLowerCase(): {
if (this.city !== CityName.Sector12) return false;
loc = LocationName.Sector12RothmanUniversity;
2021-09-05 01:09:30 +02:00
break;
2022-07-28 02:37:32 +02:00
}
case LocationName.VolhavenZBInstituteOfTechnology.toLowerCase(): {
if (this.city !== CityName.Volhaven) return false;
loc = LocationName.VolhavenZBInstituteOfTechnology;
2021-09-05 01:09:30 +02:00
break;
2022-07-28 02:37:32 +02:00
}
2021-09-05 01:09:30 +02:00
}
2022-07-28 02:37:32 +02:00
if (!loc) return false;
2021-09-05 01:09:30 +02:00
// Set experience/money gains based on class
2022-07-28 02:37:32 +02:00
let classType: ClassType | undefined;
2021-09-05 01:09:30 +02:00
switch (className.toLowerCase()) {
case "study computer science":
2022-07-28 02:37:32 +02:00
classType = ClassType.StudyComputerScience;
2021-09-05 01:09:30 +02:00
break;
case "data structures":
2022-07-28 02:37:32 +02:00
classType = ClassType.DataStructures;
2021-09-05 01:09:30 +02:00
break;
case "networks":
2022-07-28 02:37:32 +02:00
classType = ClassType.Networks;
2021-09-05 01:09:30 +02:00
break;
case "algorithms":
2022-07-28 02:37:32 +02:00
classType = ClassType.Algorithms;
2021-09-05 01:09:30 +02:00
break;
case "management":
2022-07-28 02:37:32 +02:00
classType = ClassType.Management;
2021-09-05 01:09:30 +02:00
break;
case "leadership":
2022-07-28 02:37:32 +02:00
classType = ClassType.Leadership;
2021-09-05 01:09:30 +02:00
break;
}
2022-07-28 02:37:32 +02:00
if (!classType) return false;
2022-07-28 08:46:34 +02:00
this.startWork(
p,
new SleeveClassWork({
classType: classType,
location: loc,
}),
);
2021-09-05 01:09:30 +02:00
return true;
}
2021-09-05 01:09:30 +02:00
/**
* Travel to another City. Costs money from player
*/
travel(p: IPlayer, newCity: CityName): boolean {
2021-10-27 20:18:33 +02:00
p.loseMoney(CONSTANTS.TravelCost, "sleeves");
2021-09-05 01:09:30 +02:00
this.city = newCity;
2021-09-05 01:09:30 +02:00
return true;
}
2021-09-05 01:09:30 +02:00
tryBuyAugmentation(p: IPlayer, aug: Augmentation): boolean {
if (!p.canAfford(aug.baseCost)) {
2021-09-05 01:09:30 +02:00
return false;
}
2021-09-05 01:09:30 +02:00
// Verify that this sleeve does not already have that augmentation.
if (this.augmentations.some((a) => a.name === aug.name)) {
return false;
}
p.loseMoney(aug.baseCost, "sleeves");
2021-09-05 01:09:30 +02:00
this.installAugmentation(aug);
return true;
}
upgradeMemory(n: number): void {
if (n < 0) {
console.warn(`Sleeve.upgradeMemory() called with negative value: ${n}`);
return;
}
2021-09-05 01:09:30 +02:00
this.memory = Math.min(100, Math.round(this.memory + n));
}
/**
* Start work for one of the player's companies
* Returns boolean indicating success
*/
workForCompany(p: IPlayer, companyName: string): boolean {
2021-09-09 05:47:34 +02:00
if (!(Companies[companyName] instanceof Company) || p.jobs[companyName] == null) {
2021-09-05 01:09:30 +02:00
return false;
}
2021-09-05 01:09:30 +02:00
const company: Company | null = Companies[companyName];
2021-09-09 05:47:34 +02:00
const companyPosition: CompanyPosition | null = CompanyPositions[p.jobs[companyName]];
2022-07-28 02:37:32 +02:00
if (company == null) return false;
if (companyPosition == null) return false;
2022-07-28 08:46:34 +02:00
this.startWork(p, new SleeveCompanyWork({ companyName: companyName }));
2021-09-05 01:09:30 +02:00
return true;
}
/**
* Start work for one of the player's factions
* Returns boolean indicating success
*/
workForFaction(p: IPlayer, factionName: string, workType: string): boolean {
2022-04-07 01:30:08 +02:00
const faction = Factions[factionName];
if (factionName === "" || !faction || !(faction instanceof Faction) || !p.factions.includes(factionName)) {
2021-09-05 01:09:30 +02:00
return false;
}
2019-07-16 06:40:13 +02:00
const factionInfo = faction.getInfo();
2021-09-05 01:09:30 +02:00
// Set type of work (hacking/field/security), and the experience gains
2022-07-28 02:37:32 +02:00
const sanitizedWorkType = workType.toLowerCase();
let factionWorkType: FactionWorkType;
2021-09-05 01:09:30 +02:00
if (sanitizedWorkType.includes("hack")) {
2022-07-28 02:37:32 +02:00
if (!factionInfo.offerHackingWork) return false;
factionWorkType = FactionWorkType.HACKING;
2021-09-05 01:09:30 +02:00
} else if (sanitizedWorkType.includes("field")) {
2022-07-28 02:37:32 +02:00
if (!factionInfo.offerFieldWork) return false;
factionWorkType = FactionWorkType.FIELD;
2021-09-05 01:09:30 +02:00
} else if (sanitizedWorkType.includes("security")) {
2022-07-28 02:37:32 +02:00
if (!factionInfo.offerSecurityWork) return false;
factionWorkType = FactionWorkType.SECURITY;
2021-09-05 01:09:30 +02:00
} else {
return false;
}
2019-07-16 06:40:13 +02:00
2022-07-28 08:46:34 +02:00
this.startWork(
p,
new SleeveFactionWork({
factionWorkType: factionWorkType,
factionName: faction.name,
}),
);
2021-09-05 01:09:30 +02:00
return true;
}
/**
* Begin a gym workout task
*/
workoutAtGym(p: IPlayer, gymName: string, stat: string): boolean {
// Set exp/money multipliers based on which university.
// Also check that the sleeve is in the right city
2022-07-28 02:37:32 +02:00
let loc: LocationName | undefined;
2021-09-05 01:09:30 +02:00
switch (gymName.toLowerCase()) {
2022-07-28 02:37:32 +02:00
case LocationName.AevumCrushFitnessGym.toLowerCase(): {
if (this.city != CityName.Aevum) return false;
loc = LocationName.AevumCrushFitnessGym;
2021-09-05 01:09:30 +02:00
break;
2022-07-28 02:37:32 +02:00
}
case LocationName.AevumSnapFitnessGym.toLowerCase(): {
if (this.city != CityName.Aevum) return false;
loc = LocationName.AevumSnapFitnessGym;
2021-09-05 01:09:30 +02:00
break;
2022-07-28 02:37:32 +02:00
}
case LocationName.Sector12IronGym.toLowerCase(): {
if (this.city != CityName.Sector12) return false;
loc = LocationName.Sector12IronGym;
2021-09-05 01:09:30 +02:00
break;
2022-07-28 02:37:32 +02:00
}
case LocationName.Sector12PowerhouseGym.toLowerCase(): {
if (this.city != CityName.Sector12) return false;
loc = LocationName.Sector12PowerhouseGym;
2021-09-05 01:09:30 +02:00
break;
2022-07-28 02:37:32 +02:00
}
case LocationName.VolhavenMilleniumFitnessGym.toLowerCase(): {
if (this.city != CityName.Volhaven) return false;
loc = LocationName.VolhavenMilleniumFitnessGym;
2021-09-05 01:09:30 +02:00
break;
2022-07-28 02:37:32 +02:00
}
}
2022-07-28 02:37:32 +02:00
if (!loc) return false;
2021-09-05 01:09:30 +02:00
// Set experience/money gains based on class
const sanitizedStat: string = stat.toLowerCase();
// set stat to a default value.
2022-07-28 02:37:32 +02:00
let classType: ClassType | undefined;
if (sanitizedStat.includes("str")) {
2022-07-28 02:37:32 +02:00
classType = ClassType.GymStrength;
}
if (sanitizedStat.includes("def")) {
2022-07-28 02:37:32 +02:00
classType = ClassType.GymDefense;
}
if (sanitizedStat.includes("dex")) {
2022-07-28 02:37:32 +02:00
classType = ClassType.GymDexterity;
}
if (sanitizedStat.includes("agi")) {
2022-07-28 02:37:32 +02:00
classType = ClassType.GymAgility;
}
// if stat is still equals its default value, then validation has failed.
2022-07-28 02:37:32 +02:00
if (!classType) return false;
2022-07-28 08:46:34 +02:00
this.startWork(
p,
new SleeveClassWork({
classType: classType,
location: loc,
}),
);
2021-09-05 01:09:30 +02:00
return true;
}
/**
* Begin a bladeburner task
*/
bladeburner(p: IPlayer, action: string, contract: string): boolean {
switch (action) {
case "Field analysis":
2022-07-28 08:46:34 +02:00
this.startWork(p, new SleeveBladeburnerWork({ type: "General", name: "Field Analysis" }));
2022-07-28 02:37:32 +02:00
return true;
case "Recruitment":
2022-07-28 08:46:34 +02:00
this.startWork(p, new SleeveBladeburnerWork({ type: "General", name: "Recruitment" }));
return true;
case "Diplomacy":
2022-07-28 08:46:34 +02:00
this.startWork(p, new SleeveBladeburnerWork({ type: "General", name: "Diplomacy" }));
return true;
case "Infiltrate synthoids":
2022-07-28 08:46:34 +02:00
this.startWork(p, new SleeveInfiltrateWork());
return true;
case "Support main sleeve":
2022-07-28 08:46:34 +02:00
this.startWork(p, new SleeveSupportWork(p));
return true;
case "Take on contracts":
2022-07-28 08:46:34 +02:00
this.startWork(p, new SleeveBladeburnerWork({ type: "Contracts", name: contract }));
return true;
}
return true;
}
2022-05-20 23:28:21 +02:00
recruitmentSuccessChance(p: IPlayer): number {
return Math.max(0, Math.min(1, p.bladeburner?.getRecruitmentSuccessChance(this) ?? 0));
}
contractSuccessChance(p: IPlayer, type: string, name: string): string {
const bb = p.bladeburner;
2022-04-14 18:00:17 +02:00
if (bb === null) {
const errorLogText = `bladeburner is null`;
2022-04-14 18:00:17 +02:00
console.error(`Function: sleeves.contractSuccessChance; Message: '${errorLogText}'`);
return "0%";
}
const chances = bb.getActionEstimatedSuccessChanceNetscriptFn(this, type, name);
2022-04-14 18:00:17 +02:00
if (typeof chances === "string") {
console.error(`Function: sleeves.contractSuccessChance; Message: '${chances}'`);
return "0%";
}
2022-04-14 18:00:17 +02:00
if (chances[0] >= 1) {
return "100%";
} else {
return `${numeralWrapper.formatPercentage(chances[0])} - ${numeralWrapper.formatPercentage(chances[1])}`;
}
}
2022-04-14 18:00:17 +02:00
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 {
2022-04-14 18:00:17 +02:00
return "Sleeve";
}
2021-09-05 01:09:30 +02:00
/**
* Serialize the current object to a JSON save state.
*/
2022-07-15 01:00:10 +02:00
toJSON(): IReviverValue {
2021-09-05 01:09:30 +02:00
return Generic_toJSON("Sleeve", this);
}
/**
* Initiatizes a Sleeve object from a JSON save state.
*/
2022-07-15 01:00:10 +02:00
static fromJSON(value: IReviverValue): Sleeve {
2021-09-05 01:09:30 +02:00
return Generic_fromJSON(Sleeve, value.data);
}
}
Reviver.constructors.Sleeve = Sleeve;