From 1b81fe8766685788ea3b0af499cebdfd89e0739a Mon Sep 17 00:00:00 2001 From: Yichi Zhang Date: Tue, 19 Sep 2023 05:47:16 -0700 Subject: [PATCH] CORPORATION: Rework valuation calculation (#789) --- src/Corporation/Actions.ts | 10 ++-- src/Corporation/Corporation.ts | 59 +++++++++++++++---- src/Corporation/Division.ts | 19 +++++- src/Corporation/Material.ts | 3 + src/Corporation/ui/Overview.tsx | 1 + .../ui/modals/FindInvestorsModal.tsx | 2 +- src/Hacknet/HacknetHelpers.tsx | 2 +- src/NetscriptFunctions/Corporation.ts | 4 +- test/jest/__snapshots__/FullSave.test.ts.snap | 2 + 9 files changed, 78 insertions(+), 24 deletions(-) diff --git a/src/Corporation/Actions.ts b/src/Corporation/Actions.ts index 438ce97a6..afcb0c888 100644 --- a/src/Corporation/Actions.ts +++ b/src/Corporation/Actions.ts @@ -70,7 +70,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.funds = corporation.funds - corpConstants.officeInitialCost; + corporation.addNonIncomeFunds(-corpConstants.officeInitialCost); division.offices[city] = new OfficeSpace({ city: city, size: corpConstants.officeInitialSize, @@ -106,7 +106,7 @@ export function IssueNewShares(corporation: Corporation, amount: number): [numbe corporation.issuedShares += amount - privateShares; corporation.totalShares += amount; - corporation.funds = corporation.funds + profit; + corporation.addNonIncomeFunds(profit); corporation.immediatelyUpdateSharePrice(); return [profit, amount, privateShares]; @@ -325,7 +325,7 @@ export function UpgradeOfficeSize(corp: Corporation, office: OfficeSpace, size: const cost = corpConstants.officeInitialCost * mult; if (corp.funds < cost) return; office.size += size; - corp.funds = corp.funds - cost; + corp.addNonIncomeFunds(-cost); } export function BuyTea(corp: Corporation, office: OfficeSpace): boolean { @@ -353,7 +353,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.funds = corp.funds - corpConstants.warehouseInitialCost; + corp.addNonIncomeFunds(-corpConstants.warehouseInitialCost); division.warehouses[city] = new Warehouse({ division: division, loc: city, @@ -373,7 +373,7 @@ export function UpgradeWarehouse(corp: Corporation, division: Division, warehous if (corp.funds < sizeUpgradeCost) return; warehouse.level += amt; warehouse.updateSize(corp, division); - corp.funds = corp.funds - sizeUpgradeCost; + corp.addNonIncomeFunds(-sizeUpgradeCost); } export function HireAdVert(corp: Corporation, division: Division): void { diff --git a/src/Corporation/Corporation.ts b/src/Corporation/Corporation.ts index e849d692a..5156f0b10 100644 --- a/src/Corporation/Corporation.ts +++ b/src/Corporation/Corporation.ts @@ -4,6 +4,7 @@ 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 { Division } from "./Division"; import { currentNodeMults } from "../BitNode/BitNodeMultipliers"; @@ -56,6 +57,8 @@ export class Corporation { value: name === CorpUpgradeName.DreamSense ? 0 : 1, })); + previousTotalAssets = 150e9; + totalAssets = 150e9; cycleValuation = 0; valuationsList = [0]; valuation = 0; @@ -77,6 +80,17 @@ export class Corporation { 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; + } + getState(): CorpStateName { return this.state.getState(); } @@ -126,8 +140,6 @@ export class Corporation { this.expenses = this.expenses + ind.lastCycleExpenses; }); const profit = this.revenue - this.expenses; - this.cycleValuation = this.determineCycleValuation(); - this.determineValuation(); const cycleProfit = profit * (marketCycles * corpConstants.secondsPerMarketCycle); if (isNaN(this.funds) || this.funds === Infinity || this.funds === -Infinity) { dialogBoxCreate( @@ -137,7 +149,6 @@ export class Corporation { ); this.funds = 150e9; } - if (this.dividendRate > 0 && cycleProfit > 0) { // Validate input again, just to be safe if (isNaN(this.dividendRate) || this.dividendRate < 0 || this.dividendRate > corpConstants.dividendMaxRate) { @@ -151,7 +162,9 @@ export class Corporation { } else { this.addFunds(cycleProfit); } - + this.updateTotalAssets(); + this.cycleValuation = this.determineCycleValuation(); + this.determineValuation(); this.updateSharePrice(); } @@ -170,24 +183,27 @@ export class Corporation { determineCycleValuation(): number { let val, - profit = this.revenue - this.expenses; + assetDelta = (this.totalAssets - this.previousTotalAssets) / corpConstants.secondsPerMarketCycle; + // Handle pre-totalAssets saves + assetDelta ??= this.revenue - this.expenses; if (this.public) { // Account for dividends if (this.dividendRate > 0) { - profit *= 1 - this.dividendRate; + assetDelta *= 1 - this.dividendRate; } - val = this.funds + profit * 85e3; + val = this.funds + assetDelta * 85e3; val *= Math.pow(1.1, this.divisions.size); val = Math.max(val, 0); } else { - val = 10e9 + Math.max(this.funds, 0) / 3; //Base valuation - if (profit > 0) { - val += profit * 315e3; + val = 10e9 + this.funds / 3; + if (assetDelta > 0) { + val += assetDelta * 315e3; } val *= Math.pow(1.1, this.divisions.size); val -= val % 1e6; //Round down to nearest millionth } + if (val < 10e9) val = 10e9; // Base valuation return val * currentNodeMults.CorporationValuation; } @@ -195,10 +211,27 @@ export class Corporation { this.valuationsList.push(this.cycleValuation); //Add current valuation to the list if (this.valuationsList.length > corpConstants.valuationLength) this.valuationsList.shift(); let val = this.valuationsList.reduce((a, b) => a + b); //Calculate valuations sum - val /= corpConstants.valuationLength; //Calculate the average + val /= this.valuationsList.length; //Calculate the average this.valuation = val; } + updateTotalAssets(): void { + let assets = this.funds; + this.divisions.forEach((ind) => { + assets += IndustriesData[ind.type].startingCost; + for (const warehouse of getRecordValues(ind.warehouses)) { + for (const mat of getRecordValues(warehouse.materials)) { + assets += mat.stored * mat.averagePrice; + } + for (const prod of ind.products.values()) { + assets += prod.cityData[warehouse.city].stored * prod.productionCost; + } + } + }); + this.previousTotalAssets = this.totalAssets; + this.totalAssets = assets; + } + getTargetSharePrice(): number { // Note: totalShares - numShares is not the same as issuedShares because // issuedShares does not account for private investors @@ -291,7 +324,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.funds -= price; + this.addNonIncomeFunds(-price); this.unlocks.add(unlockName); // Apply effects for one-time unlocks @@ -308,7 +341,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.funds -= totalCost; + this.addNonIncomeFunds(-totalCost); 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 bd84301e0..5c5414cf7 100644 --- a/src/Corporation/Division.ts +++ b/src/Corporation/Division.ts @@ -311,6 +311,7 @@ export class Division { buyAmt = Math.min(buyAmt, maxAmt); if (buyAmt > 0) { mat.quality = Math.max(0.1, (mat.quality * mat.stored + 1 * buyAmt) / (mat.stored + buyAmt)); + mat.averagePrice = (mat.stored * mat.averagePrice + buyAmt * mat.marketPrice) / (mat.stored + buyAmt); mat.stored += buyAmt; expenses += buyAmt * mat.marketPrice; } @@ -364,8 +365,13 @@ export class Division { // buy them for (const [matName, [buyAmt]] of getRecordEntries(smartBuy)) { const mat = warehouse.materials[matName]; - if (mat.stored + buyAmt != 0) mat.quality = (mat.quality * mat.stored + 1 * buyAmt) / (mat.stored + buyAmt); - else mat.quality = 1; + if (mat.stored + buyAmt != 0) { + mat.quality = (mat.quality * mat.stored + 1 * buyAmt) / (mat.stored + buyAmt); + mat.averagePrice = (mat.averagePrice * mat.stored + mat.marketPrice * buyAmt) / (mat.stored + buyAmt); + } else { + mat.quality = 1; + mat.averagePrice = mat.marketPrice; + } mat.stored += buyAmt; mat.buyAmount = buyAmt / (corpConstants.secondsPerMarketCycle * marketCycles); expenses += buyAmt * mat.marketPrice; @@ -460,6 +466,11 @@ export class Division { tempQlt * prod * producableFrac) / (warehouse.materials[this.producedMaterials[j]].stored + prod * producableFrac), ); + warehouse.materials[this.producedMaterials[j]].averagePrice = + (warehouse.materials[this.producedMaterials[j]].averagePrice * + warehouse.materials[this.producedMaterials[j]].stored + + warehouse.materials[this.producedMaterials[j]].marketPrice * prod * producableFrac) / + (warehouse.materials[this.producedMaterials[j]].stored + prod * producableFrac); warehouse.materials[this.producedMaterials[j]].stored += prod * producableFrac; } } else { @@ -678,6 +689,10 @@ export class Division { (expWarehouse.materials[matName].stored + amt), ); + expWarehouse.materials[matName].averagePrice = + (expWarehouse.materials[matName].averagePrice * expWarehouse.materials[matName].stored + + expWarehouse.materials[matName].marketPrice * amt) / + (expWarehouse.materials[matName].stored + amt); expWarehouse.materials[matName].stored += amt; mat.stored -= amt; mat.exportedLastCycle += amt; diff --git a/src/Corporation/Material.ts b/src/Corporation/Material.ts index 574233c6a..4d5136fcd 100644 --- a/src/Corporation/Material.ts +++ b/src/Corporation/Material.ts @@ -52,6 +52,9 @@ export class Material { // Cost / sec to buy this material. AKA Market Price marketPrice = 0; + // Average price paid for the material (accounted as marketPrice for produced/imported materials) + averagePrice = 0; + /** null if there is no limit set on production. 0 actually limits production to 0. */ productionLimit: number | null = null; diff --git a/src/Corporation/ui/Overview.tsx b/src/Corporation/ui/Overview.tsx index 833d9e40c..9df0ae8a3 100644 --- a/src/Corporation/ui/Overview.tsx +++ b/src/Corporation/ui/Overview.tsx @@ -62,6 +62,7 @@ export function Overview({ rerender }: IProps): React.ReactElement { ], + ["Total Assets:", ], ["Total Revenue:", ], ["Total Expenses:", ], ["Total Profit:", ], diff --git a/src/Corporation/ui/modals/FindInvestorsModal.tsx b/src/Corporation/ui/modals/FindInvestorsModal.tsx index e1d0b168f..b7657e32e 100644 --- a/src/Corporation/ui/modals/FindInvestorsModal.tsx +++ b/src/Corporation/ui/modals/FindInvestorsModal.tsx @@ -29,7 +29,7 @@ export function FindInvestorsModal(props: IProps): React.ReactElement { function findInvestors(): void { corporation.fundingRound++; - corporation.addFunds(funding); + corporation.addNonIncomeFunds(funding); corporation.numShares -= investShares; props.rerender(); props.onClose(); diff --git a/src/Hacknet/HacknetHelpers.tsx b/src/Hacknet/HacknetHelpers.tsx index 3a862fc24..d7649feda 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.funds = corp.funds + upg.value * count; + corp.addNonIncomeFunds(upg.value * count); break; } case "Reduce Minimum Security": { diff --git a/src/NetscriptFunctions/Corporation.ts b/src/NetscriptFunctions/Corporation.ts index 43f60dcf2..189c23026 100644 --- a/src/NetscriptFunctions/Corporation.ts +++ b/src/NetscriptFunctions/Corporation.ts @@ -142,7 +142,7 @@ export function NetscriptCorporation(): InternalAPI { const funding = val * percShares * roundMultiplier; const investShares = Math.floor(corpConstants.initialShares * percShares); corporation.fundingRound++; - corporation.addFunds(funding); + corporation.addNonIncomeFunds(funding); corporation.numShares -= investShares; return true; } @@ -157,7 +157,7 @@ export function NetscriptCorporation(): InternalAPI { corporation.sharePrice = initialSharePrice; corporation.issuedShares = numShares; corporation.numShares -= numShares; - corporation.addFunds(numShares * initialSharePrice); + corporation.addNonIncomeFunds(numShares * initialSharePrice); return true; } diff --git a/test/jest/__snapshots__/FullSave.test.ts.snap b/test/jest/__snapshots__/FullSave.test.ts.snap index 5050dd2ef..d58fcf253 100644 --- a/test/jest/__snapshots__/FullSave.test.ts.snap +++ b/test/jest/__snapshots__/FullSave.test.ts.snap @@ -1155,6 +1155,7 @@ exports[`Check Save File Continuity PlayerSave continuity 1`] = ` "maxDivisions": 20, "name": "Test Corp", "numShares": 1000000000, + "previousTotalAssets": 150000000000, "public": false, "revenue": 0, "seedFunded": false, @@ -1168,6 +1169,7 @@ exports[`Check Save File Continuity PlayerSave continuity 1`] = ` }, }, "storedCycles": 0, + "totalAssets": 150000000000, "totalShares": 1000000000, "unlocks": { "ctor": "JSONSet",