CORPORATION: Use accounting methods for all funds transactions (#949)

This commit is contained in:
Jesse Clark 2023-12-07 18:22:21 -08:00 committed by GitHub
parent b114fb9eed
commit 902306530c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
12 changed files with 93 additions and 64 deletions

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

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

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

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

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

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

@ -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(
<Typography>
Sold <b>{divisionToSell.name}</b> for <Money money={price} />, you now have space for
Sold <b>{divisionToSell.name}</b> for <Money money={soldPrice} />, you now have space for
{corp.maxDivisions - corp.divisions.size} more divisions.
</Typography>,
);

@ -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");
}
}

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

@ -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");
}
}

@ -132,7 +132,7 @@ export function NetscriptCorporation(): InternalAPI<NSCorporation> {
const repGain = amountCash / corpConstants.bribeAmountPerReputation;
faction.playerReputation += repGain;
corporation.funds = corporation.funds - amountCash;
corporation.loseFunds(amountCash, "bribery");
return true;
}

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