UI: Show hints of Gang mechanic in pre-endgame (#2723)

This commit is contained in:
catloversg
2026-05-07 05:22:43 +07:00
committed by GitHub
parent 2ab144cff2
commit eb431145ee
9 changed files with 216 additions and 106 deletions
+6
View File
@@ -40,6 +40,8 @@ import { CONSTANTS } from "../Constants";
import { BladeburnerConstants } from "../Bladeburner/data/Constants";
import type { PlayerObject } from "../PersonObjects/Player/PlayerObject";
import { CovenantCampaign } from "./ui/CovenantCampaign";
import { GangCampaign } from "./ui/GangCampaign";
import { GangConstants } from "../Gang/data/Constants";
interface FactionInfoParams {
infoText?: JSX.Element;
@@ -812,3 +814,7 @@ export const FactionInfos: Record<FactionName, FactionInfo> = {
},
}),
};
for (const factionName of GangConstants.Names) {
FactionInfos[factionName].campaign = () => <GangCampaign factionName={factionName} />;
}
+7
View File
@@ -7,6 +7,8 @@ import Typography from "@mui/material/Typography";
import Button from "@mui/material/Button";
import { KEY } from "../../utils/KeyboardEventKey";
import { FactionName } from "@enums";
import { canCreateGang } from "../../Gang/helpers";
import { dialogBoxCreate } from "../../ui/React/DialogBox";
interface IProps {
open: boolean;
@@ -33,6 +35,11 @@ export function CreateGangModal(props: IProps): React.ReactElement {
}
function createGang(): void {
const checkResult = canCreateGang(props.facName);
if (!checkResult.success) {
dialogBoxCreate(checkResult.message);
return;
}
Player.startGang(props.facName, isHacking());
props.onClose();
Router.toPage(Page.Gang);
-2
View File
@@ -17,7 +17,6 @@ import { Player } from "@player";
import { Typography, Button } from "@mui/material";
import { FactionWorkType } from "@enums";
import { GangButton } from "./GangButton";
import { FactionWork } from "../../Work/FactionWork";
import { useCycleRerender } from "../../ui/React/hooks";
import { favorNeededToDonate } from "../formulas/donation";
@@ -111,7 +110,6 @@ function MainPage({ faction, rerender, onAugmentations }: IMainProps): React.Rea
{faction.name}
</Typography>
<Info faction={faction} factionInfo={factionInfo} />
<GangButton faction={faction} />
{!isPlayersGang && (
<>
{factionInfo.offersWork() && (
-80
View File
@@ -1,80 +0,0 @@
import { Button, Typography, Box, Paper, Tooltip } from "@mui/material";
import React, { useState } from "react";
import { GangConstants } from "../../Gang/data/Constants";
import { Router } from "../../ui/GameRoot";
import { Page } from "../../ui/Router";
import { Player } from "@player";
import { Faction } from "../Faction";
import { CreateGangModal } from "./CreateGangModal";
interface IProps {
faction: Faction;
}
export function GangButton({ faction }: IProps): React.ReactElement {
const [gangOpen, setGangOpen] = useState(false);
if (
!GangConstants.Names.includes(faction.name) || // not even a gang
!Player.isAwareOfGang() || // doesn't know about gang
(Player.gang && Player.getGangName() !== faction.name) // already in another gang
) {
return <></>;
}
let data = {
enabled: false,
title: "",
tooltip: "" as string | React.ReactElement,
description: "",
};
if (Player.gang) {
data = {
enabled: true,
title: "Manage Gang",
tooltip: "",
description: "Manage a gang for this Faction. Gangs will earn you money and faction reputation",
};
} else {
const checkResult = Player.canAccessGang();
data = {
enabled: checkResult.success,
title: "Create Gang",
tooltip: !checkResult.success ? (
<Typography>Unlocked when reaching {GangConstants.GangKarmaRequirement} karma</Typography>
) : (
""
),
description: "Create a gang for this Faction. Gangs will earn you money and faction reputation",
};
}
const manageGang = (): void => {
// If player already has a gang, just go to the gang UI
if (Player.inGang()) {
return Router.toPage(Page.Gang);
}
setGangOpen(true);
};
return (
<>
<Box>
<Paper sx={{ my: 1, p: 1 }}>
<Tooltip title={data.tooltip}>
<span>
<Button onClick={manageGang} disabled={!data.enabled}>
{data.title}
</Button>
</span>
</Tooltip>
<Typography>{data.description}</Typography>
</Paper>
</Box>
<CreateGangModal facName={faction.name} open={gangOpen} onClose={() => setGangOpen(false)} />
</>
);
}
+110
View File
@@ -0,0 +1,110 @@
import Box from "@mui/material/Box";
import Button from "@mui/material/Button";
import Paper from "@mui/material/Paper";
import Tooltip from "@mui/material/Tooltip";
import Typography from "@mui/material/Typography";
import { Player } from "@player";
import React, { useState } from "react";
import { knowAboutBitverse } from "../../BitNode/BitNodeUtils";
import { GangConstants } from "../../Gang/data/Constants";
import { Router } from "../../ui/GameRoot";
import { Modal } from "../../ui/React/Modal";
import { Page } from "../../ui/Router";
import { FactionName } from "../Enums";
import { CreateGangModal } from "./CreateGangModal";
import { Option } from "./Option";
function GangIncompleteCampaign() {
const [open, setOpen] = useState(false);
return (
<>
<Option
buttonText={"Execute the formation plan"}
infoText={
"The tension between our faction and its rivals has been rising. The leader plans to form a specialized " +
"group under your command to strengthen our position by improving our standing and expanding our resources."
}
onClick={() => setOpen(true)}
></Option>
<Modal open={open} onClose={() => setOpen(false)}>
<Typography component="div">
Each time you attempt to execute the plan, it is abruptly interrupted for reasons no one can explain. You
receive the same distorted message every time:
<br />
<br />
#@)($*&@__Y0U__^%$#@&*()__HAV3__(&@#*$%(@
<br />
()@#*$%(__N0T__@&$#)@*(__S33N__)(*@#&$)(
<br />
@&*($#@&__TH3__#@A&#@*)(@$#@)*
<br />
%$#@&()@__TRU1H__()*@#$&()@#$
</Typography>
</Modal>
</>
);
}
export function GangCampaign({ factionName }: { factionName: FactionName }) {
const [gangOpen, setGangOpen] = useState(false);
if (!GangConstants.Names.includes(factionName)) {
throw new Error(`Cannot create gang with ${factionName}`);
}
if (!knowAboutBitverse()) {
return <GangIncompleteCampaign />;
}
const data = {
enabled: false,
title: "",
tooltip: "" as string | React.ReactElement,
description: "",
};
if (Player.gang) {
if (Player.getGangName() !== factionName) {
data.enabled = false;
data.title = "Create Gang";
data.tooltip = "You already created a gang with another faction";
} else {
data.enabled = true;
data.title = "Manage Gang";
data.description = "Manage a gang for this Faction. Gangs will earn you money and faction reputation";
}
} else {
const checkResult = Player.canAccessGang();
data.enabled = checkResult.success;
data.title = "Create Gang";
data.tooltip = !checkResult.success ? checkResult.message : "";
data.description = "Create a gang for this Faction. Gangs will earn you money and faction reputation";
}
const manageGang = (): void => {
// If player already has a gang, just go to the gang UI
if (Player.inGang()) {
return Router.toPage(Page.Gang);
}
setGangOpen(true);
};
return (
<>
<Box>
<Paper sx={{ my: 1, p: 1 }}>
<Tooltip title={data.tooltip}>
<span>
<Button onClick={manageGang} disabled={!data.enabled}>
{data.title}
</Button>
</span>
</Tooltip>
<Typography>{data.description}</Typography>
</Paper>
</Box>
<CreateGangModal facName={factionName} open={gangOpen} onClose={() => setGangOpen(false)} />
</>
);
}
+1 -1
View File
@@ -23,7 +23,7 @@ export const GangConstants = {
FactionName.SpeakersForTheDead,
FactionName.NiteSec,
FactionName.TheBlackHand,
] as string[],
] as FactionName[],
GangKarmaRequirement: -54000,
/** Normal number of game cycles processed at once (2 seconds) */
minCyclesToProcess: 2000 / CONSTANTS.MilliPerCycle,
+26
View File
@@ -0,0 +1,26 @@
import { Result } from "@nsdefs";
import { Player } from "@player";
import { FactionName } from "../Enums";
import { GangConstants } from "./data/Constants";
export function canCreateGang(faction: FactionName): Result {
if (Player.gang) {
return { success: false, message: "You already have a gang." };
}
const checkResult = Player.canAccessGang();
if (!checkResult.success) {
return { success: false, message: checkResult.message };
}
if (!GangConstants.Names.includes(faction)) {
return {
success: false,
message: `${faction} does not allow creating a gang. You can only do that with ${GangConstants.Names.join(
", ",
)}.`,
};
}
if (!Player.factions.includes(faction)) {
return { success: false, message: `You are not a member of ${faction}.` };
}
return { success: true };
}
+2 -17
View File
@@ -7,13 +7,13 @@ import { type InternalAPI, type NetscriptContext, setRemovedFunctions } from "..
import { GangPromise, RecruitmentResult } from "../Gang/Gang";
import { Player } from "@player";
import { FactionName } from "@enums";
import { GangConstants } from "../Gang/data/Constants";
import { AllGangs } from "../Gang/AllGangs";
import { GangMemberTasks } from "../Gang/GangMemberTasks";
import { GangMemberUpgrades } from "../Gang/GangMemberUpgrades";
import { helpers } from "../Netscript/NetscriptHelpers";
import { getEnumHelper } from "../utils/EnumHelper";
import { CONSTANTS } from "../Constants";
import { canCreateGang } from "../Gang/helpers";
export function NetscriptGang(): InternalAPI<IGang> {
/** Functions as an API check and also returns the gang object */
@@ -40,26 +40,11 @@ export function NetscriptGang(): InternalAPI<IGang> {
const gangFunctions: InternalAPI<IGang> = {
createGang: (ctx) => (_faction) => {
const faction = getEnumHelper("FactionName").nsGetMember(ctx, _faction);
if (Player.gang) {
return false;
}
const checkResult = Player.canAccessGang();
const checkResult = canCreateGang(faction);
if (!checkResult.success) {
helpers.log(ctx, () => checkResult.message);
return false;
}
if (!GangConstants.Names.includes(faction)) {
helpers.log(
ctx,
() =>
`${faction} does not allow creating a gang. You can only do that with ${GangConstants.Names.join(", ")}.`,
);
return false;
}
if (!Player.factions.includes(faction)) {
helpers.log(ctx, () => `You are not a member of ${faction}.`);
return false;
}
const isHacking = faction === FactionName.NiteSec || faction === FactionName.TheBlackHand;
Player.startGang(faction, isHacking);
+64 -6
View File
@@ -1,8 +1,10 @@
import { FactionName } from "@enums";
import { Player } from "@player";
import { Gang } from "../../../src/Gang/Gang";
import { AllGangs } from "../../../src/Gang/AllGangs";
import { getNS, initGameEnvironment, setupBasicTestingEnvironment } from "../Utilities";
import { getNS, getWorkerScriptAndNS, initGameEnvironment, setupBasicTestingEnvironment } from "../Utilities";
import { GangConstants } from "../../../src/Gang/data/Constants";
import { joinFaction } from "../../../src/Faction/FactionHelpers";
import { Factions } from "../../../src/Faction/Factions";
beforeAll(() => {
initGameEnvironment();
@@ -10,12 +12,16 @@ beforeAll(() => {
beforeEach(() => {
setupBasicTestingEnvironment();
// Give the player a gang so gang API is accessible
Player.gang = new Gang(FactionName.SlumSnakes, false);
Player.sourceFiles.set(2, 3);
});
describe("ns.gang.getAllGangInformation", () => {
it("should return territory and power info for all gangs including the player's", () => {
beforeEach(() => {
// Give the player a gang so gang API is accessible
Player.startGang(FactionName.SlumSnakes, false);
});
test("should return territory and power info for all gangs including the player's", () => {
const ns = getNS();
const info = ns.gang.getAllGangInformation();
const gangNames = Object.keys(info);
@@ -35,7 +41,7 @@ describe("ns.gang.getAllGangInformation", () => {
}
});
it("should return copies, not references to the original AllGangs data", () => {
test("should return copies, not references to the original AllGangs data", () => {
const ns = getNS();
const info = ns.gang.getAllGangInformation();
@@ -44,3 +50,55 @@ describe("ns.gang.getAllGangInformation", () => {
expect(AllGangs[FactionName.SlumSnakes].power).not.toBe(999999);
});
});
describe("createGang", () => {
test("Success", () => {
const ns = getNS();
Player.karma = GangConstants.GangKarmaRequirement;
joinFaction(Factions[FactionName.SlumSnakes]);
expect(ns.gang.createGang(FactionName.SlumSnakes)).toBe(true);
});
describe("Failure", () => {
test("Already have a gang", () => {
const { ws, ns } = getWorkerScriptAndNS();
Player.karma = GangConstants.GangKarmaRequirement;
joinFaction(Factions[FactionName.SlumSnakes]);
expect(ns.gang.createGang(FactionName.SlumSnakes)).toBe(true);
expect(ns.gang.createGang(FactionName.SlumSnakes)).toBe(false);
expect(ws.scriptRef.logs[0]).toMatch("You already have a gang");
expect(ns.gang.createGang(FactionName.Tetrads)).toBe(false);
expect(ws.scriptRef.logs[1]).toMatch("You already have a gang");
});
test("Disabled by advanced options", () => {
const { ws, ns } = getWorkerScriptAndNS();
Player.bitNodeOptions.disableGang = true;
expect(ns.gang.createGang(FactionName.SlumSnakes)).toBe(false);
expect(ws.scriptRef.logs[0]).toMatch("Gang is disabled by advanced options");
});
test("Not have Source-File 2", () => {
const { ws, ns } = getWorkerScriptAndNS();
Player.sourceFiles.set(2, 0);
expect(ns.gang.createGang(FactionName.SlumSnakes)).toBe(false);
expect(ws.scriptRef.logs[0]).toMatch("You do not have Source-File 2");
});
test("Not enough karma", () => {
const { ws, ns } = getWorkerScriptAndNS();
expect(ns.gang.createGang(FactionName.SlumSnakes)).toBe(false);
expect(ws.scriptRef.logs[0]).toMatch("Your karma must be less than or equal to");
});
test("Invalid gang faction", () => {
const { ws, ns } = getWorkerScriptAndNS();
Player.karma = GangConstants.GangKarmaRequirement;
joinFaction(Factions[FactionName.SlumSnakes]);
expect(ns.gang.createGang(FactionName.Illuminati)).toBe(false);
expect(ws.scriptRef.logs[0]).toMatch("does not allow creating a gang");
});
test("Not a faction member", () => {
const { ws, ns } = getWorkerScriptAndNS();
Player.karma = GangConstants.GangKarmaRequirement;
expect(ns.gang.createGang(FactionName.SlumSnakes)).toBe(false);
expect(ws.scriptRef.logs[0]).toMatch("You are not a member of");
});
});
});