bitburner-src/src/Corporation/OfficeSpace.ts

245 lines
8.7 KiB
TypeScript

import { CityName, CorpEmployeeJob } from "@enums";
import * as corpConstants from "./data/Constants";
import { Generic_fromJSON, Generic_toJSON, IReviverValue, constructorsForReviver } from "../utils/JSONReviver";
import { Division } from "./Division";
import { Corporation } from "./Corporation";
import { getRandomInt } from "../utils/helpers/getRandomInt";
import { createEnumKeyedRecord, getRecordKeys } from "../Types/Record";
interface IParams {
city: CityName;
size: number;
}
export class OfficeSpace {
city = CityName.Sector12;
size = 1;
maxEnergy = 100;
maxMorale = 100;
avgEnergy = 75;
avgMorale = 75;
avgIntelligence = 75;
avgCharisma = 75;
avgCreativity = 75;
avgEfficiency = 75;
totalExperience = 0;
numEmployees = 0;
totalSalary = 0;
autoTea = false;
autoParty = false;
teaPending = false;
partyMult = 1;
employeeProductionByJob = { total: 0, ...createEnumKeyedRecord(CorpEmployeeJob, () => 0) };
employeeJobs = createEnumKeyedRecord(CorpEmployeeJob, () => 0);
employeeNextJobs = createEnumKeyedRecord(CorpEmployeeJob, () => 0);
constructor(params: IParams | null = null) {
if (!params) return;
this.city = params.city;
this.size = params.size;
}
atCapacity(): boolean {
return this.numEmployees >= this.size;
}
process(marketCycles = 1, corporation: Corporation, industry: Division): number {
// HRBuddy AutoRecruitment and Interning
if (industry.hasResearch("HRBuddy-Recruitment") && !this.atCapacity()) {
this.hireRandomEmployee(
industry.hasResearch("HRBuddy-Training") ? CorpEmployeeJob.Intern : CorpEmployeeJob.Unassigned,
);
}
// Update employee jobs and job counts
for (const [pos, jobCount] of Object.entries(this.employeeNextJobs) as [CorpEmployeeJob, number][]) {
this.employeeJobs[pos] = jobCount;
}
// Process Office properties
this.maxEnergy = 100;
this.maxMorale = 100;
if (industry.hasResearch("Go-Juice")) this.maxEnergy += 10;
if (industry.hasResearch("Sti.mu")) this.maxMorale += 10;
if (industry.hasResearch("AutoBrew")) this.autoTea = true;
if (industry.hasResearch("AutoPartyManager")) this.autoParty = true;
if (this.numEmployees > 0) {
/** Multiplier for employee morale/energy based on company performance */
let perfMult = 1.002;
if (this.numEmployees >= 9) {
perfMult = Math.pow(
1 +
0.002 * Math.min(1 / 9, this.employeeJobs.Intern / this.numEmployees - 1 / 9) * 9 -
(corporation.funds < 0 && industry.lastCycleRevenue < industry.lastCycleExpenses ? 0.001 : 0),
marketCycles,
);
}
// Flat reduction per cycle.
// This does not cause a noticable decrease (it's only -.001% per cycle).
const reduction = 0.002 * marketCycles;
if (this.autoTea) {
this.avgEnergy = this.maxEnergy;
} else {
// Tea gives a flat +2 to energy
this.avgEnergy = (this.avgEnergy - reduction * Math.random()) * perfMult + (this.teaPending ? 2 : 0);
}
if (this.autoParty) {
this.avgMorale = this.maxMorale;
} else {
// Each 10% multiplier gives an extra flat +1 to morale to make recovering from low morale easier.
const increase = this.partyMult > 1 ? (this.partyMult - 1) * 10 : 0;
this.avgMorale = ((this.avgMorale - reduction * Math.random()) * perfMult + increase) * this.partyMult;
}
this.avgEnergy = Math.max(Math.min(this.avgEnergy, this.maxEnergy), corpConstants.minEmployeeDecay);
this.avgMorale = Math.max(Math.min(this.avgMorale, this.maxMorale), corpConstants.minEmployeeDecay);
this.teaPending = false;
this.partyMult = 1;
}
// Get experience increase; unassigned employees do not contribute, interning employees contribute 10x
this.totalExperience +=
0.0015 *
marketCycles *
(this.numEmployees -
this.employeeJobs[CorpEmployeeJob.Unassigned] +
this.employeeJobs[CorpEmployeeJob.Intern] * 9);
this.calculateEmployeeProductivity(corporation, industry);
if (this.numEmployees === 0) {
this.totalSalary = 0;
} else {
this.totalSalary =
corpConstants.employeeSalaryMultiplier *
marketCycles *
this.numEmployees *
(this.avgIntelligence +
this.avgCharisma +
this.totalExperience / this.numEmployees +
this.avgCreativity +
this.avgEfficiency);
}
return this.totalSalary;
}
calculateEmployeeProductivity(corporation: Corporation, industry: Division): void {
const effCre = this.avgCreativity * corporation.getEmployeeCreMultiplier() * industry.getEmployeeCreMultiplier(),
effCha = this.avgCharisma * corporation.getEmployeeChaMult() * industry.getEmployeeChaMultiplier(),
effInt = this.avgIntelligence * corporation.getEmployeeIntMult() * industry.getEmployeeIntMultiplier(),
effEff = this.avgEfficiency * corporation.getEmployeeEffMult() * industry.getEmployeeEffMultiplier();
const prodBase = this.avgMorale * this.avgEnergy * 1e-4;
let total = 0;
const exp = this.totalExperience / this.numEmployees || 0;
for (const name of getRecordKeys(this.employeeProductionByJob)) {
let prodMult = 0;
switch (name) {
case CorpEmployeeJob.Operations:
prodMult = 0.6 * effInt + 0.1 * effCha + exp + 0.5 * effCre + effEff;
break;
case CorpEmployeeJob.Engineer:
prodMult = effInt + 0.1 * effCha + 1.5 * exp + effEff;
break;
case CorpEmployeeJob.Business:
prodMult = 0.4 * effInt + effCha + 0.5 * exp;
break;
case CorpEmployeeJob.Management:
prodMult = 2 * effCha + exp + 0.2 * effCre + 0.7 * effEff;
break;
case CorpEmployeeJob.RandD:
prodMult = 1.5 * effInt + 0.8 * exp + effCre + 0.5 * effEff;
break;
case CorpEmployeeJob.Unassigned:
case CorpEmployeeJob.Intern:
case "total":
continue;
default:
console.error(`Invalid employee position: ${name}`);
break;
}
this.employeeProductionByJob[name] = this.employeeJobs[name] * prodMult * prodBase;
total += this.employeeProductionByJob[name];
}
this.employeeProductionByJob.total = total;
}
hireRandomEmployee(position: CorpEmployeeJob): boolean {
if (this.atCapacity()) return false;
if (document.getElementById("cmpy-mgmt-hire-employee-popup") != null) return false;
++this.numEmployees;
++this.employeeJobs[position];
++this.employeeNextJobs[position];
this.totalExperience += getRandomInt(50, 100);
this.avgMorale = (this.avgMorale * this.numEmployees + getRandomInt(50, 100)) / (this.numEmployees + 1);
this.avgEnergy = (this.avgEnergy * this.numEmployees + getRandomInt(50, 100)) / (this.numEmployees + 1);
this.avgIntelligence = (this.avgIntelligence * this.numEmployees + getRandomInt(50, 100)) / (this.numEmployees + 1);
this.avgCharisma = (this.avgCharisma * this.numEmployees + getRandomInt(50, 100)) / (this.numEmployees + 1);
this.avgCreativity = (this.avgCreativity * this.numEmployees + getRandomInt(50, 100)) / (this.numEmployees + 1);
this.avgEfficiency = (this.avgEfficiency * this.numEmployees + getRandomInt(50, 100)) / (this.numEmployees + 1);
return true;
}
autoAssignJob(job: CorpEmployeeJob, target: number): boolean {
if (job === CorpEmployeeJob.Unassigned) {
throw new Error("internal autoAssignJob function called with EmployeePositions.Unassigned");
}
const diff = target - this.employeeNextJobs[job];
if (diff === 0) return true;
// We are already at the desired number
else if (diff <= this.employeeNextJobs[CorpEmployeeJob.Unassigned]) {
// This covers both a negative diff (reducing the amount of employees in position) and a positive (increasing and using up unassigned employees)
this.employeeNextJobs[CorpEmployeeJob.Unassigned] -= diff;
this.employeeNextJobs[job] = target;
return true;
}
return false;
}
getTeaCost(): number {
return corpConstants.teaCostPerEmployee * this.numEmployees;
}
setTea(): boolean {
if (!this.teaPending && !this.autoTea && this.numEmployees > 0) {
this.teaPending = true;
return true;
}
return false;
}
setParty(mult: number): boolean {
if (mult > 1 && this.partyMult === 1 && !this.autoParty && this.numEmployees > 0) {
this.partyMult = mult;
return true;
}
return false;
}
toJSON(): IReviverValue {
return Generic_toJSON("OfficeSpace", this);
}
static fromJSON(value: IReviverValue): OfficeSpace {
return Generic_fromJSON(OfficeSpace, value.data);
}
}
constructorsForReviver.OfficeSpace = OfficeSpace;