Merge pull request #3184 from nickofolas/improvement/sleeve-and-clash-ui

Sleeve and Territory UI refresh
This commit is contained in:
hydroflame 2022-03-19 11:23:28 -04:00 committed by GitHub
commit 15badafd37
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 457 additions and 360 deletions

@ -3,7 +3,6 @@
*/ */
import React, { useState } from "react"; import React, { useState } from "react";
import { useGang } from "./Context"; import { useGang } from "./Context";
import { generateTableRow } from "./GangMemberStats";
import Typography from "@mui/material/Typography"; import Typography from "@mui/material/Typography";
import Button from "@mui/material/Button"; import Button from "@mui/material/Button";
@ -22,7 +21,7 @@ import { GangMember } from "../GangMember";
import { UpgradeType } from "../data/upgrades"; import { UpgradeType } from "../data/upgrades";
import { use } from "../../ui/Context"; import { use } from "../../ui/Context";
import { Settings } from "../../Settings/Settings"; import { Settings } from "../../Settings/Settings";
import { characterOverviewStyles as useStyles } from "../../ui/React/CharacterOverview"; import { StatsRow } from "../../ui/React/StatsRow";
interface INextRevealProps { interface INextRevealProps {
upgrades: string[]; upgrades: string[];
@ -91,7 +90,6 @@ interface IPanelProps {
} }
function GangMemberUpgradePanel(props: IPanelProps): React.ReactElement { function GangMemberUpgradePanel(props: IPanelProps): React.ReactElement {
const classes = useStyles();
const gang = useGang(); const gang = useGang();
const player = use.Player(); const player = use.Player();
const setRerender = useState(false)[1]; const setRerender = useState(false)[1];
@ -178,12 +176,12 @@ function GangMemberUpgradePanel(props: IPanelProps): React.ReactElement {
> >
<Table> <Table>
<TableBody> <TableBody>
{generateTableRow("Hacking", props.member.hack, props.member.hack_exp, Settings.theme.hack, classes)} <StatsRow name="Hacking" color={Settings.theme.hack} data={{ level: props.member.hack, exp: props.member.hack_exp }} />
{generateTableRow("Strength", props.member.str, props.member.str_exp, Settings.theme.combat, classes)} <StatsRow name="Strength" color={Settings.theme.combat} data={{ level: props.member.str, exp: props.member.str_exp }} />
{generateTableRow("Defense", props.member.def, props.member.def_exp, Settings.theme.combat, classes)} <StatsRow name="Defense" color={Settings.theme.combat} data={{ level: props.member.def, exp: props.member.def_exp }} />
{generateTableRow("Dexterity", props.member.dex, props.member.dex_exp, Settings.theme.combat, classes)} <StatsRow name="Dexterity" color={Settings.theme.combat} data={{ level: props.member.dex, exp: props.member.dex_exp }} />
{generateTableRow("Agility", props.member.agi, props.member.agi_exp, Settings.theme.combat, classes)} <StatsRow name="Agility" color={Settings.theme.combat} data={{ level: props.member.agi, exp: props.member.agi_exp }} />
{generateTableRow("Charisma", props.member.cha, props.member.cha_exp, Settings.theme.cha, classes)} <StatsRow name="Charisma" color={Settings.theme.cha} data={{ level: props.member.cha, exp: props.member.cha_exp }} />
</TableBody> </TableBody>
</Table> </Table>
</Tooltip> </Tooltip>

@ -17,36 +17,14 @@ import {
import { numeralWrapper } from "../../ui/numeralFormat"; import { numeralWrapper } from "../../ui/numeralFormat";
import { GangMember } from "../GangMember"; import { GangMember } from "../GangMember";
import { Settings } from "../../Settings/Settings"; import { Settings } from "../../Settings/Settings";
import { formatNumber } from "../../utils/StringHelperFunctions";
import { MoneyRate } from "../../ui/React/MoneyRate"; import { MoneyRate } from "../../ui/React/MoneyRate";
import { StatsRow } from "../../ui/React/StatsRow";
import { characterOverviewStyles as useStyles } from "../../ui/React/CharacterOverview"; import { characterOverviewStyles as useStyles } from "../../ui/React/CharacterOverview";
interface IProps { interface IProps {
member: GangMember; member: GangMember;
} }
export const generateTableRow = (
name: string,
level: number,
exp: number,
color: string,
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
classes: any
): React.ReactElement => {
return (
<TableRow>
<TableCell classes={{ root: classes.cellNone }}>
<Typography style={{ color: color }}>{name}</Typography>
</TableCell>
<TableCell align="right" classes={{ root: classes.cellNone }}>
<Typography style={{ color: color }}>
{formatNumber(level, 0)} ({numeralWrapper.formatExp(exp)} exp)
</Typography>
</TableCell>
</TableRow>
)
}
export function GangMemberStats(props: IProps): React.ReactElement { export function GangMemberStats(props: IProps): React.ReactElement {
const classes = useStyles(); const classes = useStyles();
@ -102,12 +80,12 @@ export function GangMemberStats(props: IProps): React.ReactElement {
> >
<Table sx={{ display: 'table', mb: 1, width: '100%' }}> <Table sx={{ display: 'table', mb: 1, width: '100%' }}>
<TableBody> <TableBody>
{generateTableRow("Hacking", props.member.hack, props.member.hack_exp, Settings.theme.hack, classes)} <StatsRow name="Hacking" color={Settings.theme.hack} data={{ level: props.member.hack, exp: props.member.hack_exp }} />
{generateTableRow("Strength", props.member.str, props.member.str_exp, Settings.theme.combat, classes)} <StatsRow name="Strength" color={Settings.theme.combat} data={{ level: props.member.str, exp: props.member.str_exp }} />
{generateTableRow("Defense", props.member.def, props.member.def_exp, Settings.theme.combat, classes)} <StatsRow name="Defense" color={Settings.theme.combat} data={{ level: props.member.def, exp: props.member.def_exp }} />
{generateTableRow("Dexterity", props.member.dex, props.member.dex_exp, Settings.theme.combat, classes)} <StatsRow name="Dexterity" color={Settings.theme.combat} data={{ level: props.member.dex, exp: props.member.dex_exp }} />
{generateTableRow("Agility", props.member.agi, props.member.agi_exp, Settings.theme.combat, classes)} <StatsRow name="Agility" color={Settings.theme.combat} data={{ level: props.member.agi, exp: props.member.agi_exp }} />
{generateTableRow("Charisma", props.member.cha, props.member.cha_exp, Settings.theme.cha, classes)} <StatsRow name="Charisma" color={Settings.theme.cha} data={{ level: props.member.cha, exp: props.member.cha_exp }} />
<TableRow> <TableRow>
<TableCell classes={{ root: classes.cellNone }}> <TableCell classes={{ root: classes.cellNone }}>
<br /> <br />

@ -0,0 +1,54 @@
import React from "react";
import Typography from "@mui/material/Typography";
import { Modal } from "../../ui/React/Modal";
interface IProps {
open: boolean;
onClose: () => void;
}
export const TerritoryInfoModal = ({ open, onClose }: IProps): React.ReactElement => {
return (
<Modal open={open} onClose={onClose}>
<>
<Typography variant='h4'>
Clashing
</Typography>
<Typography>
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
accumulation rate is determined by the stats of all Gang members you have assigned to the 'Territory Warfare'
task. Gang members that are not assigned to this task do not contribute to your gang's power. Your gang also
loses a small amount of power whenever you lose a clash.
<br />
<br />
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
gang.
</Typography>
<br />
<Typography variant='h4'>
Territory
</Typography>
<Typography>
The amount of territory you have affects all aspects of your Gang members' production, including money, respect,
and wanted level. It is very beneficial to have high territory control.
<br />
<br />
To increase your chances of winning territory, assign gang members to "Territory Warfare". This will build your
gang power. Then, enable "Engage in Territory Warfare" to start fighting over territory.
</Typography>
<br />
<Typography variant='h4'>
Territory Clash Chance
</Typography>
<Typography>
This percentage represents the chance you have of 'clashing' with another gang. If you do not wish to
gain/lose territory, then keep this percentage at 0% by not engaging in territory warfare.
</Typography>
</>
</Modal >
);
}

@ -1,133 +1,100 @@
/** /**
* React Component for the territory subpage. * React Component for the territory subpage.
*/ */
import React from "react"; import React, { useState } from "react";
import {
Container,
Button,
Paper,
Box,
Tooltip,
Switch,
FormControlLabel,
Typography
} from "@mui/material";
import { Help } from "@mui/icons-material";
import { numeralWrapper } from "../../ui/numeralFormat"; import { numeralWrapper } from "../../ui/numeralFormat";
import { formatNumber } from "../../utils/StringHelperFunctions"; import { formatNumber } from "../../utils/StringHelperFunctions";
import { AllGangs } from "../AllGangs";
import { useGang } from "./Context";
import Typography from "@mui/material/Typography"; import { AllGangs } from "../AllGangs";
import FormControlLabel from "@mui/material/FormControlLabel";
import Switch from "@mui/material/Switch"; import { useGang } from "./Context";
import Tooltip from "@mui/material/Tooltip"; import { TerritoryInfoModal } from "./TerritoryInfoModal";
import Box from "@mui/material/Box";
import Paper from "@mui/material/Paper";
export function TerritorySubpage(): React.ReactElement { export function TerritorySubpage(): React.ReactElement {
const gang = useGang(); const gang = useGang();
const gangNames = Object.keys(AllGangs).filter((g) => g != gang.facName); const gangNames = Object.keys(AllGangs).filter((g) => g != gang.facName);
const [infoOpen, setInfoOpen] = useState(false);
return ( return (
<> <Container disableGutters maxWidth="md" sx={{ mx: 0 }}>
<Typography> <Typography>
This page shows how much territory your Gang controls. This statistic is listed as a percentage, which This page shows how much territory your Gang controls. This statistic is listed as a percentage, which
represents how much of the total territory you control. represents how much of the total territory you control.
<br />
<br />
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
accumulation rate is determined by the stats of all Gang members you have assigned to the 'Territory Warfare'
task. Gang members that are not assigned to this task do not contribute to your gang's power. Your gang also
loses a small amount of power whenever you lose a clash.
<br />
<br />
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
gang.
<br />
<br />
The amount of territory you have affects all aspects of your Gang members' production, including money, respect,
and wanted level. It is very beneficial to have high territory control.
<br />
<br />
To increase your chances of winning territory assign gang members to "Territory Warfare", this will build your
gang power. Then enable "Engage in Territory Warfare" to start fighting over territory.
</Typography> </Typography>
<FormControlLabel
control={ <Button onClick={() => setInfoOpen(true)} sx={{ my: 1 }}>
<Switch <Help sx={{ mr: 1 }} />
About Gang Territory
</Button>
<Box component={Paper} sx={{ p: 1, mb: 1 }}>
<Typography variant="h6" sx={{ display: 'flex', alignItems: 'center', flexWrap: 'wrap' }}>
{gang.facName} (Your gang)
</Typography>
<FormControlLabel
control={<Switch
checked={gang.territoryWarfareEngaged} checked={gang.territoryWarfareEngaged}
onChange={(event) => (gang.territoryWarfareEngaged = event.target.checked)} onChange={(event) => (gang.territoryWarfareEngaged = event.target.checked)}
/> />}
} label={<Tooltip
label={ title={<Typography>
<Tooltip Engaging in Territory Warfare sets your clash chance to 100%. Disengaging will cause your clash chance
title={ to gradually decrease until it reaches 0%.
<Typography> </Typography>}>
Engaging in Territory Warfare sets your clash chance to 100%. Disengaging will cause your clash chance
to gradually decrease until it reaches 0%.
</Typography>
}
>
<Typography>Engage in Territory Warfare</Typography> <Typography>Engage in Territory Warfare</Typography>
</Tooltip> </Tooltip>} />
} <br />
/> <FormControlLabel
<br /> control={<Switch
<Box display="flex">
<Tooltip
title={
<Typography>
This percentage represents the chance you have of 'clashing' with with another gang. If you do not wish to
gain/lose territory, then keep this percentage at 0% by not engaging in territory warfare.
</Typography>
}
>
<Typography>
Territory Clash Chance: {numeralWrapper.formatPercentage(gang.territoryClashChance, 3)}
</Typography>
</Tooltip>
</Box>
<br />
<FormControlLabel
control={
<Switch
checked={gang.notifyMemberDeath} checked={gang.notifyMemberDeath}
onChange={(event) => (gang.notifyMemberDeath = event.target.checked)} onChange={(event) => (gang.notifyMemberDeath = event.target.checked)}
/> />}
} label={<Tooltip
label={ title={<Typography>
<Tooltip If this is enabled, then you will receive a pop-up notifying you whenever one of your Gang Members dies
title={ in a territory clash.
<Typography> </Typography>}>
If this is enabled, then you will receive a pop-up notifying you whenever one of your Gang Members dies
in a territory clash.
</Typography>
}
>
<Typography>Notify about Gang Member Deaths</Typography> <Typography>Notify about Gang Member Deaths</Typography>
</Tooltip> </Tooltip>} />
}
/>
<br />
<Paper>
<Typography> <Typography>
<b> <b>Territory Clash Chance:</b> {numeralWrapper.formatPercentage(gang.territoryClashChance, 3)} <br />
<u>{gang.facName}</u> <b>Power:</b> {formatNumber(AllGangs[gang.facName].power, 3)} <br />
</b> <b>Territory:</b> {formatTerritory(AllGangs[gang.facName].territory)}% <br />
<br />
Power: {formatNumber(AllGangs[gang.facName].power, 6)}
<br />
Territory: {formatTerritory(AllGangs[gang.facName].territory)}%
<br />
<br />
</Typography> </Typography>
</Box>
<Box sx={{ display: 'grid', gridTemplateColumns: 'repeat(3, 1fr)' }}>
{gangNames.map((name) => ( {gangNames.map((name) => (
<OtherGangTerritory key={name} name={name} /> <OtherGangTerritory key={name} name={name} />
))} ))}
</Paper> </Box>
</> <TerritoryInfoModal open={infoOpen} onClose={() => setInfoOpen(false)} />
</Container >
); );
} }
function formatTerritory(n: number): string { function formatTerritory(n: number): string {
const v = n * 100; const v = n * 100;
const precision = 3;
if (v <= 0) { if (v <= 0) {
return formatNumber(0, 2); return formatNumber(0, precision);
} else if (v >= 100) { } else if (v >= 100) {
return formatNumber(100, 2); return formatNumber(100, precision);
} else { } else {
return formatNumber(v, 2); return formatNumber(v, precision);
} }
} }
@ -141,15 +108,15 @@ function OtherGangTerritory(props: ITerritoryProps): React.ReactElement {
const power = AllGangs[props.name].power; const power = AllGangs[props.name].power;
const clashVictoryChance = playerPower / (power + playerPower); const clashVictoryChance = playerPower / (power + playerPower);
return ( return (
<Typography> <Box component={Paper} sx={{ p: 1 }}>
<u>{props.name}</u> <Typography variant="h6" sx={{ display: 'flex', alignItems: 'center', flexWrap: 'wrap' }}>
<br /> {props.name}
Power: {formatNumber(power, 6)} </Typography>
<br /> <Typography>
Territory: {formatTerritory(AllGangs[props.name].territory)}%<br /> <b>Power:</b> {formatNumber(power, 3)} <br />
Chance to win clash with this gang: {numeralWrapper.formatPercentage(clashVictoryChance, 3)} <b>Territory:</b> {formatTerritory(AllGangs[props.name].territory)}% <br />
<br /> <b>Clash Win Chance:</b> {numeralWrapper.formatPercentage(clashVictoryChance, 3)}
<br /> </Typography>
</Typography> </Box>
); );
} }

@ -50,74 +50,62 @@ export function SleeveAugmentationsModal(props: IProps): React.ReactElement {
return ( return (
<Modal open={props.open} onClose={props.onClose}> <Modal open={props.open} onClose={props.onClose}>
<> <>
<Typography> <Box sx={{ mx: 1 }}>
You can purchase Augmentations for your Duplicate Sleeves. These Augmentations have the same effect as they <Typography>
would for you. You can only purchase Augmentations that you have unlocked through Factions. You can purchase Augmentations for your Duplicate Sleeves. These Augmentations have the same effect as they
<br /> would for you. You can only purchase Augmentations that you have unlocked through Factions.
<br /> <br />
When purchasing an Augmentation for a Duplicate Sleeve, they are immediately installed. This means that the <br />
Duplicate Sleeve will immediately lose all of its stat experience. When purchasing an Augmentation for a Duplicate Sleeve, they are immediately installed. This means that the
</Typography> Duplicate Sleeve will immediately lose all of its stat experience.
<Table size="small" padding="none"> </Typography>
<TableBody> <Box component={Paper} sx={{ my: 1, p: 1 }}>
{availableAugs.map((aug) => { <Table size="small" padding="none">
return ( <TableBody>
<TableRow key={aug.name}> {availableAugs.map((aug) => {
<TableCell> return (
<Button onClick={() => purchaseAugmentation(aug)} disabled={player.money < aug.startingCost}> <TableRow key={aug.name}>
Buy <TableCell>
</Button> <Button onClick={() => purchaseAugmentation(aug)} disabled={player.money < aug.startingCost}>
</TableCell> Buy
<TableCell> </Button>
<Box display="flex"> </TableCell>
<Tooltip title={aug.stats || ""}> <TableCell>
<Typography>{aug.name}</Typography> <Box display="flex">
</Tooltip> <Tooltip title={aug.stats || ""}>
</Box> <Typography>{aug.name}</Typography>
</TableCell> </Tooltip>
<TableCell> </Box>
<Money money={aug.startingCost} player={player} /> </TableCell>
</TableCell> <TableCell>
</TableRow> <Money money={aug.startingCost} player={player} />
); </TableCell>
})} </TableRow>
</TableBody> );
</Table> })}
</TableBody>
</Table>
</Box>
</Box>
{ownedAugNames.length > 0 && ( {ownedAugNames.length > 0 && (
<> <>
<Typography>Owned Augmentations:</Typography> <Typography sx={{ mx: 1 }}>Owned Augmentations:</Typography>
{ownedAugNames.map((augName) => { <Box display="grid" sx={{ gridTemplateColumns: 'repeat(5, 1fr)', m: 1 }}>
const aug = Augmentations[augName]; {ownedAugNames.map((augName) => {
let tooltip = <></>; const aug = Augmentations[augName];
if (typeof aug.info === "string") { const info = typeof aug.info === "string" ? <span>{aug.info}</span> : aug.info
tooltip = ( const tooltip = (<>{info}<br /><br />{aug.stats}</>);
<>
<span>{aug.info}</span>
<br />
<br />
{aug.stats}
</>
);
} else {
tooltip = (
<>
{aug.info}
<br />
<br />
{aug.stats}
</>
);
}
return ( return (
<Tooltip key={augName} title={<Typography>{tooltip}</Typography>}> <Tooltip key={augName} title={<Typography>{tooltip}</Typography>}>
<Paper> <Paper sx={{ p: 1 }}>
<Typography>{augName}</Typography> <Typography>{augName}</Typography>
</Paper> </Paper>
</Tooltip> </Tooltip>
); );
})} })}
</Box>
</> </>
)} )}
</> </>

@ -1,34 +1,29 @@
import React, { useState } from "react"; import React, { useState } from "react";
import {
Box,
Paper,
Typography,
Button,
Tooltip
} from "@mui/material";
import { CONSTANTS } from "../../../Constants";
import { Crimes } from "../../../Crime/Crimes";
import { numeralWrapper } from "../../../ui/numeralFormat";
import { createProgressBarText } from "../../../utils/helpers/createProgressBarText";
import { use } from "../../../ui/Context";
import { FactionWorkType } from "../../../Faction/FactionWorkTypeEnum";
import { Sleeve } from "../Sleeve"; import { Sleeve } from "../Sleeve";
import { SleeveTaskType } from "../SleeveTaskTypesEnum"; import { SleeveTaskType } from "../SleeveTaskTypesEnum";
import { CONSTANTS } from "../../../Constants";
import { Crimes } from "../../../Crime/Crimes";
import { numeralWrapper } from "../../../ui/numeralFormat";
import { createProgressBarText } from "../../../utils/helpers/createProgressBarText";
import { SleeveAugmentationsModal } from "./SleeveAugmentationsModal"; import { SleeveAugmentationsModal } from "./SleeveAugmentationsModal";
import { TravelModal } from "./TravelModal"; import { TravelModal } from "./TravelModal";
import { Money } from "../../../ui/React/Money"; import { StatsElement, EarningsElement } from "./StatsElement";
import { MoneyRate } from "../../../ui/React/MoneyRate";
import { use } from "../../../ui/Context";
import { ReputationRate } from "../../../ui/React/ReputationRate";
import { StatsElement } from "./StatsElement";
import { MoreStatsModal } from "./MoreStatsModal"; import { MoreStatsModal } from "./MoreStatsModal";
import { MoreEarningsModal } from "./MoreEarningsModal"; import { MoreEarningsModal } from "./MoreEarningsModal";
import { TaskSelector } from "./TaskSelector"; import { TaskSelector } from "./TaskSelector";
import { FactionWorkType } from "../../../Faction/FactionWorkTypeEnum";
import { StatsTable } from "../../../ui/React/StatsTable";
import Typography from "@mui/material/Typography";
import Paper from "@mui/material/Paper";
import Grid from "@mui/material/Grid";
import Button from "@mui/material/Button";
import Tooltip from "@mui/material/Tooltip";
interface IProps { interface IProps {
sleeve: Sleeve; sleeve: Sleeve;
@ -141,86 +136,71 @@ export function SleeveElem(props: IProps): React.ReactElement {
console.error(`Invalid/Unrecognized taskValue in updateSleeveTaskDescription(): ${abc[0]}`); console.error(`Invalid/Unrecognized taskValue in updateSleeveTaskDescription(): ${abc[0]}`);
} }
let data: any[][] = [];
if (props.sleeve.currentTask === SleeveTaskType.Crime) {
data = [
[`Money`, <Money money={parseFloat(props.sleeve.currentTaskLocation)} />, `(on success)`],
[`Hacking Exp`, numeralWrapper.formatExp(props.sleeve.gainRatesForTask.hack), `(2x on success)`],
[`Strength Exp`, numeralWrapper.formatExp(props.sleeve.gainRatesForTask.str), `(2x on success)`],
[`Defense Exp`, numeralWrapper.formatExp(props.sleeve.gainRatesForTask.def), `(2x on success)`],
[`Dexterity Exp`, numeralWrapper.formatExp(props.sleeve.gainRatesForTask.dex), `(2x on success)`],
[`Agility Exp`, numeralWrapper.formatExp(props.sleeve.gainRatesForTask.agi), `(2x on success)`],
[`Charisma Exp`, numeralWrapper.formatExp(props.sleeve.gainRatesForTask.cha), `(2x on success)`],
];
} else {
data = [
[`Money:`, <MoneyRate money={5 * props.sleeve.gainRatesForTask.money} />],
[`Hacking Exp:`, `${numeralWrapper.formatExp(5 * props.sleeve.gainRatesForTask.hack)} / s`],
[`Strength Exp:`, `${numeralWrapper.formatExp(5 * props.sleeve.gainRatesForTask.str)} / s`],
[`Defense Exp:`, `${numeralWrapper.formatExp(5 * props.sleeve.gainRatesForTask.def)} / s`],
[`Dexterity Exp:`, `${numeralWrapper.formatExp(5 * props.sleeve.gainRatesForTask.dex)} / s`],
[`Agility Exp:`, `${numeralWrapper.formatExp(5 * props.sleeve.gainRatesForTask.agi)} / s`],
[`Charisma Exp:`, `${numeralWrapper.formatExp(5 * props.sleeve.gainRatesForTask.cha)} / s`],
];
if (props.sleeve.currentTask === SleeveTaskType.Company || props.sleeve.currentTask === SleeveTaskType.Faction) {
const repGain: number = props.sleeve.getRepGain(player);
data.push([`Reputation:`, <ReputationRate reputation={5 * repGain} />]);
}
}
return ( return (
<> <Box component={Paper} sx={{ width: 'auto' }}>
<Grid container component={Paper}> <Box sx={{ m: 1 }}>
<Grid item xs={3}> <Box display="grid" sx={{ gridTemplateColumns: '1fr 1fr', width: '100%', gap: 1 }}>
<StatsElement sleeve={props.sleeve} /> <Box>
<Button onClick={() => setStatsOpen(true)}>More Stats</Button> <StatsElement sleeve={props.sleeve} />
<Tooltip title={player.money < CONSTANTS.TravelCost ? <Typography>Insufficient funds</Typography> : ""}> <Box display="grid" sx={{ gridTemplateColumns: '1fr 1fr', width: '100%' }}>
<span> <Button onClick={() => setStatsOpen(true)}>More Stats</Button>
<Button onClick={() => setTravelOpen(true)} disabled={player.money < CONSTANTS.TravelCost}> <Button onClick={() => setEarningsOpen(true)}>More Earnings Info</Button>
Travel <Tooltip title={player.money < CONSTANTS.TravelCost ? <Typography>Insufficient funds</Typography> : ""}>
</Button> <span>
</span> <Button
</Tooltip> onClick={() => setTravelOpen(true)}
<Tooltip disabled={player.money < CONSTANTS.TravelCost}
title={props.sleeve.shock < 100 ? <Typography>Unlocked when sleeve has fully recovered</Typography> : ""} sx={{ width: '100%', height: '100%' }}
> >
<span> Travel
<Button onClick={() => setAugmentationsOpen(true)} disabled={props.sleeve.shock < 100}> </Button>
Manage Augmentations </span>
</Button> </Tooltip>
</span> <Tooltip
</Tooltip> title={props.sleeve.shock < 100 ? <Typography>Unlocked when sleeve has fully recovered</Typography> : ""}
</Grid> >
<Grid item xs={5}> <span>
<TaskSelector player={player} sleeve={props.sleeve} setABC={setABC} /> <Button
<Typography>{desc}</Typography> onClick={() => setAugmentationsOpen(true)}
<Typography> disabled={props.sleeve.shock < 100}
{props.sleeve.currentTask === SleeveTaskType.Crime && sx={{ width: '100%', height: '100%' }}
createProgressBarText({ >
progress: props.sleeve.currentTaskTime / props.sleeve.currentTaskMaxTime, Manage Augmentations
totalTicks: 25, </Button>
})} </span>
</Typography> </Tooltip>
<Button onClick={setTask}>Set Task</Button> </Box>
</Grid> </Box>
<Grid item xs={4}> <Box>
<StatsTable title="Earnings (Pre-Synchronization)" rows={data} /> <EarningsElement sleeve={props.sleeve} />
<Button onClick={() => setEarningsOpen(true)}>More Earnings Info</Button> <Box>
</Grid> <TaskSelector player={player} sleeve={props.sleeve} setABC={setABC} />
</Grid> <Button onClick={setTask} sx={{ width: '100%' }}>Set Task</Button>
<MoreStatsModal open={statsOpen} onClose={() => setStatsOpen(false)} sleeve={props.sleeve} /> <Typography>{desc}</Typography>
<MoreEarningsModal open={earningsOpen} onClose={() => setEarningsOpen(false)} sleeve={props.sleeve} /> <Typography>
<TravelModal {props.sleeve.currentTask === SleeveTaskType.Crime &&
open={travelOpen} createProgressBarText({
onClose={() => setTravelOpen(false)} progress: props.sleeve.currentTaskTime / props.sleeve.currentTaskMaxTime,
sleeve={props.sleeve} totalTicks: 25,
rerender={props.rerender} })}
/> </Typography>
<SleeveAugmentationsModal </Box>
open={augmentationsOpen} </Box>
onClose={() => setAugmentationsOpen(false)} <MoreStatsModal open={statsOpen} onClose={() => setStatsOpen(false)} sleeve={props.sleeve} />
sleeve={props.sleeve} <MoreEarningsModal open={earningsOpen} onClose={() => setEarningsOpen(false)} sleeve={props.sleeve} />
/> <TravelModal
</> open={travelOpen}
onClose={() => setTravelOpen(false)}
sleeve={props.sleeve}
rerender={props.rerender}
/>
<SleeveAugmentationsModal
open={augmentationsOpen}
onClose={() => setAugmentationsOpen(false)}
sleeve={props.sleeve}
/>
</Box>
</Box>
</Box >
); );
} }

@ -1,12 +1,16 @@
import React, { useState, useEffect } from "react"; import React, { useState, useEffect } from "react";
import {
Box,
Typography,
Button,
Container
} from "@mui/material";
import { use } from "../../../ui/Context";
import { SleeveElem } from "./SleeveElem"; import { SleeveElem } from "./SleeveElem";
import { FAQModal } from "./FAQModal"; import { FAQModal } from "./FAQModal";
import { use } from "../../../ui/Context";
import Typography from "@mui/material/Typography";
import Button from "@mui/material/Button";
import Link from "@mui/material/Link";
export function SleeveRoot(): React.ReactElement { export function SleeveRoot(): React.ReactElement {
const player = use.Player(); const player = use.Player();
@ -23,27 +27,29 @@ export function SleeveRoot(): React.ReactElement {
return ( return (
<> <>
<Typography variant="h4">Sleeves</Typography> <Container disableGutters maxWidth="md" sx={{ mx: 0 }}>
<Typography> <Typography variant="h4">Sleeves</Typography>
Duplicate Sleeves are MK-V Synthoids (synthetic androids) into which your consciousness has been copied. In <Typography>
other words, these Synthoids contain a perfect duplicate of your mind. Duplicate Sleeves are MK-V Synthoids (synthetic androids) into which your consciousness has been copied. In
<br /> other words, these Synthoids contain a perfect duplicate of your mind.
<br /> <br />
Sleeves can be used to perform different tasks synchronously. <br />
<br /> Sleeves can be used to perform different tasks synchronously.
<br /> <br />
</Typography> <br />
</Typography>
</Container>
<Button onClick={() => setFAQOpen(true)}>FAQ</Button> <Button onClick={() => setFAQOpen(true)}>FAQ</Button>
<Link <Button href="https://bitburner.readthedocs.io/en/latest/advancedgameplay/sleeves.html#duplicate-sleeves" target="_blank">
target="_blank" Wiki Documentation
href="https://bitburner.readthedocs.io/en/latest/advancedgameplay/sleeves.html#duplicate-sleeves" </Button>
> <Box display="grid" sx={{ gridTemplateColumns: 'repeat(2, 1fr)', mt: 1 }}>
<Typography> Documentation</Typography> {player.sleeves.map((sleeve, i) => (
</Link> <SleeveElem key={i} rerender={rerender} sleeve={sleeve} />
{player.sleeves.map((sleeve, i) => ( ))}
<SleeveElem key={i} rerender={rerender} sleeve={sleeve} /> </Box>
))}
<FAQModal open={FAQOpen} onClose={() => setFAQOpen(false)} /> <FAQModal open={FAQOpen} onClose={() => setFAQOpen(false)} />
</> </>
); );

@ -1,31 +1,110 @@
import { Sleeve } from "../Sleeve";
import { numeralWrapper } from "../../../ui/numeralFormat";
import React from "react"; import React from "react";
import { StatsTable } from "../../../ui/React/StatsTable"; import {
Typography,
Table,
TableBody,
TableCell,
TableRow,
} from "@mui/material";
import { numeralWrapper } from "../../../ui/numeralFormat";
import { Settings } from "../../../Settings/Settings";
import { StatsRow } from "../../../ui/React/StatsRow";
import { characterOverviewStyles as useStyles } from "../../../ui/React/CharacterOverview";
import { Money } from "../../../ui/React/Money";
import { MoneyRate } from "../../../ui/React/MoneyRate";
import { ReputationRate } from "../../../ui/React/ReputationRate";
import { use } from "../../../ui/Context";
import { Sleeve } from "../Sleeve";
import { SleeveTaskType } from "../SleeveTaskTypesEnum";
interface IProps { interface IProps {
sleeve: Sleeve; sleeve: Sleeve;
} }
export function StatsElement(props: IProps): React.ReactElement { export function StatsElement(props: IProps): React.ReactElement {
const rows = [ const classes = useStyles();
[
"HP: ", return (
<> <Table sx={{ display: 'table', mb: 1, width: '100%' }}>
{numeralWrapper.formatHp(props.sleeve.hp)} / {numeralWrapper.formatHp(props.sleeve.max_hp)} <TableBody>
</>, <StatsRow name="City" color={Settings.theme.primary} data={{ content: props.sleeve.city }} />
], <StatsRow name="HP" color={Settings.theme.hp}
["City: ", <>{props.sleeve.city}</>], data={{ content: `${numeralWrapper.formatHp(props.sleeve.hp)} / ${numeralWrapper.formatHp(props.sleeve.max_hp)}` }}
["Hacking: ", <>{numeralWrapper.formatSkill(props.sleeve.hacking)}</>], />
["Strength: ", <>{numeralWrapper.formatSkill(props.sleeve.strength)}</>], <StatsRow name="Hacking" color={Settings.theme.hack} data={{ level: props.sleeve.hacking, exp: props.sleeve.hacking_exp }} />
["Defense: ", <>{numeralWrapper.formatSkill(props.sleeve.defense)}</>], <StatsRow name="Strength" color={Settings.theme.combat} data={{ level: props.sleeve.strength, exp: props.sleeve.strength_exp }} />
["Dexterity: ", <>{numeralWrapper.formatSkill(props.sleeve.dexterity)}</>], <StatsRow name="Defense" color={Settings.theme.combat} data={{ level: props.sleeve.defense, exp: props.sleeve.defense_exp }} />
["Agility: ", <>{numeralWrapper.formatSkill(props.sleeve.agility)}</>], <StatsRow name="Dexterity" color={Settings.theme.combat} data={{ level: props.sleeve.dexterity, exp: props.sleeve.dexterity_exp }} />
["Charisma: ", <>{numeralWrapper.formatSkill(props.sleeve.charisma)}</>], <StatsRow name="Agility" color={Settings.theme.combat} data={{ level: props.sleeve.agility, exp: props.sleeve.agility_exp }} />
["Shock: ", <>{numeralWrapper.formatSleeveShock(100 - props.sleeve.shock)}</>], <StatsRow name="Charisma" color={Settings.theme.cha} data={{ level: props.sleeve.charisma, exp: props.sleeve.charisma_exp }} />
["Sync: ", <>{numeralWrapper.formatSleeveSynchro(props.sleeve.sync)}</>], <TableRow>
["Memory: ", <>{numeralWrapper.formatSleeveMemory(props.sleeve.memory)}</>], <TableCell classes={{ root: classes.cellNone }}>
]; <br />
return <StatsTable rows={rows} />; </TableCell>
</TableRow>
<StatsRow name="Shock" color={Settings.theme.primary} data={{ content: numeralWrapper.formatSleeveShock(100 - props.sleeve.shock) }} />
<StatsRow name="Sync" color={Settings.theme.primary} data={{ content: numeralWrapper.formatSleeveSynchro(props.sleeve.sync) }} />
<StatsRow name="Memory" color={Settings.theme.primary} data={{ content: numeralWrapper.formatSleeveMemory(props.sleeve.memory) }} />
</TableBody>
</Table>
)
}
export function EarningsElement(props: IProps): React.ReactElement {
const classes = useStyles();
const player = use.Player();
let data: any[][] = [];
if (props.sleeve.currentTask === SleeveTaskType.Crime) {
data = [
[`Money`, <><Money money={parseFloat(props.sleeve.currentTaskLocation)} /> (on success)</>],
[`Hacking Exp`, `${numeralWrapper.formatExp(props.sleeve.gainRatesForTask.hack)} (2x on success)`],
[`Strength Exp`, `${numeralWrapper.formatExp(props.sleeve.gainRatesForTask.str)} (2x on success)`],
[`Defense Exp`, `${numeralWrapper.formatExp(props.sleeve.gainRatesForTask.def)} (2x on success)`],
[`Dexterity Exp`, `${numeralWrapper.formatExp(props.sleeve.gainRatesForTask.dex)} (2x on success)`],
[`Agility Exp`, `${numeralWrapper.formatExp(props.sleeve.gainRatesForTask.agi)} (2x on success)`],
[`Charisma Exp`, `${numeralWrapper.formatExp(props.sleeve.gainRatesForTask.cha)} (2x on success)`],
];
} else {
data = [
[`Money:`, <MoneyRate money={5 * props.sleeve.gainRatesForTask.money} />],
[`Hacking Exp:`, `${numeralWrapper.formatExp(5 * props.sleeve.gainRatesForTask.hack)} / sec`],
[`Strength Exp:`, `${numeralWrapper.formatExp(5 * props.sleeve.gainRatesForTask.str)} / sec`],
[`Defense Exp:`, `${numeralWrapper.formatExp(5 * props.sleeve.gainRatesForTask.def)} / sec`],
[`Dexterity Exp:`, `${numeralWrapper.formatExp(5 * props.sleeve.gainRatesForTask.dex)} / sec`],
[`Agility Exp:`, `${numeralWrapper.formatExp(5 * props.sleeve.gainRatesForTask.agi)} / sec`],
[`Charisma Exp:`, `${numeralWrapper.formatExp(5 * props.sleeve.gainRatesForTask.cha)} / sec`],
];
if (props.sleeve.currentTask === SleeveTaskType.Company || props.sleeve.currentTask === SleeveTaskType.Faction) {
const repGain: number = props.sleeve.getRepGain(player);
data.push([`Reputation:`, <ReputationRate reputation={5 * repGain} />]);
}
}
return (
<Table sx={{ display: 'table', mb: 1, width: '100%', lineHeight: 0 }}>
<TableBody>
<TableRow>
<TableCell classes={{ root: classes.cellNone }}>
<Typography variant='h6'>
Earnings
</Typography>
</TableCell>
</TableRow>
{data.map(([a, b]) => (
<TableRow key={a.toString() + b.toString()}>
<TableCell classes={{ root: classes.cellNone }}>
<Typography>{a}</Typography>
</TableCell>
<TableCell align="right" classes={{ root: classes.cellNone }}>
<Typography>{b}</Typography>
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
)
} }

@ -279,7 +279,7 @@ export function TaskSelector(props: IProps): React.ReactElement {
return ( return (
<> <>
<Select onChange={onS0Change} value={s0}> <Select onChange={onS0Change} value={s0} sx={{ width: '100%' }}>
{validActions.map((task) => ( {validActions.map((task) => (
<MenuItem key={task} value={task}> <MenuItem key={task} value={task}>
{task} {task}
@ -288,8 +288,7 @@ export function TaskSelector(props: IProps): React.ReactElement {
</Select> </Select>
{!(details.first.length === 1 && details.first[0] === "------") && ( {!(details.first.length === 1 && details.first[0] === "------") && (
<> <>
<br /> <Select onChange={onS1Change} value={s1} sx={{ width: '100%' }}>
<Select onChange={onS1Change} value={s1}>
{details.first.map((detail) => ( {details.first.map((detail) => (
<MenuItem key={detail} value={detail}> <MenuItem key={detail} value={detail}>
{detail} {detail}
@ -300,8 +299,7 @@ export function TaskSelector(props: IProps): React.ReactElement {
)} )}
{!(details2.length === 1 && details2[0] === "------") && ( {!(details2.length === 1 && details2[0] === "------") && (
<> <>
<br /> <Select onChange={onS2Change} value={s2} sx={{ width: '100%' }}>
<Select onChange={onS2Change} value={s2}>
{details2.map((detail) => ( {details2.map((detail) => (
<MenuItem key={detail} value={detail}> <MenuItem key={detail} value={detail}>
{detail} {detail}

49
src/ui/React/StatsRow.tsx Normal file

@ -0,0 +1,49 @@
import React from "react";
import {
Typography,
TableCell,
TableRow,
} from "@mui/material";
import { numeralWrapper } from "../numeralFormat";
import { formatNumber } from "../../utils/StringHelperFunctions";
import { characterOverviewStyles as useStyles } from "./CharacterOverview";
interface ITableRowData {
content?: string;
level?: number;
exp?: number;
}
interface IProps {
name: string;
color: string;
classes?: any;
data: ITableRowData;
}
export const StatsRow = ({ name, color, classes = useStyles(), data }: IProps): React.ReactElement => {
let content;
if (data.content !== undefined) {
content = data.content;
} else if (data.level !== undefined && data.exp !== undefined) {
content = `${formatNumber(data.level, 0)} (${numeralWrapper.formatExp(data.exp)} exp)`;
} else if (data.level !== undefined && data.exp === undefined) {
content = `${formatNumber(data.level, 0)}`;
}
return (
<TableRow>
<TableCell classes={{ root: classes.cellNone }}>
<Typography style={{ color: color }}>{name}</Typography>
</TableCell>
<TableCell align="right" classes={{ root: classes.cellNone }}>
<Typography style={{ color: color }}>
{content}
</Typography>
</TableCell>
</TableRow>
)
}