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 ## Corporation.buyBackShares() method
Buyback Shares Buyback Shares. Spend money from the player's wallet to transfer shares from public traders to the CEO.
**Signature:** **Signature:**

@ -19,7 +19,7 @@ export interface Corporation extends WarehouseAPI, OfficeAPI
| --- | --- | | --- | --- |
| [acceptInvestmentOffer()](./bitburner.corporation.acceptinvestmentoffer.md) | Accept investment based on you companies current valuation | | [acceptInvestmentOffer()](./bitburner.corporation.acceptinvestmentoffer.md) | Accept investment based on you companies current valuation |
| [bribe(factionName, amountCash)](./bitburner.corporation.bribe.md) | Bribe a faction | | [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 | | [createCorporation(corporationName, selfFund)](./bitburner.corporation.createcorporation.md) | Create a Corporation |
| [expandCity(divisionName, city)](./bitburner.corporation.expandcity.md) | Expand to a new city | | [expandCity(divisionName, city)](./bitburner.corporation.expandcity.md) | Expand to a new city |
| [expandIndustry(industryType, divisionName)](./bitburner.corporation.expandindustry.md) | Expand to a new industry | | [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 | | [issueNewShares(amount)](./bitburner.corporation.issuenewshares.md) | Issue new shares |
| [levelUpgrade(upgradeName)](./bitburner.corporation.levelupgrade.md) | Level an upgrade. | | [levelUpgrade(upgradeName)](./bitburner.corporation.levelupgrade.md) | Level an upgrade. |
| [purchaseUnlock(upgradeName)](./bitburner.corporation.purchaseunlock.md) | Unlock 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 ## Corporation.sellShares() method
Sell Shares Sell Shares. Transfer shares from the CEO to public traders to receive money in the player's wallet.
**Signature:** **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 ## CorporationInfo.issuedShares property
Amount of acquirable shares. Amount of shares owned by public traders. Available for CEO buyback.
**Signature:** **Signature:**

@ -22,14 +22,15 @@ interface CorporationInfo
| [divisions](./bitburner.corporationinfo.divisions.md) | | string\[\] | Array of all division names | | [divisions](./bitburner.corporationinfo.divisions.md) | | string\[\] | Array of all division names |
| [expenses](./bitburner.corporationinfo.expenses.md) | | number | Expenses per second this cycle | | [expenses](./bitburner.corporationinfo.expenses.md) | | number | Expenses per second this cycle |
| [funds](./bitburner.corporationinfo.funds.md) | | number | Funds available | | [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 | | [issueNewSharesCooldown](./bitburner.corporationinfo.issuenewsharescooldown.md) | | number | Cooldown until new shares can be issued |
| [name](./bitburner.corporationinfo.name.md) | | string | Name of the corporation | | [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 | | [public](./bitburner.corporationinfo.public.md) | | boolean | Indicating if the company is public |
| [revenue](./bitburner.corporationinfo.revenue.md) | | number | Revenue per second this cycle | | [revenue](./bitburner.corporationinfo.revenue.md) | | number | Revenue per second this cycle |
| [sharePrice](./bitburner.corporationinfo.shareprice.md) | | number | Price of the shares | | [sharePrice](./bitburner.corporationinfo.shareprice.md) | | number | Price of the shares |
| [shareSaleCooldown](./bitburner.corporationinfo.sharesalecooldown.md) | | number | Cooldown until shares can be sold again | | [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> | | [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 ## CorporationInfo.numShares property
Amount of share owned Amount of shares owned by the CEO.
**Signature:** **Signature:**

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

@ -1,5 +1,3 @@
import { isInteger } from "lodash";
import { Player } from "@player"; import { Player } from "@player";
import { CorpResearchName, CorpSmartSupplyOption } from "@nsdefs"; import { CorpResearchName, CorpSmartSupplyOption } from "@nsdefs";
@ -18,6 +16,7 @@ import { isRelevantMaterial } from "./ui/Helpers";
import { CityName } from "@enums"; import { CityName } from "@enums";
import { getRandomInt } from "../utils/helpers/getRandomInt"; import { getRandomInt } from "../utils/helpers/getRandomInt";
import { getRecordValues } from "../Types/Record"; import { getRecordValues } from "../Types/Record";
import { sellSharesFailureReason, buybackSharesFailureReason, issueNewSharesFailureReason } from "./helpers";
export function NewDivision(corporation: Corporation, industry: IndustryType, name: string): void { export function NewDivision(corporation: Corporation, industry: IndustryType, name: string): void {
if (corporation.divisions.size >= corporation.maxDivisions) if (corporation.divisions.size >= corporation.maxDivisions)
@ -85,33 +84,72 @@ export function IssueDividends(corporation: Corporation, rate: number): void {
corporation.dividendRate = rate; corporation.dividendRate = rate;
} }
export function IssueNewShares(corporation: Corporation, amount: number): [number, number, number] { export function GoPublic(corporation: Corporation, numShares: number): void {
const max = corporation.calculateMaxNewShares(); const ceoOwnership = (corporation.numShares - numShares) / corporation.totalShares;
const initialSharePrice = corporation.getTargetSharePrice(ceoOwnership);
// Round to nearest ten-millionth if (isNaN(numShares) || numShares < 0) {
amount = Math.round(amount / 10e6) * 10e6; throw new Error("Invalid value for number of issued shares");
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 (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; const ceoOwnership = corporation.numShares / (corporation.totalShares + amount);
corporation.issueNewSharesCooldown = corpConstants.issueNewSharesCooldown; 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 maxPrivateShares = Math.round((amount / 2) * privateOwnedRatio);
const privateShares = Math.round(getRandomInt(0, maxPrivateShares) / 10e6) * 10e6; const privateShares = Math.round(getRandomInt(0, maxPrivateShares) / 10e6) * 10e6;
corporation.issuedShares += amount - privateShares; corporation.issuedShares += amount - privateShares;
corporation.investorShares += privateShares;
corporation.totalShares += amount; corporation.totalShares += amount;
corporation.addNonIncomeFunds(profit); 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]; 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 { export function SellMaterial(material: Material, amount: string, price: string): void {
if (price === "") price = "0"; if (price === "") price = "0";
if (amount === "") amount = "0"; if (amount === "") amount = "0";
@ -280,17 +318,10 @@ export function BulkPurchase(
} }
export function SellShares(corporation: Corporation, numShares: number): number { export function SellShares(corporation: Corporation, numShares: number): number {
if (isNaN(numShares) || !isInteger(numShares)) throw new Error("Invalid value for number of shares"); const failureReason = sellSharesFailureReason(corporation, numShares);
if (numShares <= 0) throw new Error("Invalid value for number of shares"); if (failureReason) throw new Error(failureReason);
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!"); const [profit, newSharePrice, newSharesUntilUpdate] = corporation.calculateShareSale(numShares);
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];
corporation.numShares -= numShares; corporation.numShares -= numShares;
corporation.issuedShares += numShares; corporation.issuedShares += numShares;
@ -302,15 +333,16 @@ export function SellShares(corporation: Corporation, numShares: number): number
} }
export function BuyBackShares(corporation: Corporation, numShares: number): boolean { export function BuyBackShares(corporation: Corporation, numShares: number): boolean {
if (isNaN(numShares) || !isInteger(numShares)) throw new Error("Invalid value for number of shares"); const failureReason = buybackSharesFailureReason(corporation, numShares);
if (numShares <= 0) throw new Error("Invalid value for number of shares"); if (failureReason) throw new Error(failureReason);
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 [cost, newSharePrice, newSharesUntilUpdate] = corporation.calculateShareBuyback(numShares);
const buybackPrice = corporation.sharePrice * 1.1;
if (Player.money < numShares * buybackPrice) throw new Error("You cant afford that many shares!");
corporation.numShares += numShares; corporation.numShares += numShares;
corporation.issuedShares -= numShares; corporation.issuedShares -= numShares;
Player.loseMoney(numShares * buybackPrice, "corporation"); corporation.sharePrice = newSharePrice;
corporation.shareSalesUntilPriceUpdate = newSharesUntilUpdate;
Player.loseMoney(cost, "corporation");
return true; return true;
} }

@ -12,7 +12,7 @@ import { showLiterature } from "../Literature/LiteratureHelpers";
import { dialogBoxCreate } from "../ui/React/DialogBox"; import { dialogBoxCreate } from "../ui/React/DialogBox";
import { constructorsForReviver, Generic_toJSON, Generic_fromJSON, IReviverValue } from "../utils/JSONReviver"; import { constructorsForReviver, Generic_toJSON, Generic_fromJSON, IReviverValue } from "../utils/JSONReviver";
import { CorpStateName } from "@nsdefs"; import { CorpStateName, InvestmentOffer } from "@nsdefs";
import { calculateUpgradeCost } from "./helpers"; import { calculateUpgradeCost } from "./helpers";
import { JSONMap, JSONSet } from "../Types/Jsonable"; import { JSONMap, JSONSet } from "../Types/Jsonable";
import { formatMoney } from "../ui/formatNumber"; import { formatMoney } from "../ui/formatNumber";
@ -46,6 +46,7 @@ export class Corporation {
issueNewSharesCooldown = 0; // Game cycles until player can issue shares again issueNewSharesCooldown = 0; // Game cycles until player can issue shares again
dividendRate = 0; dividendRate = 0;
dividendTax = 1 - currentNodeMults.CorporationSoftcap + 0.15; dividendTax = 1 - currentNodeMults.CorporationSoftcap + 0.15;
investorShares = 0;
issuedShares = 0; issuedShares = 0;
sharePrice = 0; sharePrice = 0;
storedCycles = 0; storedCycles = 0;
@ -232,10 +233,17 @@ export class Corporation {
this.totalAssets = assets; this.totalAssets = assets;
} }
getTargetSharePrice(): number { getTargetSharePrice(ceoOwnership: number | null = null): number {
// Note: totalShares - numShares is not the same as issuedShares because // Share price is proportional to total corporation valuation.
// issuedShares does not account for private investors // When the CEO owns 0% of the company, market cap is 0.5x valuation.
return this.valuation / (2 * (this.totalShares - this.numShares) + 1); // 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 { updateSharePrice(): void {
@ -250,10 +258,6 @@ export class Corporation {
} }
} }
immediatelyUpdateSharePrice(): void {
this.sharePrice = this.getTargetSharePrice();
}
calculateMaxNewShares(): number { calculateMaxNewShares(): number {
const maxNewSharesUnrounded = Math.round(this.totalShares * 0.2); const maxNewSharesUnrounded = Math.round(this.totalShares * 0.2);
const maxNewShares = maxNewSharesUnrounded - (maxNewSharesUnrounded % 10e6); 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 // 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] // @return - [Player profit, final stock price, end shareSalesUntilPriceUpdate property]
calculateShareSale(numShares: number): [number, number, number] { calculateShareSale(numShares: number): [profit: number, sharePrice: number, sharesUntilUpdate: number] {
let sharesTracker = numShares; let sharesRemaining = numShares;
let sharesUntilUpdate = this.shareSalesUntilPriceUpdate; let sharesUntilUpdate = this.shareSalesUntilPriceUpdate;
let sharePrice = this.sharePrice; let sharePrice = this.sharePrice;
let sharesSold = 0; let sharesSold = 0;
let profit = 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) { if (isNaN(maxIterations) || maxIterations > 10e6) {
console.error( console.error(
`Something went wrong or unexpected when calculating share sale. Max iterations calculated to be ${maxIterations}`, `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) { for (let i = 0; i < maxIterations; ++i) {
if (sharesTracker < sharesUntilUpdate) { if (Math.abs(sharesRemaining) < Math.abs(sharesUntilUpdate)) {
profit += sharePrice * sharesTracker; profit += sharePrice * sharesRemaining;
sharesUntilUpdate -= sharesTracker; sharesUntilUpdate -= sharesRemaining;
break; break;
} else { } else {
profit += sharePrice * sharesUntilUpdate; profit += sharePrice * sharesPerStep;
sharesUntilUpdate = corpConstants.sharesPerPriceUpdate; sharesRemaining -= sharesPerStep;
sharesTracker -= sharesUntilUpdate; sharesSold += sharesPerStep;
sharesSold += sharesUntilUpdate;
targetPrice = this.valuation / (2 * (this.totalShares + sharesSold - this.numShares)); // Update the share price
// Calculate what new share price would be const ceoOwnership = (this.numShares - sharesSold) / this.totalShares;
const targetPrice = this.getTargetSharePrice(ceoOwnership);
if (sharePrice <= targetPrice) { if (sharePrice <= targetPrice) {
sharePrice *= 1 + 0.5 * 0.01; sharePrice *= 1 + 0.5 * 0.01;
} else { } else {
sharePrice *= 1 - 0.5 * 0.01; sharePrice *= 1 - 0.5 * 0.01;
} }
sharesUntilUpdate = corpConstants.sharesPerPriceUpdate;
} }
} }
return [profit, sharePrice, sharesUntilUpdate]; 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 { convertCooldownToString(cd: number): string {
// The cooldown value is based on game cycles. Convert to a simple string // The cooldown value is based on game cycles. Convert to a simple string
const seconds = cd / 5; const seconds = cd / 5;

@ -33,7 +33,7 @@ export const stateNames: CorpStateName[] = ["START", "PURCHASE", "PRODUCTION", "
/** Names of all one-time corporation-wide unlocks */ /** Names of all one-time corporation-wide unlocks */
unlockNames: APIUnlockName[] = Object.values(CorpUnlockName), unlockNames: APIUnlockName[] = Object.values(CorpUnlockName),
upgradeNames: APIUpgradeName[] = Object.values(CorpUpgradeName), 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), researchNamesBase: CorpResearchName[] = Object.values(CorpBaseResearchName),
/** Names of all researches only available to product industries */ /** Names of all researches only available to product industries */
researchNamesProductOnly: CorpResearchName[] = Object.values(CorpProductResearchName), researchNamesProductOnly: CorpResearchName[] = Object.values(CorpProductResearchName),
@ -42,8 +42,8 @@ export const stateNames: CorpStateName[] = ["START", "PURCHASE", "PRODUCTION", "
initialShares = 1e9, initialShares = 1e9,
/** When selling large number of shares, price is dynamically updated for every batch of this amount */ /** When selling large number of shares, price is dynamically updated for every batch of this amount */
sharesPerPriceUpdate = 1e6, sharesPerPriceUpdate = 1e6,
/** Cooldown for issue new shares cooldown in game cycles. 12 hours. */ /** Cooldown for issue new shares cooldown in game cycles. Initially 4 hours. */
issueNewSharesCooldown = 216e3, issueNewSharesCooldown = 72e3,
/** Cooldown for selling shares in game cycles. 1 hour. */ /** Cooldown for selling shares in game cycles. 1 hour. */
sellSharesCooldown = 18e3, sellSharesCooldown = 18e3,
teaCostPerEmployee = 500e3, 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 { Corporation } from "./Corporation";
import { CorpUpgrade } from "./data/CorporationUpgrades"; import { CorpUpgrade } from "./data/CorporationUpgrades";
@ -29,3 +31,43 @@ export function calculateMaxAffordableUpgrade(corp: Corporation, upgrade: CorpUp
const sanitizedValue = maxAffordableUpgrades >= 0 ? maxAffordableUpgrades : 0; const sanitizedValue = maxAffordableUpgrades >= 0 ? maxAffordableUpgrades : 0;
return sanitizedValue as PositiveInteger | 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 { ExportModal } from "./modals/ExportModal";
import { SellMaterialModal } from "./modals/SellMaterialModal"; import { SellMaterialModal } from "./modals/SellMaterialModal";
import { PurchaseMaterialModal } from "./modals/PurchaseMaterialModal"; 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 { isString } from "../../utils/helpers/string";
import { Money } from "../../ui/React/Money"; import { Money } from "../../ui/React/Money";
import { useCorporation, useDivision } from "./Context"; import { useCorporation, useDivision } from "./Context";
@ -118,7 +118,9 @@ export function MaterialElem(props: IMaterialProps): React.ReactElement {
</Typography> </Typography>
} }
> >
<Typography>MP: {formatMoney(mat.marketPrice)}</Typography> <Typography>
MP: <Money money={mat.marketPrice} />
</Typography>
</Tooltip> </Tooltip>
<Tooltip <Tooltip
title={<Typography>The quality of your material. Higher quality will lead to more sales</Typography>} 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={ title={
<StatsTable <StatsTable
rows={[ 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 <ButtonWithTooltip
normalTooltip={ normalTooltip={
<> <>
Get a copy of and read 'The Complete Handbook for Creating a Successful Corporation.' This is a .lit file Get a copy of and read <i>The Complete Handbook for Creating a Successful Corporation</i>. This is a .lit
that guides you through the beginning of setting up a Corporation and provides some tips/pointers for 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. helping you get started with managing it.
</> </>
} }
@ -125,7 +138,7 @@ function PrivateButtons({ rerender }: IPrivateButtonsProps): React.ReactElement
const [findInvestorsopen, setFindInvestorsopen] = useState(false); const [findInvestorsopen, setFindInvestorsopen] = useState(false);
const [goPublicopen, setGoPublicopen] = useState(false); const [goPublicopen, setGoPublicopen] = useState(false);
const fundingAvailable = corp.fundingRound < 4; const fundingAvailable = corp.fundingRound < corpConstants.fundingRoundShares.length;
const findInvestorsTooltip = fundingAvailable const findInvestorsTooltip = fundingAvailable
? "Search for private investors who will give you startup funding in exchange for equity (stock shares) in your company" ? "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 [issueDividendsOpen, setIssueDividendsOpen] = useState(false);
const sellSharesOnCd = corp.shareSaleCooldown > 0; const sellSharesOnCd = corp.shareSaleCooldown > 0;
const sellSharesTooltip = sellSharesOnCd const sellSharesTooltip =
? "Cannot sell shares for " + corp.convertCooldownToString(corp.shareSaleCooldown) "Sell your shares in the company. The money earned from selling your " +
: "Sell your shares in the company. The money earned from selling your " + "shares goes into your personal account, not the Corporation's. " +
"shares goes into your personal account, not the Corporation's. " + "This is one of the only ways to profit from your business venture.";
"This is one of the only ways to profit from your business venture.";
const issueNewSharesOnCd = corp.issueNewSharesCooldown > 0; 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 ( return (
<> <>
<ButtonWithTooltip <ButtonWithTooltip
normalTooltip={sellSharesTooltip} normalTooltip={sellSharesTooltip}
disabledTooltip={sellSharesOnCd ? "On cooldown" : ""} disabledTooltip={
sellSharesOnCd ? "Cannot sell shares for " + corp.convertCooldownToString(corp.shareSaleCooldown) : ""
}
onClick={() => setSellSharesOpen(true)} onClick={() => setSellSharesOpen(true)}
> >
Sell Shares Sell Shares
</ButtonWithTooltip> </ButtonWithTooltip>
<SellSharesModal open={sellSharesOpen} onClose={() => setSellSharesOpen(false)} rerender={rerender} /> <SellSharesModal open={sellSharesOpen} onClose={() => setSellSharesOpen(false)} rerender={rerender} />
<ButtonWithTooltip <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" : ""} disabledTooltip={corp.issuedShares < 1 ? "No shares available to buy back" : ""}
onClick={() => setBuybackSharesOpen(true)} onClick={() => setBuybackSharesOpen(true)}
> >
@ -243,13 +254,15 @@ function PublicButtons({ rerender }: IPublicButtonsProps): React.ReactElement {
</ButtonWithTooltip> </ButtonWithTooltip>
<BuybackSharesModal open={buybackSharesOpen} onClose={() => setBuybackSharesOpen(false)} rerender={rerender} /> <BuybackSharesModal open={buybackSharesOpen} onClose={() => setBuybackSharesOpen(false)} rerender={rerender} />
<ButtonWithTooltip <ButtonWithTooltip
normalTooltip={issueNewSharesTooltip} normalTooltip={"Issue new equity shares to raise capital"}
disabledTooltip={issueNewSharesOnCd ? "On cooldown" : ""} disabledTooltip={
issueNewSharesOnCd ? `On cooldown for ${corp.convertCooldownToString(corp.issueNewSharesCooldown)}` : ""
}
onClick={() => setIssueNewSharesOpen(true)} onClick={() => setIssueNewSharesOpen(true)}
> >
Issue New Shares Issue New Shares
</ButtonWithTooltip> </ButtonWithTooltip>
<IssueNewSharesModal open={issueNewSharesOpen} onClose={() => setIssueNewSharesOpen(false)} /> <IssueNewSharesModal open={issueNewSharesOpen} onClose={() => setIssueNewSharesOpen(false)} rerender={rerender} />
<ButtonWithTooltip <ButtonWithTooltip
normalTooltip={"Manage the dividends that are paid out to shareholders (including yourself)"} normalTooltip={"Manage the dividends that are paid out to shareholders (including yourself)"}
onClick={() => setIssueDividendsOpen(true)} onClick={() => setIssueDividendsOpen(true)}
@ -306,13 +319,22 @@ function SellDivisionButton(): React.ReactElement {
function RestartButton(): React.ReactElement { function RestartButton(): React.ReactElement {
const [open, setOpen] = useState(false); const [open, setOpen] = useState(false);
const corp = useCorporation();
const sellSharesOnCd = corp.shareSaleCooldown > 0;
function restart(): void { function restart(): void {
setOpen(true); setOpen(true);
} }
return ( 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 Sell CEO position
</ButtonWithTooltip> </ButtonWithTooltip>
<SellCorporationModal open={open} onClose={() => setOpen(false)} /> <SellCorporationModal open={open} onClose={() => setOpen(false)} />

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

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

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

@ -1,8 +1,10 @@
import React from "react"; import React from "react";
import { formatMoney, formatPercent, formatShares } from "../../../ui/formatNumber"; import { dialogBoxCreate } from "../../../ui/React/DialogBox";
import * as corpConstants from "../../data/Constants"; import { formatPercent, formatShares } from "../../../ui/formatNumber";
import { Modal } from "../../../ui/React/Modal"; import { Modal } from "../../../ui/React/Modal";
import { Money } from "../../../ui/React/Money";
import { useCorporation } from "../Context"; import { useCorporation } from "../Context";
import { AcceptInvestmentOffer } from "../../Actions";
import Typography from "@mui/material/Typography"; import Typography from "@mui/material/Typography";
import Button from "@mui/material/Button"; import Button from "@mui/material/Button";
@ -13,40 +15,52 @@ interface IProps {
rerender: () => void; 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 { export function FindInvestorsModal(props: IProps): React.ReactElement {
const corporation = useCorporation(); const corp = useCorporation();
const val = corporation.valuation; const { funds, shares } = corp.getInvestmentOffer();
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);
function findInvestors(): void { function findInvestors(): void {
corporation.fundingRound++; if (shares === 0) return;
corporation.addNonIncomeFunds(funding); try {
corporation.numShares -= investShares; AcceptInvestmentOffer(corp);
props.rerender(); dialogBoxCreate(
props.onClose(); <>
<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 ( return (
<Modal open={props.open} onClose={props.onClose}> <Modal open={props.open} onClose={props.onClose}>
<Typography> <Typography>
An investment firm has offered you {formatMoney(funding)} in funding in exchange for a{" "} An investment firm has offered to buy {formatShares(shares)} shares of stock (a{" "}
{formatPercent(percShares, 3)} stake in the company ({formatShares(investShares)} shares). <b>{formatPercent(shares / corp.totalShares, 1)}</b> stake in the company).
<br /> <br />
<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 />
<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> </Typography>
<Button onClick={findInvestors}>Accept</Button> <br />
<Button onClick={findInvestors}>Accept</Button> <Button onClick={props.onClose}>Ignore</Button>
</Modal> </Modal>
); );
} }

@ -1,7 +1,8 @@
import React, { useState } from "react"; import React, { useState } from "react";
import { dialogBoxCreate } from "../../../ui/React/DialogBox"; import { dialogBoxCreate } from "../../../ui/React/DialogBox";
import { Modal } from "../../../ui/React/Modal"; 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 { useCorporation } from "../Context";
import Typography from "@mui/material/Typography"; import Typography from "@mui/material/Typography";
import { ButtonWithTooltip } from "../../../ui/Components/ButtonWithTooltip"; import { ButtonWithTooltip } from "../../../ui/Components/ButtonWithTooltip";
@ -9,6 +10,7 @@ import { NumberInput } from "../../../ui/React/NumberInput";
import Box from "@mui/material/Box"; import Box from "@mui/material/Box";
import { KEY } from "../../../utils/helpers/keyCodes"; import { KEY } from "../../../utils/helpers/keyCodes";
import { isPositiveInteger } from "../../../types"; import { isPositiveInteger } from "../../../types";
import { GoPublic } from "../../Actions";
interface IProps { interface IProps {
open: boolean; open: boolean;
@ -20,26 +22,9 @@ interface IProps {
export function GoPublicModal(props: IProps): React.ReactElement { export function GoPublicModal(props: IProps): React.ReactElement {
const corp = useCorporation(); const corp = useCorporation();
const [shares, setShares] = useState<number>(NaN); const [shares, setShares] = useState<number>(NaN);
const initialSharePrice = corp.valuation / corp.totalShares;
function goPublic(): void { const ceoOwnership = (corp.numShares - (shares || 0)) / corp.totalShares;
const initialSharePrice = corp.valuation / corp.totalShares; const initialSharePrice = corp.getTargetSharePrice(ceoOwnership);
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 disabledText = const disabledText =
shares >= corp.numShares shares >= corp.numShares
@ -48,22 +33,62 @@ export function GoPublicModal(props: IProps): React.ReactElement {
? "Must issue an non-negative integer number of shares" ? "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 ( return (
<Modal open={props.open} onClose={props.onClose}> <Modal open={props.open} onClose={props.onClose}>
<Typography> <Typography component="div">
Enter the number of shares you would like to issue for your IPO. These shares will be publicly sold and you will Enter the number of shares you would like to issue for your IPO.
no longer own them. Your Corporation will receive {formatMoney(initialSharePrice)} per share (the IPO money will <ul>
be deposited directly into your Corporation's funds). <li>These shares will be publicly sold and you will no longer own them.</li>
<br /> <li>The IPO money will be deposited directly into your Corporation's funds.</li>
<br /> </ul>
You can issue some, but not all, of your {formatShares(corp.numShares)} shares. You can issue some, but not all, of your {formatShares(corp.numShares)} shares.
</Typography> </Typography>
<br />
<Box display="flex" alignItems="center"> <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}> <ButtonWithTooltip disabledTooltip={disabledText} onClick={goPublic}>
Go Public Go Public
</ButtonWithTooltip> </ButtonWithTooltip>
</Box> </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> </Modal>
); );
} }

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

@ -1,50 +1,21 @@
import React, { useState } from "react"; import React, { useState } from "react";
import { formatMoney, formatShares } from "../../../ui/formatNumber"; import { formatShares, formatPercent } from "../../../ui/formatNumber";
import { dialogBoxCreate } from "../../../ui/React/DialogBox"; import { dialogBoxCreate } from "../../../ui/React/DialogBox";
import { Modal } from "../../../ui/React/Modal"; import { Modal } from "../../../ui/React/Modal";
import { Money } from "../../../ui/React/Money";
import { useCorporation } from "../Context"; import { useCorporation } from "../Context";
import Typography from "@mui/material/Typography"; import Typography from "@mui/material/Typography";
import { NumberInput } from "../../../ui/React/NumberInput"; 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 { KEY } from "../../../utils/helpers/keyCodes";
import { IssueNewShares } from "../../Actions"; import { IssueNewShares } from "../../Actions";
import * as corpConstants from "../../data/Constants";
interface IEffectTextProps { import { issueNewSharesFailureReason } from "../../helpers";
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>
);
}
interface IProps { interface IProps {
open: boolean; open: boolean;
onClose: () => void; onClose: () => void;
rerender: () => void;
} }
// Create a popup that lets the player issue new shares // Create a popup that lets the player issue new shares
@ -52,56 +23,103 @@ interface IProps {
export function IssueNewSharesModal(props: IProps): React.ReactElement { export function IssueNewSharesModal(props: IProps): React.ReactElement {
const corp = useCorporation(); const corp = useCorporation();
const [shares, setShares] = useState<number>(NaN); const [shares, setShares] = useState<number>(NaN);
const maxNewShares = corp.calculateMaxNewShares();
const maxNewShares = corp.calculateMaxNewShares();
const newShares = Math.round((shares || 0) / 10e6) * 10e6; 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 { function issueNewShares(): void {
if (isNaN(shares)) return; if (disabledText) return;
if (disabled) return; try {
const [profit, newShares, privateShares] = IssueNewShares(corp, shares); const [profit, newShares, privateShares] = IssueNewShares(corp, shares);
dialogBoxCreate(
props.onClose(); <>
<Typography>
let dialogContents = Issued {formatShares(newShares)} new shares and raised <Money money={profit} />.
`Issued ${formatShares(newShares)} new shares` + ` and raised ${formatMoney(profit)}.` + (privateShares > 0) </Typography>
? "\n" + formatShares(privateShares) + " of these shares were bought by private investors." {privateShares > 0 ? (
: ""; <Typography>{formatShares(privateShares)} of these shares were bought by private investors.</Typography>
dialogContents += `\n\nStock price decreased to ${formatMoney(corp.sharePrice)}`; ) : null}
dialogBoxCreate(dialogContents); <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 { function onKeyDown(event: React.KeyboardEvent<HTMLInputElement>): void {
if (event.key === KEY.ENTER) issueNewShares(); if (event.key === KEY.ENTER) issueNewShares();
} }
const nextCooldown = corpConstants.issueNewSharesCooldown * (corp.totalShares / corpConstants.initialShares);
return ( return (
<Modal open={props.open} onClose={props.onClose}> <Modal open={props.open} onClose={props.onClose}>
<Typography> <Typography component="div">
You can issue new equity shares (i.e. stocks) in order to raise capital for your corporation. 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 />
<br /> The number of new shares issued must be a multiple of 10 million.
&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.
</Typography> </Typography>
<EffectText shares={shares} /> <br />
<NumberInput autoFocus placeholder="# New Shares" onChange={setShares} onKeyDown={onKeyDown} /> <NumberInput
<Button disabled={disabled} onClick={issueNewShares} sx={{ mx: 1 }}> defaultValue={shares || ""}
autoFocus
placeholder="# New Shares"
onChange={setShares}
onKeyDown={onKeyDown}
/>
<ButtonWithTooltip disabledTooltip={disabledText} onClick={issueNewShares}>
Issue New Shares 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> </Modal>
); );
} }

@ -3,9 +3,10 @@ import { dialogBoxCreate } from "../../../ui/React/DialogBox";
import { MaterialInfo } from "../../MaterialInfo"; import { MaterialInfo } from "../../MaterialInfo";
import { Warehouse } from "../../Warehouse"; import { Warehouse } from "../../Warehouse";
import { Material } from "../../Material"; import { Material } from "../../Material";
import { formatMatPurchaseAmount, formatMoney } from "../../../ui/formatNumber"; import { formatMatPurchaseAmount } from "../../../ui/formatNumber";
import { BulkPurchase, BuyMaterial } from "../../Actions"; import { BulkPurchase, BuyMaterial } from "../../Actions";
import { Modal } from "../../../ui/React/Modal"; import { Modal } from "../../../ui/React/Modal";
import { Money } from "../../../ui/React/Money";
import { useCorporation, useDivision } from "../Context"; import { useCorporation, useDivision } from "../Context";
import Typography from "@mui/material/Typography"; import Typography from "@mui/material/Typography";
import TextField from "@mui/material/TextField"; import TextField from "@mui/material/TextField";
@ -56,7 +57,7 @@ function BulkPurchaseSection(props: IBPProps): React.ReactElement {
return ( return (
<> <>
<Typography> <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> </Typography>
</> </>
); );

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

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

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

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

@ -3,7 +3,7 @@ import { formatMultiplier, formatPercent } from "../../../ui/formatNumber";
import { dialogBoxCreate } from "../../../ui/React/DialogBox"; import { dialogBoxCreate } from "../../../ui/React/DialogBox";
import { OfficeSpace } from "../../OfficeSpace"; import { OfficeSpace } from "../../OfficeSpace";
import { ThrowParty } from "../../Actions"; import { ThrowParty } from "../../Actions";
import { Money } from "../../../ui/React/Money"; import { MoneyCost } from "../MoneyCost";
import { Modal } from "../../../ui/React/Modal"; import { Modal } from "../../../ui/React/Modal";
import { useCorporation } from "../Context"; import { useCorporation } from "../Context";
import Typography from "@mui/material/Typography"; 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>; if (isNaN(cost) || cost < 0) return <Typography>Invalid value entered!</Typography>;
return ( return (
<Typography> <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> </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 ( return (
<Accordion TransitionProps={{ unmountOnExit: true }}> <Accordion TransitionProps={{ unmountOnExit: true }}>
<AccordionSummary expandIcon={<ExpandMoreIcon />}> <AccordionSummary expandIcon={<ExpandMoreIcon />}>
@ -117,6 +123,11 @@ export function CorporationDev(): React.ReactElement {
<Button onClick={addCorporationResearch}>Tons of research</Button> <Button onClick={addCorporationResearch}>Tons of research</Button>
</td> </td>
</tr> </tr>
<tr>
<td>
<Button onClick={resetCorporationCooldowns}>Reset stock cooldowns</Button>
</td>
</tr>
</tbody> </tbody>
</table> </table>
</AccordionDetails> </AccordionDetails>

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

@ -13,7 +13,6 @@ import {
Division as NSDivision, Division as NSDivision,
WarehouseAPI, WarehouseAPI,
OfficeAPI, OfficeAPI,
InvestmentOffer,
CorpResearchName, CorpResearchName,
CorpMaterialName, CorpMaterialName,
} from "@nsdefs"; } from "@nsdefs";
@ -22,7 +21,9 @@ import {
NewDivision, NewDivision,
purchaseOffice, purchaseOffice,
IssueDividends, IssueDividends,
GoPublic,
IssueNewShares, IssueNewShares,
AcceptInvestmentOffer,
SellMaterial, SellMaterial,
SellProduct, SellProduct,
SetSmartSupply, SetSmartSupply,
@ -104,63 +105,6 @@ export function NetscriptCorporation(): InternalAPI<NSCorporation> {
return cost; 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 { function getResearchCost(division: Division, researchName: CorpResearchName): number {
const researchTree = IndustryResearchTrees[division.type]; const researchTree = IndustryResearchTrees[division.type];
if (researchTree === undefined) throw new Error(`No research tree for industry '${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(); const maxNewShares = corporation.calculateMaxNewShares();
if (_amount == undefined) _amount = maxNewShares; if (_amount == undefined) _amount = maxNewShares;
const amount = helpers.number(ctx, "amount", _amount); 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); const [funds] = IssueNewShares(corporation, amount);
return funds; return funds;
}, },
@ -768,6 +706,7 @@ export function NetscriptCorporation(): InternalAPI<NSCorporation> {
totalShares: corporation.totalShares, totalShares: corporation.totalShares,
numShares: corporation.numShares, numShares: corporation.numShares,
shareSaleCooldown: corporation.shareSaleCooldown, shareSaleCooldown: corporation.shareSaleCooldown,
investorShares: corporation.investorShares,
issuedShares: corporation.issuedShares, issuedShares: corporation.issuedShares,
issueNewSharesCooldown: corporation.issueNewSharesCooldown, issueNewSharesCooldown: corporation.issueNewSharesCooldown,
sharePrice: corporation.sharePrice, sharePrice: corporation.sharePrice,
@ -807,18 +746,26 @@ export function NetscriptCorporation(): InternalAPI<NSCorporation> {
}, },
getInvestmentOffer: (ctx) => () => { getInvestmentOffer: (ctx) => () => {
checkAccess(ctx); checkAccess(ctx);
return getInvestmentOffer(); const corporation = getCorporation();
return corporation.getInvestmentOffer();
}, },
acceptInvestmentOffer: (ctx) => () => { acceptInvestmentOffer: (ctx) => () => {
checkAccess(ctx); checkAccess(ctx);
return acceptInvestmentOffer(); const corporation = getCorporation();
try {
AcceptInvestmentOffer(corporation);
return true;
} catch (err) {
return false;
}
}, },
goPublic: (ctx) => (_numShares) => { goPublic: (ctx) => (_numShares) => {
checkAccess(ctx); checkAccess(ctx);
const corporation = getCorporation(); const corporation = getCorporation();
if (corporation.public) throw helpers.makeRuntimeErrorMsg(ctx, "corporation is already public"); if (corporation.public) throw helpers.makeRuntimeErrorMsg(ctx, "corporation is already public");
const numShares = helpers.number(ctx, "numShares", _numShares); const numShares = helpers.number(ctx, "numShares", _numShares);
return goPublic(numShares); GoPublic(corporation, numShares);
return true;
}, },
sellShares: (ctx) => (_numShares) => { sellShares: (ctx) => (_numShares) => {
checkAccess(ctx); checkAccess(ctx);

@ -21,5 +21,8 @@ export function startCorporation(this: PlayerObject, corpName: string, seedFunde
this.corporation.unlocks.add(CorpUnlockName.OfficeAPI); 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. */ * @returns Amount of funds generated for the corporation. */
issueNewShares(amount?: number): number; 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 */ * @param amount - Amount of shares to buy back, must be integer and larger than 0 */
buyBackShares(amount: number): void; 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 */ * @param amount - Amount of shares to sell, must be integer between 1 and 100t */
sellShares(amount: number): void; sellShares(amount: number): void;
@ -7515,13 +7517,15 @@ interface CorporationInfo {
expenses: number; expenses: number;
/** Indicating if the company is public */ /** Indicating if the company is public */
public: boolean; public: boolean;
/** Total number of shares issues by this corporation */ /** Total number of shares issued by this corporation. */
totalShares: number; totalShares: number;
/** Amount of share owned */ /** Amount of shares owned by the CEO. */
numShares: number; numShares: number;
/** Cooldown until shares can be sold again */ /** Cooldown until shares can be sold again */
shareSaleCooldown: number; 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; issuedShares: number;
/** Cooldown until new shares can be issued */ /** Cooldown until new shares can be issued */
issueNewSharesCooldown: number; issueNewSharesCooldown: number;

@ -2,11 +2,22 @@ import { PositiveInteger } from "../../src/types";
import { Corporation } from "../../src/Corporation/Corporation"; import { Corporation } from "../../src/Corporation/Corporation";
import { CorpUpgrades } from "../../src/Corporation/data/CorporationUpgrades"; import { CorpUpgrades } from "../../src/Corporation/data/CorporationUpgrades";
import { calculateMaxAffordableUpgrade, calculateUpgradeCost } from "../../src/Corporation/helpers"; 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", () => { describe("Corporation", () => {
let corporation: Corporation; let corporation: Corporation;
beforeEach(() => { beforeEach(() => {
setPlayer(new PlayerObject());
Player.init();
corporation = new Corporation({ name: "Test" }); 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, "expenses": 0,
"fundingRound": 0, "fundingRound": 0,
"funds": 150000000000, "funds": 150000000000,
"investorShares": 0,
"issueNewSharesCooldown": 0, "issueNewSharesCooldown": 0,
"issuedShares": 0, "issuedShares": 0,
"maxDivisions": 20, "maxDivisions": 20,