mirror of
https://github.com/bitburner-official/bitburner-src.git
synced 2026-04-17 06:48:42 +02:00
238 lines
9.7 KiB
TypeScript
238 lines
9.7 KiB
TypeScript
import type { Division } from "./Division";
|
|
|
|
import { CorpMaterialName } from "@nsdefs";
|
|
import { CityName, CorpEmployeeJob } from "@enums";
|
|
import { IndustriesData } from "./data/IndustryData";
|
|
import { MaterialInfo } from "./MaterialInfo";
|
|
|
|
import { Generic_fromJSON, Generic_toJSON, IReviverValue, constructorsForReviver } from "../utils/JSONReviver";
|
|
import { getRandomIntInclusive } from "../utils/helpers/getRandomIntInclusive";
|
|
import { PartialRecord, createEnumKeyedRecord, getRecordEntries, getRecordKeys } from "../Types/Record";
|
|
|
|
interface IConstructorParams {
|
|
name: string;
|
|
createCity: CityName;
|
|
designInvestment: number;
|
|
advertisingInvestment: number;
|
|
}
|
|
|
|
/** A corporation product. Products are shared across the entire division, unlike materials which are per-warehouse */
|
|
export class Product {
|
|
/** Name of the product */
|
|
name = "DefaultProductName";
|
|
|
|
/** Demand for this product, which goes down over time. */
|
|
demand = 0;
|
|
|
|
/** Competition for this product */
|
|
competition = 0;
|
|
|
|
/** Markup. Affects how high of a price you can charge for this Product
|
|
without suffering a loss in the # of sales */
|
|
markup = 0;
|
|
|
|
/** Whether the development for this product is finished yet */
|
|
finished = false;
|
|
developmentProgress = 0; // Creation progress - A number between 0-100 representing percentage
|
|
creationCity = CityName.Sector12; // City in which the product is/was being created
|
|
designInvestment = 0; // How much money was invested into designing this Product
|
|
advertisingInvestment = 0; // How much money was invested into advertising this Product
|
|
|
|
// The average employee productivity and scientific research across the creation of the Product
|
|
creationJobFactors = {
|
|
[CorpEmployeeJob.Operations]: 0,
|
|
[CorpEmployeeJob.Engineer]: 0,
|
|
[CorpEmployeeJob.Business]: 0,
|
|
[CorpEmployeeJob.Management]: 0,
|
|
[CorpEmployeeJob.RandD]: 0,
|
|
total: 0,
|
|
};
|
|
|
|
// Aggregate score for this Product's 'rating'
|
|
// This is based on the stats/properties below. The weighting of the
|
|
// stats/properties below differs between different industries
|
|
rating = 0;
|
|
|
|
/** Stats of the product */
|
|
stats = {
|
|
quality: 0,
|
|
performance: 0,
|
|
durability: 0,
|
|
reliability: 0,
|
|
aesthetics: 0,
|
|
features: 0,
|
|
};
|
|
|
|
// data that is stored per city
|
|
cityData = createEnumKeyedRecord(CityName, () => ({
|
|
/** Amount of product stored in warehouse */
|
|
stored: 0,
|
|
/** Amount of this product produced per cycle in this city */
|
|
productionAmount: 0,
|
|
/** Amount of this product that was sold last cycle in this city */
|
|
actualSellAmount: 0,
|
|
/** Total effective rating of the product in this city */
|
|
effectiveRating: 0,
|
|
/** Manual limit on production amount for the product in this city*/
|
|
productionLimit: null as number | null,
|
|
/** Player input sell amount e.g. "MAX" */
|
|
desiredSellAmount: 0 as number | string,
|
|
/** Player input sell price e.g. "MP * 5" */
|
|
desiredSellPrice: "" as string | number,
|
|
/** Cost of producing this product if buying its component materials at market price */
|
|
productionCost: 0,
|
|
}));
|
|
|
|
/** How much warehouse space is occupied per unit of this product */
|
|
size = 0;
|
|
|
|
/** Required materials per unit of this product */
|
|
requiredMaterials: PartialRecord<CorpMaterialName, number> = {};
|
|
|
|
// Flags that signal whether automatic sale pricing through Market TA is enabled
|
|
marketTa1 = false;
|
|
marketTa2 = false;
|
|
uiMarketPrice = createEnumKeyedRecord(CityName, () => 0);
|
|
|
|
/** Effective number that "MAX" represents in a sell amount */
|
|
maxSellAmount = 0;
|
|
|
|
constructor(params: IConstructorParams | null = null) {
|
|
if (!params) return;
|
|
this.name = params.name;
|
|
this.creationCity = params.createCity;
|
|
this.designInvestment = params.designInvestment;
|
|
this.advertisingInvestment = params.advertisingInvestment;
|
|
}
|
|
|
|
// Make progress on this product based on current employee productivity
|
|
createProduct(marketCycles: number, employeeProd: typeof Product.prototype.creationJobFactors): void {
|
|
if (this.finished) return;
|
|
|
|
// Designing/Creating a Product is based mostly off Engineers
|
|
const opProd = employeeProd[CorpEmployeeJob.Operations];
|
|
const engrProd = employeeProd[CorpEmployeeJob.Engineer];
|
|
const mgmtProd = employeeProd[CorpEmployeeJob.Management];
|
|
const total = opProd + engrProd + mgmtProd;
|
|
if (total <= 0) {
|
|
return;
|
|
}
|
|
|
|
// Management is a multiplier for the production from Engineers
|
|
const mgmtFactor = 1 + mgmtProd / (1.2 * total);
|
|
const prodMult = (Math.pow(engrProd, 0.34) + Math.pow(opProd, 0.2)) * mgmtFactor;
|
|
const progress = Math.min(marketCycles * 0.01 * prodMult, 100 - this.developmentProgress);
|
|
if (progress <= 0) {
|
|
return;
|
|
}
|
|
|
|
this.developmentProgress += progress;
|
|
for (const pos of getRecordKeys(employeeProd)) {
|
|
this.creationJobFactors[pos] += (employeeProd[pos] * progress) / 100;
|
|
}
|
|
}
|
|
|
|
// @param division - Division object. Reference to division that makes this Product
|
|
finishProduct(division: Division): void {
|
|
this.finished = true;
|
|
|
|
// Calculate properties
|
|
const totalProd = this.creationJobFactors.total;
|
|
const engrRatio = this.creationJobFactors[CorpEmployeeJob.Engineer] / totalProd;
|
|
const mgmtRatio = this.creationJobFactors[CorpEmployeeJob.Management] / totalProd;
|
|
const rndRatio = this.creationJobFactors[CorpEmployeeJob.RandD] / totalProd;
|
|
const opsRatio = this.creationJobFactors[CorpEmployeeJob.Operations] / totalProd;
|
|
const busRatio = this.creationJobFactors[CorpEmployeeJob.Business] / totalProd;
|
|
|
|
const designMult = 1 + Math.pow(this.designInvestment, 0.1) / 100;
|
|
const balanceMult = 1.2 * engrRatio + 0.9 * mgmtRatio + 1.3 * rndRatio + 1.5 * opsRatio + busRatio;
|
|
const sciMult = 1 + Math.pow(division.researchPoints, division.researchFactor) / 800;
|
|
const totalMult = balanceMult * designMult * sciMult;
|
|
|
|
this.stats.quality =
|
|
totalMult *
|
|
(0.1 * this.creationJobFactors[CorpEmployeeJob.Engineer] +
|
|
0.05 * this.creationJobFactors[CorpEmployeeJob.Management] +
|
|
0.05 * this.creationJobFactors[CorpEmployeeJob.RandD] +
|
|
0.02 * this.creationJobFactors[CorpEmployeeJob.Operations] +
|
|
0.02 * this.creationJobFactors[CorpEmployeeJob.Business]);
|
|
this.stats.performance =
|
|
totalMult *
|
|
(0.15 * this.creationJobFactors[CorpEmployeeJob.Engineer] +
|
|
0.02 * this.creationJobFactors[CorpEmployeeJob.Management] +
|
|
0.02 * this.creationJobFactors[CorpEmployeeJob.RandD] +
|
|
0.02 * this.creationJobFactors[CorpEmployeeJob.Operations] +
|
|
0.02 * this.creationJobFactors[CorpEmployeeJob.Business]);
|
|
this.stats.durability =
|
|
totalMult *
|
|
(0.05 * this.creationJobFactors[CorpEmployeeJob.Engineer] +
|
|
0.02 * this.creationJobFactors[CorpEmployeeJob.Management] +
|
|
0.08 * this.creationJobFactors[CorpEmployeeJob.RandD] +
|
|
0.05 * this.creationJobFactors[CorpEmployeeJob.Operations] +
|
|
0.05 * this.creationJobFactors[CorpEmployeeJob.Business]);
|
|
this.stats.reliability =
|
|
totalMult *
|
|
(0.02 * this.creationJobFactors[CorpEmployeeJob.Engineer] +
|
|
0.08 * this.creationJobFactors[CorpEmployeeJob.Management] +
|
|
0.02 * this.creationJobFactors[CorpEmployeeJob.RandD] +
|
|
0.05 * this.creationJobFactors[CorpEmployeeJob.Operations] +
|
|
0.08 * this.creationJobFactors[CorpEmployeeJob.Business]);
|
|
this.stats.aesthetics =
|
|
totalMult *
|
|
(0.0 * this.creationJobFactors[CorpEmployeeJob.Engineer] +
|
|
0.08 * this.creationJobFactors[CorpEmployeeJob.Management] +
|
|
0.05 * this.creationJobFactors[CorpEmployeeJob.RandD] +
|
|
0.02 * this.creationJobFactors[CorpEmployeeJob.Operations] +
|
|
0.1 * this.creationJobFactors[CorpEmployeeJob.Business]);
|
|
this.stats.features =
|
|
totalMult *
|
|
(0.08 * this.creationJobFactors[CorpEmployeeJob.Engineer] +
|
|
0.05 * this.creationJobFactors[CorpEmployeeJob.Management] +
|
|
0.02 * this.creationJobFactors[CorpEmployeeJob.RandD] +
|
|
0.05 * this.creationJobFactors[CorpEmployeeJob.Operations] +
|
|
0.05 * this.creationJobFactors[CorpEmployeeJob.Business]);
|
|
this.calculateRating(division);
|
|
const advMult = 1 + Math.pow(this.advertisingInvestment, 0.1) / 100;
|
|
const busmgtgRatio = Math.max(busRatio + mgmtRatio, 1 / totalProd);
|
|
this.markup = 100 / (advMult * Math.pow(this.stats.quality + 0.001, 0.65) * busmgtgRatio);
|
|
|
|
// I actually don't understand well enough to know if this is right.
|
|
// I'm adding this to prevent a crash.
|
|
if (this.markup === 0 || !isFinite(this.markup)) this.markup = 1;
|
|
|
|
this.demand =
|
|
division.awareness === 0 ? 20 : Math.min(100, advMult * (100 * (division.popularity / division.awareness)));
|
|
this.competition = getRandomIntInclusive(0, 70);
|
|
|
|
//Calculate the product's required materials and size
|
|
this.size = 0;
|
|
for (const [matName, reqQty] of getRecordEntries(division.requiredMaterials)) {
|
|
this.requiredMaterials[matName] = reqQty;
|
|
this.size += MaterialInfo[matName].size * reqQty;
|
|
}
|
|
}
|
|
|
|
calculateRating(industry: Division): void {
|
|
const weights = IndustriesData[industry.industry].product?.ratingWeights;
|
|
if (!weights) {
|
|
return console.error(`Could not find product rating weights for: ${industry.name}`);
|
|
}
|
|
this.rating = getRecordEntries(weights).reduce(
|
|
(total, [statName, weight]) => total + this.stats[statName] * weight,
|
|
0,
|
|
);
|
|
}
|
|
|
|
// Serialize the current object to a JSON save state.
|
|
toJSON(): IReviverValue {
|
|
return Generic_toJSON("Product", this);
|
|
}
|
|
|
|
// Initializes a Product object from a JSON save state.
|
|
static fromJSON(value: IReviverValue): Product {
|
|
return Generic_fromJSON(Product, value.data);
|
|
}
|
|
}
|
|
|
|
constructorsForReviver.Product = Product;
|