CORPORATION: Rework share price calculation + UI improvements (#782)

This commit is contained in:
Jesse Clark 2023-09-19 21:36:48 -07:00 committed by GitHub
parent f6e1c171ae
commit 3ae3f947ac
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
34 changed files with 654 additions and 417 deletions

@ -4,7 +4,7 @@
## Corporation.buyBackShares() method
Buyback Shares
Buyback Shares. Spend money from the player's wallet to transfer shares from public traders to the CEO.
**Signature:**

@ -19,7 +19,7 @@ export interface Corporation extends WarehouseAPI, OfficeAPI
| --- | --- |
| [acceptInvestmentOffer()](./bitburner.corporation.acceptinvestmentoffer.md) | Accept investment based on you companies current valuation |
| [bribe(factionName, amountCash)](./bitburner.corporation.bribe.md) | Bribe a faction |
| [buyBackShares(amount)](./bitburner.corporation.buybackshares.md) | Buyback Shares |
| [buyBackShares(amount)](./bitburner.corporation.buybackshares.md) | Buyback Shares. Spend money from the player's wallet to transfer shares from public traders to the CEO. |
| [createCorporation(corporationName, selfFund)](./bitburner.corporation.createcorporation.md) | Create a Corporation |
| [expandCity(divisionName, city)](./bitburner.corporation.expandcity.md) | Expand to a new city |
| [expandIndustry(industryType, divisionName)](./bitburner.corporation.expandindustry.md) | Expand to a new industry |
@ -40,5 +40,5 @@ export interface Corporation extends WarehouseAPI, OfficeAPI
| [issueNewShares(amount)](./bitburner.corporation.issuenewshares.md) | Issue new shares |
| [levelUpgrade(upgradeName)](./bitburner.corporation.levelupgrade.md) | Level an upgrade. |
| [purchaseUnlock(upgradeName)](./bitburner.corporation.purchaseunlock.md) | Unlock an upgrade |
| [sellShares(amount)](./bitburner.corporation.sellshares.md) | Sell Shares |
| [sellShares(amount)](./bitburner.corporation.sellshares.md) | Sell Shares. Transfer shares from the CEO to public traders to receive money in the player's wallet. |

@ -4,7 +4,7 @@
## Corporation.sellShares() method
Sell Shares
Sell Shares. Transfer shares from the CEO to public traders to receive money in the player's wallet.
**Signature:**

@ -0,0 +1,13 @@
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
[Home](./index.md) &gt; [bitburner](./bitburner.md) &gt; [CorporationInfo](./bitburner.corporationinfo.md) &gt; [investorShares](./bitburner.corporationinfo.investorshares.md)
## CorporationInfo.investorShares property
Amount of shares owned by private investors. Not available for public sale or CEO buyback.
**Signature:**
```typescript
investorShares: number;
```

@ -4,7 +4,7 @@
## CorporationInfo.issuedShares property
Amount of acquirable shares.
Amount of shares owned by public traders. Available for CEO buyback.
**Signature:**

@ -22,14 +22,15 @@ interface CorporationInfo
| [divisions](./bitburner.corporationinfo.divisions.md) | | string\[\] | Array of all division names |
| [expenses](./bitburner.corporationinfo.expenses.md) | | number | Expenses per second this cycle |
| [funds](./bitburner.corporationinfo.funds.md) | | number | Funds available |
| [issuedShares](./bitburner.corporationinfo.issuedshares.md) | | number | Amount of acquirable shares. |
| [investorShares](./bitburner.corporationinfo.investorshares.md) | | number | Amount of shares owned by private investors. Not available for public sale or CEO buyback. |
| [issuedShares](./bitburner.corporationinfo.issuedshares.md) | | number | Amount of shares owned by public traders. Available for CEO buyback. |
| [issueNewSharesCooldown](./bitburner.corporationinfo.issuenewsharescooldown.md) | | number | Cooldown until new shares can be issued |
| [name](./bitburner.corporationinfo.name.md) | | string | Name of the corporation |
| [numShares](./bitburner.corporationinfo.numshares.md) | | number | Amount of share owned |
| [numShares](./bitburner.corporationinfo.numshares.md) | | number | Amount of shares owned by the CEO. |
| [public](./bitburner.corporationinfo.public.md) | | boolean | Indicating if the company is public |
| [revenue](./bitburner.corporationinfo.revenue.md) | | number | Revenue per second this cycle |
| [sharePrice](./bitburner.corporationinfo.shareprice.md) | | number | Price of the shares |
| [shareSaleCooldown](./bitburner.corporationinfo.sharesalecooldown.md) | | number | Cooldown until shares can be sold again |
| [state](./bitburner.corporationinfo.state.md) | | string | <p>The next state to be processed.</p><p>I.e. when the state is PURCHASE, it means purchasing will occur during the next state transition.</p><p>Possible states are START, PURCHASE, PRODUCTION, EXPORT, SALE.</p> |
| [totalShares](./bitburner.corporationinfo.totalshares.md) | | number | Total number of shares issues by this corporation |
| [totalShares](./bitburner.corporationinfo.totalshares.md) | | number | Total number of shares issued by this corporation. |

@ -4,7 +4,7 @@
## CorporationInfo.numShares property
Amount of share owned
Amount of shares owned by the CEO.
**Signature:**

@ -4,7 +4,7 @@
## CorporationInfo.totalShares property
Total number of shares issues by this corporation
Total number of shares issued by this corporation.
**Signature:**

@ -1,5 +1,3 @@
import { isInteger } from "lodash";
import { Player } from "@player";
import { CorpResearchName, CorpSmartSupplyOption } from "@nsdefs";
@ -18,6 +16,7 @@ import { isRelevantMaterial } from "./ui/Helpers";
import { CityName } from "@enums";
import { getRandomInt } from "../utils/helpers/getRandomInt";
import { getRecordValues } from "../Types/Record";
import { sellSharesFailureReason, buybackSharesFailureReason, issueNewSharesFailureReason } from "./helpers";
export function NewDivision(corporation: Corporation, industry: IndustryType, name: string): void {
if (corporation.divisions.size >= corporation.maxDivisions)
@ -85,33 +84,72 @@ export function IssueDividends(corporation: Corporation, rate: number): void {
corporation.dividendRate = rate;
}
export function IssueNewShares(corporation: Corporation, amount: number): [number, number, number] {
const max = corporation.calculateMaxNewShares();
export function GoPublic(corporation: Corporation, numShares: number): void {
const ceoOwnership = (corporation.numShares - numShares) / corporation.totalShares;
const initialSharePrice = corporation.getTargetSharePrice(ceoOwnership);
// Round to nearest ten-millionth
amount = Math.round(amount / 10e6) * 10e6;
if (isNaN(amount) || amount < 10e6 || amount > max) {
throw new Error(`Invalid value. Must be an number between 10m and ${max} (20% of total shares)`);
if (isNaN(numShares) || numShares < 0) {
throw new Error("Invalid value for number of issued shares");
}
if (numShares > corporation.numShares) {
throw new Error("You don't have that many shares to issue!");
}
corporation.public = true;
corporation.sharePrice = initialSharePrice;
corporation.issuedShares += numShares;
corporation.numShares -= numShares;
corporation.addNonIncomeFunds(numShares * initialSharePrice);
}
const newSharePrice = Math.round(corporation.sharePrice * 0.8);
export function IssueNewShares(
corporation: Corporation,
amount: number,
): [profit: number, amount: number, privateShares: number] {
const failureReason = issueNewSharesFailureReason(corporation, amount);
if (failureReason) throw new Error(failureReason);
const profit = amount * newSharePrice;
corporation.issueNewSharesCooldown = corpConstants.issueNewSharesCooldown;
const ceoOwnership = corporation.numShares / (corporation.totalShares + amount);
const newSharePrice = corporation.getTargetSharePrice(ceoOwnership);
const privateOwnedRatio = 1 - (corporation.numShares + corporation.issuedShares) / corporation.totalShares;
const profit = (amount * (corporation.sharePrice + newSharePrice)) / 2;
const cooldownMultiplier = corporation.totalShares / corpConstants.initialShares;
corporation.issueNewSharesCooldown = corpConstants.issueNewSharesCooldown * cooldownMultiplier;
const privateOwnedRatio = corporation.investorShares / corporation.totalShares;
const maxPrivateShares = Math.round((amount / 2) * privateOwnedRatio);
const privateShares = Math.round(getRandomInt(0, maxPrivateShares) / 10e6) * 10e6;
corporation.issuedShares += amount - privateShares;
corporation.investorShares += privateShares;
corporation.totalShares += amount;
corporation.addNonIncomeFunds(profit);
corporation.immediatelyUpdateSharePrice();
// Set sharePrice directly because all formulas will be based on stale cycleValuation data
corporation.sharePrice = newSharePrice;
return [profit, amount, privateShares];
}
export function AcceptInvestmentOffer(corporation: Corporation): void {
if (
corporation.fundingRound >= corpConstants.fundingRoundShares.length ||
corporation.fundingRound >= corpConstants.fundingRoundMultiplier.length ||
corporation.public
) {
throw new Error("No more investment offers are available.");
}
const val = corporation.valuation;
const percShares = corpConstants.fundingRoundShares[corporation.fundingRound];
const roundMultiplier = corpConstants.fundingRoundMultiplier[corporation.fundingRound];
const funding = val * percShares * roundMultiplier;
const investShares = Math.floor(corpConstants.initialShares * percShares);
corporation.fundingRound++;
corporation.addNonIncomeFunds(funding);
corporation.numShares -= investShares;
corporation.investorShares += investShares;
}
export function SellMaterial(material: Material, amount: string, price: string): void {
if (price === "") price = "0";
if (amount === "") amount = "0";
@ -280,17 +318,10 @@ export function BulkPurchase(
}
export function SellShares(corporation: Corporation, numShares: number): number {
if (isNaN(numShares) || !isInteger(numShares)) throw new Error("Invalid value for number of shares");
if (numShares <= 0) throw new Error("Invalid value for number of shares");
if (numShares > corporation.numShares) throw new Error("You don't have that many shares to sell!");
if (numShares === corporation.numShares) throw new Error("You cant't sell all your shares!");
if (numShares > 1e14) throw new Error("Invalid value for number of shares");
if (!corporation.public) throw new Error("You haven't gone public!");
if (corporation.shareSaleCooldown) throw new Error("Share sale on cooldown!");
const stockSaleResults = corporation.calculateShareSale(numShares);
const profit = stockSaleResults[0];
const newSharePrice = stockSaleResults[1];
const newSharesUntilUpdate = stockSaleResults[2];
const failureReason = sellSharesFailureReason(corporation, numShares);
if (failureReason) throw new Error(failureReason);
const [profit, newSharePrice, newSharesUntilUpdate] = corporation.calculateShareSale(numShares);
corporation.numShares -= numShares;
corporation.issuedShares += numShares;
@ -302,15 +333,16 @@ export function SellShares(corporation: Corporation, numShares: number): number
}
export function BuyBackShares(corporation: Corporation, numShares: number): boolean {
if (isNaN(numShares) || !isInteger(numShares)) throw new Error("Invalid value for number of shares");
if (numShares <= 0) throw new Error("Invalid value for number of shares");
if (numShares > corporation.issuedShares) throw new Error("You don't have that many shares to buy!");
if (!corporation.public) throw new Error("You haven't gone public!");
const buybackPrice = corporation.sharePrice * 1.1;
if (Player.money < numShares * buybackPrice) throw new Error("You cant afford that many shares!");
const failureReason = buybackSharesFailureReason(corporation, numShares);
if (failureReason) throw new Error(failureReason);
const [cost, newSharePrice, newSharesUntilUpdate] = corporation.calculateShareBuyback(numShares);
corporation.numShares += numShares;
corporation.issuedShares -= numShares;
Player.loseMoney(numShares * buybackPrice, "corporation");
corporation.sharePrice = newSharePrice;
corporation.shareSalesUntilPriceUpdate = newSharesUntilUpdate;
Player.loseMoney(cost, "corporation");
return true;
}

@ -12,7 +12,7 @@ import { showLiterature } from "../Literature/LiteratureHelpers";
import { dialogBoxCreate } from "../ui/React/DialogBox";
import { constructorsForReviver, Generic_toJSON, Generic_fromJSON, IReviverValue } from "../utils/JSONReviver";
import { CorpStateName } from "@nsdefs";
import { CorpStateName, InvestmentOffer } from "@nsdefs";
import { calculateUpgradeCost } from "./helpers";
import { JSONMap, JSONSet } from "../Types/Jsonable";
import { formatMoney } from "../ui/formatNumber";
@ -46,6 +46,7 @@ export class Corporation {
issueNewSharesCooldown = 0; // Game cycles until player can issue shares again
dividendRate = 0;
dividendTax = 1 - currentNodeMults.CorporationSoftcap + 0.15;
investorShares = 0;
issuedShares = 0;
sharePrice = 0;
storedCycles = 0;
@ -232,10 +233,17 @@ export class Corporation {
this.totalAssets = assets;
}
getTargetSharePrice(): number {
// Note: totalShares - numShares is not the same as issuedShares because
// issuedShares does not account for private investors
return this.valuation / (2 * (this.totalShares - this.numShares) + 1);
getTargetSharePrice(ceoOwnership: number | null = null): number {
// Share price is proportional to total corporation valuation.
// When the CEO owns 0% of the company, market cap is 0.5x valuation.
// When the CEO owns 25% of the company, market cap is 1.0x valuation.
// When the CEO owns 100% of shares, market cap is 1.5x valuation.
if (ceoOwnership === null) {
ceoOwnership = this.numShares / this.totalShares;
}
const ceoConfidence = 0.5 + Math.sqrt(Math.max(0, ceoOwnership));
const marketCap = this.valuation * ceoConfidence;
return marketCap / this.totalShares;
}
updateSharePrice(): void {
@ -250,10 +258,6 @@ export class Corporation {
}
}
immediatelyUpdateSharePrice(): void {
this.sharePrice = this.getTargetSharePrice();
}
calculateMaxNewShares(): number {
const maxNewSharesUnrounded = Math.round(this.totalShares * 0.2);
const maxNewShares = maxNewSharesUnrounded - (maxNewSharesUnrounded % 10e6);
@ -261,17 +265,18 @@ export class Corporation {
}
// Calculates how much money will be made and what the resulting stock price
// will be when the player sells his/her shares
// will be when the player sells their shares
// @return - [Player profit, final stock price, end shareSalesUntilPriceUpdate property]
calculateShareSale(numShares: number): [number, number, number] {
let sharesTracker = numShares;
calculateShareSale(numShares: number): [profit: number, sharePrice: number, sharesUntilUpdate: number] {
let sharesRemaining = numShares;
let sharesUntilUpdate = this.shareSalesUntilPriceUpdate;
let sharePrice = this.sharePrice;
let sharesSold = 0;
let profit = 0;
let targetPrice = this.getTargetSharePrice();
const maxIterations = Math.ceil(numShares / corpConstants.sharesPerPriceUpdate);
const sharesPerStep = Math.sign(numShares || 1) * corpConstants.sharesPerPriceUpdate;
const maxIterations = Math.ceil(numShares / sharesPerStep);
if (isNaN(maxIterations) || maxIterations > 10e6) {
console.error(
`Something went wrong or unexpected when calculating share sale. Max iterations calculated to be ${maxIterations}`,
@ -280,28 +285,59 @@ export class Corporation {
}
for (let i = 0; i < maxIterations; ++i) {
if (sharesTracker < sharesUntilUpdate) {
profit += sharePrice * sharesTracker;
sharesUntilUpdate -= sharesTracker;
if (Math.abs(sharesRemaining) < Math.abs(sharesUntilUpdate)) {
profit += sharePrice * sharesRemaining;
sharesUntilUpdate -= sharesRemaining;
break;
} else {
profit += sharePrice * sharesUntilUpdate;
sharesUntilUpdate = corpConstants.sharesPerPriceUpdate;
sharesTracker -= sharesUntilUpdate;
sharesSold += sharesUntilUpdate;
targetPrice = this.valuation / (2 * (this.totalShares + sharesSold - this.numShares));
// Calculate what new share price would be
profit += sharePrice * sharesPerStep;
sharesRemaining -= sharesPerStep;
sharesSold += sharesPerStep;
// Update the share price
const ceoOwnership = (this.numShares - sharesSold) / this.totalShares;
const targetPrice = this.getTargetSharePrice(ceoOwnership);
if (sharePrice <= targetPrice) {
sharePrice *= 1 + 0.5 * 0.01;
} else {
sharePrice *= 1 - 0.5 * 0.01;
}
sharesUntilUpdate = corpConstants.sharesPerPriceUpdate;
}
}
return [profit, sharePrice, sharesUntilUpdate];
}
calculateShareBuyback(numShares: number): [cost: number, sharePrice: number, sharesUntilUpdate: number] {
const [profit, sharePrice, sharesUntilUpdate] = this.calculateShareSale(-numShares);
const cost = -1.1 * profit;
return [cost, sharePrice, sharesUntilUpdate];
}
getInvestmentOffer(): InvestmentOffer {
if (
this.fundingRound >= corpConstants.fundingRoundShares.length ||
this.fundingRound >= corpConstants.fundingRoundMultiplier.length ||
this.public
)
return {
funds: 0,
shares: 0,
round: this.fundingRound + 1, // Make more readable
}; // Don't throw an error here, no reason to have a second function to check if you can get investment.
const val = this.valuation;
const percShares = corpConstants.fundingRoundShares[this.fundingRound];
const roundMultiplier = corpConstants.fundingRoundMultiplier[this.fundingRound];
const funding = val * percShares * roundMultiplier;
const investShares = Math.floor(corpConstants.initialShares * percShares);
return {
funds: funding,
shares: investShares,
round: this.fundingRound + 1, // Make more readable
};
}
convertCooldownToString(cd: number): string {
// The cooldown value is based on game cycles. Convert to a simple string
const seconds = cd / 5;

@ -33,7 +33,7 @@ export const stateNames: CorpStateName[] = ["START", "PURCHASE", "PRODUCTION", "
/** Names of all one-time corporation-wide unlocks */
unlockNames: APIUnlockName[] = Object.values(CorpUnlockName),
upgradeNames: APIUpgradeName[] = Object.values(CorpUpgradeName),
/** Names of all reasearches common to all industries */
/** Names of all researches common to all industries */
researchNamesBase: CorpResearchName[] = Object.values(CorpBaseResearchName),
/** Names of all researches only available to product industries */
researchNamesProductOnly: CorpResearchName[] = Object.values(CorpProductResearchName),
@ -42,8 +42,8 @@ export const stateNames: CorpStateName[] = ["START", "PURCHASE", "PRODUCTION", "
initialShares = 1e9,
/** When selling large number of shares, price is dynamically updated for every batch of this amount */
sharesPerPriceUpdate = 1e6,
/** Cooldown for issue new shares cooldown in game cycles. 12 hours. */
issueNewSharesCooldown = 216e3,
/** Cooldown for issue new shares cooldown in game cycles. Initially 4 hours. */
issueNewSharesCooldown = 72e3,
/** Cooldown for selling shares in game cycles. 1 hour. */
sellSharesCooldown = 18e3,
teaCostPerEmployee = 500e3,

@ -1,4 +1,6 @@
import { PositiveInteger } from "../types";
import { Player } from "@player";
import { PositiveInteger, isPositiveInteger } from "../types";
import { formatShares } from "../ui/formatNumber";
import { Corporation } from "./Corporation";
import { CorpUpgrade } from "./data/CorporationUpgrades";
@ -29,3 +31,43 @@ export function calculateMaxAffordableUpgrade(corp: Corporation, upgrade: CorpUp
const sanitizedValue = maxAffordableUpgrades >= 0 ? maxAffordableUpgrades : 0;
return sanitizedValue as PositiveInteger | 0;
}
/** Returns a string representing the reason a share sale should fail, or empty string if there is no issue. */
export function sellSharesFailureReason(corp: Corporation, numShares: number): string {
if (!isPositiveInteger(numShares)) return "Number of shares must be a positive integer.";
else if (numShares > corp.numShares) return "You do not have that many shares to sell.";
else if (numShares === corp.numShares) return "You cannot sell all your shares.";
else if (numShares > 1e14) return `Cannot sell more than ${formatShares(1e14)} shares at a time.`;
else if (!corp.public) return "Cannot sell shares before going public.";
else if (corp.shareSaleCooldown)
return `Cannot sell shares for another ${corp.convertCooldownToString(corp.shareSaleCooldown)}.`;
return "";
}
/** Returns a string representing the reason a share buyback should fail, or empty string if there is no issue. */
export function buybackSharesFailureReason(corp: Corporation, numShares: number): string {
if (!isPositiveInteger(numShares)) return "Number of shares must be a positive integer.";
if (numShares > corp.issuedShares) return "Not enough shares are available for buyback.";
if (numShares > 1e14) return `Cannot buy more than ${formatShares(1e14)} shares at a time.`;
if (!corp.public) return "Cannot buy back shares before going public.";
const [cost] = corp.calculateShareBuyback(numShares);
if (Player.money < cost) return "You cannot afford that many shares.";
return "";
}
/** Returns a string representing the reason issuing new shares should fail, or empty string if there is no issue. */
export function issueNewSharesFailureReason(corp: Corporation, numShares: number): string {
if (!isPositiveInteger(numShares)) return "Number of shares must be a positive integer.";
if (numShares % 10e6 !== 0) return "Number of shares must be a multiple of 10 million.";
if (!corp.public) return "Cannot issue new shares before going public.";
const maxNewShares = corp.calculateMaxNewShares();
if (numShares > maxNewShares) return `Number of shares cannot exceed ${maxNewShares} (20% of total shares).`;
const cooldown = corp.issueNewSharesCooldown;
if (cooldown > 0) return `Cannot issue new shares for another ${corp.convertCooldownToString(cooldown)}.`;
return "";
}

@ -8,7 +8,7 @@ import { Warehouse } from "../Warehouse";
import { ExportModal } from "./modals/ExportModal";
import { SellMaterialModal } from "./modals/SellMaterialModal";
import { PurchaseMaterialModal } from "./modals/PurchaseMaterialModal";
import { formatBigNumber, formatCorpStat, formatMoney, formatQuality } from "../../ui/formatNumber";
import { formatBigNumber, formatCorpStat, formatQuality } from "../../ui/formatNumber";
import { isString } from "../../utils/helpers/string";
import { Money } from "../../ui/React/Money";
import { useCorporation, useDivision } from "./Context";
@ -118,7 +118,9 @@ export function MaterialElem(props: IMaterialProps): React.ReactElement {
</Typography>
}
>
<Typography>MP: {formatMoney(mat.marketPrice)}</Typography>
<Typography>
MP: <Money money={mat.marketPrice} />
</Typography>
</Tooltip>
<Tooltip
title={<Typography>The quality of your material. Higher quality will lead to more sales</Typography>}

@ -77,8 +77,21 @@ export function Overview({ rerender }: IProps): React.ReactElement {
title={
<StatsTable
rows={[
["Outstanding Shares:", formatShares(corp.issuedShares)],
["Private Shares:", formatShares(corp.totalShares - corp.issuedShares - corp.numShares)],
[
"Owned Stock Shares:",
<>&nbsp;{formatShares(corp.numShares)}&nbsp;</>,
<>({formatPercent(corp.numShares / corp.totalShares)})</>,
],
[
"Outstanding Shares:",
<>&nbsp;{formatShares(corp.issuedShares)}&nbsp;</>,
<>({formatPercent(corp.issuedShares / corp.totalShares)})</>,
],
[
"Private Shares:",
<>&nbsp;{formatShares(corp.investorShares)}&nbsp;</>,
<>({formatPercent(corp.investorShares / corp.totalShares)})</>,
],
]}
/>
}
@ -96,8 +109,8 @@ export function Overview({ rerender }: IProps): React.ReactElement {
<ButtonWithTooltip
normalTooltip={
<>
Get a copy of and read 'The Complete Handbook for Creating a Successful Corporation.' This is a .lit file
that guides you through the beginning of setting up a Corporation and provides some tips/pointers for
Get a copy of and read <i>The Complete Handbook for Creating a Successful Corporation</i>. This is a .lit
file that guides you through the beginning of setting up a Corporation and provides some tips/pointers for
helping you get started with managing it.
</>
}
@ -125,7 +138,7 @@ function PrivateButtons({ rerender }: IPrivateButtonsProps): React.ReactElement
const [findInvestorsopen, setFindInvestorsopen] = useState(false);
const [goPublicopen, setGoPublicopen] = useState(false);
const fundingAvailable = corp.fundingRound < 4;
const fundingAvailable = corp.fundingRound < corpConstants.fundingRoundShares.length;
const findInvestorsTooltip = fundingAvailable
? "Search for private investors who will give you startup funding in exchange for equity (stock shares) in your company"
: "";
@ -213,29 +226,27 @@ function PublicButtons({ rerender }: IPublicButtonsProps): React.ReactElement {
const [issueDividendsOpen, setIssueDividendsOpen] = useState(false);
const sellSharesOnCd = corp.shareSaleCooldown > 0;
const sellSharesTooltip = sellSharesOnCd
? "Cannot sell shares for " + corp.convertCooldownToString(corp.shareSaleCooldown)
: "Sell your shares in the company. The money earned from selling your " +
"shares goes into your personal account, not the Corporation's. " +
"This is one of the only ways to profit from your business venture.";
const sellSharesTooltip =
"Sell your shares in the company. The money earned from selling your " +
"shares goes into your personal account, not the Corporation's. " +
"This is one of the only ways to profit from your business venture.";
const issueNewSharesOnCd = corp.issueNewSharesCooldown > 0;
const issueNewSharesTooltip = issueNewSharesOnCd
? "Cannot issue new shares for " + corp.convertCooldownToString(corp.issueNewSharesCooldown)
: "Issue new equity shares to raise capital.";
return (
<>
<ButtonWithTooltip
normalTooltip={sellSharesTooltip}
disabledTooltip={sellSharesOnCd ? "On cooldown" : ""}
disabledTooltip={
sellSharesOnCd ? "Cannot sell shares for " + corp.convertCooldownToString(corp.shareSaleCooldown) : ""
}
onClick={() => setSellSharesOpen(true)}
>
Sell Shares
</ButtonWithTooltip>
<SellSharesModal open={sellSharesOpen} onClose={() => setSellSharesOpen(false)} rerender={rerender} />
<ButtonWithTooltip
normalTooltip={"Buy back shares you that previously issued or sold at market price."}
normalTooltip={"Buy back shares you that previously issued or sold on the market"}
disabledTooltip={corp.issuedShares < 1 ? "No shares available to buy back" : ""}
onClick={() => setBuybackSharesOpen(true)}
>
@ -243,13 +254,15 @@ function PublicButtons({ rerender }: IPublicButtonsProps): React.ReactElement {
</ButtonWithTooltip>
<BuybackSharesModal open={buybackSharesOpen} onClose={() => setBuybackSharesOpen(false)} rerender={rerender} />
<ButtonWithTooltip
normalTooltip={issueNewSharesTooltip}
disabledTooltip={issueNewSharesOnCd ? "On cooldown" : ""}
normalTooltip={"Issue new equity shares to raise capital"}
disabledTooltip={
issueNewSharesOnCd ? `On cooldown for ${corp.convertCooldownToString(corp.issueNewSharesCooldown)}` : ""
}
onClick={() => setIssueNewSharesOpen(true)}
>
Issue New Shares
</ButtonWithTooltip>
<IssueNewSharesModal open={issueNewSharesOpen} onClose={() => setIssueNewSharesOpen(false)} />
<IssueNewSharesModal open={issueNewSharesOpen} onClose={() => setIssueNewSharesOpen(false)} rerender={rerender} />
<ButtonWithTooltip
normalTooltip={"Manage the dividends that are paid out to shareholders (including yourself)"}
onClick={() => setIssueDividendsOpen(true)}
@ -306,13 +319,22 @@ function SellDivisionButton(): React.ReactElement {
function RestartButton(): React.ReactElement {
const [open, setOpen] = useState(false);
const corp = useCorporation();
const sellSharesOnCd = corp.shareSaleCooldown > 0;
function restart(): void {
setOpen(true);
}
return (
<>
<ButtonWithTooltip normalTooltip={"Sell corporation and start over"} onClick={restart}>
<ButtonWithTooltip
normalTooltip={"Sell corporation and start over"}
disabledTooltip={
sellSharesOnCd ? "Sell corporation and start over. Cannot do this while Sell Shares is on cooldown." : ""
}
onClick={restart}
>
Sell CEO position
</ButtonWithTooltip>
<SellCorporationModal open={open} onClose={() => setOpen(false)} />

@ -8,7 +8,7 @@ import { LimitProductProductionModal } from "./modals/LimitProductProductionModa
import { SellProductModal } from "./modals/SellProductModal";
import { CancelProductModal } from "./modals/CancelProductModal";
import { formatBigNumber, formatMoney, formatPercent } from "../../ui/formatNumber";
import { formatBigNumber, formatPercent } from "../../ui/formatNumber";
import { isString } from "../../utils/helpers/string";
import { Money } from "../../ui/React/Money";
@ -135,7 +135,7 @@ export function ProductElem(props: IProductProps): React.ReactElement {
<Box display="flex">
<Tooltip title={<Typography>An estimate of the material cost it takes to create this Product.</Typography>}>
<Typography>
Est. Production Cost: {formatMoney(product.productionCost / corpConstants.baseProductProfitMult)}
Est. Production Cost: <Money money={product.productionCost / corpConstants.baseProductProfitMult} />
</Typography>
</Tooltip>
</Box>
@ -148,7 +148,9 @@ export function ProductElem(props: IProductProps): React.ReactElement {
</Typography>
}
>
<Typography>Est. Market Price: {formatMoney(product.productionCost)}</Typography>
<Typography>
Est. Market Price: <Money money={product.productionCost} />
</Typography>
</Tooltip>
</Box>
<Button onClick={() => setDiscontinueOpen(true)}>Discontinue</Button>

@ -1,15 +1,15 @@
import React, { useState } from "react";
import { dialogBoxCreate } from "../../../ui/React/DialogBox";
import { Modal } from "../../../ui/React/Modal";
import { formatBigNumber, formatMoney } from "../../../ui/formatNumber";
import { Player } from "@player";
import { Money } from "../../../ui/React/Money";
import { formatShares } from "../../../ui/formatNumber";
import { useCorporation } from "../Context";
import Typography from "@mui/material/Typography";
import { ButtonWithTooltip } from "../../../ui/Components/ButtonWithTooltip";
import { NumberInput } from "../../../ui/React/NumberInput";
import { BuyBackShares } from "../../Actions";
import { dialogBoxCreate } from "../../../ui/React/DialogBox";
import { KEY } from "../../../utils/helpers/keyCodes";
import { isPositiveInteger } from "../../../types";
import { buybackSharesFailureReason } from "../../helpers";
interface IProps {
open: boolean;
@ -23,44 +23,28 @@ export function BuybackSharesModal(props: IProps): React.ReactElement {
const corp = useCorporation();
const [shares, setShares] = useState<number>(NaN);
const currentStockPrice = corp.sharePrice;
const buybackPrice = currentStockPrice * 1.1;
const disabledText = !isPositiveInteger(shares)
? "Number of shares must be a positive integer"
: shares > corp.issuedShares
? "There are not enough shares available to buyback this many"
: shares * buybackPrice > Player.money
? "Insufficient player funds"
: "";
const [cost, sharePrice] = corp.calculateShareBuyback((props.open && shares) || 0);
const disabledText = buybackSharesFailureReason(corp, shares);
function buy(): void {
if (disabledText) return;
try {
BuyBackShares(corp, shares);
dialogBoxCreate(
<>
<Typography>
You bought {formatShares(shares)} shares for <Money money={cost} />.
</Typography>
<Typography>
<b>{corp.name}</b>'s stock price rose to <Money money={sharePrice} /> per share.
</Typography>
</>,
);
props.onClose();
props.rerender();
setShares(NaN);
} catch (err) {
dialogBoxCreate(err + "");
}
props.onClose();
props.rerender();
}
function CostIndicator(): React.ReactElement {
if (shares === null) return <></>;
if (isNaN(shares) || shares <= 0) {
return <>ERROR: Invalid value entered for number of shares to buyback</>;
} else if (shares > corp.issuedShares) {
return (
<>
There are not this many shares available to buy back. There are only {formatBigNumber(corp.issuedShares)}{" "}
outstanding shares.
</>
);
} else {
return (
<>
Purchase {shares} shares for a total of {formatMoney(shares * buybackPrice)}
</>
);
dialogBoxCreate(`${err}`);
}
}
@ -70,23 +54,45 @@ export function BuybackSharesModal(props: IProps): React.ReactElement {
return (
<Modal open={props.open} onClose={props.onClose}>
<Typography>
Enter the number of outstanding shares you would like to buy back. These shares must be bought at a 10% premium.
However, repurchasing shares from the market tends to lead to an increase in stock price.
<br />
<br />
To purchase these shares, you must use your own money (NOT your Corporation's funds).
<br />
<br />
The current buyback price of your company's stock is {formatMoney(buybackPrice)}. Your company currently has{" "}
{formatBigNumber(corp.issuedShares)} outstanding stock shares.
<Typography component="div">
Enter the number of outstanding shares you would like to buy back.
<ul>
<li>Buying back shares will cause the stock price to rise due to market forces.</li>
<li>These shares must be bought at a 10% premium over the market price.</li>
<li>You purchase these shares with your own money (NOT your Corporation's funds).</li>
</ul>
<b>{corp.name}</b> currently has {formatShares(corp.issuedShares)} outstanding stock shares, valued at{" "}
<Money money={corp.sharePrice} /> per share.
</Typography>
<CostIndicator />
<br />
<NumberInput autoFocus={true} placeholder="Shares to buyback" onChange={setShares} onKeyDown={onKeyDown} />
<NumberInput
defaultValue={shares || ""}
autoFocus={true}
placeholder="Shares to buyback"
onChange={setShares}
onKeyDown={onKeyDown}
/>
<ButtonWithTooltip disabledTooltip={disabledText} onClick={buy}>
Buy shares
{cost > 0 ? (
<>
&nbsp;-&nbsp;
<Money money={cost} forPurchase={true} />{" "}
</>
) : (
<></>
)}
</ButtonWithTooltip>
<br />
<Typography sx={{ minHeight: "1.5em" }}>
{!shares ? null : disabledText ? (
disabledText
) : (
<>
<b>{corp.name}</b>'s stock price will rise to <Money money={sharePrice} /> per share.
</>
)}
</Typography>
</Modal>
);
}

@ -4,6 +4,7 @@ import { Money } from "../../../ui/React/Money";
import { Modal } from "../../../ui/React/Modal";
import { Router } from "../../../ui/GameRoot";
import { Page } from "../../../ui/Router";
import { formatShares } from "../../../ui/formatNumber";
import { Player } from "@player";
import Typography from "@mui/material/Typography";
import { ButtonWithTooltip } from "../../../ui/Components/ButtonWithTooltip";
@ -54,15 +55,19 @@ export function CreateCorporationModal(props: IProps): React.ReactElement {
return (
<Modal open={props.open} onClose={props.onClose}>
<Typography>
Would you like to start a corporation? This will require $150b for registration and initial funding.{" "}
{Player.bitNodeN === 3 &&
`This $150b
can either be self-funded, or you can obtain the seed money from the government in exchange for 500 million
shares`}
Would you like to start a corporation? This will require <Money money={150e9} forPurchase={true} /> for
registration and initial funding.{" "}
{Player.bitNodeN === 3 && (
<>
This <Money money={150e9} /> can either be self-funded, or you can obtain the seed money from the government
in exchange for {formatShares(500e6)} shares (a <b>33.3%</b> stake in the company).
</>
)}
<br />
<br />
If you would like to start one, please enter a name for your corporation below:
</Typography>
<br />
<TextField autoFocus={true} placeholder="Corporation Name" onChange={onChange} value={name} />
{Player.bitNodeN === 3 && (
<ButtonWithTooltip onClick={seed} disabledTooltip={disabledTextForNoName}>
@ -71,7 +76,7 @@ export function CreateCorporationModal(props: IProps): React.ReactElement {
)}
<ButtonWithTooltip
onClick={selfFund}
disabledTooltip={disabledTextForNoName || canSelfFund ? "" : "Insufficient player funds"}
disabledTooltip={disabledTextForNoName || (canSelfFund ? "" : "Insufficient player funds")}
>
Self-Fund (<Money money={150e9} forPurchase={true} />)
</ButtonWithTooltip>

@ -1,8 +1,10 @@
import React from "react";
import { formatMoney, formatPercent, formatShares } from "../../../ui/formatNumber";
import * as corpConstants from "../../data/Constants";
import { dialogBoxCreate } from "../../../ui/React/DialogBox";
import { formatPercent, formatShares } from "../../../ui/formatNumber";
import { Modal } from "../../../ui/React/Modal";
import { Money } from "../../../ui/React/Money";
import { useCorporation } from "../Context";
import { AcceptInvestmentOffer } from "../../Actions";
import Typography from "@mui/material/Typography";
import Button from "@mui/material/Button";
@ -13,40 +15,52 @@ interface IProps {
rerender: () => void;
}
// Create a popup that lets the player manage exports
// Create a popup that lets the player manage investment offers
export function FindInvestorsModal(props: IProps): React.ReactElement {
const corporation = useCorporation();
const val = corporation.valuation;
if (
corporation.fundingRound >= corpConstants.fundingRoundShares.length ||
corporation.fundingRound >= corpConstants.fundingRoundMultiplier.length
)
return <></>;
const percShares = corpConstants.fundingRoundShares[corporation.fundingRound];
const roundMultiplier = corpConstants.fundingRoundMultiplier[corporation.fundingRound];
const funding = val * percShares * roundMultiplier;
const investShares = Math.floor(corpConstants.initialShares * percShares);
const corp = useCorporation();
const { funds, shares } = corp.getInvestmentOffer();
function findInvestors(): void {
corporation.fundingRound++;
corporation.addNonIncomeFunds(funding);
corporation.numShares -= investShares;
props.rerender();
props.onClose();
if (shares === 0) return;
try {
AcceptInvestmentOffer(corp);
dialogBoxCreate(
<>
<Typography>You accepted the investment offer.</Typography>
<Typography>
<b>{corp.name}</b> received <Money money={funds} />.
</Typography>
<Typography>
Your remaining equity is <b>{formatPercent(corp.numShares / corp.totalShares, 1)}</b>.
</Typography>
</>,
);
props.onClose();
props.rerender();
} catch (err) {
dialogBoxCreate(`${err}`);
}
}
return (
<Modal open={props.open} onClose={props.onClose}>
<Typography>
An investment firm has offered you {formatMoney(funding)} in funding in exchange for a{" "}
{formatPercent(percShares, 3)} stake in the company ({formatShares(investShares)} shares).
An investment firm has offered to buy {formatShares(shares)} shares of stock (a{" "}
<b>{formatPercent(shares / corp.totalShares, 1)}</b> stake in the company).
<br />
<br />
Do you accept or reject this offer?
<b>{corp.name}</b> will receive <Money money={funds} />.
<br />
Your equity will fall to <b>{formatPercent((corp.numShares - shares) / corp.totalShares, 1)}</b>.
<br />
<br />
Hint: Investment firms will offer more money if your corporation is turning a profit
<b>Hint</b>: Investment firms will offer more money if your Corporation is turning a profit.
<br />
<br />
Do you accept this offer?
</Typography>
<Button onClick={findInvestors}>Accept</Button>
<br />
<Button onClick={findInvestors}>Accept</Button> <Button onClick={props.onClose}>Ignore</Button>
</Modal>
);
}

@ -1,7 +1,8 @@
import React, { useState } from "react";
import { dialogBoxCreate } from "../../../ui/React/DialogBox";
import { Modal } from "../../../ui/React/Modal";
import { formatMoney, formatShares } from "../../../ui/formatNumber";
import { Money } from "../../../ui/React/Money";
import { formatShares } from "../../../ui/formatNumber";
import { useCorporation } from "../Context";
import Typography from "@mui/material/Typography";
import { ButtonWithTooltip } from "../../../ui/Components/ButtonWithTooltip";
@ -9,6 +10,7 @@ import { NumberInput } from "../../../ui/React/NumberInput";
import Box from "@mui/material/Box";
import { KEY } from "../../../utils/helpers/keyCodes";
import { isPositiveInteger } from "../../../types";
import { GoPublic } from "../../Actions";
interface IProps {
open: boolean;
@ -20,26 +22,9 @@ interface IProps {
export function GoPublicModal(props: IProps): React.ReactElement {
const corp = useCorporation();
const [shares, setShares] = useState<number>(NaN);
const initialSharePrice = corp.valuation / corp.totalShares;
function goPublic(): void {
const initialSharePrice = corp.valuation / corp.totalShares;
if (shares >= corp.numShares || (shares !== 0 && !isPositiveInteger(shares))) return;
corp.public = true;
corp.sharePrice = initialSharePrice;
corp.issuedShares = shares;
corp.numShares -= shares;
corp.addFunds(shares * initialSharePrice);
props.rerender();
dialogBoxCreate(
`You took your ${corp.name} public and earned ` + `${formatMoney(shares * initialSharePrice)} in your IPO`,
);
props.onClose();
}
function onKeyDown(event: React.KeyboardEvent<HTMLInputElement>): void {
if (event.key === KEY.ENTER) goPublic();
}
const ceoOwnership = (corp.numShares - (shares || 0)) / corp.totalShares;
const initialSharePrice = corp.getTargetSharePrice(ceoOwnership);
const disabledText =
shares >= corp.numShares
@ -48,22 +33,62 @@ export function GoPublicModal(props: IProps): React.ReactElement {
? "Must issue an non-negative integer number of shares"
: "";
function goPublic(): void {
if (disabledText) return;
try {
GoPublic(corp, shares);
dialogBoxCreate(
<Typography>
<b>{corp.name}</b> went public and earned <Money money={shares * initialSharePrice} /> in its IPO.
</Typography>,
);
props.onClose();
props.rerender();
setShares(NaN);
} catch (err) {
dialogBoxCreate(`${err}`);
}
}
function onKeyDown(event: React.KeyboardEvent<HTMLInputElement>): void {
if (event.key === KEY.ENTER) goPublic();
}
return (
<Modal open={props.open} onClose={props.onClose}>
<Typography>
Enter the number of shares you would like to issue for your IPO. These shares will be publicly sold and you will
no longer own them. Your Corporation will receive {formatMoney(initialSharePrice)} per share (the IPO money will
be deposited directly into your Corporation's funds).
<br />
<br />
<Typography component="div">
Enter the number of shares you would like to issue for your IPO.
<ul>
<li>These shares will be publicly sold and you will no longer own them.</li>
<li>The IPO money will be deposited directly into your Corporation's funds.</li>
</ul>
You can issue some, but not all, of your {formatShares(corp.numShares)} shares.
</Typography>
<br />
<Box display="flex" alignItems="center">
<NumberInput onChange={setShares} autoFocus placeholder="Shares to issue" onKeyDown={onKeyDown} />
<NumberInput
defaultValue={shares || ""}
onChange={setShares}
autoFocus
placeholder="Shares to issue"
onKeyDown={onKeyDown}
/>
<ButtonWithTooltip disabledTooltip={disabledText} onClick={goPublic}>
Go Public
</ButtonWithTooltip>
</Box>
<br />
<Typography sx={{ minHeight: "3em" }}>
{isNaN(shares) ? null : disabledText ? (
disabledText
) : (
<>
Go public at <Money money={initialSharePrice} /> per share?
<br />
<b>{corp.name}</b> will receive <Money money={initialSharePrice * (shares || 0)} />.
</>
)}
</Typography>
</Modal>
);
}

@ -1,6 +1,8 @@
import React, { useState } from "react";
import { dialogBoxCreate } from "../../../ui/React/DialogBox";
import { Modal } from "../../../ui/React/Modal";
import { Money } from "../../../ui/React/Money";
import { MoneyRate } from "../../../ui/React/MoneyRate";
import * as corpConstants from "../../data/Constants";
import { IssueDividends } from "../../Actions";
import { useCorporation } from "../Context";
@ -53,20 +55,19 @@ export function IssueDividendsModal(props: IProps): React.ReactElement {
yourself, as well.
<br />
<br />
In order to issue dividends, simply allocate some percentage of your corporation's profits to dividends. This
percentage must be an integer between 0 and 100. (A percentage of 0 means no dividends will be issued)
Note that issuing dividends will negatively affect <b>{corp.name}</b>'s stock price.
<br />
<br />
Two important things to note:
<br />
* Issuing dividends will negatively affect your corporation's stock price
In order to issue dividends, simply allocate some percentage of your Corporation's profits to dividends. This
percentage must be an integer between 0 and 100. (A percentage of 0 means no dividends will be issued.)
<br />
<br />
Example: Assume your corporation makes $100m / sec in profit and you allocate 40% of that towards dividends.
That means your corporation will gain $60m / sec in funds and the remaining $40m / sec will be paid as
dividends. Since your corporation starts with 1 billion shares, every shareholder will be paid $0.04 per share
per second before taxes.
<b>Example:</b> Assume your corporation makes <MoneyRate money={100e6} /> in profit and you allocate 40% of that
towards dividends. That means your corporation will gain <MoneyRate money={60e6} /> in funds and the remaining{" "}
<MoneyRate money={40e6} /> will be paid as dividends. Since your corporation starts with 1 billion shares, every
shareholder will be paid <Money money={0.04} /> per share per second before taxes.
</Typography>
<br />
<TextField
autoFocus
value={percent}

@ -1,50 +1,21 @@
import React, { useState } from "react";
import { formatMoney, formatShares } from "../../../ui/formatNumber";
import { formatShares, formatPercent } from "../../../ui/formatNumber";
import { dialogBoxCreate } from "../../../ui/React/DialogBox";
import { Modal } from "../../../ui/React/Modal";
import { Money } from "../../../ui/React/Money";
import { useCorporation } from "../Context";
import Typography from "@mui/material/Typography";
import { NumberInput } from "../../../ui/React/NumberInput";
import Button from "@mui/material/Button";
import { ButtonWithTooltip } from "../../../ui/Components/ButtonWithTooltip";
import { KEY } from "../../../utils/helpers/keyCodes";
import { IssueNewShares } from "../../Actions";
interface IEffectTextProps {
shares: number | null;
}
function EffectText(props: IEffectTextProps): React.ReactElement {
const corp = useCorporation();
if (props.shares === null) return <></>;
const newSharePrice = Math.round(corp.sharePrice * 0.9);
const maxNewShares = corp.calculateMaxNewShares();
let newShares = props.shares;
if (isNaN(newShares)) {
return <Typography>Invalid input</Typography>;
}
// Round to nearest ten-millionth
newShares /= 10e6;
newShares = Math.round(newShares) * 10e6;
if (newShares < 10e6) {
return <Typography>Must issue at least 10 million new shares</Typography>;
}
if (newShares > maxNewShares) {
return <Typography>You cannot issue that many shares</Typography>;
}
return (
<Typography>
Issue {formatShares(newShares)} new shares for {formatMoney(newShares * newSharePrice)}?
</Typography>
);
}
import * as corpConstants from "../../data/Constants";
import { issueNewSharesFailureReason } from "../../helpers";
interface IProps {
open: boolean;
onClose: () => void;
rerender: () => void;
}
// Create a popup that lets the player issue new shares
@ -52,56 +23,103 @@ interface IProps {
export function IssueNewSharesModal(props: IProps): React.ReactElement {
const corp = useCorporation();
const [shares, setShares] = useState<number>(NaN);
const maxNewShares = corp.calculateMaxNewShares();
const maxNewShares = corp.calculateMaxNewShares();
const newShares = Math.round((shares || 0) / 10e6) * 10e6;
const disabled = isNaN(shares) || isNaN(newShares) || newShares < 10e6 || newShares > maxNewShares;
const ceoOwnership = corp.numShares / (corp.totalShares + (newShares || 0));
const newSharePrice = corp.getTargetSharePrice(ceoOwnership);
const profit = ((shares || 0) * (corp.sharePrice + newSharePrice)) / 2;
const privateOwnedRatio = corp.investorShares / corp.totalShares;
const maxPrivateShares = Math.round(((newShares / 2) * privateOwnedRatio) / 10e6) * 10e6;
const disabledText = issueNewSharesFailureReason(corp, shares);
function issueNewShares(): void {
if (isNaN(shares)) return;
if (disabled) return;
const [profit, newShares, privateShares] = IssueNewShares(corp, shares);
props.onClose();
let dialogContents =
`Issued ${formatShares(newShares)} new shares` + ` and raised ${formatMoney(profit)}.` + (privateShares > 0)
? "\n" + formatShares(privateShares) + " of these shares were bought by private investors."
: "";
dialogContents += `\n\nStock price decreased to ${formatMoney(corp.sharePrice)}`;
dialogBoxCreate(dialogContents);
if (disabledText) return;
try {
const [profit, newShares, privateShares] = IssueNewShares(corp, shares);
dialogBoxCreate(
<>
<Typography>
Issued {formatShares(newShares)} new shares and raised <Money money={profit} />.
</Typography>
{privateShares > 0 ? (
<Typography>{formatShares(privateShares)} of these shares were bought by private investors.</Typography>
) : null}
<Typography>
<b>{corp.name}</b>'s stock price fell to <Money money={corp.sharePrice} />.
</Typography>
</>,
);
props.onClose();
props.rerender();
} catch (err) {
dialogBoxCreate(`${err}`);
}
}
function onKeyDown(event: React.KeyboardEvent<HTMLInputElement>): void {
if (event.key === KEY.ENTER) issueNewShares();
}
const nextCooldown = corpConstants.issueNewSharesCooldown * (corp.totalShares / corpConstants.initialShares);
return (
<Modal open={props.open} onClose={props.onClose}>
<Typography>
You can issue new equity shares (i.e. stocks) in order to raise capital for your corporation.
<Typography component="div">
You can issue new equity shares (i.e. stocks) in order to raise capital.
<ul>
<li>Issuing new shares will cause dilution, lowering stock price and reducing dividends per share.</li>
<li>New shares are sold between the current price and the updated price.</li>
<li>The money from issuing new shares will be deposited directly into your Corporation's funds.</li>
<li>
Private shareholders have first priority for buying new shares, up to half of their existing stake in the
company <b>({formatPercent(privateOwnedRatio / 2, 1)})</b>.
<br />
If they choose to exercise this option, these newly issued shares become private, restricted shares, which
means you cannot buy them back.
</li>
<li>
You will not be able to issue new shares again for <b>{corp.convertCooldownToString(nextCooldown)}</b>.
</li>
</ul>
You can issue at most {formatShares(maxNewShares)} new shares.
<br />
<br />
&nbsp;* You can issue at most {formatShares(maxNewShares)} new shares
<br />
&nbsp;* New shares are sold at a 10% discount
<br />
&nbsp;* You can only issue new shares once every 12 hours
<br />
&nbsp;* Issuing new shares causes dilution, resulting in a decrease in stock price and lower dividends per share
<br />
&nbsp;* Number of new shares issued must be a multiple of 10 million
<br />
<br />
When you choose to issue new equity, private shareholders have first priority for up to 0.5n% of the new shares,
where n is the percentage of the company currently owned by private shareholders. If they choose to exercise
this option, these newly issued shares become private, restricted shares, which means you cannot buy them back.
The number of new shares issued must be a multiple of 10 million.
</Typography>
<EffectText shares={shares} />
<NumberInput autoFocus placeholder="# New Shares" onChange={setShares} onKeyDown={onKeyDown} />
<Button disabled={disabled} onClick={issueNewShares} sx={{ mx: 1 }}>
<br />
<NumberInput
defaultValue={shares || ""}
autoFocus
placeholder="# New Shares"
onChange={setShares}
onKeyDown={onKeyDown}
/>
<ButtonWithTooltip disabledTooltip={disabledText} onClick={issueNewShares}>
Issue New Shares
</Button>
</ButtonWithTooltip>
<br />
<Typography sx={{ minHeight: "6em" }}>
{disabledText ? (
disabledText
) : (
<>
Issue {formatShares(newShares)} new shares?
<br />
{maxPrivateShares > 0
? `Private investors may buy up to ${formatShares(
maxPrivateShares,
)} of these shares and keep them off the market.`
: null}
<br />
<b>{corp.name}</b> will receive <Money money={profit} />.
<br />
<b>{corp.name}</b>'s stock price will fall to <Money money={newSharePrice} /> per share.
</>
)}
</Typography>
</Modal>
);
}

@ -3,9 +3,10 @@ import { dialogBoxCreate } from "../../../ui/React/DialogBox";
import { MaterialInfo } from "../../MaterialInfo";
import { Warehouse } from "../../Warehouse";
import { Material } from "../../Material";
import { formatMatPurchaseAmount, formatMoney } from "../../../ui/formatNumber";
import { formatMatPurchaseAmount } from "../../../ui/formatNumber";
import { BulkPurchase, BuyMaterial } from "../../Actions";
import { Modal } from "../../../ui/React/Modal";
import { Money } from "../../../ui/React/Money";
import { useCorporation, useDivision } from "../Context";
import Typography from "@mui/material/Typography";
import TextField from "@mui/material/TextField";
@ -56,7 +57,7 @@ function BulkPurchaseSection(props: IBPProps): React.ReactElement {
return (
<>
<Typography>
Purchasing {formatMatPurchaseAmount(parsedAmt)} of {props.mat.name} will cost {formatMoney(cost)}
Purchasing {formatMatPurchaseAmount(parsedAmt)} of {props.mat.name} will cost <Money money={cost} />
</Typography>
</>
);

@ -57,6 +57,7 @@ export function SellCorporationModal(props: IProps): React.ReactElement {
<br />
If you would like to start new one, please enter a name for your corporation below:
</Typography>
<br />
<TextField autoFocus={true} placeholder="Corporation Name" onChange={onChange} value={name} />
{Player.bitNodeN === 3 && (
<Button onClick={seed} disabled={name == ""}>

@ -1,6 +1,7 @@
import React, { useState } from "react";
import { Modal } from "../../../ui/React/Modal";
import { Money } from "../../../ui/React/Money";
import Typography from "@mui/material/Typography";
import Button from "@mui/material/Button";
import MenuItem from "@mui/material/MenuItem";
@ -8,7 +9,6 @@ import Select, { SelectChangeEvent } from "@mui/material/Select";
import { useCorporation } from "../../ui/Context";
import { CityName } from "@enums";
import * as corpConstants from "../../data/Constants";
import { formatMoney } from "../../../ui/formatNumber";
import { removeDivision as removeDivision } from "../../Actions";
import { dialogBoxCreate } from "../../../ui/React/DialogBox";
import { getRecordKeys } from "../../../Types/Record";
@ -47,9 +47,10 @@ export function SellDivisionModal(props: IProps): React.ReactElement {
corp.funds += price;
props.onClose();
dialogBoxCreate(
`Sold ${divisionToSell.name} for ${formatMoney(price)}, you now have space for ${
corp.maxDivisions - corp.divisions.size
} more divisions.`,
<Typography>
Sold <b>{divisionToSell.name}</b> for <Money money={price} />, you now have space for
{corp.maxDivisions - corp.divisions.size} more divisions.
</Typography>,
);
}
@ -70,13 +71,15 @@ export function SellDivisionModal(props: IProps): React.ReactElement {
</Select>
<Typography>Division {divisionToSell.name} has:</Typography>
<Typography>
Profit: {formatMoney((divisionToSell.lastCycleRevenue - divisionToSell.lastCycleExpenses) / 10)} / sec{" "}
Profit: <Money money={(divisionToSell.lastCycleRevenue - divisionToSell.lastCycleExpenses) / 10} /> / sec{" "}
</Typography>
<Typography>Cities:{getRecordKeys(divisionToSell.offices).length}</Typography>
<Typography>Warehouses:{getRecordKeys(divisionToSell.warehouses).length}</Typography>
{divisionToSell.makesProducts ?? <Typography>Products: {divisionToSell.products.size}</Typography>}
<br />
<Typography>Sell price: {formatMoney(price)}</Typography>
<Typography>
Sell price: <Money money={price} />
</Typography>
<Button onClick={sellDivision}>Sell division</Button>
</>
</Modal>

@ -1,16 +1,17 @@
import React, { useState } from "react";
import { formatMoney } from "../../../ui/formatNumber";
import { dialogBoxCreate } from "../../../ui/React/DialogBox";
import { formatShares } from "../../../ui/formatNumber";
import { Modal } from "../../../ui/React/Modal";
import { useCorporation } from "../Context";
import { Corporation } from "../../Corporation";
import Typography from "@mui/material/Typography";
import Button from "@mui/material/Button";
import { Money } from "../../../ui/React/Money";
import { useCorporation } from "../Context";
import * as corpConstants from "../../data/Constants";
import Typography from "@mui/material/Typography";
import { ButtonWithTooltip } from "../../../ui/Components/ButtonWithTooltip";
import { SellShares } from "../../Actions";
import { KEY } from "../../../utils/helpers/keyCodes";
import { NumberInput } from "../../../ui/React/NumberInput";
import { isInteger } from "lodash";
import { sellSharesFailureReason } from "../../helpers";
interface IProps {
open: boolean;
onClose: () => void;
@ -23,48 +24,28 @@ export function SellSharesModal(props: IProps): React.ReactElement {
const corp = useCorporation();
const [shares, setShares] = useState<number>(NaN);
const disabled = isNaN(shares) || shares <= 0 || shares >= corp.numShares;
function ProfitIndicator(props: { shares: number | null; corp: Corporation }): React.ReactElement {
if (props.shares === null) return <></>;
let text = "";
if (isNaN(props.shares) || props.shares <= 0 || !isInteger(props.shares)) {
text = `ERROR: Invalid value entered for number of shares to sell`;
} else if (props.shares > corp.numShares) {
text = `You don't have this many shares to sell!`;
} else if (props.shares === corp.numShares) {
text = `You can not sell all your shares!`;
} else if (props.shares > 1e14) {
text = `You can't sell more than 100t shares at once!`;
} else {
const stockSaleResults = corp.calculateShareSale(props.shares);
const profit = stockSaleResults[0];
text = `Sell ${props.shares} shares for a total of ${formatMoney(profit)}`;
}
return (
<Typography>
<small>{text}</small>
</Typography>
);
}
const [profit, sharePrice] = corp.calculateShareSale((props.open && shares) || 0);
const disabledText = sellSharesFailureReason(corp, shares);
function sell(): void {
if (disabled) return;
if (disabledText) return;
try {
const profit = SellShares(corp, shares);
props.onClose();
SellShares(corp, shares);
dialogBoxCreate(
<>
Sold {formatMoney(shares)} shares for
<Money money={profit} />. The corporation's stock price fell to&nbsp; <Money money={corp.sharePrice} />
as a result of dilution.
<Typography>
You sold {formatShares(shares)} shares for <Money money={profit} />.
</Typography>
<Typography>
<b>{corp.name}</b>'s stock price fell to <Money money={sharePrice} /> per share.
</Typography>
</>,
);
props.onClose();
props.rerender();
setShares(NaN);
} catch (err) {
dialogBoxCreate(err + "");
dialogBoxCreate(`${err as Error}`);
}
}
@ -74,32 +55,43 @@ export function SellSharesModal(props: IProps): React.ReactElement {
return (
<Modal open={props.open} onClose={props.onClose}>
<Typography>
Enter the number of shares you would like to sell. The money from selling your shares will go directly to you
(NOT your Corporation).
<br />
<br />
The amount sold must be an integer between 1 and 100t.
<br />
<br />
Selling your shares will cause your corporation's stock price to fall due to dilution. Furthermore, selling a
large number of shares all at once will have an immediate effect in reducing your stock price.
<br />
<br />
The current price of your company's stock is {formatMoney(corp.sharePrice)}
<Typography component="div">
Enter the number of shares you would like to sell.
<ul>
<li>Selling shares will cause stock price to fall due to market forces.</li>
<li>The money from selling your shares will go directly to you (NOT your Corporation).</li>
<li>
You will not be able to sell shares again (or dissolve the corporation) for{" "}
<b>{corp.convertCooldownToString(corpConstants.sellSharesCooldown)}</b>.
</li>
</ul>
You currently have {formatShares(corp.numShares)} shares of <b>{corp.name}</b> stock, valued at{" "}
<Money money={corp.sharePrice} /> per share.
</Typography>
<br />
<NumberInput
defaultValue={shares || ""}
variant="standard"
autoFocus
placeholder="Shares to sell"
onChange={setShares}
onKeyDown={onKeyDown}
/>
<Button disabled={disabled} onClick={sell} sx={{ mx: 1 }}>
<ButtonWithTooltip disabledTooltip={disabledText} onClick={sell}>
Sell shares
</Button>
<ProfitIndicator shares={shares} corp={corp} />
</ButtonWithTooltip>
<br />
<Typography sx={{ minHeight: "3em" }}>
{!shares ? null : disabledText ? (
disabledText
) : (
<>
You will receive <Money money={profit} />.
<br />
<b>{corp.name}</b>'s stock price will fall to <Money money={sharePrice} /> per share.
</>
)}
</Typography>
</Modal>
);
}

@ -95,7 +95,7 @@ export function SmartSupplyModal(props: IProps): React.ReactElement {
label={<Typography>Enable Smart Supply</Typography>}
/>
<br />
<Typography>
<Typography component="div">
Options:
<ul>
<li>

@ -3,7 +3,7 @@ import { formatMultiplier, formatPercent } from "../../../ui/formatNumber";
import { dialogBoxCreate } from "../../../ui/React/DialogBox";
import { OfficeSpace } from "../../OfficeSpace";
import { ThrowParty } from "../../Actions";
import { Money } from "../../../ui/React/Money";
import { MoneyCost } from "../MoneyCost";
import { Modal } from "../../../ui/React/Modal";
import { useCorporation } from "../Context";
import Typography from "@mui/material/Typography";
@ -59,7 +59,7 @@ export function ThrowPartyModal(props: IProps): React.ReactElement {
if (isNaN(cost) || cost < 0) return <Typography>Invalid value entered!</Typography>;
return (
<Typography>
Throwing this party will cost a total of <Money money={totalCost} />
Throwing this party will cost a total of <MoneyCost money={totalCost} corp={corp} />
</Typography>
);
}

@ -69,6 +69,12 @@ export function CorporationDev(): React.ReactElement {
});
}
function resetCorporationCooldowns(): void {
if (!Player.corporation) return;
Player.corporation.shareSaleCooldown = 0;
Player.corporation.issueNewSharesCooldown = 0;
}
return (
<Accordion TransitionProps={{ unmountOnExit: true }}>
<AccordionSummary expandIcon={<ExpandMoreIcon />}>
@ -117,6 +123,11 @@ export function CorporationDev(): React.ReactElement {
<Button onClick={addCorporationResearch}>Tons of research</Button>
</td>
</tr>
<tr>
<td>
<Button onClick={resetCorporationCooldowns}>Reset stock cooldowns</Button>
</td>
</tr>
</tbody>
</table>
</AccordionDetails>

@ -130,7 +130,7 @@ const FactionElement = (props: FactionElementProps): React.ReactElement => {
{!props.joined && facInfo.enemies.length > 0 && (
<Tooltip
title={
<Typography>
<Typography component="div">
This Faction is enemies with:
<ul>
{facInfo.enemies.map((enemy) => (

@ -13,7 +13,6 @@ import {
Division as NSDivision,
WarehouseAPI,
OfficeAPI,
InvestmentOffer,
CorpResearchName,
CorpMaterialName,
} from "@nsdefs";
@ -22,7 +21,9 @@ import {
NewDivision,
purchaseOffice,
IssueDividends,
GoPublic,
IssueNewShares,
AcceptInvestmentOffer,
SellMaterial,
SellProduct,
SetSmartSupply,
@ -104,63 +105,6 @@ export function NetscriptCorporation(): InternalAPI<NSCorporation> {
return cost;
}
function getInvestmentOffer(): InvestmentOffer {
const corporation = getCorporation();
if (
corporation.fundingRound >= corpConstants.fundingRoundShares.length ||
corporation.fundingRound >= corpConstants.fundingRoundMultiplier.length ||
corporation.public
)
return {
funds: 0,
shares: 0,
round: corporation.fundingRound + 1, // Make more readable
}; // Don't throw an error here, no reason to have a second function to check if you can get investment.
const val = corporation.valuation;
const percShares = corpConstants.fundingRoundShares[corporation.fundingRound];
const roundMultiplier = corpConstants.fundingRoundMultiplier[corporation.fundingRound];
const funding = val * percShares * roundMultiplier;
const investShares = Math.floor(corpConstants.initialShares * percShares);
return {
funds: funding,
shares: investShares,
round: corporation.fundingRound + 1, // Make more readable
};
}
function acceptInvestmentOffer(): boolean {
const corporation = getCorporation();
if (
corporation.fundingRound >= corpConstants.fundingRoundShares.length ||
corporation.fundingRound >= corpConstants.fundingRoundMultiplier.length ||
corporation.public
)
return false;
const val = corporation.valuation;
const percShares = corpConstants.fundingRoundShares[corporation.fundingRound];
const roundMultiplier = corpConstants.fundingRoundMultiplier[corporation.fundingRound];
const funding = val * percShares * roundMultiplier;
const investShares = Math.floor(corpConstants.initialShares * percShares);
corporation.fundingRound++;
corporation.addNonIncomeFunds(funding);
corporation.numShares -= investShares;
return true;
}
function goPublic(numShares: number): boolean {
const corporation = getCorporation();
const initialSharePrice = corporation.valuation / corporation.totalShares;
if (isNaN(numShares)) throw new Error("Invalid value for number of issued shares");
if (numShares < 0) throw new Error("Invalid value for number of issued shares");
if (numShares > corporation.numShares) throw new Error("You don't have that many shares to issue!");
corporation.public = true;
corporation.sharePrice = initialSharePrice;
corporation.issuedShares = numShares;
corporation.numShares -= numShares;
corporation.addNonIncomeFunds(numShares * initialSharePrice);
return true;
}
function getResearchCost(division: Division, researchName: CorpResearchName): number {
const researchTree = IndustryResearchTrees[division.type];
if (researchTree === undefined) throw new Error(`No research tree for industry '${division.type}'`);
@ -741,12 +685,6 @@ export function NetscriptCorporation(): InternalAPI<NSCorporation> {
const maxNewShares = corporation.calculateMaxNewShares();
if (_amount == undefined) _amount = maxNewShares;
const amount = helpers.number(ctx, "amount", _amount);
if (corporation.issueNewSharesCooldown > 0) throw new Error(`Can't issue new shares, action on cooldown.`);
if (amount < 10e6 || amount > maxNewShares)
throw new Error(
`Invalid value for amount field! Must be numeric, greater than 10m, and less than ${maxNewShares} (20% of total shares)`,
);
if (!corporation.public) throw helpers.makeRuntimeErrorMsg(ctx, `Your company has not gone public!`);
const [funds] = IssueNewShares(corporation, amount);
return funds;
},
@ -768,6 +706,7 @@ export function NetscriptCorporation(): InternalAPI<NSCorporation> {
totalShares: corporation.totalShares,
numShares: corporation.numShares,
shareSaleCooldown: corporation.shareSaleCooldown,
investorShares: corporation.investorShares,
issuedShares: corporation.issuedShares,
issueNewSharesCooldown: corporation.issueNewSharesCooldown,
sharePrice: corporation.sharePrice,
@ -807,18 +746,26 @@ export function NetscriptCorporation(): InternalAPI<NSCorporation> {
},
getInvestmentOffer: (ctx) => () => {
checkAccess(ctx);
return getInvestmentOffer();
const corporation = getCorporation();
return corporation.getInvestmentOffer();
},
acceptInvestmentOffer: (ctx) => () => {
checkAccess(ctx);
return acceptInvestmentOffer();
const corporation = getCorporation();
try {
AcceptInvestmentOffer(corporation);
return true;
} catch (err) {
return false;
}
},
goPublic: (ctx) => (_numShares) => {
checkAccess(ctx);
const corporation = getCorporation();
if (corporation.public) throw helpers.makeRuntimeErrorMsg(ctx, "corporation is already public");
const numShares = helpers.number(ctx, "numShares", _numShares);
return goPublic(numShares);
GoPublic(corporation, numShares);
return true;
},
sellShares: (ctx) => (_numShares) => {
checkAccess(ctx);

@ -21,5 +21,8 @@ export function startCorporation(this: PlayerObject, corpName: string, seedFunde
this.corporation.unlocks.add(CorpUnlockName.OfficeAPI);
}
this.corporation.totalShares += seedFunded ? 500_000_000 : 0;
if (seedFunded) {
this.corporation.investorShares += 500e6;
this.corporation.totalShares += 500e6;
}
}

@ -7437,11 +7437,13 @@ export interface Corporation extends WarehouseAPI, OfficeAPI {
* @returns Amount of funds generated for the corporation. */
issueNewShares(amount?: number): number;
/** Buyback Shares
/** Buyback Shares.
* Spend money from the player's wallet to transfer shares from public traders to the CEO.
* @param amount - Amount of shares to buy back, must be integer and larger than 0 */
buyBackShares(amount: number): void;
/** Sell Shares
/** Sell Shares.
* Transfer shares from the CEO to public traders to receive money in the player's wallet.
* @param amount - Amount of shares to sell, must be integer between 1 and 100t */
sellShares(amount: number): void;
@ -7515,13 +7517,15 @@ interface CorporationInfo {
expenses: number;
/** Indicating if the company is public */
public: boolean;
/** Total number of shares issues by this corporation */
/** Total number of shares issued by this corporation. */
totalShares: number;
/** Amount of share owned */
/** Amount of shares owned by the CEO. */
numShares: number;
/** Cooldown until shares can be sold again */
shareSaleCooldown: number;
/** Amount of acquirable shares. */
/** Amount of shares owned by private investors. Not available for public sale or CEO buyback. */
investorShares: number;
/** Amount of shares owned by public traders. Available for CEO buyback. */
issuedShares: number;
/** Cooldown until new shares can be issued */
issueNewSharesCooldown: number;

@ -2,11 +2,22 @@ import { PositiveInteger } from "../../src/types";
import { Corporation } from "../../src/Corporation/Corporation";
import { CorpUpgrades } from "../../src/Corporation/data/CorporationUpgrades";
import { calculateMaxAffordableUpgrade, calculateUpgradeCost } from "../../src/Corporation/helpers";
import { Player, setPlayer } from "../../src/Player";
import { PlayerObject } from "../../src/PersonObjects/Player/PlayerObject";
import {
AcceptInvestmentOffer,
BuyBackShares,
GoPublic,
IssueNewShares,
SellShares,
} from "../../src/Corporation/Actions";
describe("Corporation", () => {
let corporation: Corporation;
beforeEach(() => {
setPlayer(new PlayerObject());
Player.init();
corporation = new Corporation({ name: "Test" });
});
@ -57,4 +68,48 @@ describe("Corporation", () => {
}
});
});
describe("Corporation totalShares", () => {
function expectSharesToAddUp(corp: Corporation) {
expect(corp.totalShares).toEqual(corp.numShares + corp.investorShares + corp.issuedShares);
}
it("should equal the sum of each kind of shares", () => {
expectSharesToAddUp(corporation);
});
it("should be preserved by seed funding", () => {
const seedFunded = true;
Player.startCorporation("TestCorp", seedFunded);
expectSharesToAddUp(Player.corporation!);
});
it("should be preserved by acceptInvestmentOffer", () => {
AcceptInvestmentOffer(corporation);
expectSharesToAddUp(corporation);
});
it("should be preserved by goPublic", () => {
const numShares = 1e8;
GoPublic(corporation, numShares);
expectSharesToAddUp(corporation);
});
it("should be preserved by IssueNewShares", () => {
const numShares = 1e8;
GoPublic(corporation, numShares);
corporation.issueNewSharesCooldown = 0;
IssueNewShares(corporation, numShares);
expectSharesToAddUp(corporation);
});
it("should be preserved by BuyBackShares", () => {
const numShares = 1e8;
GoPublic(corporation, numShares);
BuyBackShares(corporation, numShares);
expectSharesToAddUp(corporation);
});
it("should be preserved by SellShares", () => {
const numShares = 1e8;
GoPublic(corporation, numShares);
corporation.shareSaleCooldown = 0;
SellShares(corporation, numShares);
expectSharesToAddUp(corporation);
});
});
});

@ -1150,6 +1150,7 @@ exports[`Check Save File Continuity PlayerSave continuity 1`] = `
"expenses": 0,
"fundingRound": 0,
"funds": 150000000000,
"investorShares": 0,
"issueNewSharesCooldown": 0,
"issuedShares": 0,
"maxDivisions": 20,