CORPORATION: more granular office size upgrades (#1179)

Allows corporation.upgradeOfficeSize to increase the size of a Corporation office by a non-multiple of 3 and also be charged a corresponding amount of corporate funds. See #1166 for details of current behavior.
This commit is contained in:
adeilt 2024-03-24 17:37:08 -07:00 committed by GitHub
parent db226ce0b8
commit 08097aaf09
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
6 changed files with 91 additions and 68 deletions

@ -16,7 +16,13 @@ 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";
import {
calculateOfficeSizeUpgradeCost,
sellSharesFailureReason,
buybackSharesFailureReason,
issueNewSharesFailureReason,
} from "./helpers";
import { PositiveInteger } from "../types";
export function NewDivision(corporation: Corporation, industry: IndustryType, name: string): void {
if (corporation.divisions.size >= corporation.maxDivisions)
@ -356,17 +362,10 @@ export function BuyBackShares(corporation: Corporation, numShares: number): bool
return true;
}
export function UpgradeOfficeSize(corp: Corporation, office: OfficeSpace, size: number): void {
const initialPriceMult = Math.round(office.size / corpConstants.officeInitialSize);
const costMultiplier = 1.09;
// Calculate cost to upgrade size by 15 employees
let mult = 0;
for (let i = 0; i < size / corpConstants.officeInitialSize; ++i) {
mult += Math.pow(costMultiplier, initialPriceMult + i);
}
const cost = corpConstants.officeInitialCost * mult;
export function UpgradeOfficeSize(corp: Corporation, office: OfficeSpace, increase: PositiveInteger): void {
const cost = calculateOfficeSizeUpgradeCost(office.size, increase);
if (corp.funds < cost) return;
office.size += size;
office.size += increase;
corp.loseFunds(cost, "office");
}

@ -3,6 +3,7 @@ import { PositiveInteger, isPositiveInteger } from "../types";
import { formatShares } from "../ui/formatNumber";
import { Corporation } from "./Corporation";
import { CorpUpgrade } from "./data/CorporationUpgrades";
import * as corpConstants from "./data/Constants";
export function calculateUpgradeCost(corporation: Corporation, upgrade: CorpUpgrade, amount: PositiveInteger): number {
const priceMult = upgrade.priceMult;
@ -12,6 +13,15 @@ export function calculateUpgradeCost(corporation: Corporation, upgrade: CorpUpgr
return cost;
}
export function calculateOfficeSizeUpgradeCost(currentSize: number, sizeIncrease: PositiveInteger): number {
if (sizeIncrease <= 0) throw new Error("Invalid value for sizeIncrease argument! Must be at least 0!");
const baseCostDivisor = 0.09;
const baseCostMultiplier = 1 + baseCostDivisor;
const currentSizeFactor = baseCostMultiplier ** (currentSize / 3);
const sizeIncreaseFactor = baseCostMultiplier ** (sizeIncrease / 3) - 1;
return (corpConstants.officeInitialCost / baseCostDivisor) * currentSizeFactor * sizeIncreaseFactor;
}
export function calculateMaxAffordableUpgrade(corp: Corporation, upgrade: CorpUpgrade): 0 | PositiveInteger {
const Lvl = corp.upgrades[upgrade.name].level;
const Multi = upgrade.priceMult;

@ -10,6 +10,8 @@ import Typography from "@mui/material/Typography";
import Button from "@mui/material/Button";
import Tooltip from "@mui/material/Tooltip";
import Box from "@mui/material/Box";
import { calculateOfficeSizeUpgradeCost } from "../../helpers";
import { PositiveInteger } from "../../../types";
interface IUpgradeButton {
cost: number;
@ -22,19 +24,22 @@ interface IUpgradeButton {
function UpgradeSizeButton(props: IUpgradeButton): React.ReactElement {
const corp = useCorporation();
function upgradeSize(cost: number, size: number): void {
function upgradeSize(cost: number, increase: PositiveInteger): void {
if (corp.funds < cost) {
return;
}
UpgradeOfficeSize(corp, props.office, size);
UpgradeOfficeSize(corp, props.office, increase);
props.rerender();
props.onClose();
}
return (
<Tooltip title={formatMoney(props.cost)}>
<span>
<Button disabled={corp.funds < props.cost} onClick={() => upgradeSize(props.cost, props.size)}>
<Button
disabled={corp.funds < props.cost}
onClick={() => upgradeSize(props.cost, props.size as PositiveInteger)}
>
+{props.size}
</Button>
</span>
@ -51,33 +56,23 @@ interface IProps {
export function UpgradeOfficeSizeModal(props: IProps): React.ReactElement {
const corp = useCorporation();
const initialPriceMult = Math.round(props.office.size / corpConstants.officeInitialSize);
const costMultiplier = 1.09;
const upgradeCost = corpConstants.officeInitialCost * Math.pow(costMultiplier, initialPriceMult);
// Calculate cost to upgrade size by 15 employees
let mult = 0;
for (let i = 0; i < 5; ++i) {
mult += Math.pow(costMultiplier, initialPriceMult + i);
}
const upgradeCost15 = corpConstants.officeInitialCost * mult;
// appears as +3 office size upgrade
const upgradeSizeBase = corpConstants.officeInitialSize as PositiveInteger;
const upgradeCostBase = calculateOfficeSizeUpgradeCost(props.office.size, upgradeSizeBase);
//Calculate max upgrade size and cost
const maxMult = corp.funds / corpConstants.officeInitialCost;
let maxNum = 1;
mult = Math.pow(costMultiplier, initialPriceMult);
while (maxNum < 50) {
//Hard cap of 50x (extra 150 employees)
if (mult >= maxMult) break;
const multIncrease = Math.pow(costMultiplier, initialPriceMult + maxNum);
if (mult + multIncrease > maxMult) {
break;
} else {
mult += multIncrease;
// appears as +15 office size upgrade
const upgradeSize5x = (5 * corpConstants.officeInitialSize) as PositiveInteger;
const upgradeCost5x = calculateOfficeSizeUpgradeCost(props.office.size, upgradeSize5x);
// optionally appears as largest affordable office size upgrade (up to +150)
let upgradeSizeMax = 150;
let upgradeCostMax = Infinity;
while (upgradeSizeMax > props.office.size) {
upgradeCostMax = calculateOfficeSizeUpgradeCost(props.office.size, upgradeSizeMax as PositiveInteger);
if (corp.funds > upgradeCostMax) break;
upgradeSizeMax -= corpConstants.officeInitialSize;
}
++maxNum;
}
const upgradeCostMax = corpConstants.officeInitialCost * mult;
return (
<Modal open={props.open} onClose={props.onClose}>
@ -89,25 +84,28 @@ export function UpgradeOfficeSizeModal(props: IProps): React.ReactElement {
rerender={props.rerender}
office={props.office}
corp={corp}
cost={upgradeCost}
size={corpConstants.officeInitialSize}
cost={upgradeCostBase}
size={upgradeSizeBase}
/>
<UpgradeSizeButton
onClose={props.onClose}
rerender={props.rerender}
office={props.office}
corp={corp}
cost={upgradeCost15}
size={corpConstants.officeInitialSize * 5}
cost={upgradeCost5x}
size={upgradeSize5x}
/>
{maxNum !== 1 && maxNum !== 5 && (
{upgradeSizeMax > 0 &&
upgradeCostMax < Infinity &&
upgradeSizeMax !== upgradeSizeBase &&
upgradeSizeMax !== upgradeSize5x && (
<UpgradeSizeButton
onClose={props.onClose}
rerender={props.rerender}
office={props.office}
corp={corp}
cost={upgradeCostMax}
size={maxNum * corpConstants.officeInitialSize}
size={upgradeSizeMax}
/>
)}
</Box>

@ -55,8 +55,6 @@ Maximum size with a given `MaxCost`:
$$MaxUpgradeLevel = 3\ast\log_{1.09}\left( MaxCost\ast\frac{0.09}{BasePrice} + {1.09}^{\frac{CurrentSize}{3}} \right)$$
When we divide office's size by 3, we need to use `Math.ceil()` to round up the value. This means upgrading from size 3 to size 4 costs the same as upgrading from size 3 to size 6. The most cost-effective size is always a multiple of 3.
## Energy and morale
They are calculated in START state.

@ -63,7 +63,7 @@ import { InternalAPI, NetscriptContext, setRemovedFunctions } from "../Netscript
import { helpers } from "../Netscript/NetscriptHelpers";
import { getEnumHelper } from "../utils/EnumHelper";
import { MaterialInfo } from "../Corporation/MaterialInfo";
import { calculateUpgradeCost } from "../Corporation/helpers";
import { calculateOfficeSizeUpgradeCost, calculateUpgradeCost } from "../Corporation/helpers";
import { PositiveInteger } from "../types";
import { getRecordKeys } from "../Types/Record";
@ -504,20 +504,13 @@ export function NetscriptCorporation(): InternalAPI<NSCorporation> {
const researchName = getEnumHelper("CorpResearchName").nsGetMember(ctx, _researchName, "researchName");
return hasResearched(getDivision(divisionName), researchName);
},
getOfficeSizeUpgradeCost: (ctx) => (_divisionName, _cityName, _size) => {
getOfficeSizeUpgradeCost: (ctx) => (_divisionName, _cityName, _increase) => {
checkAccess(ctx, CorpUnlockName.OfficeAPI);
const divisionName = helpers.string(ctx, "divisionName", _divisionName);
const cityName = getEnumHelper("CityName").nsGetMember(ctx, _cityName);
const size = helpers.number(ctx, "size", _size);
if (size < 0) throw new Error("Invalid value for size field! Must be numeric and greater than 0");
const increase = helpers.positiveInteger(ctx, "increase", _increase);
const office = getOffice(divisionName, cityName);
const initialPriceMult = Math.round(office.size / corpConstants.officeInitialSize);
const costMultiplier = 1.09;
let mult = 0;
for (let i = 0; i < size / corpConstants.officeInitialSize; ++i) {
mult += Math.pow(costMultiplier, initialPriceMult + i);
}
return corpConstants.officeInitialCost * mult;
return calculateOfficeSizeUpgradeCost(office.size, increase);
},
setAutoJobAssignment: (ctx) => (_divisionName, _cityName, _job, _amount) => {
checkAccess(ctx, CorpUnlockName.OfficeAPI);
@ -558,8 +551,8 @@ export function NetscriptCorporation(): InternalAPI<NSCorporation> {
checkAccess(ctx, CorpUnlockName.OfficeAPI);
const divisionName = helpers.string(ctx, "divisionName", _divisionName);
const cityName = getEnumHelper("CityName").nsGetMember(ctx, _cityName);
const size = helpers.number(ctx, "size", _size);
if (size < 0) throw new Error("Invalid value for size field! Must be numeric and greater than 0");
const size = helpers.positiveInteger(ctx, "size", _size);
const office = getOffice(divisionName, cityName);
const corporation = getCorporation();
UpgradeOfficeSize(corporation, office, size);

@ -1,7 +1,11 @@
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 {
calculateMaxAffordableUpgrade,
calculateUpgradeCost,
calculateOfficeSizeUpgradeCost,
} from "../../src/Corporation/helpers";
import { Player, setPlayer } from "../../src/Player";
import { PlayerObject } from "../../src/PersonObjects/Player/PlayerObject";
import {
@ -112,4 +116,25 @@ describe("Corporation", () => {
expectSharesToAddUp(corporation);
});
});
describe("helpers.calculateOfficeSizeUpgradeCost matches documented formula", () => {
// for discussion and computation of these test values, see:
// https://github.com/bitburner-official/bitburner-src/pull/1179#discussion_r1534948725
it.each([
{ fromSize: 3, increaseBy: 3, expectedCost: 4360000000.0 },
{ fromSize: 3, increaseBy: 15, expectedCost: 26093338259.6 },
{ fromSize: 3, increaseBy: 150, expectedCost: 3553764305895.24902 },
{ fromSize: 6, increaseBy: 3, expectedCost: 4752400000.0 },
{ fromSize: 6, increaseBy: 15, expectedCost: 28441738702.964 },
{ fromSize: 6, increaseBy: 150, expectedCost: 3873603093425.821 },
{ fromSize: 9, increaseBy: 3, expectedCost: 5180116000.0 },
{ fromSize: 9, increaseBy: 15, expectedCost: 31001495186.23076 },
{ fromSize: 9, increaseBy: 150, expectedCost: 4222227371834.145 },
])(
"should cost $expectedCost to upgrade office by $increaseBy from size $fromSize",
({ fromSize, increaseBy, expectedCost }) => {
expect(calculateOfficeSizeUpgradeCost(fromSize, increaseBy as PositiveInteger)).toBeCloseTo(expectedCost, 1);
},
);
});
});