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 { 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"; import {
calculateOfficeSizeUpgradeCost,
sellSharesFailureReason,
buybackSharesFailureReason,
issueNewSharesFailureReason,
} from "./helpers";
import { PositiveInteger } from "../types";
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)
@ -356,17 +362,10 @@ export function BuyBackShares(corporation: Corporation, numShares: number): bool
return true; return true;
} }
export function UpgradeOfficeSize(corp: Corporation, office: OfficeSpace, size: number): void { export function UpgradeOfficeSize(corp: Corporation, office: OfficeSpace, increase: PositiveInteger): void {
const initialPriceMult = Math.round(office.size / corpConstants.officeInitialSize); const cost = calculateOfficeSizeUpgradeCost(office.size, increase);
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;
if (corp.funds < cost) return; if (corp.funds < cost) return;
office.size += size; office.size += increase;
corp.loseFunds(cost, "office"); corp.loseFunds(cost, "office");
} }

@ -3,6 +3,7 @@ import { PositiveInteger, isPositiveInteger } from "../types";
import { formatShares } from "../ui/formatNumber"; import { formatShares } from "../ui/formatNumber";
import { Corporation } from "./Corporation"; import { Corporation } from "./Corporation";
import { CorpUpgrade } from "./data/CorporationUpgrades"; import { CorpUpgrade } from "./data/CorporationUpgrades";
import * as corpConstants from "./data/Constants";
export function calculateUpgradeCost(corporation: Corporation, upgrade: CorpUpgrade, amount: PositiveInteger): number { export function calculateUpgradeCost(corporation: Corporation, upgrade: CorpUpgrade, amount: PositiveInteger): number {
const priceMult = upgrade.priceMult; const priceMult = upgrade.priceMult;
@ -12,6 +13,15 @@ export function calculateUpgradeCost(corporation: Corporation, upgrade: CorpUpgr
return cost; 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 { export function calculateMaxAffordableUpgrade(corp: Corporation, upgrade: CorpUpgrade): 0 | PositiveInteger {
const Lvl = corp.upgrades[upgrade.name].level; const Lvl = corp.upgrades[upgrade.name].level;
const Multi = upgrade.priceMult; const Multi = upgrade.priceMult;

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

@ -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)$$ $$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 ## Energy and morale
They are calculated in START state. They are calculated in START state.

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

@ -1,7 +1,11 @@
import { PositiveInteger } from "../../src/types"; 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,
calculateOfficeSizeUpgradeCost,
} from "../../src/Corporation/helpers";
import { Player, setPlayer } from "../../src/Player"; import { Player, setPlayer } from "../../src/Player";
import { PlayerObject } from "../../src/PersonObjects/Player/PlayerObject"; import { PlayerObject } from "../../src/PersonObjects/Player/PlayerObject";
import { import {
@ -112,4 +116,25 @@ describe("Corporation", () => {
expectSharesToAddUp(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);
},
);
});
}); });