CORPORATION: Rework valuation calculation (#789)

This commit is contained in:
Yichi Zhang 2023-09-19 05:47:16 -07:00 committed by GitHub
parent e1d2e12747
commit 1b81fe8766
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 78 additions and 24 deletions

@ -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 {

@ -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;

@ -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;

@ -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;

@ -62,6 +62,7 @@ export function Overview({ rerender }: IProps): React.ReactElement {
<StatsTable
rows={[
["Total Funds:", <Money key="funds" money={corp.funds} />],
["Total Assets:", <Money key="assets" money={corp.totalAssets} />],
["Total Revenue:", <MoneyRate key="revenue" money={corp.revenue} />],
["Total Expenses:", <MoneyRate key="expenses" money={corp.expenses} />],
["Total Profit:", <MoneyRate key="profit" money={corp.revenue - corp.expenses} />],

@ -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();

@ -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": {

@ -142,7 +142,7 @@ export function NetscriptCorporation(): InternalAPI<NSCorporation> {
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<NSCorporation> {
corporation.sharePrice = initialSharePrice;
corporation.issuedShares = numShares;
corporation.numShares -= numShares;
corporation.addFunds(numShares * initialSharePrice);
corporation.addNonIncomeFunds(numShares * initialSharePrice);
return true;
}

@ -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",