mirror of
https://github.com/bitburner-official/bitburner-src.git
synced 2024-11-18 13:43:49 +01:00
CORPORATION: Use accounting methods for all funds transactions (#949)
This commit is contained in:
parent
b114fb9eed
commit
902306530c
@ -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();
|
||||
|
||||
|
30
src/Corporation/data/FundsSource.ts
Normal file
30
src/Corporation/data/FundsSource.ts
Normal file
@ -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
|
||||
|
Loading…
Reference in New Issue
Block a user