bitburner-src/src/Corporation/Actions.ts

531 lines
19 KiB
TypeScript
Raw Normal View History

import { Player } from "@player";
import { MaterialInfo } from "./MaterialInfo";
2022-09-23 11:54:04 +02:00
import { Corporation } from "./Corporation";
import { IndustryResearchTrees, IndustriesData } from "./IndustryData";
import { Division } from "./Division";
import * as corpConstants from "./data/Constants";
2021-09-02 04:16:48 +02:00
import { OfficeSpace } from "./OfficeSpace";
2021-09-02 06:36:33 +02:00
import { Material } from "./Material";
import { Product } from "./Product";
import { Warehouse } from "./Warehouse";
2023-01-29 14:14:12 +01:00
import { IndustryType } from "./data/Enums";
2021-09-10 08:17:55 +02:00
import { ResearchMap } from "./ResearchMap";
import { isRelevantMaterial } from "./ui/Helpers";
import { CityName } from "../Enums";
import { getRandomInt } from "../utils/helpers/getRandomInt";
import { CorpResearchName } from "@nsdefs";
import { isInteger } from "lodash";
import { getRecordValues } from "../Types/Record";
2021-09-02 04:16:48 +02:00
export function NewDivision(corporation: Corporation, industry: IndustryType, name: string): void {
if (corporation.divisions.size >= corporation.maxDivisions)
throw new Error(`Cannot expand into ${industry} industry, too many divisions!`);
if (corporation.divisions.has(name)) throw new Error(`Division name ${name} is already in use!`);
// "Overview" is forbidden as a division name, see CorporationRoot.tsx for why this would cause issues.
if (name === "Overview") throw new Error(`"Overview" is a forbidden division name.`);
2021-09-02 04:16:48 +02:00
const data = IndustriesData[industry];
if (!data) throw new Error(`Invalid industry: '${industry}'`);
const cost = data.startingCost;
2021-11-12 03:35:26 +01:00
if (corporation.funds < cost) {
2021-09-09 05:47:34 +02:00
throw new Error("Not enough money to create a new division in this industry");
2021-09-05 01:09:30 +02:00
} else if (name === "") {
throw new Error("New division must have a name!");
} else {
2021-11-12 03:35:26 +01:00
corporation.funds = corporation.funds - cost;
corporation.divisions.set(
name,
new Division({
2021-09-05 01:09:30 +02:00
corp: corporation,
name: name,
type: industry,
}),
);
}
2021-09-02 04:16:48 +02:00
}
export function removeDivision(corporation: Corporation, name: string) {
if (!corporation.divisions.has(name)) throw new Error("There is no division called " + name);
corporation.divisions.delete(name);
}
export function purchaseOffice(corporation: Corporation, division: Division, city: CityName): void {
if (corporation.funds < corpConstants.officeInitialCost) {
2021-09-09 05:47:34 +02:00
throw new Error("You don't have enough company funds to open a new office!");
2021-09-05 01:09:30 +02:00
}
if (division.offices[city]) {
throw new Error(`You have already expanded into ${city} for ${division.name}`);
}
corporation.funds = corporation.funds - corpConstants.officeInitialCost;
division.offices[city] = new OfficeSpace({
city: city,
size: corpConstants.officeInitialSize,
});
2021-09-02 04:16:48 +02:00
}
2022-09-20 12:47:54 +02:00
export function IssueDividends(corporation: Corporation, rate: number): void {
if (isNaN(rate) || rate < 0 || rate > corpConstants.dividendMaxRate) {
throw new Error(`Invalid value. Must be an number between 0 and ${corpConstants.dividendMaxRate}`);
2021-09-05 01:09:30 +02:00
}
2021-09-02 04:16:48 +02:00
corporation.dividendRate = rate;
2021-09-02 06:36:33 +02:00
}
export function IssueNewShares(corporation: Corporation, amount: number): [number, number, number] {
const max = corporation.calculateMaxNewShares();
// Round to nearest ten-millionth
amount = Math.round(amount / 10e6) * 10e6;
if (isNaN(amount) || amount < 10e6 || amount > max) {
throw new Error(`Invalid value. Must be an number between 10m and ${max} (20% of total shares)`);
}
const newSharePrice = Math.round(corporation.sharePrice * 0.8);
const profit = amount * newSharePrice;
corporation.issueNewSharesCooldown = corpConstants.issueNewSharesCooldown;
const privateOwnedRatio = 1 - (corporation.numShares + corporation.issuedShares) / corporation.totalShares;
const maxPrivateShares = Math.round((amount / 2) * privateOwnedRatio);
const privateShares = Math.round(getRandomInt(0, maxPrivateShares) / 10e6) * 10e6;
corporation.issuedShares += amount - privateShares;
corporation.totalShares += amount;
corporation.funds = corporation.funds + profit;
corporation.immediatelyUpdateSharePrice();
return [profit, amount, privateShares];
}
export function SellMaterial(material: Material, amount: string, price: string): void {
2021-09-05 01:09:30 +02:00
if (price === "") price = "0";
if (amount === "") amount = "0";
2021-09-05 01:09:30 +02:00
let cost = price.replace(/\s+/g, "");
2021-11-16 05:49:33 +01:00
cost = cost.replace(/[^-()\d/*+.MPe]/g, ""); //Sanitize cost
let temp = cost.replace(/MP/, "1.234e5");
2021-09-05 01:09:30 +02:00
try {
if (temp.includes("MP")) throw "Only one reference to MP is allowed in sell price.";
2021-09-05 01:09:30 +02:00
temp = eval(temp);
} catch (e) {
throw new Error("Invalid value or expression for sell price field: " + e);
}
if (temp == null || isNaN(parseFloat(temp))) {
2021-09-05 01:09:30 +02:00
throw new Error("Invalid value or expression for sell price field");
}
if (cost.includes("MP")) {
material.desiredSellPrice = cost; //Dynamically evaluated
2021-09-05 01:09:30 +02:00
} else {
material.desiredSellPrice = temp;
2021-09-05 01:09:30 +02:00
}
//Parse quantity
amount = amount.toUpperCase();
if (amount.includes("MAX") || amount.includes("PROD") || amount.includes("INV")) {
let q = amount.replace(/\s+/g, "");
q = q.replace(/[^-()\d/*+.MAXPRODINV]/g, "");
let tempQty = q.replace(/MAX/g, material.maxSellPerCycle.toString());
tempQty = tempQty.replace(/PROD/g, material.productionAmount.toString());
tempQty = tempQty.replace(/INV/g, material.productionAmount.toString());
2021-09-02 06:36:33 +02:00
try {
2021-09-05 01:09:30 +02:00
tempQty = eval(tempQty);
} catch (e) {
2022-03-16 12:08:00 +01:00
throw new Error("Invalid value or expression for sell quantity field: " + e);
2021-09-02 06:36:33 +02:00
}
if (tempQty == null || isNaN(parseFloat(tempQty))) {
2022-03-16 12:08:00 +01:00
throw new Error("Invalid value or expression for sell quantity field");
2021-09-02 06:36:33 +02:00
}
material.desiredSellAmount = q; //Use sanitized input
} else if (isNaN(parseFloat(amount)) || parseFloat(amount) < 0) {
2022-03-16 12:08:00 +01:00
throw new Error("Invalid value for sell quantity field! Must be numeric or 'PROD' or 'MAX'");
2021-09-05 01:09:30 +02:00
} else {
let q = parseFloat(amount);
2021-09-05 01:09:30 +02:00
if (isNaN(q)) {
q = 0;
}
material.desiredSellAmount = q;
2021-09-05 01:09:30 +02:00
}
}
2021-09-02 06:36:33 +02:00
export function SellProduct(product: Product, city: CityName, amt: string, price: string, all: boolean): void {
2021-09-05 01:09:30 +02:00
//Parse price
if (price.includes("MP")) {
//Dynamically evaluated quantity. First test to make sure its valid
//Sanitize input, then replace dynamic variables with arbitrary numbers
price = price.replace(/\s+/g, "");
price = price.replace(/[^-()\d/*+.MPe]/g, "");
let temp = price.replace(/MP/, "1.234e5");
2021-09-05 01:09:30 +02:00
try {
if (temp.includes("MP")) throw "Only one reference to MP is allowed in sell price.";
2021-09-05 01:09:30 +02:00
temp = eval(temp);
} catch (e) {
2022-03-16 12:08:00 +01:00
throw new Error("Invalid value or expression for sell price field: " + e);
2021-09-05 01:09:30 +02:00
}
if (temp == null || isNaN(parseFloat(temp))) {
2022-03-16 12:08:00 +01:00
throw new Error("Invalid value or expression for sell price field.");
2021-09-05 01:09:30 +02:00
}
product.cityData[city].desiredSellPrice = price; //Use sanitized price
2021-09-05 01:09:30 +02:00
} else {
const cost = parseFloat(price);
if (isNaN(cost)) {
throw new Error("Invalid value for sell price field");
}
product.cityData[city].desiredSellPrice = cost;
2021-09-05 01:09:30 +02:00
}
2021-09-02 06:36:33 +02:00
2021-09-05 01:09:30 +02:00
// Parse quantity
2021-10-12 04:54:28 +02:00
amt = amt.toUpperCase();
if (amt.includes("MAX") || amt.includes("PROD") || amt.includes("INV")) {
2021-09-05 01:09:30 +02:00
//Dynamically evaluated quantity. First test to make sure its valid
let qty = amt.replace(/\s+/g, "");
qty = qty.replace(/[^-()\d/*+.MAXPRODINV]/g, "");
let temp = qty.replace(/MAX/g, product.maxSellAmount.toString());
temp = temp.replace(/PROD/g, product.cityData[city].productionAmount.toString());
temp = temp.replace(/INV/g, product.cityData[city].stored.toString());
2021-09-05 01:09:30 +02:00
try {
temp = eval(temp);
} catch (e) {
2022-03-16 12:08:00 +01:00
throw new Error("Invalid value or expression for sell quantity field: " + e);
2021-09-02 06:36:33 +02:00
}
if (temp == null || isNaN(parseFloat(temp))) {
2022-03-16 12:08:00 +01:00
throw new Error("Invalid value or expression for sell quantity field");
2021-09-05 01:09:30 +02:00
}
if (all) {
for (const cityName of Object.values(CityName)) {
product.cityData[cityName].desiredSellAmount = qty; //Use sanitized input
2021-09-05 01:09:30 +02:00
}
2021-09-02 06:36:33 +02:00
} else {
product.cityData[city].desiredSellAmount = qty; //Use sanitized input
2021-09-02 06:36:33 +02:00
}
} else if (isNaN(parseFloat(amt)) || parseFloat(amt) < 0) {
2022-03-16 12:08:00 +01:00
throw new Error("Invalid value for sell quantity field! Must be numeric or 'PROD' or 'MAX'");
2021-09-05 01:09:30 +02:00
} else {
let qty = parseFloat(amt);
if (isNaN(qty)) {
qty = 0;
}
if (qty === 0) {
if (all) {
for (const cityName of Object.values(CityName)) {
product.cityData[cityName].desiredSellAmount = 0;
2021-09-02 06:36:33 +02:00
}
2021-09-05 01:09:30 +02:00
} else {
product.cityData[city].desiredSellAmount = 0;
2021-09-05 01:09:30 +02:00
}
} else if (all) {
for (const cityName of Object.values(CityName)) {
product.cityData[cityName].desiredSellAmount = qty;
2021-09-05 01:09:30 +02:00
}
} else {
product.cityData[city].desiredSellAmount = qty;
}
2021-09-05 01:09:30 +02:00
}
2021-09-02 06:36:33 +02:00
}
2021-09-09 05:47:34 +02:00
export function SetSmartSupply(warehouse: Warehouse, smartSupply: boolean): void {
2021-09-05 01:09:30 +02:00
warehouse.smartSupplyEnabled = smartSupply;
2021-09-03 22:02:41 +02:00
}
export function SetSmartSupplyOption(warehouse: Warehouse, material: Material, useOption: string): void {
if (!corpConstants.smartSupplyUseOptions.includes(useOption)) {
throw new Error(`Invalid Smart Supply option '${useOption}'`);
}
warehouse.smartSupplyOptions[material.name] = useOption;
2021-09-10 08:17:55 +02:00
}
2021-09-03 22:02:41 +02:00
export function BuyMaterial(material: Material, amt: number): void {
if (isNaN(amt) || amt < 0) {
2021-09-09 05:47:34 +02:00
throw new Error(`Invalid amount '${amt}' to buy material '${material.name}'`);
2021-09-05 01:09:30 +02:00
}
material.buyAmount = amt;
2021-09-05 01:09:30 +02:00
}
2021-09-10 06:13:28 +02:00
2022-09-20 12:47:54 +02:00
export function BulkPurchase(corp: Corporation, warehouse: Warehouse, material: Material, amt: number): void {
const matSize = MaterialInfo[material.name].size;
const maxAmount = (warehouse.size - warehouse.sizeUsed) / matSize;
if (isNaN(amt) || amt < 0) {
throw new Error(`Invalid input amount`);
}
2022-04-02 14:17:30 +02:00
if (amt > maxAmount) {
throw new Error(`You do not have enough warehouse size to fit this purchase`);
}
const cost = amt * material.marketPrice;
if (corp.funds >= cost) {
corp.funds = corp.funds - cost;
material.stored += amt;
} else {
throw new Error(`You cannot afford this purchase.`);
}
}
2022-09-20 12:47:54 +02:00
export function SellShares(corporation: Corporation, numShares: number): number {
if (isNaN(numShares) || !isInteger(numShares)) throw new Error("Invalid value for number of shares");
if (numShares <= 0) throw new Error("Invalid value for number of shares");
if (numShares > corporation.numShares) throw new Error("You don't have that many shares to sell!");
if (numShares === corporation.numShares) throw new Error("You cant't sell all your shares!");
if (numShares > 1e14) throw new Error("Invalid value for number of shares");
if (!corporation.public) throw new Error("You haven't gone public!");
if (corporation.shareSaleCooldown) throw new Error("Share sale on cooldown!");
const stockSaleResults = corporation.calculateShareSale(numShares);
const profit = stockSaleResults[0];
const newSharePrice = stockSaleResults[1];
const newSharesUntilUpdate = stockSaleResults[2];
corporation.numShares -= numShares;
corporation.issuedShares += numShares;
corporation.sharePrice = newSharePrice;
corporation.shareSalesUntilPriceUpdate = newSharesUntilUpdate;
corporation.shareSaleCooldown = corpConstants.sellSharesCooldown;
2022-09-06 15:07:12 +02:00
Player.gainMoney(profit, "corporation");
return profit;
}
2022-09-20 12:47:54 +02:00
export function BuyBackShares(corporation: Corporation, numShares: number): boolean {
if (isNaN(numShares) || !isInteger(numShares)) throw new Error("Invalid value for number of shares");
if (numShares <= 0) throw new Error("Invalid value for number of shares");
if (numShares > corporation.issuedShares) throw new Error("You don't have that many shares to buy!");
if (!corporation.public) throw new Error("You haven't gone public!");
const buybackPrice = corporation.sharePrice * 1.1;
2022-09-06 15:07:12 +02:00
if (Player.money < numShares * buybackPrice) throw new Error("You cant afford that many shares!");
corporation.numShares += numShares;
corporation.issuedShares -= numShares;
2022-09-06 15:07:12 +02:00
Player.loseMoney(numShares * buybackPrice, "corporation");
return true;
}
2022-09-20 12:47:54 +02:00
export function UpgradeOfficeSize(corp: Corporation, office: OfficeSpace, size: number): void {
const initialPriceMult = Math.round(office.size / corpConstants.officeInitialSize);
2021-09-10 06:13:28 +02:00
const costMultiplier = 1.09;
// Calculate cost to upgrade size by 15 employees
let mult = 0;
for (let i = 0; i < size / corpConstants.officeInitialSize; ++i) {
2021-09-10 06:13:28 +02:00
mult += Math.pow(costMultiplier, initialPriceMult + i);
}
const cost = corpConstants.officeInitialCost * mult;
2021-11-12 03:35:26 +01:00
if (corp.funds < cost) return;
2021-09-10 06:13:28 +02:00
office.size += size;
2021-11-12 03:35:26 +01:00
corp.funds = corp.funds - cost;
2021-09-10 06:13:28 +02:00
}
export function BuyTea(corp: Corporation, office: OfficeSpace): boolean {
const cost = office.getTeaCost();
if (corp.funds < cost || !office.setTea()) return false;
corp.funds -= cost;
return true;
}
2022-09-20 12:47:54 +02:00
export function ThrowParty(corp: Corporation, office: OfficeSpace, costPerEmployee: number): number {
const mult = 1 + costPerEmployee / 10e6;
const cost = costPerEmployee * office.numEmployees;
if (corp.funds < cost) {
return 0;
}
if (!office.setParty(mult)) {
return 0;
}
corp.funds -= cost;
2021-09-10 06:13:28 +02:00
return mult;
}
export function purchaseWarehouse(corp: Corporation, division: Division, city: CityName): void {
if (corp.funds < corpConstants.warehouseInitialCost) return;
if (division.warehouses[city]) return;
corp.funds = corp.funds - corpConstants.warehouseInitialCost;
2021-09-10 06:13:28 +02:00
division.warehouses[city] = new Warehouse({
division: division,
2021-09-10 06:13:28 +02:00
loc: city,
size: corpConstants.warehouseInitialSize,
2021-09-10 06:13:28 +02:00
});
}
export function UpgradeWarehouseCost(warehouse: Warehouse, amt: number): number {
return Array.from(Array(amt).keys()).reduce(
(acc, index) => acc + corpConstants.warehouseSizeUpgradeCostBase * Math.pow(1.07, warehouse.level + 1 + index),
0,
);
}
export function UpgradeWarehouse(corp: Corporation, division: Division, warehouse: Warehouse, amt = 1): void {
const sizeUpgradeCost = UpgradeWarehouseCost(warehouse, amt);
if (corp.funds < sizeUpgradeCost) return;
warehouse.level += amt;
warehouse.updateSize(corp, division);
2021-11-12 03:35:26 +01:00
corp.funds = corp.funds - sizeUpgradeCost;
}
export function HireAdVert(corp: Corporation, division: Division): void {
const cost = division.getAdVertCost();
2021-11-12 03:35:26 +01:00
if (corp.funds < cost) return;
corp.funds = corp.funds - cost;
division.applyAdVert(corp);
}
export function MakeProduct(
2022-09-20 12:47:54 +02:00
corp: Corporation,
division: Division,
city: CityName,
productName: string,
designInvest: number,
marketingInvest: number,
): void {
if (designInvest < 0) {
designInvest = 0;
}
if (marketingInvest < 0) {
marketingInvest = 0;
}
if (productName == null || productName === "") {
throw new Error("You must specify a name for your product!");
}
if (!division.makesProducts) {
throw new Error("You cannot create products for this industry!");
}
if (isNaN(designInvest)) {
throw new Error("Invalid value for design investment");
}
if (isNaN(marketingInvest)) {
throw new Error("Invalid value for marketing investment");
}
2021-11-12 03:35:26 +01:00
if (corp.funds < designInvest + marketingInvest) {
throw new Error("You don't have enough company funds to make this large of an investment");
}
let maxProducts = 3;
if (division.hasResearch("uPgrade: Capacity.II")) {
maxProducts = 5;
} else if (division.hasResearch("uPgrade: Capacity.I")) {
maxProducts = 4;
}
if (division.products.size >= maxProducts) {
throw new Error(`You are already at the max products (${maxProducts}) for division: ${division.name}!`);
}
const product = new Product({
name: productName.replace(/[<>]/g, "").trim(), //Sanitize for HTMl elements
createCity: city,
designInvestment: designInvest,
advertisingInvestment: marketingInvest,
});
if (division.products.has(product.name)) {
throw new Error(`You already have a product with this name!`);
}
2021-11-12 03:35:26 +01:00
corp.funds = corp.funds - (designInvest + marketingInvest);
division.products.set(product.name, product);
}
2021-09-10 08:17:55 +02:00
2023-05-31 00:47:48 +02:00
export function Research(researchingDivision: Division, researchName: CorpResearchName): void {
const corp = Player.corporation;
if (!corp) return;
2023-05-31 00:47:48 +02:00
const researchTree = IndustryResearchTrees[researchingDivision.type];
if (researchTree === undefined) throw new Error(`No research tree for industry '${researchingDivision.type}'`);
2021-09-10 08:17:55 +02:00
const research = ResearchMap[researchName];
2023-05-31 00:47:48 +02:00
if (researchingDivision.researched.has(researchName)) return;
if (researchingDivision.researchPoints < research.cost) {
2021-09-10 08:17:55 +02:00
throw new Error(`You do not have enough Scientific Research for ${research.name}`);
2023-05-31 00:47:48 +02:00
}
researchingDivision.researchPoints -= research.cost;
2021-09-10 08:17:55 +02:00
// Get the Node from the Research Tree and set its 'researched' property
researchTree.research(researchName);
2023-05-31 00:47:48 +02:00
// All divisions of the same type as the researching division get the new research.
for (const division of corp.divisions.values()) {
if (division.type !== researchingDivision.type) continue;
division.researched.add(researchName);
// Handle researches that need to have their effects manually applied here.
// Warehouse size needs to be updated here because it is not recalculated during normal processing.
if (researchName == "Drones - Transport") {
for (const warehouse of getRecordValues(division.warehouses)) {
warehouse.updateSize(corp, division);
}
2022-09-23 11:54:04 +02:00
}
}
2021-09-10 08:17:55 +02:00
}
export function ExportMaterial(
divisionName: string,
cityName: CityName,
material: Material,
amt: string,
division?: Division,
): void {
2021-09-10 08:17:55 +02:00
// Sanitize amt
2021-10-12 04:54:28 +02:00
let sanitizedAmt = amt.replace(/\s+/g, "").toUpperCase();
sanitizedAmt = sanitizedAmt.replace(/[^-()\d/*+.MAXEPRODINV]/g, "");
2021-09-10 08:17:55 +02:00
let temp = sanitizedAmt.replace(/MAX/g, "1");
temp = temp.replace(/IPROD/g, "1");
temp = temp.replace(/EPROD/g, "1");
temp = temp.replace(/IINV/g, "1");
temp = temp.replace(/EINV/g, "1");
2021-09-10 08:17:55 +02:00
try {
temp = eval(temp);
} catch (e) {
throw new Error("Invalid expression entered for export amount: " + e);
}
const n = parseFloat(temp);
if (n == null || isNaN(n)) {
2021-09-10 08:17:55 +02:00
throw new Error("Invalid amount entered for export");
}
if (!division || !isRelevantMaterial(material.name, division)) {
throw new Error(`You cannot export material: ${material.name} to division: ${divisionName}!`);
}
const exportObj = { division: divisionName, city: cityName, amount: sanitizedAmt };
material.exports.push(exportObj);
2021-09-10 08:17:55 +02:00
}
export function CancelExportMaterial(divisionName: string, cityName: string, material: Material, amt: string): void {
for (let i = 0; i < material.exports.length; ++i) {
if (
material.exports[i].division !== divisionName ||
material.exports[i].city !== cityName ||
material.exports[i].amount !== amt
)
2021-09-10 08:17:55 +02:00
continue;
material.exports.splice(i, 1);
2021-09-10 08:17:55 +02:00
break;
}
}
export function LimitProductProduction(product: Product, cityName: CityName, quantity: number): void {
if (quantity < 0 || isNaN(quantity)) {
product.cityData[cityName].productionLimit = null;
2021-09-10 08:17:55 +02:00
} else {
product.cityData[cityName].productionLimit = quantity;
2021-09-10 08:17:55 +02:00
}
}
export function LimitMaterialProduction(material: Material, quantity: number): void {
if (quantity < 0 || isNaN(quantity)) {
material.productionLimit = null;
} else {
material.productionLimit = quantity;
}
}
2021-09-10 08:17:55 +02:00
export function SetMaterialMarketTA1(material: Material, on: boolean): void {
material.marketTa1 = on;
}
export function SetMaterialMarketTA2(material: Material, on: boolean): void {
material.marketTa2 = on;
}
export function SetProductMarketTA1(product: Product, on: boolean): void {
product.marketTa1 = on;
}
export function SetProductMarketTA2(product: Product, on: boolean): void {
product.marketTa2 = on;
}