GANG: Updates to Docs, UI, API (#773)

API Changes:
Adds ns.gang.getRecruitsAvailable: Gets the number of additional gang members that can currently be recruited
Adds ns.gang.respectForNextRecruit: Gets the respect threshold for recruiting the next gang member
Adds ns.gang.renameMember: Renames a gang member

Plus many doc and ui improvements
This commit is contained in:
missymae#2783 2023-09-05 16:07:19 -06:00 committed by GitHub
parent bec737a253
commit 8d3f2bd750
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
24 changed files with 367 additions and 93 deletions

@ -0,0 +1,23 @@
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
[Home](./index.md) &gt; [bitburner](./bitburner.md) &gt; [Gang](./bitburner.gang.md) &gt; [getRecruitsAvailable](./bitburner.gang.getrecruitsavailable.md)
## Gang.getRecruitsAvailable() method
Check how many gang members you can currently recruit.
**Signature:**
```typescript
getRecruitsAvailable(): number;
```
**Returns:**
number
Number indicating how many members can be recruited, considering current reputation and gang size.
## Remarks
RAM cost: 1 GB

@ -34,11 +34,14 @@ If you are not in BitNode-2, then you must have Source-File 2 in order to use th
| [getMemberInformation(name)](./bitburner.gang.getmemberinformation.md) | Get information about a specific gang member. | | [getMemberInformation(name)](./bitburner.gang.getmemberinformation.md) | Get information about a specific gang member. |
| [getMemberNames()](./bitburner.gang.getmembernames.md) | List all gang members. | | [getMemberNames()](./bitburner.gang.getmembernames.md) | List all gang members. |
| [getOtherGangInformation()](./bitburner.gang.getotherganginformation.md) | Get information about the other gangs. | | [getOtherGangInformation()](./bitburner.gang.getotherganginformation.md) | Get information about the other gangs. |
| [getRecruitsAvailable()](./bitburner.gang.getrecruitsavailable.md) | Check how many gang members you can currently recruit. |
| [getTaskNames()](./bitburner.gang.gettasknames.md) | List member task names. | | [getTaskNames()](./bitburner.gang.gettasknames.md) | List member task names. |
| [getTaskStats(name)](./bitburner.gang.gettaskstats.md) | Get stats of a task. | | [getTaskStats(name)](./bitburner.gang.gettaskstats.md) | Get stats of a task. |
| [inGang()](./bitburner.gang.ingang.md) | Check if you're in a gang. | | [inGang()](./bitburner.gang.ingang.md) | Check if you're in a gang. |
| [purchaseEquipment(memberName, equipName)](./bitburner.gang.purchaseequipment.md) | Purchase an equipment for a gang member. | | [purchaseEquipment(memberName, equipName)](./bitburner.gang.purchaseequipment.md) | Purchase an equipment for a gang member. |
| [recruitMember(name)](./bitburner.gang.recruitmember.md) | Recruit a new gang member. | | [recruitMember(name)](./bitburner.gang.recruitmember.md) | Recruit a new gang member. |
| [renameMember(memberName, newName)](./bitburner.gang.renamemember.md) | Rename a Gang member to a new unique name. |
| [respectForNextRecruit()](./bitburner.gang.respectfornextrecruit.md) | Check the amount of Respect needed for your next gang recruit. |
| [setMemberTask(memberName, taskName)](./bitburner.gang.setmembertask.md) | Set gang member to task. | | [setMemberTask(memberName, taskName)](./bitburner.gang.setmembertask.md) | Set gang member to task. |
| [setTerritoryWarfare(engage)](./bitburner.gang.setterritorywarfare.md) | Enable/Disable territory warfare. | | [setTerritoryWarfare(engage)](./bitburner.gang.setterritorywarfare.md) | Enable/Disable territory warfare. |

@ -0,0 +1,33 @@
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
[Home](./index.md) &gt; [bitburner](./bitburner.md) &gt; [Gang](./bitburner.gang.md) &gt; [renameMember](./bitburner.gang.renamemember.md)
## Gang.renameMember() method
Rename a Gang member to a new unique name.
**Signature:**
```typescript
renameMember(memberName: string, newName: string): boolean;
```
## Parameters
| Parameter | Type | Description |
| --- | --- | --- |
| memberName | string | Name of the member to change. |
| newName | string | New name for that gang member. |
**Returns:**
boolean
True if successful, and false if not.
## Remarks
RAM cost: 0 GB
Rename a Gang Member if none already has the new name.

@ -0,0 +1,23 @@
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
[Home](./index.md) &gt; [bitburner](./bitburner.md) &gt; [Gang](./bitburner.gang.md) &gt; [respectForNextRecruit](./bitburner.gang.respectfornextrecruit.md)
## Gang.respectForNextRecruit() method
Check the amount of Respect needed for your next gang recruit.
**Signature:**
```typescript
respectForNextRecruit(): number;
```
**Returns:**
number
The static number value of Respect needed for the next recruit, with consideration to your current gang size. Returns `Infinity` if you have reached the gang size limit.
## Remarks
RAM cost: 1 GB

@ -21,6 +21,7 @@ interface GangGenInfo
| [moneyGainRate](./bitburner.ganggeninfo.moneygainrate.md) | | number | Money earned per game cycle | | [moneyGainRate](./bitburner.ganggeninfo.moneygainrate.md) | | number | Money earned per game cycle |
| [power](./bitburner.ganggeninfo.power.md) | | number | Gang's power for territory warfare | | [power](./bitburner.ganggeninfo.power.md) | | number | Gang's power for territory warfare |
| [respect](./bitburner.ganggeninfo.respect.md) | | number | Gang's respect | | [respect](./bitburner.ganggeninfo.respect.md) | | number | Gang's respect |
| [respectForNextRecruit](./bitburner.ganggeninfo.respectfornextrecruit.md) | | number | Amount of Respect needed for next gang recruit, if possible |
| [respectGainRate](./bitburner.ganggeninfo.respectgainrate.md) | | number | Respect earned per game cycle | | [respectGainRate](./bitburner.ganggeninfo.respectgainrate.md) | | number | Respect earned per game cycle |
| [territory](./bitburner.ganggeninfo.territory.md) | | number | Amount of territory held | | [territory](./bitburner.ganggeninfo.territory.md) | | number | Amount of territory held |
| [territoryClashChance](./bitburner.ganggeninfo.territoryclashchance.md) | | number | Clash chance | | [territoryClashChance](./bitburner.ganggeninfo.territoryclashchance.md) | | number | Clash chance |

@ -0,0 +1,13 @@
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
[Home](./index.md) &gt; [bitburner](./bitburner.md) &gt; [GangGenInfo](./bitburner.ganggeninfo.md) &gt; [respectForNextRecruit](./bitburner.ganggeninfo.respectfornextrecruit.md)
## GangGenInfo.respectForNextRecruit property
Amount of Respect needed for next gang recruit, if possible
**Signature:**
```typescript
respectForNextRecruit: number;
```

@ -73,16 +73,16 @@ export function initBitNodes() {
<br /> <br />
Certain Factions ({FactionName.SlumSnakes}, {FactionName.Tetrads}, {FactionName.TheSyndicate},{" "} Certain Factions ({FactionName.SlumSnakes}, {FactionName.Tetrads}, {FactionName.TheSyndicate},{" "}
{FactionName.TheDarkArmy}, {FactionName.SpeakersForTheDead}, {FactionName.NiteSec}, {FactionName.TheBlackHand} {FactionName.TheDarkArmy}, {FactionName.SpeakersForTheDead}, {FactionName.NiteSec}, {FactionName.TheBlackHand}
) give the player the ability to form and manage their own gangs. These gangs will earn the player money and ) give the player the ability to form and manage their own gang, which can earn the player money and reputation
reputation with the corresponding Faction with the corresponding Faction. Gangs offer more Augmentations than Factions, and in BitNode-2 offer a way to
destroy the BitNode.
<br /> <br />
Every Augmentation in the game will be available through the Factions listed above
<br /> <br />
<br /> <br />
Destroying this BitNode will give you Source-File 2, or if you already have this Source-File it will upgrade its Destroying this BitNode will give you Source-File 2, or if you already have this Source-File it will upgrade its
level up to a maximum of 3. This Source-File allows you to form gangs in other BitNodes once your karma level up to a maximum of 3. This Source-File allows you to form gangs in other BitNodes once your karma
decreases to a certain value. It also increases the player's crime success rate, crime money, and charisma decreases to a certain value. It also increases your crime success rate, crime money, and charisma multipliers
multipliers by: by:
<br /> <br />
<br /> <br />
Level 1: 24% Level 1: 24%

@ -1,5 +1,35 @@
# Gang # Gangs
Managing a gang can be very rewarding. By rising above all other gangs you get access to almost all augmentations in the game. In the wake of crisis and war, Gang activity surged. Stronger than ever in a lawless world - Enhanced with fantastic technology, no longer held back by ethics and morals, 'free from the shadows'....
PLACEHOLDER Seen by most of the population as nihilistic, murderous and vile, occassional rumors suggest Gangs sometimes involve themselves with vigilanteism, hacktivism, perhaps even plotting against The Enders, seeking to destroy a world they cannot save.
## Starting and Recruiting
Outside of [BitNode-2](bitnodes.md) gangs require much more crime and heartbreak to create, but can still be a great help. Creating a Gang in other [BitNodes](bitnodes.md) will offer more [Augmentations](../basic/augmentations.md) than other [Factions](factions.md), but they will not be a way to destroy the [BitNode](bitnodes.md) alone.
After creating a gang, you will be able to start recruiting, adding members to your gang as you gain Respect. While in a BitNode, your gang and gang member stats will not reset if you install augmentations.
## Respect
Earned as your gang members complete tasks, Respect affects your gang's productivity, including your Faction Reputation (needed to buy augmentations from your Gang Faction), and the number of recruits you can have. An individual gang member's Respect is lost or reset if they Ascend, or are killed in a Territory Warfare clash.
## Ascending
When experienced enough, gang members are offered Ascension, a permanent boost to their stat multipliers at the cost of resetting their base stats and equipment to 0, and reducing your Gang Reputation by the same amount as that member had earned since they last Ascended.
## Equipping and Managing
Buying Equipment for a gang member will give them a stat boost until they Ascend or are killed, at which point most equipment will reset.
Augmentations you install on gang members (in the Gang Equipment subpage) do not reset when they Ascend.
Active gang members earn stats, respect and money based on their current stats, their equipment, and the effects of Ascending.
## Wanted, Territory and Clashes
Your gang's "Wanted Level" can make tasks much less productive, and is affected by the tasks assigned to gang members. "Ethical Hacking" or "Vigilante Justice" tasks can lower Wanted Level.
"Territory Warfare" is a special task that builds Power for your gang. If "Territory Warfare" is enabled [see the Territory subpage of your Gang page], members have a chance to win or lose territory by clashing with other gangs. The % of Territory you control affects most aspects of your gang productivity.
Note that gang members can die during clashes, even if your gang wins.

@ -17,13 +17,15 @@ interface IProps {
/** React Component for the popup used to create a new gang. */ /** React Component for the popup used to create a new gang. */
export function CreateGangModal(props: IProps): React.ReactElement { export function CreateGangModal(props: IProps): React.ReactElement {
const combatGangText = const combatGangText =
"This is a COMBAT gang. Members in this gang will have different tasks than HACKING gangs. " + props.facName +
"Compared to hacking gangs, progression with combat gangs can be more difficult as territory management " + " is a COMBAT gang and its members will have different tasks than in HACKING gangs. " +
"Compared to hacking gangs, progression with a combat gang can be more difficult as territory management " +
"is more important. However, well-managed combat gangs can progress faster than hacking ones."; "is more important. However, well-managed combat gangs can progress faster than hacking ones.";
const hackingGangText = const hackingGangText =
"This is a HACKING gang. Members in this gang will have different tasks than COMBAT gangs. " + props.facName +
"Compared to combat gangs, progression with hacking gangs is more straightforward as territory warfare " + " is a HACKING gang and its members will have different tasks than in COMBAT gangs. " +
"Compared to combat gangs, progression with a hacking gang is slower but more straightforward as territory warfare " +
"is not as important."; "is not as important.";
function isHacking(): boolean { function isHacking(): boolean {
@ -46,19 +48,19 @@ export function CreateGangModal(props: IProps): React.ReactElement {
Would you like to create a new Gang with {props.facName}? Would you like to create a new Gang with {props.facName}?
<br /> <br />
<br /> <br />
Note that this will prevent you from creating a Gang with any other Faction until this BitNode is destroyed. It This will prevent you from creating a Gang with any other Faction until the BitNode is destroyed or abandoned.
also resets your reputation with this faction. It will also reset your reputation with {props.facName}.
<br /> <br />
<br /> <br />
{isHacking() ? hackingGangText : combatGangText} {isHacking() ? hackingGangText : combatGangText}
<br /> <br />
<br /> <br />
Other than hacking vs combat, there are NO differences between the Factions you can create a Gang with, and each Other than hacking vs combat and name, there are no differences between gangs.
of these Factions have all Augmentations available.
</Typography> </Typography>
<Button onClick={createGang} onKeyUp={onKeyUp} autoFocus> <Button onClick={createGang} onKeyUp={onKeyUp} autoFocus>
Create Gang Create Gang
</Button> </Button>
<Button onClick={props.onClose}>Cancel</Button>
</Modal> </Modal>
); );
} }

@ -228,13 +228,28 @@ export function FactionsRoot(): React.ReactElement {
)} )}
<span> <span>
{Player.inGang() && (
<Typography variant="h5" color="primary">
Your Gang
</Typography>
)}
{Player.inGang() && (
<Box>
<FactionElement
key={Player.getGangName()}
faction={Player.getGangFaction()}
joined={true}
rerender={rerender}
/>
</Box>
)}
<Typography variant="h5" color="primary"> <Typography variant="h5" color="primary">
Your Factions Your Factions
</Typography> </Typography>
<Box> <Box>
{allJoinedFactions.length > 0 ? ( {allJoinedFactions.length > 0 ? (
allJoinedFactions.map((facName) => { allJoinedFactions.map((facName) => {
if (!Object.hasOwn(Factions, facName)) return null; if (!Object.hasOwn(Factions, facName) || Player.getGangName() === facName) return null;
return <FactionElement key={facName} faction={Factions[facName]} joined={true} rerender={rerender} />; return <FactionElement key={facName} faction={Factions[facName]} joined={true} rerender={rerender} />;
}) })
) : ( ) : (

@ -291,16 +291,25 @@ export class Gang {
canRecruitMember(): boolean { canRecruitMember(): boolean {
if (this.members.length >= GangConstants.MaximumGangMembers) return false; if (this.members.length >= GangConstants.MaximumGangMembers) return false;
return this.respect >= this.getRespectNeededToRecruitMember(); return this.respect >= this.respectForNextRecruit();
} }
getRespectNeededToRecruitMember(): number { /** @returns The respect threshold needed for the next member recruitment. Infinity if already at or above max members. */
// First N gang members are free (can be recruited at 0 respect) respectForNextRecruit(): number {
const numFreeMembers = 3; if (this.members.length < GangConstants.numFreeMembers) return 0;
if (this.members.length < numFreeMembers) return 0; if (this.members.length >= GangConstants.MaximumGangMembers) {
return Infinity;
}
const exponent = this.members.length - GangConstants.numFreeMembers + 1;
return Math.pow(GangConstants.recruitThresholdBase, exponent);
}
const i = this.members.length - (numFreeMembers - 1); getRecruitsAvailable(): number {
return Math.pow(5, i); const numFreeMembers = 3;
const recruitCostBase = 5;
if (this.members.length < numFreeMembers && this.respect < Math.pow(recruitCostBase, numFreeMembers))
return numFreeMembers - this.members.length; // if the max possible is less than freeMembers
return Math.floor(Math.log(this.respect) / Math.log(recruitCostBase)) + numFreeMembers - this.members.length; //else
} }
recruitMember(name: string): boolean { recruitMember(name: string): boolean {

@ -2,6 +2,10 @@ import { CONSTANTS } from "../../Constants";
import { FactionName } from "@enums"; import { FactionName } from "@enums";
export const GangConstants = { export const GangConstants = {
/** Number of members that can be recruited with 0 respect. */
numFreeMembers: 3,
/** Exponential base used in determining the respect threshold for recruiting a new member. */
recruitThresholdBase: 5,
// Respect is divided by this to get rep gain // Respect is divided by this to get rep gain
GangRespectToReputationRatio: 75, GangRespectToReputationRatio: 75,
MaximumGangMembers: 12, MaximumGangMembers: 12,

@ -379,7 +379,7 @@ export const gangMemberTasksMetadata: IGangMemberTaskMetadata[] = [
params: { chaWeight: 100, difficulty: 8 }, params: { chaWeight: 100, difficulty: 8 },
}, },
{ {
desc: "Assign this gang member to engage in territorial warfare with other gangs. Members assigned to this task will help increase your gang's territory and will defend your territory from being taken.", desc: "Members assigned to this task increase your gang's power and will fight for territory if 'Territory Warfare' is engaged. Note that gang members can be killed while assigned this task.",
isCombat: true, isCombat: true,
isHacking: true, isHacking: true,
name: "Territory Warfare", name: "Territory Warfare",

@ -1,4 +1,4 @@
import React from "react"; import React, { useEffect } from "react";
import Typography from "@mui/material/Typography"; import Typography from "@mui/material/Typography";
import Button from "@mui/material/Button"; import Button from "@mui/material/Button";
@ -8,6 +8,7 @@ import { dialogBoxCreate } from "../../ui/React/DialogBox";
import { Modal } from "../../ui/React/Modal"; import { Modal } from "../../ui/React/Modal";
import { useGang } from "./Context"; import { useGang } from "./Context";
import { useRerender } from "../../ui/React/hooks"; import { useRerender } from "../../ui/React/hooks";
import { Player } from "@player";
type AscensionModalProps = { type AscensionModalProps = {
open: boolean; open: boolean;
@ -20,21 +21,30 @@ type AscensionModalProps = {
* React Component for the content of the popup before the player confirms the * React Component for the content of the popup before the player confirms the
* ascension of a gang member. * ascension of a gang member.
*/ */
export function AscensionModal(props: AscensionModalProps): React.ReactElement { export function AscensionModal({ open, onClose, member, onAscend }: AscensionModalProps): React.ReactElement {
const gang = useGang(); const gang = useGang();
useRerender(1000); useRerender(1000);
//Cleanup if modal is closed for other reasons, ie. ns.gang.ascendMember()
useEffect(() => onClose, [onClose]);
function confirm(): void { function confirm(): void {
props.onAscend(); onAscend();
const res = gang.ascendMember(props.member); const res = gang.ascendMember(member);
dialogBoxCreate( dialogBoxCreate(
<> <>
You ascended {props.member.name}!<br /> {member.name} ascended!
<br /> <br />
Your gang lost {formatRespect(res.respect)} respect. {res.respect > 0 && (
<div>
<br />
Your gang, {Player.gang?.facName}, lost {formatRespect(res.respect)} respect.
<br />
</div>
)}
<br /> <br />
{member.name} gained the following stat multipliers for ascending:
<br /> <br />
{props.member.name} gained the following stat multipliers for ascending:
<br /> <br />
Hacking: x{formatPreciseMultiplier(res.hack)} Hacking: x{formatPreciseMultiplier(res.hack)}
<br /> <br />
@ -50,26 +60,31 @@ export function AscensionModal(props: AscensionModalProps): React.ReactElement {
<br /> <br />
</>, </>,
); );
props.onClose(); onClose();
} }
// const ascendBenefits = props.member.getAscensionResults(); // const ascendBenefits = props.member.getAscensionResults();
const preAscend = props.member.getCurrentAscensionMults(); const preAscend = member.getCurrentAscensionMults();
const postAscend = props.member.getAscensionMultsAfterAscend(); const postAscend = member.getAscensionMultsAfterAscend();
return ( return (
<Modal open={props.open} onClose={props.onClose}> <Modal open={open} onClose={onClose}>
<Typography> <Typography>
Are you sure you want to ascend this member? They will lose all of Are you sure you want to ascend this member? They will lose all of
<br /> <br />
their non-Augmentation upgrades and their stats will reset back to 1. their non-Augmentation upgrades and their stats will reset back to 1.
<br /> <br />
{member.earnedRespect > 0 && (
<div>
<br />
Furthermore, your gang will lose {formatRespect(member.earnedRespect)} respect.
<br />
</div>
)}
<br /> <br />
Furthermore, your gang will lose {formatRespect(props.member.earnedRespect)} respect In return, {member.name} will gain the following permanent boost to stat multipliers:
<br /> <br />
<br /> <br />
In return, they will gain the following permanent boost to stat multipliers:
<br />
Hacking: x{formatPreciseMultiplier(preAscend.hack)} =&gt; x{formatPreciseMultiplier(postAscend.hack)} Hacking: x{formatPreciseMultiplier(preAscend.hack)} =&gt; x{formatPreciseMultiplier(postAscend.hack)}
<br /> <br />
Strength: x{formatPreciseMultiplier(preAscend.str)} =&gt; x{formatPreciseMultiplier(postAscend.str)} Strength: x{formatPreciseMultiplier(preAscend.str)} =&gt; x{formatPreciseMultiplier(postAscend.str)}
@ -82,8 +97,10 @@ export function AscensionModal(props: AscensionModalProps): React.ReactElement {
<br /> <br />
Charisma: x{formatPreciseMultiplier(preAscend.cha)} =&gt; x{formatPreciseMultiplier(postAscend.cha)} Charisma: x{formatPreciseMultiplier(preAscend.cha)} =&gt; x{formatPreciseMultiplier(postAscend.cha)}
<br /> <br />
<br />
</Typography> </Typography>
<Button onClick={confirm}>Ascend</Button> <Button onClick={confirm}>Ascend</Button>
<Button onClick={onClose}>Cancel</Button>
</Modal> </Modal>
); );
} }

@ -248,8 +248,8 @@ export function EquipmentsSubpage(): React.ReactElement {
<Tooltip <Tooltip
title={ title={
<Typography> <Typography>
You get a discount on equipment and upgrades based on your gang's respect and power. More respect and power A discount on equipment and upgrades based on your gang's respect and power. More respect and power leads to
leads to more discounts. more discounts.
</Typography> </Typography>
} }
> >

@ -2,7 +2,7 @@
* React Component for the content of the accordion of gang members on the * React Component for the content of the accordion of gang members on the
* management subpage. * management subpage.
*/ */
import React, { useState } from "react"; import React from "react";
import { GangMemberStats } from "./GangMemberStats"; import { GangMemberStats } from "./GangMemberStats";
import { TaskSelector } from "./TaskSelector"; import { TaskSelector } from "./TaskSelector";
import { AscensionModal } from "./AscensionModal"; import { AscensionModal } from "./AscensionModal";
@ -13,51 +13,47 @@ import HelpIcon from "@mui/icons-material/Help";
import { GangMember } from "../GangMember"; import { GangMember } from "../GangMember";
import { StaticModal } from "../../ui/React/StaticModal"; import { StaticModal } from "../../ui/React/StaticModal";
import { useBoolean, useRerender } from "../../ui/React/hooks";
interface IProps { interface IProps {
member: GangMember; member: GangMember;
} }
export function GangMemberCardContent(props: IProps): React.ReactElement { export function GangMemberCardContent(props: IProps): React.ReactElement {
const setRerender = useState(false)[1]; const rerender = useRerender();
const [helpOpen, setHelpOpen] = useState(false); const [helpOpen, { on: openHelpModal, off: closeHelpModal }] = useBoolean(false);
const [ascendOpen, setAscendOpen] = useState(false); const [ascendOpen, { on: openAscensionModal, off: closeAscensionModal }] = useBoolean(false);
return ( return (
<> <>
{props.member.canAscend() && ( {props.member.canAscend() && (
<Box sx={{ display: "flex", justifyContent: "space-between", my: 1 }}> <Box sx={{ display: "flex", justifyContent: "space-between", my: 1 }}>
<Button onClick={() => setAscendOpen(true)} style={{ flexGrow: 1, borderRightWidth: 0 }}> <Button onClick={openAscensionModal} style={{ flexGrow: 1, borderRightWidth: 0 }}>
Ascend Ascend
</Button> </Button>
<AscensionModal <AscensionModal open={ascendOpen} onClose={closeAscensionModal} member={props.member} onAscend={rerender} />
open={ascendOpen} <Button onClick={openHelpModal} style={{ width: "fit-content", borderLeftWidth: 0 }}>
onClose={() => setAscendOpen(false)}
member={props.member}
onAscend={() => setRerender((old) => !old)}
/>
<Button onClick={() => setHelpOpen(true)} style={{ width: "fit-content", borderLeftWidth: 0 }}>
<HelpIcon /> <HelpIcon />
</Button> </Button>
<StaticModal open={helpOpen} onClose={() => setHelpOpen(false)}> <StaticModal open={helpOpen} onClose={closeHelpModal}>
<Typography> <Typography>
Ascending a Gang Member resets the member's progress and stats in exchange for a permanent boost to their Ascending a Gang Member resets that member's progress and stats in exchange for a permanent boost to their
stat multipliers. stat multipliers.
<br /> <br />
<br /> <br />
The additional stat multiplier that the Gang Member gains upon ascension is based on the amount of exp The stat boost a Gang Member gains upon ascension is based on the amount of exp they have, and will be
they have. shown before you choose to ascend them.
<br /> <br />
<br /> <br />
Upon ascension, the member will lose all of its non-Augmentation Equipment and your gang will lose respect Upon ascension, they will lose all of their non-Augmentation Equipment and your gang will lose respect
equal to the total respect earned by the member. equal to the total respect earned by that member.
</Typography> </Typography>
</StaticModal> </StaticModal>
</Box> </Box>
)} )}
<Box display="grid" sx={{ gridTemplateColumns: "1fr 1fr", width: "100%", gap: 1 }}> <Box display="grid" sx={{ gridTemplateColumns: "1fr 1fr", width: "100%", gap: 1 }}>
<GangMemberStats member={props.member} /> <GangMemberStats member={props.member} />
<TaskSelector onTaskChange={() => setRerender((old) => !old)} member={props.member} /> <TaskSelector onTaskChange={rerender} member={props.member} />
</Box> </Box>
</> </>
); );

@ -2,33 +2,54 @@ import React from "react";
import { GangStats } from "./GangStats"; import { GangStats } from "./GangStats";
import { GangMemberList } from "./GangMemberList"; import { GangMemberList } from "./GangMemberList";
import { useGang } from "./Context"; import { useGang } from "./Context";
import Typography from "@mui/material/Typography"; import { Typography, FormControlLabel, Switch, Tooltip } from "@mui/material";
/** React Component for the subpage that manages gang members, the main page. */ /** React Component for the subpage that manages gang members, the main page. */
export function ManagementSubpage(): React.ReactElement { export function ManagementSubpage(): React.ReactElement {
const gang = useGang(); const gang = useGang();
return ( return (
<> <>
<Typography variant="h4" color="primary">
{gang.facName} (your Gang)
</Typography>
<Typography> <Typography>
This page is used to manage your gang members and get an overview of your gang's stats. <br />
If a gang member is not earning much money or respect, the task you assigned might be too difficult. Consider
assigning an easier task, or training them. Tasks closer to the top of the dropdown list are generally easier.
Alternatively, low production might be a sign that your wanted level is too high. Consider doing{" "}
{gang.isHackingGang ? "Ethical Hacking or " : ""}
Vigilante Justice to lower your wanted level.
<br /> <br />
<br /> <br />
If a gang member is not earning much money or respect, the task that you have assigned to that member might be Installing Augmentations does NOT reset progress with your Gang. Furthermore, after installing Augmentations,
too difficult. Consider training that member's stats or choosing an easier task. The tasks closer to the top of you will automatically join whatever Faction you created your gang with.
the dropdown list are generally easier. Alternatively, the gang member's low production might be due to the fact
that your wanted level is too high. Consider assigning a few members to the '
{gang.isHackingGang ? "Ethical Hacking" : "Vigilante Justice"}' task to lower your wanted level.
<br /> <br />
<br /> <br />
Installing Augmentations does NOT reset your progress with your Gang. Furthermore, after installing You can also manage your gang programmatically through Netscript using the Gang API.
Augmentations, you will automatically be a member of whatever Faction you created your gang with.
<br />
<br />
You can also manage your gang programmatically through Netscript using the Gang API
</Typography> </Typography>
<br /> <br />
<GangStats /> <GangStats />
<br /> <br />
<FormControlLabel
control={
<Switch
checked={gang.territoryWarfareEngaged}
onChange={(event) => (gang.territoryWarfareEngaged = event.target.checked)}
/>
}
label={
<Tooltip
title={
<Typography>
Engaging in Territory Warfare sets your clash chance to 100%. Disengaging will cause your clash chance
to gradually decrease until it reaches 0%. See the "Territory" subpage for more information.
</Typography>
}
>
<Typography>Engage in Territory Warfare</Typography>
</Tooltip>
}
/>
<GangMemberList /> <GangMemberList />
</> </>
); );

@ -1,6 +1,5 @@
import React, { useState } from "react"; import React, { useState } from "react";
import { RecruitModal } from "./RecruitModal"; import { RecruitModal } from "./RecruitModal";
import { GangConstants } from "../data/Constants";
import { formatRespect } from "../../ui/formatNumber"; import { formatRespect } from "../../ui/formatNumber";
import { useGang } from "./Context"; import { useGang } from "./Context";
import Typography from "@mui/material/Typography"; import Typography from "@mui/material/Typography";
@ -15,24 +14,29 @@ interface IProps {
export function RecruitButton(props: IProps): React.ReactElement { export function RecruitButton(props: IProps): React.ReactElement {
const gang = useGang(); const gang = useGang();
const [open, setOpen] = useState(false); const [open, setOpen] = useState(false);
if (gang.members.length >= GangConstants.MaximumGangMembers) { const recruitsAvailable = gang.getRecruitsAvailable();
return <></>;
}
if (!gang.canRecruitMember()) { if (!gang.canRecruitMember()) {
const respect = gang.getRespectNeededToRecruitMember(); const respectNeeded = gang.respectForNextRecruit();
return ( return (
<Box display="flex" alignItems="center" sx={{ mx: 1 }}> <Box display="flex" alignItems="center" sx={{ mx: 1 }}>
<Button disabled>Recruit Gang Member</Button> <Button disabled>Recruit Gang Member</Button>
<Typography sx={{ ml: 1 }}>{formatRespect(respect)} respect needed to recruit next member</Typography> {respectNeeded === Infinity ? (
<Typography sx={{ ml: 1 }}>Maximum gang members already recruited</Typography>
) : (
<Typography sx={{ ml: 1 }}>{formatRespect(respectNeeded)} respect needed to recruit next member</Typography>
)}
</Box> </Box>
); );
} }
return ( return (
<> <>
<Box sx={{ mx: 1 }}> <Box display="flex" alignItems="center" sx={{ mx: 1 }}>
<Button onClick={() => setOpen(true)}>Recruit Gang Member</Button> <Button onClick={() => setOpen(true)}>Recruit Gang Member</Button>
<Typography sx={{ ml: 1 }}>
Can recruit {recruitsAvailable} more gang member{recruitsAvailable === 1 ? "" : "s"}
</Typography>
</Box> </Box>
<RecruitModal open={open} onClose={() => setOpen(false)} onRecruit={props.onRecruit} /> <RecruitModal open={open} onClose={() => setOpen(false)} onRecruit={props.onRecruit} />
</> </>

@ -23,12 +23,13 @@ export function RecruitModal(props: IRecruitPopupProps): React.ReactElement {
if (disabled) return; if (disabled) return;
// At this point, the only way this can fail is if you already // At this point, the only way this can fail is if you already
// have a gang member with the same name // have a gang member with the same name
if (!gang.recruitMember(name)) { if (!gang.recruitMember(name) && name !== "") {
dialogBoxCreate("You already have a gang member with this name!"); dialogBoxCreate("You already have a gang member with this name!");
return; return;
} }
props.onRecruit(); props.onRecruit();
setName("");
props.onClose(); props.onClose();
} }
@ -42,7 +43,7 @@ export function RecruitModal(props: IRecruitPopupProps): React.ReactElement {
return ( return (
<Modal open={props.open} onClose={props.onClose}> <Modal open={props.open} onClose={props.onClose}>
<Typography>Enter a name for your new Gang member:</Typography> <Typography>Enter a name for your new Gang Member:</Typography>
<br /> <br />
<TextField <TextField
autoFocus autoFocus
@ -50,6 +51,7 @@ export function RecruitModal(props: IRecruitPopupProps): React.ReactElement {
onChange={onChange} onChange={onChange}
type="text" type="text"
placeholder="unique name" placeholder="unique name"
spellCheck="false"
InputProps={{ InputProps={{
endAdornment: ( endAdornment: (
<Button disabled={disabled} onClick={recruit}> <Button disabled={disabled} onClick={recruit}>

@ -16,15 +16,15 @@ export const TerritoryInfoModal = ({ open, onClose }: IProps): React.ReactElemen
<Typography variant="h4">Clashing</Typography> <Typography variant="h4">Clashing</Typography>
<Typography> <Typography>
Every ~20 seconds, your gang has a chance to 'clash' with other gangs. Your chance to win a clash depends on Every ~20 seconds, your gang has a chance to 'clash' with other gangs. Your chance to win a clash depends on
your gang's power, which is listed in the display below. Your gang's power slowly accumulates over time. The your gang's power, found in the Territory display or with methods from the Gang API. Your gang's power slowly
accumulation rate is determined by the stats of all Gang members you have assigned to the 'Territory Warfare' accumulates over time, determined by the stats of all Gang members you have assigned to the 'Territory
task. Gang members that are not assigned to this task do not contribute to your gang's power. Your gang also Warfare' task. Gang members that are not assigned to this task do not contribute to your gang's power. Your
loses a small amount of power whenever you lose a clash. gang also loses a small amount of power whenever you lose a clash.
<br /> <br />
<br /> <br />
NOTE: Gang members assigned to 'Territory Warfare' can be killed during clashes. This can happen regardless of NOTE: Gang members assigned to 'Territory Warfare' can be killed during clashes. This can happen regardless of
whether you win or lose the clash. A gang member being killed results in both respect and power loss for your whether you win or lose the clash. A gang member being killed results in loss of both respect and power for
gang. your gang.
</Typography> </Typography>
<br /> <br />
<Typography variant="h4">Territory</Typography> <Typography variant="h4">Territory</Typography>

@ -65,8 +65,8 @@ export function TerritorySubpage(): React.ReactElement {
<Tooltip <Tooltip
title={ title={
<Typography> <Typography>
If this is enabled, then you will receive a pop-up notifying you whenever one of your Gang Members If this is enabled, you will receive a pop-up notifying you whenever one of your Gang Members dies in
dies in a territory clash. a territory clash.
</Typography> </Typography>
} }
> >

@ -213,10 +213,13 @@ const gang = {
createGang: RamCostConstants.GangApiBase / 4, createGang: RamCostConstants.GangApiBase / 4,
inGang: RamCostConstants.GangApiBase / 4, inGang: RamCostConstants.GangApiBase / 4,
getMemberNames: RamCostConstants.GangApiBase / 4, getMemberNames: RamCostConstants.GangApiBase / 4,
renameMember: 0,
getGangInformation: RamCostConstants.GangApiBase / 2, getGangInformation: RamCostConstants.GangApiBase / 2,
getOtherGangInformation: RamCostConstants.GangApiBase / 2, getOtherGangInformation: RamCostConstants.GangApiBase / 2,
getMemberInformation: RamCostConstants.GangApiBase / 2, getMemberInformation: RamCostConstants.GangApiBase / 2,
canRecruitMember: RamCostConstants.GangApiBase / 4, canRecruitMember: RamCostConstants.GangApiBase / 4,
getRecruitsAvailable: RamCostConstants.GangApiBase / 4,
respectForNextRecruit: RamCostConstants.GangApiBase / 4,
recruitMember: RamCostConstants.GangApiBase / 2, recruitMember: RamCostConstants.GangApiBase / 2,
getTaskNames: RamCostConstants.GangApiBase / 4, getTaskNames: RamCostConstants.GangApiBase / 4,
getTaskStats: RamCostConstants.GangApiBase / 4, getTaskStats: RamCostConstants.GangApiBase / 4,

@ -53,6 +53,32 @@ export function NetscriptGang(): InternalAPI<IGang> {
const gang = getGang(ctx); const gang = getGang(ctx);
return gang.members.map((member) => member.name); return gang.members.map((member) => member.name);
}, },
renameMember: (ctx) => (_memberName, _newName) => {
const gang = getGang(ctx);
const memberName = helpers.string(ctx, "memberName", _memberName);
const newName = helpers.string(ctx, "newName", _newName);
const member = gang.members.find((m) => m.name === memberName);
if (!memberName) {
throw helpers.makeRuntimeErrorMsg(ctx, `Invalid memberName: "" (empty string)`);
}
if (!newName) {
throw helpers.makeRuntimeErrorMsg(ctx, `Invalid newName: "" (empty string)`);
}
if (newName === memberName) {
throw helpers.makeRuntimeErrorMsg(ctx, `newName and memberName must be different, but both were: ${newName}`);
}
if (!member) {
helpers.log(ctx, () => `Failed to rename member: No member exists with memberName: ${memberName}`);
return false;
}
if (gang.members.map((m) => m.name).includes(newName)) {
helpers.log(ctx, () => `Failed to rename member: A different member already has the newName: ${newName}`);
return false;
}
member.name = newName;
helpers.log(ctx, () => `Renamed member from memberName: ${memberName} to newName: ${newName}`);
return true;
},
getGangInformation: (ctx) => () => { getGangInformation: (ctx) => () => {
const gang = getGang(ctx); const gang = getGang(ctx);
return { return {
@ -62,6 +88,7 @@ export function NetscriptGang(): InternalAPI<IGang> {
power: gang.getPower(), power: gang.getPower(),
respect: gang.respect, respect: gang.respect,
respectGainRate: gang.respectGainRate, respectGainRate: gang.respectGainRate,
respectForNextRecruit: gang.respectForNextRecruit(),
territory: gang.getTerritory(), territory: gang.getTerritory(),
territoryClashChance: gang.territoryClashChance, territoryClashChance: gang.territoryClashChance,
territoryWarfareEngaged: gang.territoryWarfareEngaged, territoryWarfareEngaged: gang.territoryWarfareEngaged,
@ -134,17 +161,31 @@ export function NetscriptGang(): InternalAPI<IGang> {
const gang = getGang(ctx); const gang = getGang(ctx);
return gang.canRecruitMember(); return gang.canRecruitMember();
}, },
getRecruitsAvailable: (ctx) => () => {
const gang = getGang(ctx);
return gang.getRecruitsAvailable();
},
respectForNextRecruit: (ctx) => () => {
const gang = getGang(ctx);
return gang.respectForNextRecruit();
},
recruitMember: (ctx) => (_memberName) => { recruitMember: (ctx) => (_memberName) => {
const memberName = helpers.string(ctx, "memberName", _memberName); const memberName = helpers.string(ctx, "memberName", _memberName);
const gang = getGang(ctx); const gang = getGang(ctx);
const recruited = gang.recruitMember(memberName); const recruited = gang.recruitMember(memberName);
if (recruited) { if (memberName === "") {
ctx.workerScript.log("gang.recruitMember", () => `Failed to recruit Gang Member. Name must be provided.`);
return false;
} else if (recruited) {
ctx.workerScript.log("gang.recruitMember", () => `Successfully recruited Gang Member '${memberName}'`); ctx.workerScript.log("gang.recruitMember", () => `Successfully recruited Gang Member '${memberName}'`);
return recruited;
} else { } else {
ctx.workerScript.log("gang.recruitMember", () => `Failed to recruit Gang Member '${memberName}'`); ctx.workerScript.log(
"gang.recruitMember",
() => `Failed to recruit Gang Member '${memberName}'. Name already used.`,
);
return recruited;
} }
return recruited;
}, },
getTaskNames: (ctx) => () => { getTaskNames: (ctx) => () => {
const gang = getGang(ctx); const gang = getGang(ctx);

@ -771,6 +771,8 @@ interface GangGenInfo {
respect: number; respect: number;
/** Respect earned per game cycle */ /** Respect earned per game cycle */
respectGainRate: number; respectGainRate: number;
/** Amount of Respect needed for next gang recruit, if possible */
respectForNextRecruit: number;
/** Amount of territory held */ /** Amount of territory held */
territory: number; territory: number;
/** Clash chance */ /** Clash chance */
@ -3397,6 +3399,18 @@ export interface Gang {
*/ */
getMemberNames(): string[]; getMemberNames(): string[];
/**
* Rename a Gang member to a new unique name.
* @remarks
* RAM cost: 0 GB
*
* Rename a Gang Member if none already has the new name.
* @param memberName - Name of the member to change.
* @param newName - New name for that gang member.
* @returns True if successful, and false if not.
*/
renameMember(memberName: string, newName: string): boolean;
/** /**
* Get information about your gang. * Get information about your gang.
* @remarks * @remarks
@ -3450,6 +3464,26 @@ export interface Gang {
*/ */
canRecruitMember(): boolean; canRecruitMember(): boolean;
/**
* Check how many gang members you can currently recruit.
* @remarks
* RAM cost: 1 GB
*
* @returns Number indicating how many members can be recruited,
* considering current reputation and gang size.
*/
getRecruitsAvailable(): number;
/**
* Check the amount of Respect needed for your next gang recruit.
* @remarks
* RAM cost: 1 GB
*
* @returns The static number value of Respect needed for the next
* recruit, with consideration to your current gang size.
* Returns `Infinity` if you have reached the gang size limit.
*/
respectForNextRecruit(): number;
/** /**
* Recruit a new gang member. * Recruit a new gang member.
* @remarks * @remarks