SLEEVE: Fixed inconsistencies in how sleeve work rewards are handled. (#211)

This commit is contained in:
Snarling 2022-11-10 21:56:46 -05:00 committed by GitHub
parent 426ad5f296
commit c669e473d1
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
14 changed files with 58 additions and 73 deletions

@ -4,7 +4,7 @@ import { Sleeve } from "../Sleeve";
import { applySleeveGains, Work, WorkType } from "./Work"; import { applySleeveGains, Work, WorkType } from "./Work";
import { CONSTANTS } from "../../../Constants"; import { CONSTANTS } from "../../../Constants";
import { GeneralActions } from "../../../Bladeburner/data/GeneralActions"; import { GeneralActions } from "../../../Bladeburner/data/GeneralActions";
import { WorkStats } from "../../../Work/WorkStats"; import { scaleWorkStats } from "../../../Work/WorkStats";
interface SleeveBladeburnerWorkParams { interface SleeveBladeburnerWorkParams {
type: string; type: string;
@ -31,7 +31,7 @@ export class SleeveBladeburnerWork extends Work {
return ret / CONSTANTS._idleSpeed; return ret / CONSTANTS._idleSpeed;
} }
process(sleeve: Sleeve, cycles: number): number { process(sleeve: Sleeve, cycles: number) {
if (!Player.bladeburner) throw new Error("sleeve doing blade work without being a member"); if (!Player.bladeburner) throw new Error("sleeve doing blade work without being a member");
this.cyclesWorked += cycles; this.cyclesWorked += cycles;
const actionIdent = Player.bladeburner.getActionIdFromTypeAndName(this.actionType, this.actionName); const actionIdent = Player.bladeburner.getActionIdFromTypeAndName(this.actionType, this.actionName);
@ -39,35 +39,27 @@ export class SleeveBladeburnerWork extends Work {
if (this.actionType === "Contracts") { if (this.actionType === "Contracts") {
const action = Player.bladeburner.getActionObject(actionIdent); const action = Player.bladeburner.getActionObject(actionIdent);
if (!action) throw new Error(`Error getting ${this.actionName} action object`); if (!action) throw new Error(`Error getting ${this.actionName} action object`);
if (action.count <= 0) { if (action.count <= 0) return sleeve.stopWork();
sleeve.stopWork();
return 0;
}
} }
while (this.cyclesWorked > this.cyclesNeeded(sleeve)) { while (this.cyclesWorked > this.cyclesNeeded(sleeve)) {
if (this.actionType === "Contracts") { if (this.actionType === "Contracts") {
const action = Player.bladeburner.getActionObject(actionIdent); const action = Player.bladeburner.getActionObject(actionIdent);
if (!action) throw new Error(`Error getting ${this.actionName} action object`); if (!action) throw new Error(`Error getting ${this.actionName} action object`);
if (action.count <= 0) { if (action.count <= 0) return sleeve.stopWork();
sleeve.stopWork();
return 0;
}
} }
const retValue = Player.bladeburner.completeAction(sleeve, actionIdent, false); const retValue = Player.bladeburner.completeAction(sleeve, actionIdent, false);
let exp: WorkStats | undefined;
if (this.actionType === "General") { if (this.actionType === "General") {
exp = GeneralActions[this.actionName]?.exp; const exp = GeneralActions[this.actionName]?.exp;
if (!exp) throw new Error(`Somehow there was no exp for action ${this.actionType} ${this.actionName}`); if (!exp) throw new Error(`Somehow there was no exp for action ${this.actionType} ${this.actionName}`);
applySleeveGains(sleeve, exp, 1); applySleeveGains(sleeve, scaleWorkStats(exp, sleeve.shockBonus(), false));
} }
if (this.actionType === "Contracts") { if (this.actionType === "Contracts") {
applySleeveGains(sleeve, retValue, 1); applySleeveGains(sleeve, scaleWorkStats(retValue, sleeve.shockBonus(), false));
} }
this.cyclesWorked -= this.cyclesNeeded(sleeve); this.cyclesWorked -= this.cyclesNeeded(sleeve);
} }
return 0;
} }
APICopy(): Record<string, unknown> { APICopy(): Record<string, unknown> {

@ -33,11 +33,11 @@ export class SleeveClassWork extends Work {
); );
} }
process(sleeve: Sleeve, cycles: number): number { process(sleeve: Sleeve, cycles: number) {
const rate = this.calculateRates(sleeve); const rate = this.calculateRates(sleeve);
applySleeveGains(sleeve, rate, cycles); applySleeveGains(sleeve, rate, cycles);
return 0;
} }
APICopy(): Record<string, unknown> { APICopy(): Record<string, unknown> {
return { return {
type: this.type, type: this.type,

@ -5,7 +5,7 @@ import { LocationName } from "../../../Locations/data/LocationNames";
import { Companies } from "../../../Company/Companies"; import { Companies } from "../../../Company/Companies";
import { Company } from "../../../Company/Company"; import { Company } from "../../../Company/Company";
import { calculateCompanyWorkStats } from "../../../Work/Formulas"; import { calculateCompanyWorkStats } from "../../../Work/Formulas";
import { WorkStats } from "../../../Work/WorkStats"; import { scaleWorkStats, WorkStats } from "../../../Work/WorkStats";
import { influenceStockThroughCompanyWork } from "../../../StockMarket/PlayerInfluencing"; import { influenceStockThroughCompanyWork } from "../../../StockMarket/PlayerInfluencing";
import { Player } from "@player"; import { Player } from "@player";
import { CompanyPositions } from "../../../Company/CompanyPositions"; import { CompanyPositions } from "../../../Company/CompanyPositions";
@ -33,16 +33,19 @@ export class SleeveCompanyWork extends Work {
getGainRates(sleeve: Sleeve): WorkStats { getGainRates(sleeve: Sleeve): WorkStats {
const company = this.getCompany(); const company = this.getCompany();
return calculateCompanyWorkStats(sleeve, company, CompanyPositions[Player.jobs[company.name]], company.favor); return scaleWorkStats(
calculateCompanyWorkStats(sleeve, company, CompanyPositions[Player.jobs[company.name]], company.favor),
sleeve.shockBonus(),
false,
);
} }
process(sleeve: Sleeve, cycles: number): number { process(sleeve: Sleeve, cycles: number) {
const company = this.getCompany(); const company = this.getCompany();
const gains = this.getGainRates(sleeve); const gains = this.getGainRates(sleeve);
applySleeveGains(sleeve, gains, cycles); applySleeveGains(sleeve, gains, cycles);
company.playerReputation += gains.reputation * cycles; company.playerReputation += gains.reputation * cycles;
influenceStockThroughCompanyWork(company, gains.reputation, cycles); influenceStockThroughCompanyWork(company, gains.reputation, cycles);
return 0;
} }
APICopy(): Record<string, unknown> { APICopy(): Record<string, unknown> {

@ -26,29 +26,24 @@ export class SleeveCrimeWork extends Work {
} }
getExp(sleeve: Sleeve): WorkStats { getExp(sleeve: Sleeve): WorkStats {
return calculateCrimeWorkStats(sleeve, this.getCrime()); return scaleWorkStats(calculateCrimeWorkStats(sleeve, this.getCrime()), sleeve.shockBonus(), false);
} }
cyclesNeeded(): number { cyclesNeeded(): number {
return this.getCrime().time / CONSTANTS._idleSpeed; return this.getCrime().time / CONSTANTS._idleSpeed;
} }
process(sleeve: Sleeve, cycles: number): number { process(sleeve: Sleeve, cycles: number) {
this.cyclesWorked += cycles; this.cyclesWorked += cycles;
if (this.cyclesWorked < this.cyclesNeeded()) return;
const crime = this.getCrime(); const crime = this.getCrime();
let gains = this.getExp(sleeve); const gains = this.getExp(sleeve);
if (this.cyclesWorked >= this.cyclesNeeded()) { const success = Math.random() < crime.successRate(sleeve);
if (Math.random() < crime.successRate(sleeve)) { if (success) Player.karma -= crime.karma * sleeve.syncBonus();
Player.karma -= crime.karma * sleeve.syncBonus(); else gains.money = 0;
} else { applySleeveGains(sleeve, gains, success ? 1 : 0.25);
gains.money = 0; this.cyclesWorked -= this.cyclesNeeded();
gains = scaleWorkStats(gains, 0.25);
}
applySleeveGains(sleeve, gains, cycles);
this.cyclesWorked -= this.cyclesNeeded();
}
return 0;
} }
APICopy(): Record<string, unknown> { APICopy(): Record<string, unknown> {

@ -28,7 +28,7 @@ export class SleeveFactionWork extends Work {
} }
getExpRates(sleeve: Sleeve): WorkStats { getExpRates(sleeve: Sleeve): WorkStats {
return scaleWorkStats(calculateFactionExp(sleeve, this.factionWorkType), sleeve.shockBonus()); return scaleWorkStats(calculateFactionExp(sleeve, this.factionWorkType), sleeve.shockBonus(), false);
} }
getReputationRate(sleeve: Sleeve): number { getReputationRate(sleeve: Sleeve): number {
@ -41,17 +41,13 @@ export class SleeveFactionWork extends Work {
return f; return f;
} }
process(sleeve: Sleeve, cycles: number): number { process(sleeve: Sleeve, cycles: number) {
if (this.factionName === Player.gang?.facName) { if (this.factionName === Player.gang?.facName) return sleeve.stopWork();
sleeve.stopWork();
return 0;
}
const exp = this.getExpRates(sleeve); const exp = this.getExpRates(sleeve);
applySleeveGains(sleeve, exp, cycles); applySleeveGains(sleeve, exp, cycles);
const rep = this.getReputationRate(sleeve); const rep = this.getReputationRate(sleeve);
this.getFaction().playerReputation += rep; this.getFaction().playerReputation += rep * cycles;
return 0;
} }
APICopy(): Record<string, unknown> { APICopy(): Record<string, unknown> {

@ -20,14 +20,13 @@ export class SleeveInfiltrateWork extends Work {
return infiltrateCycles; return infiltrateCycles;
} }
process(_sleeve: Sleeve, cycles: number): number { process(_sleeve: Sleeve, cycles: number) {
if (!Player.bladeburner) throw new Error("sleeve doing blade work without being a member"); if (!Player.bladeburner) throw new Error("sleeve doing blade work without being a member");
this.cyclesWorked += cycles; this.cyclesWorked += cycles;
if (this.cyclesWorked > this.cyclesNeeded()) { if (this.cyclesWorked > this.cyclesNeeded()) {
this.cyclesWorked -= this.cyclesNeeded(); this.cyclesWorked -= this.cyclesNeeded();
Player.bladeburner.infiltrateSynthoidCommunities(); Player.bladeburner.infiltrateSynthoidCommunities();
} }
return 0;
} }
APICopy(): Record<string, unknown> { APICopy(): Record<string, unknown> {

@ -10,10 +10,9 @@ export class SleeveRecoveryWork extends Work {
super(WorkType.RECOVERY); super(WorkType.RECOVERY);
} }
process(sleeve: Sleeve, cycles: number): number { process(sleeve: Sleeve, cycles: number) {
sleeve.shock = Math.min(100, sleeve.shock + 0.0002 * cycles); sleeve.shock = Math.min(100, sleeve.shock + 0.0002 * cycles);
if (sleeve.shock >= 100) sleeve.stopWork(); if (sleeve.shock >= 100) sleeve.stopWork();
return 0;
} }
APICopy(): Record<string, unknown> { APICopy(): Record<string, unknown> {

@ -11,8 +11,8 @@ export class SleeveSupportWork extends Work {
Player.bladeburner?.sleeveSupport(true); Player.bladeburner?.sleeveSupport(true);
} }
process(): number { process() {
return 0; return;
} }
finish(): void { finish(): void {

@ -12,13 +12,12 @@ export class SleeveSynchroWork extends Work {
super(WorkType.SYNCHRO); super(WorkType.SYNCHRO);
} }
process(sleeve: Sleeve, cycles: number): number { process(sleeve: Sleeve, cycles: number) {
sleeve.sync = Math.min( sleeve.sync = Math.min(
100, 100,
sleeve.sync + calculateIntelligenceBonus(Player.skills.intelligence, 0.5) * 0.0002 * cycles, sleeve.sync + calculateIntelligenceBonus(Player.skills.intelligence, 0.5) * 0.0002 * cycles,
); );
if (sleeve.sync >= 100) sleeve.stopWork(); if (sleeve.sync >= 100) sleeve.stopWork();
return 0;
} }
APICopy(): Record<string, unknown> { APICopy(): Record<string, unknown> {

@ -1,14 +1,16 @@
import { Player } from "@player"; import { Player } from "@player";
import { IReviverValue } from "../../../utils/JSONReviver"; import { IReviverValue } from "../../../utils/JSONReviver";
import { Sleeve } from "../Sleeve"; import { Sleeve } from "../Sleeve";
import { applyWorkStats, applyWorkStatsExp, scaleWorkStats, WorkStats } from "../../../Work/WorkStats"; import { applyWorkStatsExp, WorkStats } from "../../../Work/WorkStats";
export const applySleeveGains = (sleeve: Sleeve, rawStats: WorkStats, cycles = 1): void => { export const applySleeveGains = (sleeve: Sleeve, shockedStats: WorkStats, mult = 1): void => {
const shockedStats = scaleWorkStats(rawStats, sleeve.shockBonus(), rawStats.money > 0); applyWorkStatsExp(sleeve, shockedStats, mult);
applyWorkStatsExp(sleeve, shockedStats, cycles); Player.gainMoney(shockedStats.money * mult, "sleeves");
const syncStats = scaleWorkStats(shockedStats, sleeve.syncBonus(), rawStats.money > 0); const sync = sleeve.syncBonus();
applyWorkStats(Player, syncStats, cycles, "sleeves"); // The receiving sleeves and the player do not apply their xp multipliers from augs (avoid double dipping xp mults)
Player.sleeves.filter((s) => s !== sleeve).forEach((s) => applyWorkStatsExp(s, syncStats, cycles)); applyWorkStatsExp(Player, shockedStats, mult * sync);
// Sleeves apply their own shock bonus to the XP they receive, even though it is also shocked by the working sleeve
Player.sleeves.forEach((s) => s !== sleeve && applyWorkStatsExp(s, shockedStats, mult * sync * s.shockBonus()));
}; };
export abstract class Work { export abstract class Work {
@ -18,7 +20,7 @@ export abstract class Work {
this.type = type; this.type = type;
} }
abstract process(sleeve: Sleeve, cycles: number): number; abstract process(sleeve: Sleeve, cycles: number): void;
abstract APICopy(): Record<string, unknown>; abstract APICopy(): Record<string, unknown>;
abstract toJSON(): IReviverValue; abstract toJSON(): IReviverValue;
finish(): void { finish(): void {

@ -133,7 +133,7 @@ export function EarningsElement(props: IProps): React.ReactElement {
[`Dexterity Exp:`, `${numeralWrapper.formatExp(CYCLES_PER_SEC * rates.dexExp)} / sec`], [`Dexterity Exp:`, `${numeralWrapper.formatExp(CYCLES_PER_SEC * rates.dexExp)} / sec`],
[`Agility Exp:`, `${numeralWrapper.formatExp(CYCLES_PER_SEC * rates.agiExp)} / sec`], [`Agility Exp:`, `${numeralWrapper.formatExp(CYCLES_PER_SEC * rates.agiExp)} / sec`],
[`Charisma Exp:`, `${numeralWrapper.formatExp(CYCLES_PER_SEC * rates.chaExp)} / sec`], [`Charisma Exp:`, `${numeralWrapper.formatExp(CYCLES_PER_SEC * rates.chaExp)} / sec`],
[`Reputation:`, <ReputationRate reputation={repGain} />], [`Reputation:`, <ReputationRate reputation={CYCLES_PER_SEC * repGain} />],
]; ];
} }

@ -3814,10 +3814,14 @@ export interface WorkStats {
* @public * @public
*/ */
interface WorkFormulas { interface WorkFormulas {
crimeSuccessChance(person: Person, crimeType: CrimeType | CrimeNames): number; crimeSuccessChance(person: Person, crimeType: CrimeType | `${CrimeType}`): number;
/** @returns The WorkStats gained when completing one instance of the specified crime. */
crimeGains(person: Person, crimeType: CrimeType | `${CrimeType}`): WorkStats; crimeGains(person: Person, crimeType: CrimeType | `${CrimeType}`): WorkStats;
/** @returns The WorkStats applied every game cycle (200ms) by taking the specified class. */
classGains(person: Person, classType: ClassType | `${ClassType}`, locationName: string): WorkStats; classGains(person: Person, classType: ClassType | `${ClassType}`, locationName: string): WorkStats;
/** @returns The WorkStats applied every game cycle (200ms) by performing the specified faction work. */
factionGains(person: Person, workType: FactionWorkType | `${FactionWorkType}`, favor: number): WorkStats; factionGains(person: Person, workType: FactionWorkType | `${FactionWorkType}`, favor: number): WorkStats;
/** @returns The WorkStats applied every game cycle (200ms) by performing the specified company work. */
companyGains( companyGains(
person: Person, person: Person,
companyName: string, companyName: string,

@ -63,6 +63,7 @@ export function calculateCrimeWorkStats(person: IPerson, crime: Crime): WorkStat
return gains; return gains;
} }
/** @returns faction rep rate per cycle */
export const calculateFactionRep = (person: IPerson, type: FactionWorkType, favor: number): number => { export const calculateFactionRep = (person: IPerson, type: FactionWorkType, favor: number): number => {
const repFormulas = { const repFormulas = {
[FactionWorkType.HACKING]: getHackingWorkRepGain, [FactionWorkType.HACKING]: getHackingWorkRepGain,
@ -72,6 +73,7 @@ export const calculateFactionRep = (person: IPerson, type: FactionWorkType, favo
return repFormulas[type](person, favor); return repFormulas[type](person, favor);
}; };
/** @returns per-cycle WorkStats */
export function calculateFactionExp(person: IPerson, type: FactionWorkType): WorkStats { export function calculateFactionExp(person: IPerson, type: FactionWorkType): WorkStats {
return scaleWorkStats( return scaleWorkStats(
multWorkStats(FactionWorkStats[type], person.mults), multWorkStats(FactionWorkStats[type], person.mults),
@ -87,6 +89,7 @@ export function calculateCost(classs: Class, location: Location): number {
return classs.earnings.money * location.costMult * discount; return classs.earnings.money * location.costMult * discount;
} }
/** @returns per-cycle WorkStats */
export function calculateClassEarnings(person: IPerson, type: ClassType, locationName: LocationName): WorkStats { export function calculateClassEarnings(person: IPerson, type: ClassType, locationName: LocationName): WorkStats {
const hashManager = Player.hashManager; const hashManager = Player.hashManager;
const classs = Classes[type]; const classs = Classes[type];
@ -106,6 +109,7 @@ export function calculateClassEarnings(person: IPerson, type: ClassType, locatio
return earnings; return earnings;
} }
/** @returns per-cycle WorkStats */
export const calculateCompanyWorkStats = ( export const calculateCompanyWorkStats = (
worker: IPerson, worker: IPerson,
company: Company, company: Company,

@ -77,18 +77,10 @@ export const applyWorkStats = (target: Person, workStats: WorkStats, cycles: num
return gains; return gains;
}; };
export const applyWorkStatsExp = (target: Person, workStats: WorkStats, cycles: number): WorkStats => { export const applyWorkStatsExp = (target: Person, workStats: WorkStats, mult = 1): WorkStats => {
const gains = { const gains = scaleWorkStats(workStats, mult, false);
money: 0, gains.money = 0;
reputation: 0, gains.reputation = 0;
hackExp: workStats.hackExp * cycles,
strExp: workStats.strExp * cycles,
defExp: workStats.defExp * cycles,
dexExp: workStats.dexExp * cycles,
agiExp: workStats.agiExp * cycles,
chaExp: workStats.chaExp * cycles,
intExp: workStats.intExp * cycles,
};
target.gainHackingExp(gains.hackExp); target.gainHackingExp(gains.hackExp);
target.gainStrengthExp(gains.strExp); target.gainStrengthExp(gains.strExp);
target.gainDefenseExp(gains.defExp); target.gainDefenseExp(gains.defExp);