SLEEVES: Add sleeve commands to purchase sleeves and memory via the API (#2443)

This commit is contained in:
TheAimMan
2026-01-20 15:55:51 -05:00
committed by GitHub
parent 19064a1b12
commit 210c338f86
17 changed files with 673 additions and 105 deletions
@@ -28,7 +28,6 @@ import { Faction } from "../../Faction/Faction";
import { Factions } from "../../Faction/Factions";
import { FactionInvitationEvents } from "../../Faction/ui/FactionInvitationManager";
import { resetGangs } from "../../Gang/AllGangs";
import { Sleeve } from "../Sleeve/Sleeve";
import { SleeveWorkType } from "../Sleeve/Work/Work";
import { calculateSkillProgress as calculateSkillProgressF, ISkillProgress } from "../formulas/skill";
import { AddToAllServers, createUniqueRandomIp } from "../../Server/AllServers";
@@ -57,6 +56,7 @@ import { PlayerEventType, PlayerEvents } from "./PlayerEvents";
import type { Result } from "@nsdefs";
import type { AchievementId } from "../../Achievements/Types";
import { Infiltration } from "../../Infiltration/Infiltration";
import { recalculateNumberOfOwnedSleeves } from "../Sleeve/SleeveCovenantPurchases";
export function init(this: PlayerObject): void {
/* Initialize Player's home computer */
@@ -114,7 +114,7 @@ export function prestigeAugmentation(this: PlayerObject): void {
this.queuedAugmentations = [];
Sleeve.recalculateNumOwned();
recalculateNumberOfOwnedSleeves();
this.sleeves.forEach((sleeve) => (sleeve.shock <= 0 ? sleeve.synchronize() : sleeve.shockRecovery()));
+2 -25
View File
@@ -195,13 +195,8 @@ export class Sleeve extends Person implements SleevePerson {
/** Returns the cost of upgrading this sleeve's memory by a certain amount */
getMemoryUpgradeCost(n: number): number {
const amt = Math.round(n);
if (amt < 0) {
return 0;
}
if (this.memory + amt > 100) {
return this.getMemoryUpgradeCost(100 - this.memory);
if (!Number.isInteger(n) || n < 0 || this.memory + n > 100) {
return Infinity;
}
const mult = 1.02;
@@ -524,24 +519,6 @@ export class Sleeve extends Person implements SleevePerson {
}
}
static recalculateNumOwned() {
/**
* Don't change sourceFileLvl to activeSourceFileLvl. The number of sleeves is a permanent effect. It's too
* troublesome for the player if they lose Sleeves and have to go BN10 to buy them again when they override the
* level of SF 10.
*/
const numSleeves =
Math.min(3, Player.sourceFileLvl(10) + (Player.bitNodeN === 10 ? 1 : 0)) + Player.sleevesFromCovenant;
while (Player.sleeves.length > numSleeves) {
const destroyedSleeve = Player.sleeves.pop();
// This should not happen, but avoid an infinite loop in case sleevesFromCovenent or sf10 level are somehow negative
if (!destroyedSleeve) return;
// Stop work, to prevent destroyed sleeves from continuing their tasks in the void
destroyedSleeve.stopWork();
}
while (Player.sleeves.length < numSleeves) Player.sleeves.push(new Sleeve());
}
whoAmI(): string {
return "Sleeve";
}
@@ -1,3 +1,10 @@
import { Player } from "@player";
import type { Result } from "@nsdefs";
import { Sleeve } from "./Sleeve";
import { Factions } from "../../Faction/Factions";
import { FactionName } from "@enums";
import { formatMoney } from "../../ui/formatNumber";
/**
* Implements the purchasing of extra Duplicate Sleeves from The Covenant,
* as well as the purchasing of upgrades (memory)
@@ -5,3 +12,112 @@
export const MaxSleevesFromCovenant = 5;
export const BaseCostPerSleeve = 10e12;
export function getSleeveCost(sleevesFromCovenant: number): number {
// The first two checks are a bit over-the-top, but they are useful if we allow passing an arbitrary value to this
// function instead of only Player.sleevesFromCovenant.
if (
!Number.isInteger(sleevesFromCovenant) ||
sleevesFromCovenant < 0 ||
sleevesFromCovenant >= MaxSleevesFromCovenant
) {
return Infinity;
}
return Math.pow(10, sleevesFromCovenant) * BaseCostPerSleeve;
}
export function canPurchaseSleeve(): Result {
if (Player.bitNodeN !== 10) {
return {
success: false,
message: "You must be in BitNode 10 to purchase sleeves.",
};
}
if (!Factions[FactionName.TheCovenant].isMember) {
return {
success: false,
message: `You must be a member of ${FactionName.TheCovenant} to purchase sleeves.`,
};
}
if (Player.sleevesFromCovenant >= MaxSleevesFromCovenant) {
return {
success: false,
message: `You already have the maximum amount of sleeves purchasable from ${FactionName.TheCovenant}.`,
};
}
const cost = getSleeveCost(Player.sleevesFromCovenant);
if (!Player.canAfford(cost)) {
return {
success: false,
message: `You must have at least ${formatMoney(cost)} to buy this sleeve.`,
};
}
return { success: true };
}
export function recalculateNumberOfOwnedSleeves(): void {
// Don't change sourceFileLvl to activeSourceFileLvl. The number of sleeves is a permanent effect. It's too
// troublesome for the player if they lose Sleeves and have to go BN10 to buy them again when they override the
// level of SF 10.
const numSleeves =
Math.min(3, Player.sourceFileLvl(10) + (Player.bitNodeN === 10 ? 1 : 0)) + Player.sleevesFromCovenant;
while (Player.sleeves.length > numSleeves) {
const destroyedSleeve = Player.sleeves.pop();
// This should not happen, but avoid an infinite loop in case Player.sleevesFromCovenant or sf10 level are somehow
// negative
if (!destroyedSleeve) return;
// Stop work, to prevent destroyed sleeves from continuing their tasks in the void
destroyedSleeve.stopWork();
}
while (Player.sleeves.length < numSleeves) Player.sleeves.push(new Sleeve());
}
export function purchaseSleeve(): Result {
const validationResult = canPurchaseSleeve();
if (!validationResult.success) {
return validationResult;
}
Player.loseMoney(getSleeveCost(Player.sleevesFromCovenant), "sleeves");
Player.sleevesFromCovenant += 1;
recalculateNumberOfOwnedSleeves();
return { success: true };
}
export function canPurchaseMemoryUpgrade(sleeve: Sleeve, amount: number): Result {
if (Player.bitNodeN !== 10) {
return {
success: false,
message: "You must be in BitNode 10 to purchase sleeves' memory upgrade.",
};
}
if (!Factions[FactionName.TheCovenant].isMember) {
return {
success: false,
message: `You must be a member of ${FactionName.TheCovenant} to purchase sleeves' memory upgrade.`,
};
}
if (sleeve.memory + amount > 100) {
return {
success: false,
message: `Invalid amount of upgrade: ${amount}. The max memory of a sleeve is 100.`,
};
}
const cost = sleeve.getMemoryUpgradeCost(amount);
if (!Player.canAfford(cost)) {
return {
success: false,
message: `You must have at least ${formatMoney(cost)} to buy this sleeve's memory upgrade.`,
};
}
return { success: true };
}
export function purchaseSleeveMemoryUpgrade(sleeve: Sleeve, amount: number): Result {
const validationResult = canPurchaseMemoryUpgrade(sleeve, amount);
if (!validationResult.success) {
return validationResult;
}
Player.loseMoney(sleeve.getMemoryUpgradeCost(amount), "sleeves");
sleeve.upgradeMemory(amount);
return { success: true };
}
@@ -6,8 +6,7 @@ import React from "react";
import { CovenantSleeveMemoryUpgrade } from "./CovenantSleeveMemoryUpgrade";
import { Sleeve } from "../Sleeve";
import { BaseCostPerSleeve, MaxSleevesFromCovenant } from "../SleeveCovenantPurchases";
import { MaxSleevesFromCovenant, canPurchaseSleeve, getSleeveCost, purchaseSleeve } from "../SleeveCovenantPurchases";
import { Money } from "../../../ui/React/Money";
import { Modal } from "../../../ui/React/Modal";
@@ -27,31 +26,13 @@ interface IProps {
export function CovenantPurchasesRoot(props: IProps): React.ReactElement {
const rerender = useRerender();
/** Get the cost to purchase a new Duplicate Sleeve */
function purchaseCost(): number {
return Math.pow(10, Player.sleevesFromCovenant) * BaseCostPerSleeve;
}
// Purchasing a new Duplicate Sleeve
let purchaseDisabled = false;
if (!Player.canAfford(purchaseCost())) {
purchaseDisabled = true;
}
if (Player.sleevesFromCovenant >= MaxSleevesFromCovenant) {
purchaseDisabled = true;
}
function purchaseOnClick(): void {
if (Player.sleevesFromCovenant >= MaxSleevesFromCovenant) return;
if (Player.canAfford(purchaseCost())) {
Player.loseMoney(purchaseCost(), "sleeves");
Player.sleevesFromCovenant += 1;
Sleeve.recalculateNumOwned();
rerender();
} else {
dialogBoxCreate(`You cannot afford to purchase a Duplicate Sleeve`);
const result = purchaseSleeve();
if (!result.success) {
dialogBoxCreate(result.message);
return;
}
rerender();
}
// Purchasing Upgrades for Sleeves
@@ -70,14 +51,14 @@ export function CovenantPurchasesRoot(props: IProps): React.ReactElement {
Purchase an additional Sleeve. These Duplicate Sleeves are permanent (they persist through BitNodes). You
can purchase a total of {MaxSleevesFromCovenant} from {FactionName.TheCovenant}.
</Typography>
<Button disabled={purchaseDisabled} onClick={purchaseOnClick}>
<Button disabled={!canPurchaseSleeve().success} onClick={purchaseOnClick}>
Purchase -&nbsp;
<Money money={purchaseCost()} forPurchase={true} />
<Money money={getSleeveCost(Player.sleevesFromCovenant)} forPurchase={true} />
</Button>
<br />
<br />
</>
)}
<br />
<br />
<Typography>You can also purchase upgrades for your Sleeves. These upgrades are also permanent.</Typography>
{upgradePanels}
</>
@@ -5,7 +5,6 @@
import React, { useState } from "react";
import { Sleeve } from "../Sleeve";
import { Player } from "@player";
import { formatSleeveMemory } from "../../../ui/formatNumber";
import { Money } from "../../../ui/React/Money";
@@ -15,6 +14,8 @@ import TextField from "@mui/material/TextField";
import Button from "@mui/material/Button";
import Box from "@mui/material/Box";
import Paper from "@mui/material/Paper";
import { canPurchaseMemoryUpgrade, purchaseSleeveMemoryUpgrade } from "../SleeveCovenantPurchases";
import { dialogBoxCreate } from "../../../ui/React/DialogBox";
interface IProps {
index: number;
@@ -26,51 +27,29 @@ export function CovenantSleeveMemoryUpgrade(props: IProps): React.ReactElement {
const [amt, setAmt] = useState(1);
function changePurchaseAmount(e: React.ChangeEvent<HTMLInputElement>): void {
let n: number = parseInt(e.target.value);
let n = parseInt(e.target.value);
if (isNaN(n)) n = 1;
if (n < 1) n = 1;
const maxMemory = 100 - props.sleeve.memory;
if (n > maxMemory) n = maxMemory;
if (!Number.isInteger(n)) {
n = 1;
}
const maxAmount = 100 - props.sleeve.memory;
if (n > maxAmount) {
n = maxAmount;
}
if (n < 1) {
n = 1;
}
setAmt(n);
}
function getPurchaseCost(): number {
if (isNaN(amt)) {
return Infinity;
}
const maxMemory = 100 - props.sleeve.memory;
if (amt > maxMemory) {
return Infinity;
}
return props.sleeve.getMemoryUpgradeCost(amt);
}
function purchaseMemory(): void {
const cost = getPurchaseCost();
if (Player.canAfford(cost)) {
props.sleeve.upgradeMemory(amt);
Player.loseMoney(cost, "sleeves");
props.rerender();
const result = purchaseSleeveMemoryUpgrade(props.sleeve, amt);
if (!result.success) {
dialogBoxCreate(result.message);
return;
}
}
// Purchase button props
const cost = getPurchaseCost();
const purchaseBtnDisabled = !Player.canAfford(cost);
let purchaseBtnContent = <></>;
if (isNaN(amt)) {
purchaseBtnContent = <>Invalid value</>;
} else {
purchaseBtnContent = (
<>
Purchase {amt} memory&nbsp;-&nbsp;
<Money money={cost} forPurchase={true} />
</>
);
props.rerender();
}
return (
@@ -88,8 +67,9 @@ export function CovenantSleeveMemoryUpgrade(props: IProps): React.ReactElement {
<TextField onChange={changePurchaseAmount} type={"number"} value={amt} />
</Box>
<br />
<Button disabled={purchaseBtnDisabled} onClick={purchaseMemory}>
{purchaseBtnContent}
<Button disabled={!canPurchaseMemoryUpgrade(props.sleeve, amt).success} onClick={purchaseMemory}>
Purchase {amt} memory&nbsp;-&nbsp;
<Money money={props.sleeve.getMemoryUpgradeCost(amt)} forPurchase={true} />
</Button>
</Paper>
);