FEATURE: BitNode options (#1411)

This commit is contained in:
catloversg
2024-07-15 04:30:30 +07:00
committed by GitHub
parent 0e1e8a9862
commit 783120c886
71 changed files with 1315 additions and 308 deletions
+433
View File
@@ -0,0 +1,433 @@
import { type BitNodeBooleanOptions } from "@nsdefs";
import React from "react";
import {
Box,
Button,
Collapse,
ListItemButton,
ListItemText,
MenuItem,
Paper,
Select,
TextField,
Tooltip,
Typography,
} from "@mui/material";
import { OptionSwitch } from "../../ui/React/OptionSwitch";
import { ButtonWithTooltip } from "../../ui/Components/ButtonWithTooltip";
import { ExpandLess, ExpandMore } from "@mui/icons-material";
import { JSONMap } from "../../Types/Jsonable";
import { Settings } from "../../Settings/Settings";
import { Player } from "@player";
interface SourceFileButtonRowProps {
sfNumber: number;
sfLevel: number;
sfActiveLevel: number;
callbacks: BitNodeAdvancedOptionsProps["callbacks"];
}
function SourceFileButtonRow({
sfNumber,
sfLevel,
sfActiveLevel,
callbacks,
}: SourceFileButtonRowProps): React.ReactElement {
const title = `SF-${sfNumber}`;
const sourceFileLevelTool =
sfNumber !== 12 ? (
[...Array(sfLevel + 1).keys()].map((level) => (
<Button
key={level}
onClick={() => {
callbacks.setSfActiveLevel(sfNumber, level);
}}
sx={{
marginRight: "0.5rem !important",
border: level === sfActiveLevel ? `1px solid ${Settings.theme.info}` : "",
minWidth: "40px",
}}
>
{level}
</Button>
))
) : (
// The usage of TextField instead of NumberInput is intentional.
<TextField
sx={{ maxWidth: "185px" }}
value={sfActiveLevel}
onChange={(event) => {
// Empty string will be automatically changed to "0".
if (event.target.value === "") {
callbacks.setSfActiveLevel(sfNumber, 0);
return;
}
const level = Number.parseInt(event.target.value);
if (!Number.isFinite(level) || level < 0 || level > sfLevel) {
return;
}
callbacks.setSfActiveLevel(sfNumber, level);
}}
></TextField>
);
const extraInfo =
sfNumber === 12 ? (
<td>
<Typography marginLeft="1rem">Max level: {sfLevel}</Typography>
</td>
) : null;
return (
<tr>
<td>
<Typography>{title}</Typography>
</td>
<td>{sourceFileLevelTool}</td>
{extraInfo}
</tr>
);
}
function SourceFileOverrides({
currentSourceFiles,
sourceFileOverrides,
callbacks,
getSfLevel,
}: {
currentSourceFiles: BitNodeAdvancedOptionsProps["currentSourceFiles"];
sourceFileOverrides: BitNodeAdvancedOptionsProps["sourceFileOverrides"];
callbacks: BitNodeAdvancedOptionsProps["callbacks"];
getSfLevel: (sfNumber: number) => number;
}): React.ReactElement {
const firstSourceFile = React.useMemo(
() => (currentSourceFiles.size > 0 ? [...currentSourceFiles.keys()][0] : null),
[currentSourceFiles],
);
const [selectElementValue, setSelectElementValue] = React.useState<number | null>(firstSourceFile);
const getMenuItemList = (data: typeof sourceFileOverrides): number[] => {
return [...currentSourceFiles.keys()].filter((sfNumber) => ![...data.keys()].includes(sfNumber));
};
const menuItemList = getMenuItemList(sourceFileOverrides);
React.useEffect(() => {
if (sourceFileOverrides.size === 0) {
setSelectElementValue(firstSourceFile);
}
}, [sourceFileOverrides, firstSourceFile]);
const basicNote = `Changing the active level of a SF is temporary; you still permanently own that SF level. For example, if
you enter BN 1.3 while having SF 1.2 but with the active level set to 0, you will not get the bonuses from SF
1.1 or SF 1.2, but you will still earn SF 1.3 when destroying the BN.`;
const note = currentSourceFiles.has(10) ? (
<>
<Typography>Note:</Typography>
<ul style={{ marginTop: 0 }}>
<li>{basicNote}</li>
<li>
Changing the active level of SF 10 does not affect your current sleeves or the maximum number of sleeves.
</li>
</ul>
</>
) : (
<>
<Typography>Note: {basicNote}</Typography>
<br />
</>
);
return (
<>
<Typography>Override active level of Source-File:</Typography>
<br />
<Typography component="div">{note}</Typography>
<div>
<Select
disabled={menuItemList.length === 0}
value={selectElementValue ?? ""}
onChange={(event) => {
setSelectElementValue(Number(event.target.value));
}}
sx={{ minWidth: "80px" }}
>
{menuItemList.map((sfNumber) => (
<MenuItem key={sfNumber} value={sfNumber}>
SF-{sfNumber}
</MenuItem>
))}
</Select>
<Button
disabled={menuItemList.length === 0}
onClick={() => {
if (!selectElementValue) {
return;
}
const newSfOverrides = new JSONMap(sourceFileOverrides);
newSfOverrides.set(selectElementValue, getSfLevel(selectElementValue));
const newMenuItemList = getMenuItemList(newSfOverrides);
if (newMenuItemList.length > 0) {
setSelectElementValue(newMenuItemList[0]);
} else {
setSelectElementValue(null);
}
callbacks.setSfOverrides(newSfOverrides);
}}
sx={{ marginLeft: "1rem" }}
>
Add
</Button>
<ButtonWithTooltip
normalTooltip="Remove all overridden SF"
disabledTooltip={sourceFileOverrides.size === 0 ? "No overridden SF" : ""}
onClick={() => {
callbacks.setSfOverrides(new JSONMap());
}}
buttonProps={{ sx: { marginLeft: "1rem" } }}
>
Remove all
</ButtonWithTooltip>
</div>
<br />
{sourceFileOverrides.size > 0 && (
<>
<table>
<tbody>
<tr>
<td>
<Tooltip title="Set active level for all chosen SF">
<Typography minWidth="7rem">Set all SF</Typography>
</Tooltip>
</td>
<td>
{[0, 1, 2, 3].map((level) => (
<ButtonWithTooltip
key={level}
onClick={() => {
const newSfOverrides = new JSONMap(sourceFileOverrides);
for (const [sfNumber] of newSfOverrides) {
newSfOverrides.set(sfNumber, Math.min(level, getSfLevel(sfNumber)));
}
callbacks.setSfOverrides(newSfOverrides);
}}
buttonProps={{ sx: { marginRight: "0.5rem", minWidth: "40px" } }}
>
{level}
</ButtonWithTooltip>
))}
</td>
</tr>
{[...sourceFileOverrides.keys()].map((sfNumber) => (
<SourceFileButtonRow
key={sfNumber}
sfNumber={sfNumber}
sfLevel={getSfLevel(sfNumber)}
sfActiveLevel={sourceFileOverrides.get(sfNumber) ?? 0}
callbacks={callbacks}
></SourceFileButtonRow>
))}
</tbody>
</table>
<br />
</>
)}
</>
);
}
function IntelligenceOverride({
intelligenceOverride,
callbacks,
}: {
intelligenceOverride: BitNodeAdvancedOptionsProps["intelligenceOverride"];
callbacks: BitNodeAdvancedOptionsProps["callbacks"];
}): React.ReactElement {
const [enabled, setEnabled] = React.useState<boolean>(false);
const [lastValueOfIntelligenceOverride, setLastValueOfIntelligenceOverride] = React.useState<number | undefined>(
Player.skills.intelligence,
);
return (
<OptionSwitch
disabled={Player.skills.intelligence <= 0}
checked={false}
onChange={(value) => {
setEnabled(value);
if (!value) {
// If this option is disabled, save last value and reset data.
setLastValueOfIntelligenceOverride(intelligenceOverride);
callbacks.setIntelligenceOverride(undefined);
return;
} else {
// If this option is enabled, load last value.
callbacks.setIntelligenceOverride(lastValueOfIntelligenceOverride);
}
}}
text={
<>
<Typography component="div" display="flex" gap="1rem">
<Typography display="flex" alignItems="center">
Override Intelligence:
</Typography>
<TextField
sx={{ maxWidth: "4rem" }}
disabled={!enabled}
value={intelligenceOverride !== undefined ? intelligenceOverride : ""}
onChange={(event) => {
// Empty string will be automatically changed to "0".
if (event.target.value === "") {
callbacks.setIntelligenceOverride(0);
return;
}
const value = Number.parseInt(event.target.value);
if (!Number.isInteger(value) || value < 0) {
return;
}
callbacks.setIntelligenceOverride(value);
}}
></TextField>
</Typography>
</>
}
tooltip={
<>
<Typography component="div">
The Intelligence bonuses for you and your Sleeves will be limited by this value. For example:
<ul>
<li>
If your Intelligence is 1000 and you set this value to 500, the "effective" Intelligence, which is used
for bonus calculation, is only 500.
</li>
<li>
If a Sleeve's Intelligence is 200 and you set this value to 500, the "effective" Intelligence, which is
used for bonus calculation, is still 200.
</li>
</ul>
</Typography>
<Typography>
You will still gain Intelligence experience as normal. Intelligence Override only affects the Intelligence
bonus.
</Typography>
<br />
<Typography>
The "effective" Intelligence will be shown in the character overview. If the effective value is different
from the original value, you can hover your mouse over it to see the original value.
</Typography>
</>
}
/>
);
}
interface BitNodeAdvancedOptionsProps {
targetBitNode: number;
currentSourceFiles: Map<number, number>;
sourceFileOverrides: JSONMap<number, number>;
intelligenceOverride: number | undefined;
bitNodeBooleanOptions: BitNodeBooleanOptions;
callbacks: {
setSfOverrides: (value: JSONMap<number, number>) => void;
setSfActiveLevel: (sfNumber: number, sfLevel: number) => void;
setIntelligenceOverride: (value: number | undefined) => void;
setBooleanOption: (key: keyof BitNodeBooleanOptions, value: boolean) => void;
};
}
export function BitNodeAdvancedOptions({
targetBitNode,
currentSourceFiles,
sourceFileOverrides,
intelligenceOverride,
bitNodeBooleanOptions,
callbacks,
}: BitNodeAdvancedOptionsProps): React.ReactElement {
const [open, setOpen] = React.useState(false);
const getSfLevel = React.useCallback(
(sfNumber: number): number => {
return currentSourceFiles.get(sfNumber) ?? 0;
},
[currentSourceFiles],
);
return (
<Box component={Paper} sx={{ mt: 1, p: 1 }}>
<ListItemButton disableGutters onClick={() => setOpen((old) => !old)} sx={{ padding: "4px 8px" }}>
<ListItemText primary={<Typography variant="h6">Advanced options</Typography>} />
{open ? <ExpandLess color="primary" /> : <ExpandMore color="primary" />}
</ListItemButton>
<Collapse in={open}>
<Box sx={{ padding: "0 1rem" }}>
<OptionSwitch
checked={bitNodeBooleanOptions.restrictHomePCUpgrade}
onChange={(value) => {
callbacks.setBooleanOption("restrictHomePCUpgrade", value);
}}
text="Restrict max RAM and core of Home PC"
tooltip="The home computer's maximum RAM and number of cores are lower than normal. Max RAM: 128GB. Max core: 1."
/>
<OptionSwitch
disabled={getSfLevel(2) === 0 && targetBitNode !== 2}
checked={bitNodeBooleanOptions.disableGang}
onChange={(value) => {
callbacks.setBooleanOption("disableGang", value);
}}
text="Disable Gang"
tooltip="Disable Gang"
/>
<OptionSwitch
disabled={getSfLevel(3) === 0 && targetBitNode !== 3}
checked={bitNodeBooleanOptions.disableCorporation}
onChange={(value) => {
callbacks.setBooleanOption("disableCorporation", value);
}}
text="Disable Corporation"
tooltip="Disable Corporation"
/>
<OptionSwitch
disabled={getSfLevel(6) === 0 && getSfLevel(7) === 0 && targetBitNode !== 6 && targetBitNode !== 7}
checked={bitNodeBooleanOptions.disableBladeburner}
onChange={(value) => {
callbacks.setBooleanOption("disableBladeburner", value);
}}
text="Disable Bladeburner"
tooltip="Disable Bladeburner"
/>
<OptionSwitch
checked={bitNodeBooleanOptions.disable4SData}
onChange={(value) => {
callbacks.setBooleanOption("disable4SData", value);
}}
text="Disable 4S Market Data"
tooltip="Disable 4S Market Data"
/>
<OptionSwitch
disabled={getSfLevel(9) === 0 && targetBitNode !== 9}
checked={bitNodeBooleanOptions.disableHacknetServer}
onChange={(value) => {
callbacks.setBooleanOption("disableHacknetServer", value);
}}
text="Disable Hacknet Server"
tooltip="Hacknet Node is re-enabled in place of Hacknet Server."
/>
<OptionSwitch
disabled={getSfLevel(10) === 0 && targetBitNode !== 10}
checked={bitNodeBooleanOptions.disableSleeveExpAndAugmentation}
onChange={(value) => {
callbacks.setBooleanOption("disableSleeveExpAndAugmentation", value);
}}
text="Disable Sleeves' experience and augmentation"
tooltip="Sleeves cannot gain experience or install augmentations"
/>
<IntelligenceOverride
intelligenceOverride={intelligenceOverride}
callbacks={callbacks}
></IntelligenceOverride>
<br />
<SourceFileOverrides
currentSourceFiles={currentSourceFiles}
sourceFileOverrides={sourceFileOverrides}
callbacks={callbacks}
getSfLevel={getSfLevel}
></SourceFileOverrides>
</Box>
</Collapse>
</Box>
);
}