From 902306530c04c6bfc12eb2ffb1d5932be5eea35f Mon Sep 17 00:00:00 2001 From: Jesse Clark Date: Thu, 7 Dec 2023 18:22:21 -0800 Subject: [PATCH] CORPORATION: Use accounting methods for all funds transactions (#949) --- src/Corporation/Actions.ts | 35 +++++++++------- src/Corporation/Corporation.ts | 41 +++++++++---------- src/Corporation/Division.ts | 11 +++++ src/Corporation/data/FundsSource.ts | 30 ++++++++++++++ src/Corporation/ui/DivisionWarehouse.tsx | 2 +- .../ui/modals/BribeFactionModal.tsx | 2 +- .../ui/modals/SellDivisionModal.tsx | 22 ++-------- src/DevMenu/ui/CorporationDev.tsx | 6 +-- src/Hacknet/HacknetHelpers.tsx | 2 +- src/Locations/ui/SpecialLocation.tsx | 2 +- src/NetscriptFunctions/Corporation.ts | 2 +- src/SaveObject.ts | 2 +- 12 files changed, 93 insertions(+), 64 deletions(-) create mode 100644 src/Corporation/data/FundsSource.ts diff --git a/src/Corporation/Actions.ts b/src/Corporation/Actions.ts index 9e653a8e2..8fb6d49a9 100644 --- a/src/Corporation/Actions.ts +++ b/src/Corporation/Actions.ts @@ -34,7 +34,7 @@ export function NewDivision(corporation: Corporation, industry: IndustryType, na } else if (name === "") { throw new Error("New division must have a name!"); } else { - corporation.funds = corporation.funds - cost; + corporation.loseFunds(cost, "division"); corporation.divisions.set( name, new Division({ @@ -46,9 +46,12 @@ export function NewDivision(corporation: Corporation, industry: IndustryType, na } } -export function removeDivision(corporation: Corporation, name: string) { - if (!corporation.divisions.has(name)) throw new Error("There is no division called " + name); +export function removeDivision(corporation: Corporation, name: string): number { + const division = corporation.divisions.get(name); + if (!division) throw new Error("There is no division called " + name); + const price = division.calculateRecoupableValue(); corporation.divisions.delete(name); + // We also need to remove any exports that were pointing to the old division for (const otherDivision of corporation.divisions.values()) { for (const warehouse of getRecordValues(otherDivision.warehouses)) { @@ -60,6 +63,8 @@ export function removeDivision(corporation: Corporation, name: string) { } } } + corporation.gainFunds(price, "division"); + return price; } export function purchaseOffice(corporation: Corporation, division: Division, city: CityName): void { @@ -69,7 +74,7 @@ export function purchaseOffice(corporation: Corporation, division: Division, cit if (division.offices[city]) { throw new Error(`You have already expanded into ${city} for ${division.name}`); } - corporation.addNonIncomeFunds(-corpConstants.officeInitialCost); + corporation.loseFunds(corpConstants.officeInitialCost, "office"); division.offices[city] = new OfficeSpace({ city: city, size: corpConstants.officeInitialSize, @@ -98,7 +103,7 @@ export function GoPublic(corporation: Corporation, numShares: number): void { corporation.sharePrice = initialSharePrice; corporation.issuedShares += numShares; corporation.numShares -= numShares; - corporation.addNonIncomeFunds(numShares * initialSharePrice); + corporation.gainFunds(numShares * initialSharePrice, "public equity"); } export function IssueNewShares( @@ -123,7 +128,7 @@ export function IssueNewShares( corporation.issuedShares += amount - privateShares; corporation.investorShares += privateShares; corporation.totalShares += amount; - corporation.addNonIncomeFunds(profit); + corporation.gainFunds(profit, "public equity"); // Set sharePrice directly because all formulas will be based on stale cycleValuation data corporation.sharePrice = newSharePrice; @@ -144,7 +149,7 @@ export function AcceptInvestmentOffer(corporation: Corporation): void { const funding = val * percShares * roundMultiplier; const investShares = Math.floor(corpConstants.initialShares * percShares); corporation.fundingRound++; - corporation.addNonIncomeFunds(funding); + corporation.gainFunds(funding, "private equity"); corporation.numShares -= investShares; corporation.investorShares += investShares; @@ -310,7 +315,7 @@ export function BulkPurchase( } const cost = amt * material.marketPrice; if (corp.funds >= cost) { - corp.funds = corp.funds - cost; + corp.loseFunds(cost, "materials"); material.stored += amt; warehouse.sizeUsed = warehouse.sizeUsed + amt * matSize; } else { @@ -358,13 +363,13 @@ export function UpgradeOfficeSize(corp: Corporation, office: OfficeSpace, size: const cost = corpConstants.officeInitialCost * mult; if (corp.funds < cost) return; office.size += size; - corp.addNonIncomeFunds(-cost); + corp.loseFunds(cost, "office"); } export function BuyTea(corp: Corporation, office: OfficeSpace): boolean { const cost = office.getTeaCost(); if (corp.funds < cost || !office.setTea()) return false; - corp.funds -= cost; + corp.loseFunds(cost, "tea"); return true; } @@ -378,7 +383,7 @@ export function ThrowParty(corp: Corporation, office: OfficeSpace, costPerEmploy if (!office.setParty(mult)) { return 0; } - corp.funds -= cost; + corp.loseFunds(cost, "parties"); return mult; } @@ -386,7 +391,7 @@ export function ThrowParty(corp: Corporation, office: OfficeSpace, costPerEmploy export function purchaseWarehouse(corp: Corporation, division: Division, city: CityName): void { if (corp.funds < corpConstants.warehouseInitialCost) return; if (division.warehouses[city]) return; - corp.addNonIncomeFunds(-corpConstants.warehouseInitialCost); + corp.loseFunds(corpConstants.warehouseInitialCost, "warehouse"); division.warehouses[city] = new Warehouse({ division: division, loc: city, @@ -406,13 +411,13 @@ export function UpgradeWarehouse(corp: Corporation, division: Division, warehous if (corp.funds < sizeUpgradeCost) return; warehouse.level += amt; warehouse.updateSize(corp, division); - corp.addNonIncomeFunds(-sizeUpgradeCost); + corp.loseFunds(sizeUpgradeCost, "warehouse"); } export function HireAdVert(corp: Corporation, division: Division): void { const cost = division.getAdVertCost(); if (corp.funds < cost) return; - corp.funds = corp.funds - cost; + corp.loseFunds(cost, "advert"); division.applyAdVert(corp); } @@ -454,7 +459,7 @@ export function MakeProduct( throw new Error(`You already have a product with this name!`); } - corp.funds = corp.funds - (designInvest + marketingInvest); + corp.loseFunds(designInvest + marketingInvest, "product development"); division.products.set(product.name, product); } diff --git a/src/Corporation/Corporation.ts b/src/Corporation/Corporation.ts index 6d54658d0..f9872091c 100644 --- a/src/Corporation/Corporation.ts +++ b/src/Corporation/Corporation.ts @@ -1,19 +1,20 @@ import { Player } from "@player"; +import { CorpStateName, InvestmentOffer } from "@nsdefs"; import { CorpUnlockName, CorpUpgradeName, LiteratureName } from "@enums"; import { CorporationState } from "./CorporationState"; import { CorpUnlocks } from "./data/CorporationUnlocks"; import { CorpUpgrades } from "./data/CorporationUpgrades"; import * as corpConstants from "./data/Constants"; import { IndustriesData } from "./data/IndustryData"; +import { FundsSource, LongTermFundsSources } from "./data/FundsSource"; import { Division } from "./Division"; +import { calculateUpgradeCost } from "./helpers"; import { currentNodeMults } from "../BitNode/BitNodeMultipliers"; import { showLiterature } from "../Literature/LiteratureHelpers"; import { dialogBoxCreate } from "../ui/React/DialogBox"; import { constructorsForReviver, Generic_toJSON, Generic_fromJSON, IReviverValue } from "../utils/JSONReviver"; -import { CorpStateName, InvestmentOffer } from "@nsdefs"; -import { calculateUpgradeCost } from "./helpers"; import { JSONMap, JSONSet } from "../Types/Jsonable"; import { formatMoney } from "../ui/formatNumber"; import { isPositiveInteger } from "../types"; @@ -77,23 +78,19 @@ export class Corporation { this.shareSaleCooldown = params.shareSaleCooldown ?? 0; } - addFunds(amt: number): void { + gainFunds(amt: number, source: FundsSource): void { if (!isFinite(amt)) { - console.error("Trying to add invalid amount of funds. Report to a developer."); + console.error("Trying to add invalid amount of funds. Please report to game developer."); return; } + if (LongTermFundsSources.has(source)) { + this.totalAssets += amt; + } this.funds += amt; } - // Add or subtract funds which should not be counted for valuation; e.g. investments, - // upgrades, stock issuance - addNonIncomeFunds(amt: number): void { - if (!isFinite(amt)) { - console.error("Trying to add invalid amount of funds. Report to a developer."); - return; - } - this.totalAssets += amt; - this.funds += amt; + loseFunds(amt: number, source: FundsSource): void { + return this.gainFunds(-amt, source); } getNextState(): CorpStateName { @@ -144,8 +141,6 @@ export class Corporation { this.revenue = this.revenue + ind.lastCycleRevenue; this.expenses = this.expenses + ind.lastCycleExpenses; }); - const profit = this.revenue - this.expenses; - const cycleProfit = profit * (marketCycles * corpConstants.secondsPerMarketCycle); if (isNaN(this.funds) || this.funds === Infinity || this.funds === -Infinity) { dialogBoxCreate( "There was an error calculating your Corporations funds and they got reset to 0. " + @@ -154,18 +149,20 @@ export class Corporation { ); this.funds = 150e9; } + const cycleRevenue = this.revenue * (marketCycles * corpConstants.secondsPerMarketCycle); + const cycleExpenses = this.expenses * (marketCycles * corpConstants.secondsPerMarketCycle); + const cycleProfit = cycleRevenue - cycleExpenses; + this.gainFunds(cycleRevenue, "operating revenue"); + this.loseFunds(cycleExpenses, "operating expenses"); if (this.dividendRate > 0 && cycleProfit > 0) { // Validate input again, just to be safe if (isNaN(this.dividendRate) || this.dividendRate < 0 || this.dividendRate > corpConstants.dividendMaxRate) { console.error(`Invalid Corporation dividend rate: ${this.dividendRate}`); } else { const totalDividends = this.dividendRate * cycleProfit; - const retainedEarnings = cycleProfit - totalDividends; Player.gainMoney(this.getCycleDividends(), "corporation"); - this.addFunds(retainedEarnings); + this.loseFunds(totalDividends, "dividends"); } - } else { - this.addFunds(cycleProfit); } this.updateTotalAssets(); this.cycleValuation = this.determineCycleValuation(); @@ -211,7 +208,7 @@ export class Corporation { val += assetDelta * 315e3; } val *= Math.pow(1.1, this.divisions.size); - val -= val % 1e6; //Round down to nearest millionth + val -= val % 1e6; //Round down to nearest million } if (val < 10e9) val = 10e9; // Base valuation return val * currentNodeMults.CorporationValuation; @@ -369,7 +366,7 @@ export class Corporation { if (this.unlocks.has(unlockName)) return `The corporation has already unlocked ${unlockName}`; const price = CorpUnlocks[unlockName].price; if (this.funds < price) return `Insufficient funds to purchase ${unlockName}, requires ${formatMoney(price)}`; - this.addNonIncomeFunds(-price); + this.loseFunds(price, "upgrades"); this.unlocks.add(unlockName); // Apply effects for one-time unlocks @@ -386,7 +383,7 @@ export class Corporation { const upgrade = CorpUpgrades[upgradeName]; const totalCost = calculateUpgradeCost(this, upgrade, amount); if (this.funds < totalCost) return `Not enough funds to purchase ${amount} of upgrade ${upgradeName}.`; - this.addNonIncomeFunds(-totalCost); + this.loseFunds(totalCost, "upgrades"); this.upgrades[upgradeName].level += amount; this.upgrades[upgradeName].value += upgrade.benefit * amount; diff --git a/src/Corporation/Division.ts b/src/Corporation/Division.ts index e987eb31d..26908a442 100644 --- a/src/Corporation/Division.ts +++ b/src/Corporation/Division.ts @@ -133,6 +133,17 @@ export class Division { multSum < 1 ? (this.productionMult = 1) : (this.productionMult = multSum); } + calculateRecoupableValue(): number { + let price = this.startingCost; + for (const city of getRecordKeys(this.offices)) { + if (city === CityName.Sector12) continue; + price += corpConstants.officeInitialCost; + if (this.warehouses[city]) price += corpConstants.warehouseInitialCost; + } + price /= 2; + return price; + } + updateWarehouseSizeUsed(warehouse: Warehouse): void { warehouse.updateMaterialSizeUsed(); diff --git a/src/Corporation/data/FundsSource.ts b/src/Corporation/data/FundsSource.ts new file mode 100644 index 000000000..b76cb4f91 --- /dev/null +++ b/src/Corporation/data/FundsSource.ts @@ -0,0 +1,30 @@ +// Funds transactions which affect valuation directly and should not be included in earnings projections. +// This includes capital expenditures (which may be recoupable), time-limited actions, and transfers to/from other game mechanics. +const FundsSourceLongTerm = [ + "product development", + "division", + "office", + "warehouse", + "upgrades", + "bribery", + "public equity", + "private equity", + "hacknet", + "force majeure", +] as const; + +// Funds transactions which should be included in earnings projections for valuation. +// This includes all automatic or indefinetly-repeatable income and operating expenses. +type FundsSourceShortTerm = + | "operating expenses" + | "operating revenue" + | "dividends" + | "tea" + | "parties" + | "advert" + | "materials" + | "glitch in reality"; + +export type FundsSource = (typeof FundsSourceLongTerm)[number] | FundsSourceShortTerm; + +export const LongTermFundsSources = new Set(FundsSourceLongTerm); diff --git a/src/Corporation/ui/DivisionWarehouse.tsx b/src/Corporation/ui/DivisionWarehouse.tsx index 4730de08b..5e0f65364 100644 --- a/src/Corporation/ui/DivisionWarehouse.tsx +++ b/src/Corporation/ui/DivisionWarehouse.tsx @@ -54,7 +54,7 @@ function WarehouseRoot(props: WarehouseProps): React.ReactElement { if (!canAffordUpgrade) return; ++props.warehouse.level; props.warehouse.updateSize(corp, division); - corp.funds = corp.funds - sizeUpgradeCost; + corp.loseFunds(sizeUpgradeCost, "warehouse"); props.rerender(); } // -1 because as soon as it hits "full" it processes and resets to 0, *2 to double the size of the bar diff --git a/src/Corporation/ui/modals/BribeFactionModal.tsx b/src/Corporation/ui/modals/BribeFactionModal.tsx index 7cc4541e7..97ede3746 100644 --- a/src/Corporation/ui/modals/BribeFactionModal.tsx +++ b/src/Corporation/ui/modals/BribeFactionModal.tsx @@ -59,7 +59,7 @@ export function BribeFactionModal(props: IProps): React.ReactElement { const rep = repGain(money); dialogBoxCreate(`You gained ${formatReputation(rep)} reputation with ${fac.name} by bribing them.`); fac.playerReputation += rep; - corp.funds = corp.funds - money; + corp.loseFunds(money, "bribery"); props.onClose(); } diff --git a/src/Corporation/ui/modals/SellDivisionModal.tsx b/src/Corporation/ui/modals/SellDivisionModal.tsx index cbd464768..d447a1ecd 100644 --- a/src/Corporation/ui/modals/SellDivisionModal.tsx +++ b/src/Corporation/ui/modals/SellDivisionModal.tsx @@ -7,9 +7,7 @@ import Button from "@mui/material/Button"; import MenuItem from "@mui/material/MenuItem"; import Select, { SelectChangeEvent } from "@mui/material/Select"; import { useCorporation } from "../../ui/Context"; -import { CityName } from "@enums"; -import * as corpConstants from "../../data/Constants"; -import { removeDivision as removeDivision } from "../../Actions"; +import { removeDivision } from "../../Actions"; import { dialogBoxCreate } from "../../../ui/React/DialogBox"; import { getRecordKeys } from "../../../Types/Record"; @@ -23,18 +21,7 @@ export function SellDivisionModal(props: IProps): React.ReactElement { const allDivisions = [...corp.divisions.values()]; const [divisionToSell, setDivisionToSell] = useState(allDivisions[0]); if (allDivisions.length === 0) return <>; - const price = calculatePrice(); - - function calculatePrice() { - let price = divisionToSell.startingCost; - for (const city of getRecordKeys(divisionToSell.offices)) { - if (city === CityName.Sector12) continue; - price += corpConstants.officeInitialCost; - if (divisionToSell.warehouses[city]) price += corpConstants.warehouseInitialCost; - } - price /= 2; - return price; - } + const price = divisionToSell.calculateRecoupableValue(); function onDivisionChange(event: SelectChangeEvent): void { const div = corp.divisions.get(event.target.value); @@ -43,12 +30,11 @@ export function SellDivisionModal(props: IProps): React.ReactElement { } function sellDivision() { - removeDivision(corp, divisionToSell.name); - corp.funds += price; + const soldPrice = removeDivision(corp, divisionToSell.name); props.onClose(); dialogBoxCreate( - Sold {divisionToSell.name} for , you now have space for + Sold {divisionToSell.name} for , you now have space for {corp.maxDivisions - corp.divisions.size} more divisions. , ); diff --git a/src/DevMenu/ui/CorporationDev.tsx b/src/DevMenu/ui/CorporationDev.tsx index 0245642d0..a99f915d6 100644 --- a/src/DevMenu/ui/CorporationDev.tsx +++ b/src/DevMenu/ui/CorporationDev.tsx @@ -15,21 +15,21 @@ const bigNumber = 1e27; export function CorporationDev(): React.ReactElement { function addTonsCorporationFunds(): void { if (Player.corporation) { - Player.corporation.funds = Player.corporation.funds + bigNumber; + Player.corporation.gainFunds(bigNumber, "force majeure"); } } function modifyCorporationFunds(modify: number): (x: number) => void { return function (funds: number): void { if (Player.corporation) { - Player.corporation.funds += funds * modify; + Player.corporation.gainFunds(funds * modify, "force majeure"); } }; } function resetCorporationFunds(): void { if (Player.corporation) { - Player.corporation.funds = Player.corporation.funds - Player.corporation.funds; + Player.corporation.loseFunds(Player.corporation.funds, "force majeure"); } } diff --git a/src/Hacknet/HacknetHelpers.tsx b/src/Hacknet/HacknetHelpers.tsx index d7649feda..f1f781e03 100644 --- a/src/Hacknet/HacknetHelpers.tsx +++ b/src/Hacknet/HacknetHelpers.tsx @@ -479,7 +479,7 @@ export function purchaseHashUpgrade(upgName: string, upgTarget: string, count = Player.hashManager.refundUpgrade(upgName, count); return false; } - corp.addNonIncomeFunds(upg.value * count); + corp.gainFunds(upg.value * count, "hacknet"); break; } case "Reduce Minimum Security": { diff --git a/src/Locations/ui/SpecialLocation.tsx b/src/Locations/ui/SpecialLocation.tsx index cbf1fcd36..27223403e 100644 --- a/src/Locations/ui/SpecialLocation.tsx +++ b/src/Locations/ui/SpecialLocation.tsx @@ -116,7 +116,7 @@ export function SpecialLocation(props: SpecialLocationProps): React.ReactElement } if (Player.corporation) { - Player.corporation.funds += Player.corporation.revenue * 0.01; + Player.corporation.gainFunds(Player.corporation.revenue * 0.01, "glitch in reality"); } } diff --git a/src/NetscriptFunctions/Corporation.ts b/src/NetscriptFunctions/Corporation.ts index 93b42f5de..0f85d88c6 100644 --- a/src/NetscriptFunctions/Corporation.ts +++ b/src/NetscriptFunctions/Corporation.ts @@ -132,7 +132,7 @@ export function NetscriptCorporation(): InternalAPI { const repGain = amountCash / corpConstants.bribeAmountPerReputation; faction.playerReputation += repGain; - corporation.funds = corporation.funds - amountCash; + corporation.loseFunds(amountCash, "bribery"); return true; } diff --git a/src/SaveObject.ts b/src/SaveObject.ts index 5e3e499de..df26a5648 100644 --- a/src/SaveObject.ts +++ b/src/SaveObject.ts @@ -651,7 +651,7 @@ function evaluateVersionCompatibility(ver: string | number): void { let valuation = oldCorp.valuation * 2 + oldCorp.revenue * 100; if (isNaN(valuation)) valuation = 300e9; Player.startCorporation(String(oldCorp.name), !!oldCorp.seedFunded); - Player.corporation?.addFunds(valuation); + Player.corporation?.gainFunds(valuation, "force majeure"); Terminal.warn("Loading corporation from version prior to 2.3. Corporation has been reset."); } // End 2.3 changes